From bb69d5c49d415497c1161569ce6637d993fc26f4 Mon Sep 17 00:00:00 2001 From: R524809 Date: Fri, 24 Apr 2026 17:49:11 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20worksheet=E5=8F=91=E5=B8=83=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cloudfunctions/worksheetsPublish/index.js | 147 ++++ cloudfunctions/worksheetsPublish/package.json | 8 + docs/Debug发布方案.md | 178 +++-- docs/小程序云开发方案.md | 71 +- miniprogram/base/pageMixin.ts | 165 ++++ .../debug-publish-tools.json | 7 + .../debug-publish-tools.less | 196 +++++ .../debug-publish-tools.ts | 350 +++++++++ .../debug-publish-tools.wxml | 183 +++++ miniprogram/constants/focusFunctions.ts | 127 +++- miniprogram/constants/mathFunctions.ts | 222 +++++- miniprogram/core/data/worksheets.ts | 545 -------------- miniprogram/core/models/category.ts | 8 + miniprogram/core/models/worksheet.ts | 174 +---- .../generators/letter-tracing-generator.ts | 2 +- .../letterTracing/letterTracing.config.ts | 123 +++ .../letterTracing/letterTracing.json | 1 + .../letterTracing/letterTracing.ts | 135 ++-- .../letterTracing/letterTracing.wxml | 20 +- miniprogram/pages/category/category.data.ts | 710 ++++++++++++++++-- miniprogram/pages/category/category.ts | 12 +- miniprogram/pages/category/category.wxml | 6 +- miniprogram/pages/home/home.data.ts | 8 +- miniprogram/utils/debugPublish.ts | 156 ++++ 24 files changed, 2540 insertions(+), 1014 deletions(-) create mode 100644 cloudfunctions/worksheetsPublish/index.js create mode 100644 cloudfunctions/worksheetsPublish/package.json create mode 100644 miniprogram/components3.0/debug-publish-tools/debug-publish-tools.json create mode 100644 miniprogram/components3.0/debug-publish-tools/debug-publish-tools.less create mode 100644 miniprogram/components3.0/debug-publish-tools/debug-publish-tools.ts create mode 100644 miniprogram/components3.0/debug-publish-tools/debug-publish-tools.wxml delete mode 100644 miniprogram/core/data/worksheets.ts create mode 100644 miniprogram/englishPages/letterTracing/letterTracing.config.ts create mode 100644 miniprogram/utils/debugPublish.ts diff --git a/cloudfunctions/worksheetsPublish/index.js b/cloudfunctions/worksheetsPublish/index.js new file mode 100644 index 0000000..6c35257 --- /dev/null +++ b/cloudfunctions/worksheetsPublish/index.js @@ -0,0 +1,147 @@ +const cloud = require('wx-server-sdk'); + +cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); + +const VALID_STATUS = new Set(['draft', 'active', 'hidden']); +const VALID_DIFFICULTY = new Set([1, 2, 3, 4]); + +function toInt(value, fallback = 0) { + const number = Number(value); + if (!Number.isFinite(number)) return fallback; + return Math.round(number); +} + +function toStringArray(value) { + if (!Array.isArray(value)) return []; + return value.map((item) => String(item || '').trim()).filter(Boolean); +} + +async function ensureCategoryExists(db, category) { + try { + await db.collection('categories').doc(category).get(); + return true; + } catch (error) { + return false; + } +} + +async function validatePayload(event, db) { + const id = String(event.id || '').trim(); + const title = String(event.title || '').trim(); + const subtitle = String(event.subtitle || '').trim(); + const category = String(event.category || '').trim(); + const subcategory = String(event.subcategory || '').trim(); + const path = String(event.path || '').trim(); + const previewImg = String(event.previewImg || '').trim(); + const ageMin = toInt(event.ageMin, 0); + const ageMax = toInt(event.ageMax, 0); + const grade = toInt(event.grade, 0); + const difficulty = toInt(event.difficulty, 0); + const tags = toStringArray(event.tags); + const isNew = !!event.isNew; + const isHot = !!event.isHot; + const sortOrder = toInt(event.sortOrder, 0); + const downloads = toInt(event.downloads, 0); + const likes = toInt(event.likes, 0); + const status = String(event.status || 'draft').trim(); + + if (!id) throw new Error('题型 id 不能为空'); + if (!title) throw new Error('标题不能为空'); + if (!subtitle) throw new Error('副标题不能为空'); + if (!category) throw new Error('分类不能为空'); + if (!path) throw new Error('页面路径不能为空'); + if (!previewImg) throw new Error('预览图不能为空'); + if (ageMin < 0 || ageMax < ageMin) throw new Error('年龄范围不合法'); + if (!VALID_DIFFICULTY.has(difficulty)) throw new Error('难度不合法'); + if (!VALID_STATUS.has(status)) throw new Error('状态不合法'); + if (!(await ensureCategoryExists(db, category))) { + throw new Error('分类不合法,categories 集合中不存在该分类'); + } + + return { + id, + title, + subtitle, + category, + subcategory, + path, + previewImg, + ageMin, + ageMax, + grade, + difficulty, + tags, + isNew, + isHot, + sortOrder, + downloads, + likes, + status, + }; +} + +exports.main = async (event) => { + try { + const db = cloud.database(); + const payload = await validatePayload(event, db); + const collection = db.collection('worksheets'); + + const record = { + title: payload.title, + subtitle: payload.subtitle, + category: payload.category, + subcategory: payload.subcategory, + path: payload.path, + ageMin: payload.ageMin, + ageMax: payload.ageMax, + grade: payload.grade, + difficulty: payload.difficulty, + previewImg: payload.previewImg, + tags: payload.tags, + isNew: payload.isNew, + isHot: payload.isHot, + sortOrder: payload.sortOrder, + downloads: payload.downloads, + likes: payload.likes, + status: payload.status, + updatedAt: db.serverDate(), + }; + + try { + const { data } = await collection.doc(payload.id).get(); + + await collection.doc(payload.id).update({ + data: record, + }); + + return { + success: true, + data: { + _id: payload.id, + createdAt: data.createdAt, + ...record, + }, + }; + } catch (error) { + await collection.doc(payload.id).set({ + data: { + ...record, + createdAt: db.serverDate(), + }, + }); + + return { + success: true, + data: { + _id: payload.id, + ...record, + }, + }; + } + } catch (error) { + return { + success: false, + message: error instanceof Error ? error.message : '发布题型失败', + }; + } +}; diff --git a/cloudfunctions/worksheetsPublish/package.json b/cloudfunctions/worksheetsPublish/package.json new file mode 100644 index 0000000..2316972 --- /dev/null +++ b/cloudfunctions/worksheetsPublish/package.json @@ -0,0 +1,8 @@ +{ + "name": "worksheets-publish", + "version": "1.0.0", + "main": "index.js", + "dependencies": { + "wx-server-sdk": "^3.0.4" + } +} diff --git a/docs/Debug发布方案.md b/docs/Debug发布方案.md index 7ee9b66..f3dac5d 100644 --- a/docs/Debug发布方案.md +++ b/docs/Debug发布方案.md @@ -18,7 +18,7 @@ → ② 放入 node-tools/entranceInput/<分类>/ → ③ 运行 processEntrancePicture.js 裁剪缩放 → ④ 从 entranceOuput/ 拷贝到 miniprogram/assets/entrancePicture/ - → ⑤ 手动编辑 category.data.ts 添加元数据(title、subtitle、path、img 等) + → ⑤ 手动编辑 category.data.ts 添加元数据(title、desc、path、previewImg 等) → ⑥ 提交代码 → 审核 → 发版 ``` @@ -67,10 +67,11 @@ │ │ 副标题: 四宫格每日字母打卡练习 │ │ │ │ 分类: english │ │ │ │ 路径: /englishPages/letterTracing/... │ │ -│ │ 难度: beginner │ │ +│ │ 难度: 1 │ │ │ │ 年龄: 4-7岁 │ │ │ │ 标签: [字母, 描红, 打卡] │ │ │ │ [预览图缩略图] │ │ +│ │ 裁剪图标:头部和底部、仅头部、不裁剪 │ │ │ │ │ │ │ │ ─ 可编辑字段(允许微调) ─ │ │ │ │ │ │ @@ -132,26 +133,20 @@ WXML 中条件渲染: */ interface PublishMeta { // ─── 必填字段 ─── - id: string; // 唯一标识,如 'letter-tracing-daily-checkin' - title: string; // 显示标题 - subtitle: string; // 显示副标题 + id: string; // 唯一标识,如 'letter-tracing-daily-checkin' + title: string; // 显示标题 + desc: string; // 显示描述 category: 'math' | 'chinese' | 'english' | 'puzzle' | 'craft'; - subcategory: string; // 子分类 - path: string; // 页面完整路径(含参数) + subcategory: string; // 子分类 + path: string; // 页面完整路径(含参数) + ageMin: number; // 适龄最小值 + ageMax: number; // 适龄最大值 + difficulty: 1 | 2 | 3 | 4; // 1-4:入门、基础、进阶、挑战 // ─── 选填字段(有默认值)─── - icon?: string; // Emoji 图标 - ageRange?: [number, number]; // 适用年龄,默认 [3, 8] - difficulty?: 'beginner' | 'basic' | 'intermediate' | 'advanced'; - tags?: string[]; // 搜索标签 - sortOrder?: number; // 排序权重 - status?: 'draft' | 'active' | 'hidden'; // 默认 'draft' - - // ─── 模板引擎相关(可选,未来扩展)─── - template?: string; - generator?: string; - generatorConfig?: Record; - layoutConfig?: Record; + previewImg?: string; // 上传入口图后回填的云存储 fileID + tags?: string[]; // 搜索标签 + sortOrder?: number; // 排序权重 } ``` @@ -175,13 +170,13 @@ getPublishMeta(): PublishMeta { return { id: this.data.currentId, title: drawService.title, - subtitle: drawService.subtitle, + desc: drawService.desc, category: 'english', subcategory: 'letter-tracing', path: `/englishPages/letterTracing/letterTracing?id=${this.data.currentId}`, - icon: '🔠', - ageRange: [4, 7], - difficulty: 'beginner', + ageMin: 4, + ageMax: 7, + difficulty: 1, tags: ['字母', '描红', '英语'], }; } @@ -194,7 +189,7 @@ getPublishMeta(): PublishMeta { ```typescript /** * 从预览 Canvas 导出入口图 - * + * * 处理流程: * 1. 从 preview-card 导出完整 A4 临时文件 * 2. 用离屏 Canvas 裁剪掉页眉/页脚区域(与 processEntrancePicture.js 逻辑对齐) @@ -204,7 +199,12 @@ getPublishMeta(): PublishMeta { async function exportEntranceImage( canvas: WechatMiniprogram.Canvas, ctx: CanvasRenderingContext2D, - paperConfig: { headerHeight: number; footerHeight: number; width: number; height: number } + paperConfig: { + headerHeight: number; + footerHeight: number; + width: number; + height: number; + }, ): Promise { // 裁剪参数 — 与 node-tools/processEntrancePicture.js 保持一致 const cropTop = paperConfig.headerHeight; @@ -220,9 +220,15 @@ async function exportEntranceImage( canvas.width = ENTRANCE_WIDTH; canvas.height = targetH; ctx.drawImage( - canvas, // 自身作为源(需先 toDataURL 再 loadImage,实际实现需用临时文件中转) - 0, cropTop, sourceW, sourceH, // 源区域:去掉页眉页脚 - 0, 0, ENTRANCE_WIDTH, targetH // 目标区域:缩放到标准宽度 + canvas, // 自身作为源(需先 toDataURL 再 loadImage,实际实现需用临时文件中转) + 0, + cropTop, + sourceW, + sourceH, // 源区域:去掉页眉页脚 + 0, + 0, + ENTRANCE_WIDTH, + targetH, // 目标区域:缩放到标准宽度 ); const tempPath = await canvasToTempFilePath(canvas, { @@ -246,21 +252,24 @@ async function exportEntranceImage( async function uploadEntranceImage( tempFilePath: string, category: string, - id: 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 + 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 { +async function publishWorksheet( + meta: PublishMeta, + imageFileID: string, +): Promise { const db = wx.cloud.database(); const collection = db.collection('worksheets'); @@ -271,7 +280,7 @@ async function publishWorksheet(meta: PublishMeta, imageFileID: string): Promise publishedAt: db.serverDate(), updatedAt: db.serverDate(), version: 1, - publishedBy: 'debug', // 标记为 debug 发布 + publishedBy: 'debug', // 标记为 debug 发布 }; // upsert:如果已存在则更新,不存在则创建 @@ -312,12 +321,16 @@ async function onDebugPublish(this: any): Promise { try { // 3. 导出入口图 const tempPath = await exportEntranceImage( - this.canvas, this.ctx, this.paperConfig + this.canvas, + this.ctx, + this.paperConfig, ); // 4. 上传图片到云存储 const fileID = await uploadEntranceImage( - tempPath, meta.category, meta.id + tempPath, + meta.category, + meta.id, ); // 5. 写入云数据库 @@ -352,33 +365,32 @@ async function onDebugPublish(this: any): Promise { ### 4.2 category.data.ts 的角色变化 -| 阶段 | category.data.ts 的作用 | -|------|------------------------| -| **当前** | 唯一数据源(硬编码所有题型信息) | -| **方案实施后** | 兜底数据源 + 离线保障(云端不可用时生效) | -| **长期** | 通过 `syncFromCloud` 脚本自动同步,保持与云端一致 | +| 阶段 | category.data.ts 的作用 | +| -------------- | ------------------------------------------------- | +| **当前** | 唯一数据源(硬编码所有题型信息) | +| **方案实施后** | 兜底数据源 + 离线保障(云端不可用时生效) | +| **长期** | 通过 `syncFromCloud` 脚本自动同步,保持与云端一致 | ### 4.3 worksheets 集合字段映射 debug 发布写入的字段,与 [小程序云开发方案](./小程序云开发方案.md) §3.3 `worksheets` 集合的字段**完全对齐**: -| PublishMeta 字段 | worksheets 集合字段 | 说明 | -|-----------------|-------------------|------| -| `id` | `id` | 业务唯一标识 | -| `title` | `title` | 显示标题 | -| `subtitle` | `desc` | 显示描述 | -| `category` | `category` | 所属大类 | -| `subcategory` | `subcategory` | 子分类 | -| `path` | 新增字段 `pagePath` | 小程序页面路径 | -| `icon` | 新增字段 `icon` | Emoji 图标 | -| `ageRange` | `ageRange` | 适用年龄段 | -| `difficulty` | `difficulty` | 难度级别 | -| `tags` | `tags` | 搜索标签 | -| `sortOrder` | `sortOrder` | 排列顺序 | -| `status` | `status` | 上架状态 | -| — | `previewImage` | 云存储 fileID | -| — | `publishedAt` | 发布时间 | -| — | `version` | 版本号(递增) | +| 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` | 更新时间 | --- @@ -414,9 +426,9 @@ cloud://doodle-xxx/ ```typescript // 三重保障 const canPublish = - isDebugPublishEnabled() // ① envVersion === 'develop' - && isDevBypassLimits() // ② 与下载绕过逻辑一致 - && wx.getStorageSync('enableDebug'); // ③ debug 页面手动开启 + isDebugPublishEnabled() && // ① envVersion === 'develop' + isDevBypassLimits() && // ② 与下载绕过逻辑一致 + wx.getStorageSync('enableDebug'); // ③ debug 页面手动开启 ``` ### 6.2 云端安全规则 @@ -450,12 +462,12 @@ exports.main = async (event, context) => { ### 6.3 数据保护 -| 措施 | 说明 | -|------|------| -| 默认 draft 状态 | 发布后默认 `status: 'draft'`,需手动激活为 `active` | -| 版本递增 | 每次更新 `version + 1`,可追踪变更历史 | -| publishedBy 标记 | `publishedBy: 'debug'` 区分来源 | -| 时间戳 | `publishedAt` / `updatedAt` 记录操作时间 | +| 措施 | 说明 | +| ---------------- | --------------------------------------------------- | +| 默认 draft 状态 | 发布后默认 `status: 'draft'`,需手动激活为 `active` | +| 版本递增 | 每次更新 `version + 1`,可追踪变更历史 | +| publishedBy 标记 | `publishedBy: 'debug'` 区分来源 | +| 时间戳 | `publishedAt` / `updatedAt` 记录操作时间 | --- @@ -571,26 +583,26 @@ Debug 发布 → 云端数据 → 线上可见 ## 十、风险与应对 -| 风险 | 影响 | 应对 | -|------|------|------| -| **误操作发布测试数据** | 污染线上列表 | 默认 `status: 'draft'`;确认面板二次确认 | -| **入口图质量不一致** | 不同设备/DPR 下渲染差异 | 统一使用开发者工具发布;导出时固定 `destWidth/destHeight` | -| **云端数据丢失** | 已发布内容消失 | `category.data.ts` 兜底;`syncFromCloud` 定期同步 | -| **安全:非授权上传** | 恶意写入数据 | 云函数白名单校验;客户端三重门控 | -| **双数据源不一致** | 本地兜底与云端数据冲突 | 云端优先,发版前 `syncFromCloud` 对齐 | -| **云存储额度** | 图片累积占用存储 | 入口图约 50~200KB/张,100 张仅 ~20MB,远低于免费额度 | +| 风险 | 影响 | 应对 | +| ---------------------- | ----------------------- | --------------------------------------------------------- | +| **误操作发布测试数据** | 污染线上列表 | 默认 `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/` | 存放入口图 | +| 现有模块 | 本方案的关联 | +| ------------------------------------------ | ------------------------------------------------------ | +| `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/` | 存放入口图 | diff --git a/docs/小程序云开发方案.md b/docs/小程序云开发方案.md index 9edb532..dfdb1cb 100644 --- a/docs/小程序云开发方案.md +++ b/docs/小程序云开发方案.md @@ -66,32 +66,35 @@ ### 3.3 集合:`worksheets`(题型配置) -| 字段 | BSON 类型 | 必填 | 说明 | -| ------------------ | --------------- | ---- | --------------------------------------------------------------- | -| `_id` | string | 是 | 主键;建议与后端一致用 cuid 或可读业务 ID。 | -| `title` | string | 是 | 标题,建议 ≤100 字符。 | -| `desc` | string | 是 | 描述,建议 ≤500 字符。 | -| `category` | string | 是 | 大类:`math` \| `chinese` \| `english` \| `puzzle` \| `craft`。 | -| `subcategory` | string | 是 | 子类,建议 ≤50 字符。 | -| `ageMin` | int | 是 | 适龄最小值(与 Prisma 一致;替代原 `ageRange` 数组)。 | -| `ageMax` | int | 是 | 适龄最大值。 | -| `difficulty` | int | 是 | 难度 1–4。 | -| `previewImage` | string | 是 | 预览图 URL/云存储 fileID,建议 ≤500 字符。 | -| `tags` | array\ | 是 | 标签列表。 | -| `isNew` | bool | 是 | 是否新品,默认 `false`。 | -| `isHot` | bool | 是 | 是否热门,默认 `false`。 | -| `sortOrder` | int | 是 | 排序权重,默认 `0`。 | -| `downloadCount` | int | 是 | 下载次数,默认 `0`。 | -| `status` | string | 是 | `active` \| `draft` \| `hidden`,默认 `active`。 | -| `template` | string | 是 | 渲染模板类型 `TemplateType`,建议 ≤30 字符。 | -| `generator` | string | 是 | 生成器类型 `GeneratorType`,建议 ≤30 字符。 | -| `generatorConfig` | object | 是 | 生成器参数(JSON 对象)。 | -| `layoutConfig` | object | 是 | 排版参数(JSON 对象)。 | -| `userConfigurable` | object \| null | 否 | 用户可调整参数定义(JSON)。 | -| `legacyPage` | string \| null | 否 | 旧页面路径,迁移过渡用,建议 ≤200 字符。 | -| `createdAt` | date | 是 | 创建时间。 | -| `updatedAt` | date | 是 | 更新时间。 | +| 字段 | BSON 类型 | 必填 | 说明 | +| ------------- | --------------- | ---- | --------------------------------------------------- | +| `_id` | string | 是 | 主键;唯一的可读业务 ID。 | +| `title` | string | 是 | 标题,建议 ≤8 字符。 | +| `subtitle` | string | 是 | 副标题,建议 ≤12 字符。 | +| `category` | string | 是 | 大类 对应categories 表中parentId=null 的分类 | +| `subcategory` | string | 是 | 子类,建议 ≤50 字符。 | +| `ageMin` | int | 是 | 适龄最小值 | +| `ageMax` | int | 是 | 适龄最大值 | +| `grade` | int | 是 | 年级(-4=托班,-3=小班 ... 0=幼小衔接,1=一年级..) | +| `difficulty` | int | 是 | 难度 1-4(1=入门,2=基础,3=进阶,4=挑战) | +| `previewImg` | string | 是 | 预览图 URL/云存储 fileID | +| `tags` | array\ | 是 | 标签列表。 | +| `isNew` | bool | 是 | 是否新品,默认 `false`。 | +| `isHot` | bool | 是 | 是否热门,默认 `false`。 | +| `sortOrder` | int | 是 | 排序权重,默认 `0`。 | +| `downloads` | int | 是 | 下载次数,默认 `0`。 | +| `likes` | int | 是 | 收藏此时,默认 `0`。 | +| `status` | string | 是 | `active` \| `draft` \| `hidden`,默认 `draft`。 | +| `createdAt` | date | 是 | 创建时间。 | +| `updatedAt` | date | 是 | 更新时间。 | +后续可能需要使用的字段,现在先不用 +| `template` | string | 是 | 渲染模板类型 `TemplateType`,建议 ≤30 字符。 | +| `generator` | string | 是 | 生成器类型 `GeneratorType`,建议 ≤30 字符。 | +| `generatorConfig` | object | 是 | 生成器参数(JSON 对象)。 | +| `layoutConfig` | object | 是 | 排版参数(JSON 对象)。 | +| `userConfigurable` | object \| null | 否 | 用户可调整参数定义(JSON)。 | +| `legacyPage` | string \| null | 否 | 旧页面路径,迁移过渡用,建议 ≤200 字符。 | **索引建议** | 索引键 | 类型 | 说明 | @@ -122,16 +125,16 @@ 对应 Prisma `User`(`@@map("users")`)。**`openid` / `unionid` 与 `_id` 的取舍、`unionid` 写入方式**见 §3.2。 -| 字段 | BSON 类型 | 必填 | 说明 | -| ---------------- | -------------- | ---- | -------------------------------------------------------------------- | +| 字段 | BSON 类型 | 必填 | 说明 | +| ---------------- | -------------- | ---- | ----------------------------------------------------------------------------------------------------- | | `_id` | string | 是 | 主键;可与 Prisma `User.id` 一样使用 cuid,也可自定义为其它字符串,团队内与用户档案查询方式统一即可。 | -| `openid` | string | 是 | 当前小程序下微信用户标识;与 Prisma 一致建议 ≤100 字符;**业务唯一**,索引见下表。 | -| `unionid` | string \| null | 否 | 开放平台下跨应用用户标识;建议 ≤100 字符;未绑开放平台或未返回时为空。 | -| `nickName` | string \| null | 否 | 用户昵称;建议 ≤50 字符。 | -| `avatarUrl` | string \| null | 否 | 头像 URL 或云存储 fileID;建议 ≤500 字符。 | -| `totalDownloads` | int | 是 | 累计下载次数,默认 `0`。 | -| `createdAt` | date | 是 | 首次创建时间。 | -| `lastActiveAt` | date | 是 | 最近一次活跃时间(登录、打开小程序、写库等策略由实现约定)。 | +| `openid` | string | 是 | 当前小程序下微信用户标识;与 Prisma 一致建议 ≤100 字符;**业务唯一**,索引见下表。 | +| `unionid` | string \| null | 否 | 开放平台下跨应用用户标识;建议 ≤100 字符;未绑开放平台或未返回时为空。 | +| `nickName` | string \| null | 否 | 用户昵称;建议 ≤50 字符。 | +| `avatarUrl` | string \| null | 否 | 头像 URL 或云存储 fileID;建议 ≤500 字符。 | +| `totalDownloads` | int | 是 | 累计下载次数,默认 `0`。 | +| `createdAt` | date | 是 | 首次创建时间。 | +| `lastActiveAt` | date | 是 | 最近一次活跃时间(登录、打开小程序、写库等策略由实现约定)。 | **索引建议** diff --git a/miniprogram/base/pageMixin.ts b/miniprogram/base/pageMixin.ts index 338c1fc..668f3d0 100644 --- a/miniprogram/base/pageMixin.ts +++ b/miniprogram/base/pageMixin.ts @@ -3,6 +3,12 @@ import { downloadPrint } from '../utils/downloadPrint'; import { BaseDrawService } from '../core/draw/baseDraw'; import tracker from '../utils/tracker'; import { defaultShareConfig } from '../config/config'; +import { + buildWorksheetPreviewCloudPath, + isDebugPublishEnabled, + type DebugPublishConfirmDetail, + type DebugPublishMeta, +} from '../utils/debugPublish'; /** * Canvas 相关的页面实例属性 @@ -17,6 +23,8 @@ export interface PageCanvasInstance { setData(data: any, callback?: () => void): void; route: string; getShareOptions(): ShareOptions; + selectComponent(selector: string): any; + getPublishMeta?(): DebugPublishMeta; } /** @@ -34,6 +42,10 @@ export interface CanvasDataState { data: any; setData(data: any, callback?: () => void): void; route: string; + isDevEnv?: boolean; + debugPublishVisible?: boolean; + debugPublishLoading?: boolean; + debugPublishMeta?: DebugPublishMeta | null; } /** @@ -275,6 +287,159 @@ export function getPageCommonMethods(config: PageCommonMethodsConfig = {}) { } }, + syncDebugPublishEnv(this: PageCanvasInstance) { + this.setData({ isDevEnv: isDebugPublishEnabled() }); + }, + + onOpenDebugPublish(this: PageCanvasInstance) { + if (!isDebugPublishEnabled()) return; + if (typeof this.getPublishMeta !== 'function') { + wx.showToast({ + title: '页面未实现发布元数据', + icon: 'none', + }); + return; + } + + try { + const meta = this.getPublishMeta(); + this.setData({ + isDevEnv: true, + debugPublishVisible: true, + debugPublishMeta: meta, + }); + } catch (error) { + wx.showToast({ + title: + error instanceof Error + ? error.message + : '准备发布数据失败', + icon: 'none', + }); + } + }, + + onCloseDebugPublish(this: PageCanvasInstance) { + this.setData({ debugPublishVisible: false }); + }, + + /** + * 确认发布 + */ + async onConfirmDebugPublish( + this: PageCanvasInstance, + e: WechatMiniprogram.CustomEvent, + ) { + const previewCard = this.selectComponent('#previewCard'); + const debugPublishTools = + this.selectComponent('#debugPublishTools'); + const baseMeta = + this.data.debugPublishMeta || + (typeof this.getPublishMeta === 'function' + ? this.getPublishMeta() + : null); + + if (!previewCard?.exportToTempFile) { + wx.showToast({ + title: '预览组件不可用', + icon: 'none', + }); + return; + } + + if (!debugPublishTools?.processImage) { + wx.showToast({ + title: '发布工具不可用', + icon: 'none', + }); + return; + } + + if (!baseMeta) { + wx.showToast({ + title: '缺少发布元数据', + icon: 'none', + }); + return; + } + + const detail = e.detail; + const publishMeta: DebugPublishMeta = { + ...baseMeta, + title: detail.meta.title, + subtitle: detail.meta.subtitle, + tags: detail.meta.tags, + status: detail.meta.status, + }; + + this.setData({ debugPublishLoading: true }); + wx.showLoading({ title: '发布中...' }); + + try { + const sourcePath = await previewCard.exportToTempFile({ + fileType: 'jpg', + quality: 1, + }); + + console.log('sourcePath', sourcePath); + + const processed = await debugPublishTools.processImage({ + sourcePath, + settings: detail.settings, + }); + console.log('processed', processed); + + const cloudPath = buildWorksheetPreviewCloudPath( + publishMeta.category, + publishMeta.id, + ); + console.log('cloudPath', cloudPath); + const uploadRes = await wx.cloud.uploadFile({ + cloudPath, + filePath: processed.tempFilePath, + }); + console.log('uploadRes', uploadRes); + + const cloudCall = (await wx.cloud.callFunction({ + name: 'worksheetsPublish', + data: { + ...publishMeta, + previewImg: uploadRes.fileID, + }, + })) as { + result?: { success?: boolean; message?: string }; + }; + console.log('cloudCall', cloudCall); + if (!cloudCall.result?.success) { + throw new Error( + cloudCall.result?.message || '云端写入失败', + ); + } + console.log('success'); + this.setData({ + debugPublishVisible: false, + debugPublishMeta: { + ...publishMeta, + previewImg: uploadRes.fileID, + }, + }); + + wx.showToast({ + title: `发布成功 ${Math.round(processed.size / 1024)}KB`, + icon: 'success', + }); + } catch (error) { + console.log('onConfirmDebugPublish error', error); + wx.showToast({ + title: error instanceof Error ? error.message : '发布失败', + icon: 'none', + }); + } finally { + wx.hideLoading(); + this.setData({ debugPublishLoading: false }); + } + }, + /** * 初始化页面信息 * diff --git a/miniprogram/components3.0/debug-publish-tools/debug-publish-tools.json b/miniprogram/components3.0/debug-publish-tools/debug-publish-tools.json new file mode 100644 index 0000000..d46687f --- /dev/null +++ b/miniprogram/components3.0/debug-publish-tools/debug-publish-tools.json @@ -0,0 +1,7 @@ +{ + "component": true, + "styleIsolation": "isolated", + "usingComponents": { + "van-popup": "@vant/weapp/popup/index" + } +} diff --git a/miniprogram/components3.0/debug-publish-tools/debug-publish-tools.less b/miniprogram/components3.0/debug-publish-tools/debug-publish-tools.less new file mode 100644 index 0000000..6194576 --- /dev/null +++ b/miniprogram/components3.0/debug-publish-tools/debug-publish-tools.less @@ -0,0 +1,196 @@ +.debug-publish-tools { + .debug-publish-tools__fab { + position: fixed; + right: 24rpx; + bottom: calc(136rpx + env(safe-area-inset-bottom)); + z-index: 40; + display: inline-flex; + align-items: center; + gap: 10rpx; + padding: 20rpx 28rpx; + border-radius: 999rpx; + background: rgba(50, 46, 37, 0.92); + box-shadow: 0 12rpx 32rpx rgba(0, 0, 0, 0.18); + } + + .debug-publish-tools__fab--hover { + transform: scale(0.98); + opacity: 0.92; + } + + .debug-publish-tools__fab-icon { + font-size: 28rpx; + line-height: 1; + } + + .debug-publish-tools__fab-text { + font-size: 24rpx; + font-weight: 700; + color: #ffffff; + line-height: 1; + } + + .debug-publish-tools__popup { + width: 680rpx; + max-height: 82vh; + overflow: hidden; + } + + .debug-publish-tools__content { + max-height: 82vh; + padding: 56rpx 32rpx 32rpx; + box-sizing: border-box; + overflow-y: auto; + } + + .debug-publish-tools__title { + display: block; + margin-bottom: 28rpx; + font-size: 36rpx; + font-weight: 800; + color: #322e25; + text-align: center; + } + + .debug-publish-tools__section + .debug-publish-tools__section { + margin-top: 28rpx; + } + + .debug-publish-tools__section-title { + display: block; + margin-bottom: 18rpx; + font-size: 26rpx; + font-weight: 700; + color: #7c766a; + } + + .debug-publish-tools__meta-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16rpx; + } + + .debug-publish-tools__meta-item, + .debug-publish-tools__path-box, + .debug-publish-tools__field { + padding: 18rpx 20rpx; + border-radius: 20rpx; + background: #f7f1e6; + } + + .debug-publish-tools__path-box { + margin-top: 16rpx; + } + + .debug-publish-tools__meta-label, + .debug-publish-tools__field-label { + display: block; + font-size: 22rpx; + font-weight: 700; + color: #8a8478; + } + + .debug-publish-tools__meta-value, + .debug-publish-tools__path { + display: block; + margin-top: 8rpx; + font-size: 24rpx; + color: #322e25; + line-height: 1.5; + word-break: break-all; + } + + .debug-publish-tools__field + .debug-publish-tools__field { + margin-top: 16rpx; + } + + .debug-publish-tools__input { + width: 100%; + margin-top: 12rpx; + min-height: 44rpx; + font-size: 26rpx; + color: #322e25; + } + + .debug-publish-tools__chip-row { + display: flex; + gap: 16rpx; + margin-top: 14rpx; + } + + .debug-publish-tools__chip-row--wrap { + flex-wrap: wrap; + } + + .debug-publish-tools__chip { + min-width: 120rpx; + padding: 16rpx 22rpx; + border-radius: 999rpx; + background: #ebe3d4; + font-size: 24rpx; + font-weight: 700; + color: #605b50; + text-align: center; + box-sizing: border-box; + } + + .debug-publish-tools__chip--active { + background: #f7ce00; + color: #453900; + } + + .debug-publish-tools__hint { + display: block; + margin-top: 10rpx; + font-size: 22rpx; + line-height: 1.6; + color: #8a8478; + } + + .debug-publish-tools__actions { + display: flex; + gap: 20rpx; + margin-top: 32rpx; + } + + .debug-publish-tools__btn { + flex: 1; + height: 88rpx; + line-height: 88rpx; + border-radius: 999rpx; + font-size: 28rpx; + font-weight: 700; + border: none; + padding: 0; + } + + .debug-publish-tools__btn::after { + border: none; + } + + .debug-publish-tools__btn--ghost { + background: #f0eadf; + color: #605b50; + } + + .debug-publish-tools__btn--primary { + background: linear-gradient(135deg, #f7ee47 0%, #f0ca08 100%); + color: #453900; + } + + .debug-publish-tools__processor { + position: fixed; + left: -9999px; + top: -9999px; + width: 2px; + height: 2px; + overflow: hidden; + opacity: 0; + pointer-events: none; + } + + .debug-publish-tools__processor-canvas { + width: 2px; + height: 2px; + } +} diff --git a/miniprogram/components3.0/debug-publish-tools/debug-publish-tools.ts b/miniprogram/components3.0/debug-publish-tools/debug-publish-tools.ts new file mode 100644 index 0000000..874eb33 --- /dev/null +++ b/miniprogram/components3.0/debug-publish-tools/debug-publish-tools.ts @@ -0,0 +1,350 @@ +import { + DEBUG_PUBLISH_DEFAULT_QUALITY, + DEBUG_PUBLISH_DEFAULT_WIDTH, + DEBUG_PUBLISH_MAX_SIZE_KB, + clampPublishQuality, + clampPublishWidth, + getCropRatios, + normalizeTagsInput, + type DebugCropMode, + type DebugProcessImageParams, + type DebugProcessImageResult, + type DebugPublishConfirmDetail, + type DebugPublishMeta, +} from '../../utils/debugPublish'; + +/** + * Debug 发布工具组件。 + * + * 组件只负责两件事: + * 1. 展示调试发布的 UI(悬浮按钮 + 弹窗表单) + * 2. 用隐藏 canvas 处理预览图的裁剪与压缩 + * + * 真正的上传云存储、调用云函数写库仍由页面层处理, + * 这样同一套组件可以复用到多个绘制页面。 + */ +Component({ + properties: { + enabled: { + type: Boolean, + value: false, + }, + visible: { + type: Boolean, + value: false, + }, + loading: { + type: Boolean, + value: false, + }, + meta: { + type: Object, + value: {}, + }, + }, + + data: { + formTitle: '', + formSubtitle: '', + formTags: '', + formStatus: 'draft', + formCropMode: 'header-footer', + formQuality: DEBUG_PUBLISH_DEFAULT_QUALITY, + formWidth: DEBUG_PUBLISH_DEFAULT_WIDTH, + maxSizeKB: DEBUG_PUBLISH_MAX_SIZE_KB, + }, + + observers: { + visible(visible: boolean) { + if (visible) { + this.syncFormState( + (this.properties as any).meta as DebugPublishMeta | null, + ); + } + }, + meta(meta: DebugPublishMeta | null) { + if ((this.properties as any).visible) { + this.syncFormState(meta); + } + }, + }, + + lifetimes: { + ready() { + this.initProcessorCanvas(); + }, + }, + + methods: { + /** 打开弹窗时,用当前题型元数据重置表单。 */ + syncFormState(meta: DebugPublishMeta | null) { + this.setData({ + formTitle: meta?.title ?? '', + formSubtitle: meta?.subtitle ?? '', + formTags: meta?.tags?.join(', ') ?? '', + formStatus: meta?.status ?? 'draft', + formCropMode: 'header-footer', + formQuality: DEBUG_PUBLISH_DEFAULT_QUALITY, + formWidth: DEBUG_PUBLISH_DEFAULT_WIDTH, + }); + }, + + /** 点击悬浮发布按钮。挂载时机已由页面层控制,这里无需再二次判断 enabled。 */ + onOpen() { + this.triggerEvent('open'); + }, + + /** 关闭发布弹窗。 */ + onClose() { + this.triggerEvent('close'); + }, + + onTitleInput(e: WechatMiniprogram.Input) { + this.setData({ formTitle: (e.detail.value ?? '').trim() }); + }, + + onSubtitleInput(e: WechatMiniprogram.Input) { + this.setData({ formSubtitle: (e.detail.value ?? '').trim() }); + }, + + onTagsInput(e: WechatMiniprogram.Input) { + this.setData({ formTags: e.detail.value ?? '' }); + }, + + onSelectStatus(e: WechatMiniprogram.TouchEvent) { + const value = e.currentTarget.dataset.value as string; + if (!value) return; + this.setData({ formStatus: value }); + }, + + onSelectCropMode(e: WechatMiniprogram.TouchEvent) { + const value = e.currentTarget.dataset.value as DebugCropMode; + if (!value) return; + this.setData({ formCropMode: value }); + }, + + onQualityChange(e: WechatMiniprogram.CustomEvent) { + this.setData({ + formQuality: clampPublishQuality(Number(e.detail.value)), + }); + }, + + onWidthChange(e: WechatMiniprogram.CustomEvent) { + this.setData({ + formWidth: clampPublishWidth(Number(e.detail.value)), + }); + }, + + /** 校验弹窗输入,并把结果回传给页面层。 */ + onConfirm() { + const meta = (this.properties as any) + .meta as DebugPublishMeta | null; + if (!meta) { + wx.showToast({ + title: '缺少发布元数据', + icon: 'none', + }); + return; + } + + const title = this.data.formTitle.trim(); + const subtitle = this.data.formSubtitle.trim(); + if (!title || !subtitle) { + wx.showToast({ + title: '标题和副标题不能为空', + icon: 'none', + }); + return; + } + + const detail: DebugPublishConfirmDetail = { + meta: { + title, + subtitle, + tags: normalizeTagsInput(this.data.formTags), + status: this.data.formStatus as DebugPublishMeta['status'], + }, + settings: { + cropMode: this.data.formCropMode as DebugCropMode, + quality: clampPublishQuality(this.data.formQuality), + width: clampPublishWidth(this.data.formWidth), + maxSizeKB: this.data.maxSizeKB, + }, + }; + + this.triggerEvent('confirm', detail); + }, + + /** 初始化隐藏处理 canvas,后续所有裁剪压缩都在这里完成。 */ + initProcessorCanvas() { + if ((this as any)._processorCanvasReadyPromise) return; + + (this as any)._processorCanvasReadyPromise = new Promise( + (resolve, reject) => { + this.createSelectorQuery() + .select('#processorCanvas') + .fields({ node: true, size: true }) + .exec((res) => { + if (!res[0] || !res[0].node) { + reject(new Error('处理画布初始化失败')); + return; + } + + (this as any)._processorCanvas = res[0].node; + (this as any)._processorCtx = ( + this as any + )._processorCanvas.getContext('2d'); + resolve(); + }); + }, + ); + }, + + /** 确保隐藏 canvas 已经 ready。 */ + async ensureProcessorCanvasReady() { + if (!(this as any)._processorCanvasReadyPromise) { + this.initProcessorCanvas(); + } + await (this as any)._processorCanvasReadyPromise; + }, + + /** 用当前 canvas 上下文加载临时图片文件。 */ + loadImage( + canvas: WechatMiniprogram.Canvas, + src: string, + ): Promise { + return new Promise((resolve, reject) => { + const image = canvas.createImage(); + image.onload = () => resolve(image); + image.onerror = reject; + image.src = src; + }); + }, + + /** 将处理后的 canvas 导出成 jpg 临时文件。 */ + exportCanvasToTempFile( + canvas: WechatMiniprogram.Canvas, + width: number, + height: number, + quality: number, + ): Promise { + return new Promise((resolve, reject) => { + wx.canvasToTempFilePath({ + canvas, + fileType: 'jpg', + quality, + destWidth: width, + destHeight: height, + success: (res) => resolve(res.tempFilePath), + fail: (err) => reject(err), + }); + }); + }, + + /** + * 读取导出文件大小,用于控制最大体积。 + * 开发者工具中 canvasToTempFilePath 可能返回 HTTP URL, + * wx.getFileSystemManager().getFileInfo() 不支持 HTTP 路径, + * 此时返回 -1 表示无法获取大小,调用方应跳过体积检测。 + */ + getFileSize(filePath: string): Promise { + return new Promise((resolve) => { + wx.getFileSystemManager().getFileInfo({ + filePath, + success: (res) => resolve(res.size), + fail: () => resolve(-1), + }); + }); + }, + + /** + * 裁剪并压缩入口图。 + * + * 流程: + * 1. 加载 preview-card 导出的完整 A4 图片 + * 2. 按裁剪模式裁掉页眉 / 页脚 + * 3. 按目标宽度重绘到隐藏 canvas + * 4. 若体积超出阈值,则逐步降低 jpg quality + */ + async processImage( + params: DebugProcessImageParams, + ): Promise { + await this.ensureProcessorCanvasReady(); + + const canvas = (this as any) + ._processorCanvas as WechatMiniprogram.Canvas; + const ctx = (this as any)._processorCtx as RenderingContext; + if (!canvas || !ctx) { + throw new Error('处理画布不可用'); + } + + const image = await this.loadImage(canvas, params.sourcePath); + const { topRatio, bottomRatio } = getCropRatios( + params.settings.cropMode, + ); + const sourceWidth = image.width; + const sourceHeight = image.height; + const cropTop = Math.round(sourceHeight * topRatio); + const cropBottom = Math.round(sourceHeight * bottomRatio); + const extractHeight = sourceHeight - cropTop - cropBottom; + + if (extractHeight <= 0) { + throw new Error('裁剪区域无效'); + } + + const targetWidth = clampPublishWidth(params.settings.width); + const targetHeight = Math.round( + (extractHeight * targetWidth) / sourceWidth, + ); + + canvas.width = targetWidth; + canvas.height = targetHeight; + ctx.clearRect(0, 0, targetWidth, targetHeight); + ctx.drawImage( + image, + 0, + cropTop, + sourceWidth, + extractHeight, + 0, + 0, + targetWidth, + targetHeight, + ); + + let currentQuality = + clampPublishQuality(params.settings.quality) / 100; + let outputPath = ''; + let outputSize = 0; + + while (currentQuality >= 0.35) { + outputPath = await this.exportCanvasToTempFile( + canvas, + targetWidth, + targetHeight, + currentQuality, + ); + outputSize = await this.getFileSize(outputPath); + + // -1 表示无法获取大小(开发者工具 HTTP 路径),直接跳过体积循环 + if ( + outputSize < 0 || + outputSize <= params.settings.maxSizeKB * 1024 + ) { + break; + } + + currentQuality = + Math.round((currentQuality - 0.05) * 100) / 100; + } + + return { + tempFilePath: outputPath, + size: outputSize, + width: targetWidth, + height: targetHeight, + quality: Math.round(currentQuality * 100), + }; + }, + }, +}); diff --git a/miniprogram/components3.0/debug-publish-tools/debug-publish-tools.wxml b/miniprogram/components3.0/debug-publish-tools/debug-publish-tools.wxml new file mode 100644 index 0000000..56db7f6 --- /dev/null +++ b/miniprogram/components3.0/debug-publish-tools/debug-publish-tools.wxml @@ -0,0 +1,183 @@ + + + 📤 + 发布到云端 + + + + + Debug 发布 + + + 基础信息 + + + ID + {{meta.id}} + + + 分类 + {{meta.category}} + + + 年龄 + + {{meta.ageMin}}-{{meta.ageMax}} 岁 + + + + 难度 + {{meta.difficulty}} + + + + 路径 + {{meta.path}} + + + + + 资料信息 + + 标题 + + + + 副标题 + + + + 标签 + + + + 状态 + + + draft + + + active + + + hidden + + + + + + + 图片处理 + + 裁剪模式 + + + 裁剪头尾 + + + 仅裁头部 + + + 不裁剪 + + + + + + 压缩质量 {{formQuality}} + + + + + + 输出宽度 {{formWidth}}px + + + + + 默认按文档规则裁掉页眉和页脚,并尽量压缩到 {{maxSizeKB}}KB 内。 + + + + + + + + + + + + + + diff --git a/miniprogram/constants/focusFunctions.ts b/miniprogram/constants/focusFunctions.ts index 207d2a7..074d867 100644 --- a/miniprogram/constants/focusFunctions.ts +++ b/miniprogram/constants/focusFunctions.ts @@ -1,8 +1,119 @@ -/** - * 桥接文件 - 保持向后兼容 - * 实际数据已迁移到 core/data/worksheets.ts - */ -export { - FOCUS_FUNCTION_TYPES, - type FocusFunctionType, -} from '../core/data/worksheets'; +export interface FocusFunctionType { + id: string; + page?: string; + title: string; + desc: string; + icon: string; + mode?: string; + img?: string; +} + +export const FOCUS_FUNCTION_TYPES: FocusFunctionType[] = [ + { + id: 'color-shape-match', + page: 'focusDraw', + title: '根据颜色画图形', + desc: '根据颜色画出对应图形', + icon: '🎯', + img: '/assets/focusEntrance/color-shape-match.png', + }, + { + id: 'shape-symbol', + page: 'focusDraw', + title: '图形符号配对', + desc: '根据图形画对应符号', + icon: '🔗', + img: '/assets/focusEntrance/shape-symbol.png', + }, + { + id: 'shape-recognition', + page: 'focusDraw', + title: '识别形状', + desc: '识别形状,涂一涂', + icon: '🔍', + img: '/assets/focusEntrance/shape-recognition.png', + }, + { + id: 'position-coloring', + page: 'focusDraw', + title: '方位涂涂乐', + desc: '观察位置,在方格中涂色', + icon: '📍', + img: '/assets/focusEntrance/position-coloring.png', + }, + { + id: 'color-pattern', + page: 'focusDraw', + title: '颜色找规律', + desc: '观察颜色规律,在空白图形中涂色', + icon: '🎨', + img: '/assets/focusEntrance/color-pattern.png', + }, + { + id: 'match-connect', + page: 'focusDraw', + title: '连连看', + desc: '根据物品连一连', + icon: '🔗', + img: '/assets/focusEntrance/match-connect.png', + }, + { + id: 'line-recognition', + page: 'focusDraw', + title: '线条识别', + desc: '认识不同线条,画出颜色对应的线条', + icon: '📏', + img: '/assets/focusEntrance/line-recognition.png', + }, + { + id: 'grid-reasoning', + page: 'focusDraw', + title: '方格推理', + desc: '推理出合并方格并连线', + icon: '🧩', + img: '/assets/focusEntrance/grid-reasoning.png', + }, + { + id: 'code-connect', + page: 'focusDraw', + title: '译码连线', + desc: '按数字顺序将数字对应颜色连线', + icon: '🔢', + img: '/assets/focusEntrance/code-connect.png', + }, + { + id: 'dot-connect', + page: 'focusDraw', + title: '数字点连线', + desc: '按数字顺序连点成图', + icon: '🔗', + img: '/assets/focusEntrance/dot-connect.png', + }, + { + id: 'grid-drawing-3x3', + page: 'focusDraw', + title: '格子仿画 3×3', + desc: '简单有趣,培养专注力', + icon: '🎨', + mode: '3x3', + img: '/assets/focusEntrance/grid-drawing-3x3.png', + }, + { + id: 'grid-drawing-5x5', + page: 'focusDraw', + title: '格子仿画 5×5', + desc: '创意挑战,提升观察力', + icon: '🎨', + mode: '5x5', + img: '/assets/focusEntrance/grid-drawing-5x5.png', + }, + { + id: 'grid-drawing-7x7', + page: 'focusDraw', + title: '格子仿画 7×7', + desc: '大师挑战,锻炼耐心', + icon: '🎨', + mode: '7x7', + img: '/assets/focusEntrance/grid-drawing-7x7.png', + }, +]; diff --git a/miniprogram/constants/mathFunctions.ts b/miniprogram/constants/mathFunctions.ts index 544b898..c506cde 100644 --- a/miniprogram/constants/mathFunctions.ts +++ b/miniprogram/constants/mathFunctions.ts @@ -1,5 +1,217 @@ -/** - * 桥接文件 - 保持向后兼容 - * 实际数据已迁移到 core/data/worksheets.ts - */ -export { MATH_FUNCTION_TYPES, type MathFunctionType } from '../core/data/worksheets'; +export interface MathFunctionType { + id: string; + page?: string; + title: string; + desc: string; + icon?: string; + img?: string; +} + +export const MATH_FUNCTION_TYPES: MathFunctionType[] = [ + { + id: 'number-find', + page: 'mathDraw', + title: '找数字,涂一涂', + desc: '在数字方阵中找出目标数字并涂色', + img: '/assets/mathEntrance/number-find.png', + }, + { + id: 'number-write', + page: 'mathDraw', + title: '看数字,写一写', + desc: '按笔画顺序练习书写数字', + img: '/assets/mathEntrance/number-write.png', + }, + { + id: 'number-coloring', + page: 'mathDraw', + title: '按数字,涂颜色', + desc: '按指定数字给对应圆圈涂色', + icon: '🎨', + img: '/assets/mathEntrance/number-coloring.png', + }, + { + id: 'counting-matching', + page: 'mathDraw', + title: '数一数,连一连', + desc: '连线配对数字和对应数量图形', + icon: '🔗', + img: '/assets/mathEntrance/count-match.png', + }, + { + id: 'number-object-match', + page: 'mathDraw', + title: '数物连线', + desc: '连线相同数量的物品和数字', + icon: '🔗', + img: '/assets/mathEntrance/number-object-match.png', + }, + { + id: 'number-object-fill', + page: 'mathDraw', + title: '数物填写', + desc: '数物品数量,填写对应数字', + icon: '✏️', + img: '/assets/mathEntrance/number-object-fill.png', + }, + { + id: 'counting-select', + page: 'mathDraw', + title: '数一数,选一选', + desc: '数出物品数量,圈出正确答案', + icon: '✓', + img: '/assets/mathEntrance/counting-select.png', + }, + { + id: 'counting-fill', + page: 'mathDraw', + title: '数一数,填一填', + desc: '数出物品数量,填写数字', + icon: '✏️', + img: '/assets/mathEntrance/counting-fill.png', + }, + { + id: 'compare', + page: 'mathDraw', + title: '数一数,比大小', + desc: '比较数量,填入 ><=', + icon: '⚖️', + img: '/assets/mathEntrance/compare.png', + }, + { + id: 'number-sort', + page: 'mathDraw', + title: '数字排序', + desc: '写出正确的数字顺序', + icon: '🔢', + img: '/assets/mathEntrance/number-sort.png', + }, + { + id: 'missing-number', + page: 'mathDraw', + title: '填上缺少的数字', + desc: '在数列中找出并填写缺失数字', + icon: '❓', + img: '/assets/mathEntrance/missing-number.png', + }, + { + id: 'number-decompose', + page: 'mathDraw', + title: '10以内数的分与合', + desc: '把数字分一分,合一合', + icon: '🔢', + img: '/assets/mathEntrance/number-decompose.png', + }, + { + id: 'number-decompose-20', + page: 'mathDraw', + title: '20以内数的分与合', + desc: '把数字分一分,合一合', + icon: '🔢', + img: '/assets/mathEntrance/number-decompose-20.png', + }, + { + id: 'one-digit-addition', + page: 'mathDraw', + title: '一位数加法', + desc: '通过圆点学习一位数加法运算', + icon: '➕', + img: '/assets/mathEntrance/one-digit-addition.png', + }, + { + id: 'addition-5', + page: 'mathDraw', + title: '5以内加法', + desc: '图形化展示 5 以内加法', + icon: '➕', + img: '/assets/mathEntrance/addition-5.png', + }, + { + id: 'addition-10', + page: 'mathDraw', + title: '10以内加法', + desc: '图形化展示 10 以内加法', + icon: '➕', + img: '/assets/mathEntrance/addition-10.png', + }, + { + id: 'subtraction-10', + page: 'mathDraw', + title: '10以内减法', + desc: '图形化展示 10 以内减法', + icon: '➖', + img: '/assets/mathEntrance/subtraction-10.png', + }, + { + id: 'addition-subtraction-10', + page: 'mathDraw', + title: '10以内加减法', + desc: '加减法混合运算', + icon: '±', + img: '/assets/mathEntrance/addition-subtraction-10.png', + }, + { + id: 'make-ten', + page: 'mathDraw', + title: '凑十法练习', + desc: '20 以内进位加法', + icon: '➕', + img: '/assets/mathEntrance/make-ten.png', + }, + { + id: 'break-ten', + page: 'mathDraw', + title: '破十法练习', + desc: '20 以内退位减法', + icon: '➖', + img: '/assets/mathEntrance/break-ten.png', + }, + { + id: 'flat-ten', + page: 'mathDraw', + title: '平十法练习', + desc: '20 以内退位减法', + icon: '➖', + img: '/assets/mathEntrance/flat-ten.png', + }, + { + id: 'borrow-ten', + page: 'mathDraw', + title: '借十法练习', + desc: '20 以上退位减法', + icon: '➖', + img: '/assets/mathEntrance/borrow-ten.png', + }, + { + id: 'practice-addition', + page: 'mathDraw', + title: '加法运算', + desc: '10/20/50/100 以内加法', + icon: '➕', + img: '/assets/mathEntrance/practice-addition.png', + }, + { + id: 'practice-subtraction', + page: 'mathDraw', + title: '减法运算', + desc: '10/20/50/100 以内减法', + icon: '➖', + img: '/assets/mathEntrance/practice-subtraction.png', + }, + { + id: 'practice-mixed', + page: 'mathDraw', + title: '混合运算', + desc: '10/20/50/100 以内加减法混合', + icon: '±', + img: '/assets/mathEntrance/practice-mixed.png', + }, + { + id: 'multiplication-table', + page: 'mathDraw', + title: '九九乘法表', + desc: '学习九九乘法口诀', + icon: '✖️', + img: '/assets/mathEntrance/multiplication-table.png', + }, +]; diff --git a/miniprogram/core/data/worksheets.ts b/miniprogram/core/data/worksheets.ts deleted file mode 100644 index 01703af..0000000 --- a/miniprogram/core/data/worksheets.ts +++ /dev/null @@ -1,545 +0,0 @@ -import type { - GeneratorType, - LayoutConfig, - TemplateType, - WorksheetDefinition, - WorksheetType, -} from '../models/worksheet'; -import { difficultyToStars } from './difficulty'; - -/** - * MathFunctionType - 保持向后兼容(mathPageMixin / mathIndex) - */ -export interface MathFunctionType { - id: string; - page?: string; - title: string; - desc: string; - icon?: string; - img?: string; -} - -/** - * FocusFunctionType - 保持向后兼容(focusPageMixin / focusIndex) - */ -export interface FocusFunctionType { - id: string; - page?: string; - title: string; - desc: string; - icon: string; - mode?: string; - img?: string; -} - -interface SheetEngine { - subcategory: string; - template: TemplateType; - generator: GeneratorType; - generatorConfig?: Record; - layoutConfig?: LayoutConfig; - tags?: string[]; - sortOrder: number; -} - -function legacyPath(ui: WorksheetType): string | null { - if (!ui.page) return null; - if (ui.subpackage) { - return `/${ui.subpackage}/${ui.page}/${ui.page}`; - } - return `/pages/${ui.page}/${ui.page}`; -} - -function sheet(ui: WorksheetType, engine: SheetEngine): WorksheetDefinition { - const ageMin = ui.ageRange?.[0] ?? 3; - const ageMax = ui.ageRange?.[1] ?? 8; - return { - ...ui, - subcategory: engine.subcategory, - ageMin, - ageMax, - difficultyLevel: difficultyToStars(ui.difficulty), - previewImage: ui.img ?? '', - tags: engine.tags?.length ? engine.tags : [engine.subcategory], - isNew: false, - isHot: false, - sortOrder: engine.sortOrder, - downloadCount: 0, - status: 'active', - template: engine.template, - generator: engine.generator, - generatorConfig: engine.generatorConfig ?? {}, - layoutConfig: engine.layoutConfig ?? { showBorder: true }, - userConfigurable: null, - legacyPage: legacyPath(ui), - }; -} - -const L = { - gridEx: { showBorder: true, fontSize: 18 } satisfies LayoutConfig, - card: { showBorder: true, columns: 2, fontSize: 16 } satisfies LayoutConfig, - trace: { - showBorder: true, - showInstruction: true, - fontSize: 20, - } satisfies LayoutConfig, -}; - -// 现有功能清单 §二:数感启蒙 26 种 + §三 13 种 + §四 §五 语文 2 -// prettier-ignore -export const ALL_WORKSHEETS: WorksheetDefinition[] = [ - sheet( - { id: 'number-find', title: '找数字,涂一涂', desc: '在数字方阵中找出目标数字并涂色', category: 'math', page: 'mathDraw', subpackage: 'mathPages', img: '/assets/mathEntrance/number-find.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#fff3e0', previewText: '🔍 1 2 3' }, - { subcategory: 'number-sense', template: 'grid-coloring', generator: 'counting', sortOrder: 1, layoutConfig: { ...L.gridEx }, generatorConfig: { functionId: 'number-find' } }, - ), - sheet( - { id: 'number-write', title: '看数字,写一写', desc: '按笔画顺序练习书写数字', category: 'math', page: 'mathDraw', subpackage: 'mathPages', img: '/assets/mathEntrance/number-write.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#fff3e0', previewText: '1 2 3' }, - { subcategory: 'number-sense', template: 'tracing-writing', generator: 'counting', sortOrder: 2, layoutConfig: { ...L.trace }, generatorConfig: { functionId: 'number-write' } }, - ), - sheet( - { id: 'number-coloring', title: '按数字,涂颜色', desc: '按指定数字给对应圆圈涂色', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '🎨', img: '/assets/mathEntrance/number-coloring.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#fce4ec', previewText: '🎨' }, - { subcategory: 'number-sense', template: 'grid-coloring', generator: 'counting', sortOrder: 3, layoutConfig: { ...L.gridEx }, generatorConfig: { functionId: 'number-coloring' } }, - ), - sheet( - { id: 'counting-matching', title: '数一数,连一连', desc: '连线配对数字和对应数量图形', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '🔗', img: '/assets/mathEntrance/count-match.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e8f5e9', previewText: '3↔🍎🍎🍎' }, - { subcategory: 'counting', template: 'match-connect', generator: 'counting', sortOrder: 4, layoutConfig: { ...L.gridEx }, generatorConfig: { functionId: 'counting-matching' } }, - ), - sheet( - { id: 'number-object-match', title: '数物连线', desc: '连线相同数量的物品和数字', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '🔗', img: '/assets/mathEntrance/number-object-match.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e8f5e9', previewText: '🔗' }, - { subcategory: 'counting', template: 'match-connect', generator: 'counting', sortOrder: 5, generatorConfig: { functionId: 'number-object-match' } }, - ), - sheet( - { id: 'number-object-fill', title: '数物填写', desc: '数物品数量,填写对应数字', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '✏️', img: '/assets/mathEntrance/number-object-fill.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e8f5e9', previewText: '✏️' }, - { subcategory: 'counting', template: 'grid-exercise', generator: 'counting', sortOrder: 6, generatorConfig: { functionId: 'number-object-fill' } }, - ), - sheet( - { id: 'counting-select', title: '数一数,选一选', desc: '数出物品数量,圈出正确答案', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '✓', img: '/assets/mathEntrance/counting-select.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e8f5e9', previewText: '✓' }, - { subcategory: 'counting', template: 'grid-exercise', generator: 'counting', sortOrder: 7, generatorConfig: { functionId: 'counting-select' } }, - ), - sheet( - { id: 'counting-fill', title: '数一数,填一填', desc: '数出物品数量,填写数字', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '✏️', img: '/assets/mathEntrance/counting-fill.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e8f5e9', previewText: '✏️' }, - { subcategory: 'counting', template: 'grid-exercise', generator: 'counting', sortOrder: 8, generatorConfig: { functionId: 'counting-fill' } }, - ), - sheet( - { id: 'compare', title: '数一数,比大小', desc: '比较数量,填入 ><=', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '⚖️', img: '/assets/mathEntrance/compare.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e3f2fd', previewText: '3 ○ 5' }, - { subcategory: 'counting', template: 'grid-exercise', generator: 'comparison', sortOrder: 9, generatorConfig: { functionId: 'compare' } }, - ), - sheet( - { id: 'number-sort', title: '数字排序', desc: '写出正确的数字顺序', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '🔢', img: '/assets/mathEntrance/number-sort.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#fff3e0', previewText: '1→2→3' }, - { subcategory: 'sequence', template: 'sequence-pattern', generator: 'number-sequence', sortOrder: 10, generatorConfig: { functionId: 'number-sort' } }, - ), - sheet( - { id: 'missing-number', title: '填上缺少的数字', desc: '在数列中找出并填写缺失数字', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '❓', img: '/assets/mathEntrance/missing-number.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#fff3e0', previewText: '1_3_5' }, - { subcategory: 'sequence', template: 'sequence-pattern', generator: 'number-sequence', sortOrder: 11, generatorConfig: { functionId: 'missing-number' } }, - ), - sheet( - { id: 'number-decompose', title: '10以内数的分与合', desc: '把数字分一分,合一合', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '🔢', img: '/assets/mathEntrance/number-decompose.png', ageRange: [4, 6], difficulty: 'beginner', previewBg: '#fff3e0', previewText: '8=?+?' }, - { subcategory: 'decompose', template: 'grid-exercise', generator: 'number-decompose', sortOrder: 12, generatorConfig: { max: 10 } }, - ), - sheet( - { id: 'number-decompose-20', title: '20以内数的分与合', desc: '把数字分一分,合一合', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '🔢', img: '/assets/mathEntrance/number-decompose-20.png', ageRange: [5, 7], difficulty: 'basic', previewBg: '#fff3e0', previewText: '15=?+?' }, - { subcategory: 'decompose', template: 'grid-exercise', generator: 'number-decompose', sortOrder: 13, generatorConfig: { max: 20 } }, - ), - sheet( - { id: 'one-digit-addition', title: '一位数加法', desc: '通过圆点学习一位数加法运算', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '➕', img: '/assets/mathEntrance/one-digit-addition.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e3f2fd', previewText: '2+3=?' }, - { subcategory: 'arithmetic-visual', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 14, generatorConfig: { operators: ['+'], maxNumber: 9, showDots: true } }, - ), - sheet( - { id: 'addition-5', title: '5以内加法', desc: '图形化展示 5 以内加法', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '➕', img: '/assets/mathEntrance/addition-5.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e3f2fd', previewText: '2+1=?' }, - { subcategory: 'arithmetic-visual', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 15, generatorConfig: { operators: ['+'], maxNumber: 5, showDots: true } }, - ), - sheet( - { id: 'addition-10', title: '10以内加法', desc: '图形化展示 10 以内加法', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '➕', img: '/assets/mathEntrance/addition-10.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e3f2fd', previewText: '3+5=?' }, - { subcategory: 'arithmetic-visual', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 16, generatorConfig: { operators: ['+'], maxNumber: 10, showDots: true } }, - ), - sheet( - { id: 'subtraction-10', title: '10以内减法', desc: '图形化展示 10 以内减法', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '➖', img: '/assets/mathEntrance/subtraction-10.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e3f2fd', previewText: '8-3=?' }, - { subcategory: 'arithmetic-visual', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 17, generatorConfig: { operators: ['-'], maxNumber: 10, showDots: true } }, - ), - sheet( - { id: 'addition-subtraction-10', title: '10以内加减法', desc: '加减法混合运算', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '±', img: '/assets/mathEntrance/addition-subtraction-10.png', ageRange: [5, 7], difficulty: 'basic', previewBg: '#e3f2fd', previewText: '±' }, - { subcategory: 'arithmetic-visual', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 18, generatorConfig: { operators: ['+', '-'], maxNumber: 10, showDots: true } }, - ), - sheet( - { id: 'make-ten', title: '凑十法练习', desc: '20 以内进位加法', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '➕', img: '/assets/mathEntrance/make-ten.png', ageRange: [5, 7], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '7+?=10' }, - { subcategory: 'methods-ten', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 19, generatorConfig: { method: 'make-ten' } }, - ), - sheet( - { id: 'break-ten', title: '破十法练习', desc: '20 以内退位减法', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '➖', img: '/assets/mathEntrance/break-ten.png', ageRange: [5, 7], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '15-8=?' }, - { subcategory: 'methods-ten', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 20, generatorConfig: { method: 'break-ten' } }, - ), - sheet( - { id: 'flat-ten', title: '平十法练习', desc: '20 以内退位减法', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '➖', img: '/assets/mathEntrance/flat-ten.png', ageRange: [5, 7], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '14-6=?' }, - { subcategory: 'methods-ten', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 21, generatorConfig: { method: 'flat-ten' } }, - ), - sheet( - { id: 'borrow-ten', title: '借十法练习', desc: '20 以上退位减法', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '➖', img: '/assets/mathEntrance/borrow-ten.png', ageRange: [6, 8], difficulty: 'advanced', previewBg: '#e3f2fd', previewText: '32-8=?' }, - { subcategory: 'methods-ten', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 22, generatorConfig: { method: 'borrow-ten' } }, - ), - sheet( - { id: 'practice-addition', title: '加法运算', desc: '10/20/50/100 以内加法', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '➕', img: '/assets/mathEntrance/practice-addition.png', ageRange: [5, 8], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '25+18=?' }, - { subcategory: 'practice', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 23, generatorConfig: { operators: ['+'], preset: 'vertical' } }, - ), - sheet( - { id: 'practice-subtraction', title: '减法运算', desc: '10/20/50/100 以内减法', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '➖', img: '/assets/mathEntrance/practice-subtraction.png', ageRange: [5, 8], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '43-17=?' }, - { subcategory: 'practice', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 24, generatorConfig: { operators: ['-'], preset: 'vertical' } }, - ), - sheet( - { id: 'practice-mixed', title: '混合运算', desc: '10/20/50/100 以内加减法混合', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '±', img: '/assets/mathEntrance/practice-mixed.png', ageRange: [5, 8], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '±' }, - { subcategory: 'practice', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 25, generatorConfig: { operators: ['+', '-'], preset: 'vertical' } }, - ), - sheet( - { id: 'multiplication-table', title: '九九乘法表', desc: '学习九九乘法口诀', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '✖️', img: '/assets/mathEntrance/multiplication-table.png', ageRange: [6, 8], difficulty: 'intermediate', previewBg: '#f3e5f5', previewText: '3×4=12' }, - { subcategory: 'practice', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 26, generatorConfig: { operators: ['×'], mode: 'multiplication-table' } }, - ), - - sheet( - { id: 'color-shape-match', title: '根据颜色画图形', desc: '根据颜色画出对应图形', category: 'focus', page: 'focusDraw', subpackage: 'focusPages', icon: '🎯', img: '/assets/focusEntrance/color-shape-match.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#f3e5f5', previewText: '🎯' }, - { subcategory: 'focus-visual', template: 'grid-exercise', generator: 'shape-grid', sortOrder: 27, generatorConfig: { functionId: 'color-shape-match' } }, - ), - sheet( - { id: 'shape-symbol', title: '图形符号配对', desc: '根据图形画对应符号', category: 'focus', page: 'focusDraw', subpackage: 'focusPages', icon: '🔗', img: '/assets/focusEntrance/shape-symbol.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e8eaf6', previewText: '△→♠' }, - { subcategory: 'focus-visual', template: 'match-connect', generator: 'shape-grid', sortOrder: 28, generatorConfig: { functionId: 'shape-symbol' } }, - ), - sheet( - { id: 'shape-recognition', title: '识别形状', desc: '识别形状,涂一涂', category: 'focus', page: 'focusDraw', subpackage: 'focusPages', icon: '🔍', img: '/assets/focusEntrance/shape-recognition.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#fce4ec', previewText: '△ ○ □' }, - { subcategory: 'focus-visual', template: 'grid-coloring', generator: 'shape-grid', sortOrder: 29, generatorConfig: { functionId: 'shape-recognition' } }, - ), - sheet( - { id: 'position-coloring', title: '方位涂涂乐', desc: '观察位置,在方格中涂色', category: 'focus', page: 'focusDraw', subpackage: 'focusPages', icon: '📍', img: '/assets/focusEntrance/position-coloring.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e8f5e9', previewText: '📍' }, - { subcategory: 'focus-grid', template: 'grid-coloring', generator: 'shape-grid', sortOrder: 30, generatorConfig: { functionId: 'position-coloring' } }, - ), - sheet( - { id: 'color-pattern', title: '颜色找规律', desc: '观察颜色规律,在空白图形中涂色', category: 'focus', page: 'focusDraw', subpackage: 'focusPages', icon: '🎨', img: '/assets/focusEntrance/color-pattern.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#fff8e1', previewText: '🎨' }, - { subcategory: 'focus-grid', template: 'grid-coloring', generator: 'color-pattern', sortOrder: 31, generatorConfig: { functionId: 'color-pattern' } }, - ), - sheet( - { id: 'match-connect', title: '连连看', desc: '根据物品连一连', category: 'focus', page: 'focusDraw', subpackage: 'focusPages', icon: '🔗', img: '/assets/focusEntrance/match-connect.png', ageRange: [3, 6], difficulty: 'beginner', previewBg: '#e0f7fa', previewText: '🔗' }, - { subcategory: 'focus-connect', template: 'match-connect', generator: 'shape-grid', sortOrder: 32, generatorConfig: { functionId: 'match-connect' } }, - ), - sheet( - { id: 'line-recognition', title: '线条识别', desc: '认识不同线条,画出颜色对应的线条', category: 'focus', page: 'focusDraw', subpackage: 'focusPages', icon: '📏', img: '/assets/focusEntrance/line-recognition.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#f1f8e9', previewText: '〰️' }, - { subcategory: 'focus-visual', template: 'tracing-writing', generator: 'shape-grid', sortOrder: 33, generatorConfig: { functionId: 'line-recognition' } }, - ), - sheet( - { id: 'grid-reasoning', title: '方格推理', desc: '推理出合并方格并连线', category: 'focus', page: 'focusDraw', subpackage: 'focusPages', icon: '🧩', img: '/assets/focusEntrance/grid-reasoning.png', ageRange: [5, 7], difficulty: 'intermediate', previewBg: '#e8eaf6', previewText: '🧩' }, - { subcategory: 'focus-logic', template: 'special-graphic', generator: 'shape-grid', sortOrder: 34, generatorConfig: { functionId: 'grid-reasoning' } }, - ), - sheet( - { id: 'code-connect', title: '译码连线', desc: '按数字顺序将数字对应颜色连线', category: 'focus', page: 'focusDraw', subpackage: 'focusPages', icon: '🔢', img: '/assets/focusEntrance/code-connect.png', ageRange: [5, 7], difficulty: 'intermediate', previewBg: '#fff3e0', previewText: '🔢' }, - { subcategory: 'focus-connect', template: 'match-connect', generator: 'dot-connect', sortOrder: 35, generatorConfig: { functionId: 'code-connect' } }, - ), - sheet( - { id: 'dot-connect', title: '数字点连线', desc: '按数字顺序连点成图', category: 'focus', page: 'focusDraw', subpackage: 'focusPages', icon: '🔗', img: '/assets/focusEntrance/dot-connect.png', ageRange: [3, 6], difficulty: 'beginner', previewBg: '#e3f2fd', previewText: '1→2→3' }, - { subcategory: 'focus-connect', template: 'match-connect', generator: 'dot-connect', sortOrder: 36, generatorConfig: { functionId: 'dot-connect' } }, - ), - sheet( - { id: 'grid-drawing-3x3', title: '格子仿画 3×3', desc: '简单有趣,培养专注力', category: 'focus', page: 'focusDraw', subpackage: 'focusPages', icon: '🎨', mode: '3x3', img: '/assets/focusEntrance/grid-drawing-3x3.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#fce4ec', previewText: '▦' }, - { subcategory: 'grid-copy', template: 'grid-exercise', generator: 'shape-grid', sortOrder: 37, generatorConfig: { grid: '3x3' } }, - ), - sheet( - { id: 'grid-drawing-5x5', title: '格子仿画 5×5', desc: '创意挑战,提升观察力', category: 'focus', page: 'focusDraw', subpackage: 'focusPages', icon: '🎨', mode: '5x5', img: '/assets/focusEntrance/grid-drawing-5x5.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#fce4ec', previewText: '▦' }, - { subcategory: 'grid-copy', template: 'grid-exercise', generator: 'shape-grid', sortOrder: 38, generatorConfig: { grid: '5x5' } }, - ), - sheet( - { id: 'grid-drawing-7x7', title: '格子仿画 7×7', desc: '大师挑战,锻炼耐心', category: 'focus', page: 'focusDraw', subpackage: 'focusPages', icon: '🎨', mode: '7x7', img: '/assets/focusEntrance/grid-drawing-7x7.png', ageRange: [5, 8], difficulty: 'intermediate', previewBg: '#fce4ec', previewText: '▦' }, - { subcategory: 'grid-copy', template: 'grid-exercise', generator: 'shape-grid', sortOrder: 39, generatorConfig: { grid: '7x7' } }, - ), - - sheet( - { id: 'word-recognition', title: '识字卡', desc: '输入或选字生成涂色识字卡(grid/find 模板)', category: 'chinese', page: 'index', subpackage: '', icon: '📖', img: '', ageRange: [3, 6], difficulty: 'beginner', previewBg: '#fce4ec', previewText: '大 小' }, - { subcategory: 'literacy', template: 'card-layout', generator: 'custom', sortOrder: 40, generatorConfig: { functionId: 'word-recognition' }, layoutConfig: { ...L.card } }, - ), - sheet( - { id: 'copybook', title: '练字帖', desc: '选字生成田字格笔顺练字帖', category: 'chinese', page: 'copyBook', subpackage: '', icon: '✏️', img: '', ageRange: [4, 7], difficulty: 'basic', previewBg: '#fff8e1', previewText: '横竖撇' }, - { subcategory: 'copybook', template: 'tracing-writing', generator: 'character-tracing', sortOrder: 41, generatorConfig: { functionId: 'copybook' }, layoutConfig: { ...L.trace } }, - ), - - sheet( - { - id: 'letter-tracing-single', - title: '看图描红', - desc: '单字母配图、例句与六行四线三格描红', - category: 'english', - page: 'letterTracing', - subpackage: 'englishPages', - icon: '🔠', - img: '', - ageRange: [4, 7], - difficulty: 'beginner', - previewBg: '#e8f4ff', - previewText: 'Aa', - }, - { - subcategory: 'letter-tracing', - template: 'tracing-writing', - generator: 'letter-tracing', - sortOrder: 42, - layoutConfig: { - ...L.trace, - instructionText: '看一看,读一读,再描一描', - }, - generatorConfig: { - mode: 'letter-tracing-single', - letterCase: 'upper', - selectedLetter: 'A', - repetitions: 5, - fadePattern: 'gradient', - }, - }, - ), - sheet( - { - id: 'letter-tracing-two-column', - title: '两列练习', - desc: '左 A–M、右 N–Z,每组包含大小写', - category: 'english', - page: 'letterTracing', - subpackage: 'englishPages', - icon: '🔤', - img: '', - ageRange: [4, 7], - difficulty: 'beginner', - previewBg: '#e8f4ff', - previewText: 'A|N', - }, - { - subcategory: 'letter-tracing', - template: 'tracing-writing', - generator: 'letter-tracing', - sortOrder: 43, - layoutConfig: { - ...L.trace, - columns: 2, - instructionText: '左 A–M、右 N–Z,大小写配对描红', - }, - generatorConfig: { - mode: 'letter-tracing-two-column', - letterCase: 'both', - repetitions: 4, - fadePattern: 'gradient', - }, - }, - ), - sheet( - { - id: 'letter-tracing-upper-lower', - title: '字母总览', - desc: 'Uppercase / Lowercase 分区,每区四行(7+7+6+6)', - category: 'english', - page: 'letterTracing', - subpackage: 'englishPages', - icon: '🔡', - img: '', - ageRange: [4, 7], - difficulty: 'beginner', - previewBg: '#e8f4ff', - previewText: 'Aa Bb', - }, - { - subcategory: 'letter-tracing', - template: 'tracing-writing', - generator: 'letter-tracing', - sortOrder: 44, - layoutConfig: { - ...L.trace, - columns: 13, - instructionText: '认读全部大写与小写字母', - }, - generatorConfig: { - mode: 'letter-tracing-upper-lower', - letterCase: 'both', - repetitions: 1, - fadePattern: 'first-only', - }, - }, - ), - sheet( - { - id: 'letter-tracing-case-pairing', - title: '大小写练习', - desc: '半组字母左大写右小写,每行四个', - category: 'english', - page: 'letterTracing', - subpackage: 'englishPages', - icon: '✏️', - img: '', - ageRange: [4, 7], - difficulty: 'beginner', - previewBg: '#e8f4ff', - previewText: 'A→Z', - }, - { - subcategory: 'letter-tracing', - template: 'tracing-writing', - generator: 'letter-tracing', - sortOrder: 45, - layoutConfig: { - ...L.trace, - instructionText: '左栏大写、右栏小写,逐行对应描红', - }, - generatorConfig: { - mode: 'letter-tracing-case-pairing', - letterCase: 'both', - alphabetHalf: 'A-M', - repetitions: 4, - fadePattern: 'gradient', - }, - }, - ), - sheet( - { - id: 'letter-tracing-three', - title: '三字母精练', - desc: '每页聚焦 3 个字母,大写 + 小写深度书写', - category: 'english', - page: 'letterTracing', - subpackage: 'englishPages', - icon: '🔤', - img: '', - ageRange: [4, 7], - difficulty: 'basic', - previewBg: '#e8f4ff', - previewText: 'ABC', - }, - { - subcategory: 'letter-tracing', - template: 'tracing-writing', - generator: 'letter-tracing', - sortOrder: 46, - layoutConfig: { - ...L.trace, - instructionText: '每页 3 个字母,逐字精练', - }, - generatorConfig: { - mode: 'letter-tracing-three', - letterCase: 'both', - repetitions: 5, - fadePattern: 'gradient', - tripleLetters: ['A', 'B', 'C'], - }, - }, - ), - sheet( - { - id: 'letter-tracing-half', - title: '单字母逐行', - desc: '每行一个字母,13 字母半表逐字练习', - category: 'english', - page: 'letterTracing', - subpackage: 'englishPages', - icon: '📝', - img: '', - ageRange: [4, 7], - difficulty: 'basic', - previewBg: '#e8f4ff', - previewText: 'A–M', - }, - { - subcategory: 'letter-tracing', - template: 'tracing-writing', - generator: 'letter-tracing', - sortOrder: 47, - layoutConfig: { - ...L.trace, - instructionText: '每行一个字母,逐字反复练习', - }, - generatorConfig: { - mode: 'letter-tracing-half', - letterCase: 'both', - alphabetHalf: 'A-M', - repetitions: 5, - fadePattern: 'first-only', - }, - }, - ), - sheet( - { - id: 'letter-tracing-daily-checkin', - title: '每日打卡', - desc: '四宫格每日字母打卡,闭合四线三格练习', - category: 'english', - page: 'letterTracing', - subpackage: 'englishPages', - icon: '🗓️', - img: '', - ageRange: [4, 7], - difficulty: 'basic', - previewBg: '#e8f4ff', - previewText: 'ABCD', - }, - { - subcategory: 'letter-tracing', - template: 'tracing-writing', - generator: 'letter-tracing', - sortOrder: 48, - layoutConfig: { - ...L.trace, - instructionText: '四宫格每日打卡字母描红', - }, - generatorConfig: { - mode: 'letter-tracing-daily-checkin', - letterCase: 'both', - repetitions: 5, - fadePattern: 'first-only', - dailyLetters: ['A', 'B', 'C', 'D'], - }, - }, - ), -]; - -export const MATH_FUNCTION_TYPES: MathFunctionType[] = ALL_WORKSHEETS.filter( - (w) => w.category === 'math', -) - .sort((a, b) => a.sortOrder - b.sortOrder) - .map((w) => ({ - id: w.id, - page: w.page, - title: w.title, - desc: w.desc, - icon: w.icon, - img: w.img, - })); - -export const FOCUS_FUNCTION_TYPES: FocusFunctionType[] = ALL_WORKSHEETS.filter( - (w) => w.category === 'focus', -) - .sort((a, b) => a.sortOrder - b.sortOrder) - .map((w) => ({ - id: w.id, - page: w.page, - title: w.title, - desc: w.desc, - icon: w.icon ?? '・', - mode: w.mode, - img: w.img, - })); - -export function getWorksheetsByCategory( - category: string, -): WorksheetDefinition[] { - if (category === 'all') return ALL_WORKSHEETS; - return ALL_WORKSHEETS.filter((w) => w.category === category); -} - -export function getWorksheetsByAge( - worksheets: readonly T[], - age: number, -): T[] { - return worksheets.filter((w) => { - if (!w.ageRange) return true; - return age >= w.ageRange[0] && age <= w.ageRange[1]; - }); -} - -export function getWorksheetPath(worksheet: WorksheetType): string { - if (!worksheet.page) return ''; - if (worksheet.subpackage) { - return `/${worksheet.subpackage}/${worksheet.page}/${worksheet.page}?id=${worksheet.id}`; - } - return `/pages/${worksheet.page}/${worksheet.page}`; -} - -export function getWorksheetById(id: string): WorksheetDefinition | undefined { - return ALL_WORKSHEETS.find((w) => w.id === id); -} diff --git a/miniprogram/core/models/category.ts b/miniprogram/core/models/category.ts index 23fd7e4..2ac936c 100644 --- a/miniprogram/core/models/category.ts +++ b/miniprogram/core/models/category.ts @@ -1,3 +1,11 @@ +export type CategoryId = + | 'math' + | 'chinese' + | 'english' + | 'puzzle' + | 'craft' + | 'pinyin'; + /** Tab / 首页用的分类展示模型 */ export interface CategoryType { id: string; diff --git a/miniprogram/core/models/worksheet.ts b/miniprogram/core/models/worksheet.ts index 129bbf0..c557129 100644 --- a/miniprogram/core/models/worksheet.ts +++ b/miniprogram/core/models/worksheet.ts @@ -1,106 +1,26 @@ +import { CategoryId } from './category'; /** * 题型与模板引擎相关模型:对齐技术架构 §5.4 与云开发 worksheets 集合 §3.3 */ -/** 小程序现状:专注力单独成类;云端 category 枚举无 focus 时可映射为 puzzle + subcategory */ -export type WorksheetCategory = - | 'math' - | 'focus' - | 'chinese' - | 'english' - | 'puzzle' - | 'craft'; - -/** 云 worksheets.category 严格枚举(§3.3) */ -export type WorksheetCloudCategory = - | 'math' - | 'chinese' - | 'english' - | 'puzzle' - | 'craft'; - -export type DifficultyLevel = - | 'beginner' - | 'basic' - | 'intermediate' - | 'advanced'; - -export type WorksheetStatus = 'active' | 'draft' | 'hidden'; - -/** 排版模板(技术架构 §5.4) */ -export type TemplateType = - | 'grid-exercise' - | 'match-connect' - | 'grid-coloring' - | 'card-layout' - | 'tracing-writing' - | 'full-page-asset' - | 'sequence-pattern' - | 'special-graphic'; - -/** 数据生成器(技术架构 §5.4) */ -export type GeneratorType = - | 'arithmetic' - | 'number-sequence' - | 'number-decompose' - | 'counting' - | 'comparison' - | 'shape-grid' - | 'color-pattern' - | 'character-tracing' - | 'letter-tracing' - | 'pinyin-tracing' - | 'static-asset' - | 'maze' - | 'dot-connect' - | 'clock' - | 'custom'; - -export interface LayoutConfig { - columns?: number; - rows?: number; - fontSize?: number; - showBorder?: boolean; - showTitle?: boolean; - padding?: number; - itemSpacing?: number; - showInstruction?: boolean; - instructionText?: string; -} - -export interface UserConfigField { - key: string; - label: string; - type: 'select' | 'slider' | 'switch'; - options?: { label: string; value: unknown }[]; - min?: number; - max?: number; - defaultValue: unknown; -} - /** 与架构文档 WorksheetConfig 一致,供云端下发 / 本地内置 */ export interface WorksheetConfig { id: string; title: string; - desc: string; - category: WorksheetCloudCategory; - subcategory: string; + subtitle: string; + category: CategoryId; + subcategory?: string; ageMin: number; ageMax: number; difficulty: 1 | 2 | 3 | 4; - previewImage: string; + previewImg: string; tags: string[]; isNew: boolean; isHot: boolean; - sortOrder: number; - status: WorksheetStatus; - template: TemplateType; - generator: GeneratorType; - generatorConfig: Record; - layoutConfig: LayoutConfig; - userConfigurable?: UserConfigField[] | null; - legacyPage?: string | null; - downloadCount: number; + sortOrder?: number; + status: 'active' | 'draft' | 'hidden'; + downloads: number; + likes: number; } /** 云集合 `worksheets` 文档(§3.3,含数据库字段) */ @@ -109,79 +29,3 @@ export interface WorksheetRecord extends WorksheetConfig { createdAt: Date; updatedAt: Date; } - -/** 列表卡片 / 导航用(兼容存量页面) */ -export interface WorksheetType { - id: string; - title: string; - desc: string; - category: WorksheetCategory; - page?: string; - subpackage?: string; - icon?: string; - img?: string; - ageRange?: [number, number]; - difficulty?: DifficultyLevel; - mode?: string; - previewBg?: string; - previewText?: string; -} - -/** 内置兜底 + 模板引擎字段(本地 core/data/worksheets) */ -export interface WorksheetDefinition extends WorksheetType { - subcategory: string; - ageMin: number; - ageMax: number; - /** 1–4,对应云 worksheets.difficulty */ - difficultyLevel: 1 | 2 | 3 | 4; - previewImage: string; - tags: string[]; - isNew: boolean; - isHot: boolean; - sortOrder: number; - downloadCount: number; - status: WorksheetStatus; - template: TemplateType; - generator: GeneratorType; - generatorConfig: Record; - layoutConfig: LayoutConfig; - userConfigurable?: UserConfigField[] | null; - legacyPage?: string | null; -} - -/** 写入云库时的 category:focus 归为 puzzle,子类区分 */ -export function toCloudCategory( - category: WorksheetCategory, -): WorksheetCloudCategory { - if (category === 'focus') return 'puzzle'; - return category; -} - -/** 由内置定义生成云侧 worksheets 文档形状(不含 _id/时间,供同步层补全) */ -export function worksheetDefinitionToConfig( - def: WorksheetDefinition, -): Omit { - return { - id: def.id, - title: def.title, - desc: def.desc, - category: toCloudCategory(def.category), - subcategory: def.subcategory, - ageMin: def.ageMin, - ageMax: def.ageMax, - difficulty: def.difficultyLevel, - previewImage: def.previewImage, - tags: def.tags, - isNew: def.isNew, - isHot: def.isHot, - sortOrder: def.sortOrder, - downloadCount: def.downloadCount, - status: def.status, - template: def.template, - generator: def.generator, - generatorConfig: def.generatorConfig, - layoutConfig: def.layoutConfig, - userConfigurable: def.userConfigurable ?? null, - legacyPage: def.legacyPage ?? null, - }; -} diff --git a/miniprogram/englishPages/letterTracing/generators/letter-tracing-generator.ts b/miniprogram/englishPages/letterTracing/generators/letter-tracing-generator.ts index 464e394..87c1aa8 100644 --- a/miniprogram/englishPages/letterTracing/generators/letter-tracing-generator.ts +++ b/miniprogram/englishPages/letterTracing/generators/letter-tracing-generator.ts @@ -425,7 +425,7 @@ export function generateLetterTracing( }; } -/** 与 WorksheetDefinition.generatorConfig 合并后的运行时配置 */ +/** 与页面内模式配置合并后的运行时配置 */ export function mergeLetterTracingConfig( base: Partial, overrides: Partial, diff --git a/miniprogram/englishPages/letterTracing/letterTracing.config.ts b/miniprogram/englishPages/letterTracing/letterTracing.config.ts new file mode 100644 index 0000000..9a071ce --- /dev/null +++ b/miniprogram/englishPages/letterTracing/letterTracing.config.ts @@ -0,0 +1,123 @@ +import type { LetterTracingMode } from './generators/letter-tracing-generator'; +import type { DebugPublishMeta } from '../../utils/debugPublish'; +import { inferGradeFromAge } from '../../utils/debugPublish'; + +interface ModeDefinition { + id: LetterTracingMode; + icon: string; + title: string; + subtitle: string; + difficulty: 1 | 2 | 3 | 4; + tags: string[]; + sortOrder: number; +} + +const MODES = [ + { + id: 'letter-tracing-single', + icon: 'start-a', + title: '默认字帖', + subtitle: '配图、例句与描红', + difficulty: 1, + tags: ['字母描红', '英语启蒙', '看图描红'], + sortOrder: 42, + }, + { + id: 'letter-tracing-upper-lower', + icon: 'draw-o', + title: '基础描红', + subtitle: 'Uppercase / Lowercase 总览', + difficulty: 1, + tags: ['字母描红', '英语启蒙', '字母总览'], + sortOrder: 44, + }, + { + id: 'letter-tracing-case-pairing', + icon: 'font-size', + title: '大小写对照', + subtitle: '半组字母左大写右小写', + difficulty: 1, + tags: ['字母描红', '英语启蒙', '大小写练习'], + sortOrder: 45, + }, + { + id: 'letter-tracing-two-column', + icon: 'two-columns', + title: '两列描红', + subtitle: '左 A–M、右 N–Z 配对描红', + difficulty: 1, + tags: ['字母描红', '英语启蒙', '两列练习'], + sortOrder: 43, + }, + { + id: 'letter-tracing-half', + icon: 'square-half', + title: '13字母半表', + subtitle: '每行一个字母,13 字母半表', + difficulty: 2, + tags: ['字母描红', '英语启蒙', '单字母逐行'], + sortOrder: 47, + }, + { + id: 'letter-tracing-three', + icon: 'ABC-list', + title: '三字母精练', + subtitle: '每页聚焦 3 个字母深度书写', + difficulty: 2, + tags: ['字母描红', '英语启蒙', '三字母精练'], + sortOrder: 46, + }, + { + id: 'letter-tracing-daily-checkin', + icon: 'draw-o', + title: '每日打卡', + subtitle: '四宫格每日字母打卡练习', + difficulty: 2, + tags: ['字母描红', '英语启蒙', '每日打卡'], + sortOrder: 48, + }, +] as const satisfies ReadonlyArray; + +type Mode = (typeof MODES)[number]; + +const MODE_BY_ID = Object.fromEntries( + MODES.map((m) => [m.id, m]), +) as Record; + +/** 页面渲染用:模式选择器列表 */ +export const LETTER_TRACING_MODE_OPTIONS = MODES; + +/** 页面 pageInfoLookup 用 */ +export function getModeInfo(id: string) { + const m = MODE_BY_ID[id]; + return m ? { title: m.title, desc: m.subtitle } : undefined; +} + +/** 判断 id 是否有效 */ +export function isValidMode(id: string): boolean { + return id in MODE_BY_ID; +} + +/** 发布用:从当前 mode 生成 DebugPublishMeta */ +export function getPublishMetaByMode(id: string): DebugPublishMeta | null { + const m = MODE_BY_ID[id]; + if (!m) return null; + return { + id: m.id, + title: m.title, + subtitle: m.subtitle, + category: 'english', + subcategory: 'letter-tracing', + path: `/englishPages/letterTracing/letterTracing?id=${m.id}`, + ageMin: 4, + ageMax: 7, + grade: inferGradeFromAge(4, 7), + difficulty: m.difficulty, + previewImg: '', + tags: [...m.tags], + isNew: false, + isHot: false, + sortOrder: m.sortOrder, + status: 'draft', + }; +} diff --git a/miniprogram/englishPages/letterTracing/letterTracing.json b/miniprogram/englishPages/letterTracing/letterTracing.json index d165539..43cea61 100644 --- a/miniprogram/englishPages/letterTracing/letterTracing.json +++ b/miniprogram/englishPages/letterTracing/letterTracing.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/englishPages/letterTracing/letterTracing.ts b/miniprogram/englishPages/letterTracing/letterTracing.ts index b04ff85..35a18e2 100644 --- a/miniprogram/englishPages/letterTracing/letterTracing.ts +++ b/miniprogram/englishPages/letterTracing/letterTracing.ts @@ -1,7 +1,6 @@ import LetterTracingDraw from './draw/letterTracingDraw'; import { createPage, type CanvasDataState } from '../../base/pageMixin'; import { defaultShareConfig } from '../../config/config'; -import { getWorksheetById } from '../../core/data/worksheets'; import { generateLetterTracing, mergeLetterTracingConfig, @@ -9,6 +8,12 @@ import { type LetterTracingGeneratorConfig, type LetterTracingMode, } from './generators/letter-tracing-generator'; +import { + getModeInfo, + getPublishMetaByMode, + isValidMode, + LETTER_TRACING_MODE_OPTIONS, +} from './letterTracing.config'; import { LETTERS_UPPER, LETTERS_PAIRS } from '../shared/data/alphabet'; import { DEFAULT_LETTER_PROFILE, @@ -16,75 +21,24 @@ import { type FontProfile, } from '../shared/data/fontProfiles'; import { loadLetterFont } from '../shared/draw/drawTools'; - -/** 练习模式定义(顺序与产品文档一致),同时用于模式选择器和页面元信息 */ -const MODES = [ - { - id: 'letter-tracing-single', - icon: 'start-a', - label: '默认字帖', - desc: '配图、例句与描红', - }, - { - id: 'letter-tracing-upper-lower', - icon: 'draw-o', - // label: '字母总览', - label: '基础描红', - desc: 'Uppercase / Lowercase 总览', - }, - { - id: 'letter-tracing-case-pairing', - icon: 'font-size', - label: '大小写对照', - desc: '半组字母左大写右小写', - }, - { - id: 'letter-tracing-two-column', - icon: 'two-columns', - label: '两列描红', - desc: '左 A–M、右 N–Z 配对描红', - }, - { - id: 'letter-tracing-half', - icon: 'square-half', - label: '13字母半表', - desc: '每行一个字母,13 字母半表', - }, - { - id: 'letter-tracing-three', - icon: 'ABC-list', - label: '三字母精练', - desc: '每页聚焦 3 个字母深度书写', - }, - { - id: 'letter-tracing-daily-checkin', - icon: 'draw-o', - label: '每日打卡', - desc: '四宫格每日字母打卡练习', - }, -] as const; - -const MODE_MAP = Object.fromEntries(MODES.map((m) => [m.id, m])) as Record< - string, - (typeof MODES)[number] ->; +import type { DebugPublishMeta } from '../../utils/debugPublish'; /** 三字母精练分组:26 字母每 3 个一组 */ -const TRIPLE_GROUPS: { label: string; letters: string[] }[] = []; +const TRIPLE_GROUPS: { title: string; letters: string[] }[] = []; for (let i = 0; i < LETTERS_UPPER.length; i += 3) { const group = LETTERS_UPPER.slice(i, i + 3); - const label = + const title = group.length > 1 ? `${group[0]}–${group[group.length - 1]}` : group[0]; - TRIPLE_GROUPS.push({ label, letters: [...group] }); + TRIPLE_GROUPS.push({ title, letters: [...group] }); } /** 每日打卡分组:4 个字母一组 */ -const DAILY_GROUPS: { label: string; letters: string[] }[] = []; +const DAILY_GROUPS: { title: string; letters: string[] }[] = []; for (let i = 0; i < LETTERS_UPPER.length; i += 4) { const group = LETTERS_UPPER.slice(i, i + 4); - const label = + const title = group.length > 1 ? `${group[0]}–${group[group.length - 1]}` : group[0]; - DAILY_GROUPS.push({ label, letters: [...group] }); + DAILY_GROUPS.push({ title, letters: [...group] }); } function getDailyGroupIndex(letters?: string[]): number { @@ -93,11 +47,7 @@ function getDailyGroupIndex(letters?: string[]): number { return idx >= 0 ? idx : 0; } -/** 根据 worksheetId 查找页面元信息 */ -function pageInfoLookup(id: string) { - const m = MODE_MAP[id]; - return m ? { title: m.label, desc: m.desc } : undefined; -} +const pageInfoLookup = getModeInfo; type PageData = CanvasDataState & { worksheetId: string; @@ -110,13 +60,17 @@ type PageData = CanvasDataState & { showNextLetter: boolean; showTripleGroupPicker: boolean; showDailyGroupPicker: boolean; - tripleGroups: { label: string; letters: string[] }[]; + tripleGroups: { title: string; letters: string[] }[]; selectedTripleGroupIdx: number; - dailyGroups: { label: string; letters: string[] }[]; + dailyGroups: { title: string; letters: string[] }[]; selectedDailyGroupIdx: number; letterGrid: string[]; - modeOptions: ReadonlyArray<{ id: string; label: string; desc: string }>; + modeOptions: typeof LETTER_TRACING_MODE_OPTIONS; isPreviewFavorite: boolean; + isDevEnv: boolean; + debugPublishVisible: boolean; + debugPublishLoading: boolean; + debugPublishMeta: DebugPublishMeta | null; }; createPage( @@ -148,8 +102,12 @@ createPage( dailyGroups: DAILY_GROUPS, selectedDailyGroupIdx: 0, letterGrid: LETTERS_PAIRS, - modeOptions: [...MODES], + modeOptions: LETTER_TRACING_MODE_OPTIONS, isPreviewFavorite: false, + isDevEnv: false, + debugPublishVisible: false, + debugPublishLoading: false, + debugPublishMeta: null, } as unknown as PageData, /** 页面加载:从路由参数获取 worksheetId、letter、case、font 并应用,同时预加载字体 */ @@ -164,9 +122,10 @@ createPage( } loadLetterFont(this.fontProfile).catch(() => {}); const worksheetId = - options.id && MODE_MAP[options.id] + options.id && isValidMode(options.id) ? options.id : 'letter-tracing-single'; + this.syncDebugPublishEnv(); this.applyWorksheet(worksheetId); const updates: Partial = {}; @@ -216,11 +175,11 @@ createPage( }); }, - /** 构建运行时生成器配置:合并 worksheet 预设 + 当前页面状态 */ + /** 构建运行时生成器配置:合并页面模式预设 + 当前页面状态 */ buildRuntimeConfig(): LetterTracingGeneratorConfig { - const def = getWorksheetById(this.data.worksheetId); - const base = (def?.generatorConfig ?? - {}) as Partial; + const base: Partial = { + mode: this.data.traceMode, + }; const letterCase = this.data.traceMode === 'letter-tracing-single' ? (base.letterCase ?? 'upper') @@ -321,6 +280,14 @@ createPage( }); }, + getPublishMeta(): DebugPublishMeta { + const meta = getPublishMetaByMode(this.data.worksheetId); + if (!meta) { + throw new Error('当前题型配置不存在'); + } + return meta; + }, + /** 用户在三字母分组选择器中选择了一组 */ onSelectTripleGroup(e: WechatMiniprogram.TouchEvent) { const idx = Number(e.currentTarget.dataset.idx); @@ -338,9 +305,9 @@ createPage( }, /** - * 应用指定的 worksheet 配置到页面状态 - * 根据 worksheet 的 generatorConfig 决定 UI 控件的显隐(字母选择器、大小写切换等) - * @param worksheetId - 要应用的 worksheet ID + * 应用指定的页面模式配置到页面状态 + * 根据当前模式的运行时默认参数决定 UI 控件的显隐(字母选择器、大小写切换等) + * @param worksheetId - 要应用的模式 ID * @param options.preserveLetter - 是否保留当前选中的字母(模式切换时使用) * @param options.redraw - 是否立即重绘 Canvas */ @@ -351,11 +318,12 @@ createPage( redraw?: boolean; }, ) { - const def = getWorksheetById(worksheetId); - console.log('def', def); - const base = (def?.generatorConfig ?? - {}) as Partial; - const merged = mergeLetterTracingConfig(base, {}); + if (!isValidMode(worksheetId)) return; + + const merged = mergeLetterTracingConfig( + { mode: worksheetId as LetterTracingMode }, + {}, + ); const traceMode = merged.mode; const showLetterPicker = traceMode === 'letter-tracing-single'; @@ -397,10 +365,7 @@ createPage( selectedDailyGroupIdx, }, () => { - this.initPageInfo( - worksheetId, - MODE_MAP[worksheetId]?.label ?? '字母描红', - ); + this.initPageInfo(worksheetId, '字母描红'); if (this.drawService) { this.drawService.options.title = this.data.pageTitle; diff --git a/miniprogram/englishPages/letterTracing/letterTracing.wxml b/miniprogram/englishPages/letterTracing/letterTracing.wxml index f477542..021979c 100644 --- a/miniprogram/englishPages/letterTracing/letterTracing.wxml +++ b/miniprogram/englishPages/letterTracing/letterTracing.wxml @@ -57,14 +57,14 @@ - {{item.label}} + {{item.title}} @@ -74,14 +74,14 @@ - {{item.label}} + {{item.title}} @@ -119,7 +119,7 @@ size="42rpx" color="{{worksheetId === item.id ? '#453900' : '#605b50'}}" custom-class="lt-mode-card__icon" /> - {{item.label}} + {{item.title}} @@ -133,6 +133,16 @@ bind:primary="exportToPrint" bind:secondary="onShare" /> + + = { - beginner: '入门', - basic: '基础', - intermediate: '进阶', - advanced: '挑战', +const DIFFICULTY_LABELS: Record = { + 1: '入门', + 2: '基础', + 3: '进阶', + 4: '挑战', }; -function d(key: string): string { - return DIFFICULTY_MAP[key] ?? key; -} - function age(min: number, max: number): string { return `${min}-${max}岁`; } -const TODAY = '2026-4-22'; - /** 基于 id 生成稳定的伪随机统计数 */ function statsFromId(id: string): { likes: number; downloads: number } { let h = 0; @@ -53,80 +52,649 @@ function statsFromId(id: string): { likes: number; downloads: number } { return { likes, downloads }; } -type ItemInput = Omit; +function dateFromId(id: string): string { + let h = 0; + for (let i = 0; i < id.length; i++) h = (h * 31 + id.charCodeAt(i)) | 0; + const days = Math.abs(h % 500); + const d = new Date('2024-01-01'); + d.setDate(d.getDate() + days); + return d.toISOString().slice(0, 10); +} + +type ItemInput = Omit< + CategoryItem, + 'likes' | 'downloads' | 'date' | 'difficultyLabel' +>; function item(input: ItemInput): CategoryItem { const { likes, downloads } = statsFromId(input.id); - return { ...input, likes, downloads, date: TODAY }; + return { + ...input, + likes, + downloads, + date: dateFromId(input.id), + difficultyLabel: DIFFICULTY_LABELS[input.difficulty], + }; } const CATEGORY_ITEMS_BY_ID: Record = { math: [ - item({ id: 'number-find', title: '找数字,涂一涂', subtitle: '在数字方阵中找出目标数字并涂色', icon: '🔍', img: '/assets/entrancePicture/math/number-find.png', ageBand: age(3, 5), difficulty: d('beginner'), path: '/mathPages/mathDraw/mathDraw?id=number-find', available: true }), - item({ id: 'number-write', title: '看数字,写一写', subtitle: '按笔画顺序练习书写数字', icon: '✏️', img: '/assets/entrancePicture/math/number-write.png', ageBand: age(3, 5), difficulty: d('beginner'), path: '/mathPages/mathDraw/mathDraw?id=number-write', available: true }), - item({ id: 'number-coloring', title: '按数字,涂颜色', subtitle: '按指定数字给对应圆圈涂色', icon: '🎨', img: '/assets/entrancePicture/math/number-coloring.png', ageBand: age(3, 5), difficulty: d('beginner'), path: '/mathPages/mathDraw/mathDraw?id=number-coloring', available: true }), - item({ id: 'counting-matching', title: '数一数,连一连', subtitle: '连线配对数字和对应数量图形', icon: '🔗', img: '/assets/entrancePicture/math/count-match.png', ageBand: age(3, 5), difficulty: d('beginner'), path: '/mathPages/mathDraw/mathDraw?id=counting-matching', available: true }), - item({ id: 'number-object-match', title: '数物连线', subtitle: '连线相同数量的物品和数字', icon: '🔗', img: '/assets/entrancePicture/math/number-object-match.png', ageBand: age(3, 5), difficulty: d('beginner'), path: '/mathPages/mathDraw/mathDraw?id=number-object-match', available: true }), - item({ id: 'number-object-fill', title: '数物填写', subtitle: '数物品数量,填写对应数字', icon: '✏️', img: '/assets/entrancePicture/math/number-object-fill.png', ageBand: age(3, 5), difficulty: d('beginner'), path: '/mathPages/mathDraw/mathDraw?id=number-object-fill', available: true }), - item({ id: 'counting-select', title: '数一数,选一选', subtitle: '数出物品数量,圈出正确答案', icon: '✓', img: '/assets/entrancePicture/math/counting-select.png', ageBand: age(3, 5), difficulty: d('beginner'), path: '/mathPages/mathDraw/mathDraw?id=counting-select', available: true }), - item({ id: 'counting-fill', title: '数一数,填一填', subtitle: '数出物品数量,填写数字', icon: '✏️', img: '/assets/entrancePicture/math/counting-fill.png', ageBand: age(3, 5), difficulty: d('beginner'), path: '/mathPages/mathDraw/mathDraw?id=counting-fill', available: true }), - item({ id: 'compare', title: '数一数,比大小', subtitle: '比较数量,填入 ><=', icon: '⚖️', img: '/assets/entrancePicture/math/compare.png', ageBand: age(4, 6), difficulty: d('basic'), path: '/mathPages/mathDraw/mathDraw?id=compare', available: true }), - item({ id: 'number-sort', title: '数字排序', subtitle: '写出正确的数字顺序', icon: '🔢', img: '/assets/entrancePicture/math/number-sort.png', ageBand: age(4, 6), difficulty: d('basic'), path: '/mathPages/mathDraw/mathDraw?id=number-sort', available: true }), - item({ id: 'missing-number', title: '填上缺少的数字', subtitle: '在数列中找出并填写缺失数字', icon: '❓', img: '/assets/entrancePicture/math/missing-number.png', ageBand: age(4, 6), difficulty: d('basic'), path: '/mathPages/mathDraw/mathDraw?id=missing-number', available: true }), - item({ id: 'number-decompose', title: '10以内数的分与合', subtitle: '把数字分一分,合一合', icon: '🔢', img: '/assets/entrancePicture/math/number-decompose.png', ageBand: age(4, 6), difficulty: d('beginner'), path: '/mathPages/mathDraw/mathDraw?id=number-decompose', available: true }), - item({ id: 'number-decompose-20', title: '20以内数的分与合', subtitle: '把数字分一分,合一合', icon: '🔢', img: '/assets/entrancePicture/math/number-decompose-20.png', ageBand: age(5, 7), difficulty: d('basic'), path: '/mathPages/mathDraw/mathDraw?id=number-decompose-20', available: true }), - item({ id: 'one-digit-addition', title: '一位数加法', subtitle: '通过圆点学习一位数加法运算', icon: '➕', img: '/assets/entrancePicture/math/one-digit-addition.png', ageBand: age(4, 6), difficulty: d('basic'), path: '/mathPages/mathDraw/mathDraw?id=one-digit-addition', available: true }), - item({ id: 'addition-5', title: '5以内加法', subtitle: '图形化展示 5 以内加法', icon: '➕', img: '/assets/entrancePicture/math/addition-5.png', ageBand: age(3, 5), difficulty: d('beginner'), path: '/mathPages/mathDraw/mathDraw?id=addition-5', available: true }), - item({ id: 'addition-10', title: '10以内加法', subtitle: '图形化展示 10 以内加法', icon: '➕', img: '/assets/entrancePicture/math/addition-10.png', ageBand: age(4, 6), difficulty: d('basic'), path: '/mathPages/mathDraw/mathDraw?id=addition-10', available: true }), - item({ id: 'subtraction-10', title: '10以内减法', subtitle: '图形化展示 10 以内减法', icon: '➖', img: '/assets/entrancePicture/math/subtraction-10.png', ageBand: age(4, 6), difficulty: d('basic'), path: '/mathPages/mathDraw/mathDraw?id=subtraction-10', available: true }), - item({ id: 'addition-subtraction-10', title: '10以内加减法', subtitle: '加减法混合运算', icon: '±', img: '/assets/entrancePicture/math/addition-subtraction-10.png', ageBand: age(5, 7), difficulty: d('basic'), path: '/mathPages/mathDraw/mathDraw?id=addition-subtraction-10', available: true }), - item({ id: 'make-ten', title: '凑十法练习', subtitle: '20 以内进位加法', icon: '➕', img: '/assets/entrancePicture/math/make-ten.png', ageBand: age(5, 7), difficulty: d('intermediate'), path: '/mathPages/mathDraw/mathDraw?id=make-ten', available: true }), - item({ id: 'break-ten', title: '破十法练习', subtitle: '20 以内退位减法', icon: '➖', img: '/assets/entrancePicture/math/break-ten.png', ageBand: age(5, 7), difficulty: d('intermediate'), path: '/mathPages/mathDraw/mathDraw?id=break-ten', available: true }), - item({ id: 'flat-ten', title: '平十法练习', subtitle: '20 以内退位减法', icon: '➖', img: '/assets/entrancePicture/math/flat-ten.png', ageBand: age(5, 7), difficulty: d('intermediate'), path: '/mathPages/mathDraw/mathDraw?id=flat-ten', available: true }), - item({ id: 'borrow-ten', title: '借十法练习', subtitle: '20 以上退位减法', icon: '➖', img: '/assets/entrancePicture/math/borrow-ten.png', ageBand: age(6, 8), difficulty: d('advanced'), path: '/mathPages/mathDraw/mathDraw?id=borrow-ten', available: true }), - item({ id: 'practice-addition', title: '加法运算', subtitle: '10/20/50/100 以内加法', icon: '➕', img: '/assets/entrancePicture/math/practice-addition.png', ageBand: age(5, 8), difficulty: d('intermediate'), path: '/mathPages/mathDraw/mathDraw?id=practice-addition', available: true }), - item({ id: 'practice-subtraction', title: '减法运算', subtitle: '10/20/50/100 以内减法', icon: '➖', img: '/assets/entrancePicture/math/practice-subtraction.png', ageBand: age(5, 8), difficulty: d('intermediate'), path: '/mathPages/mathDraw/mathDraw?id=practice-subtraction', available: true }), - item({ id: 'practice-mixed', title: '混合运算', subtitle: '10/20/50/100 以内加减法混合', icon: '±', img: '/assets/entrancePicture/math/practice-mixed.png', ageBand: age(5, 8), difficulty: d('intermediate'), path: '/mathPages/mathDraw/mathDraw?id=practice-mixed', available: true }), - item({ id: 'multiplication-table', title: '九九乘法表', subtitle: '学习九九乘法口诀', icon: '✖️', img: '/assets/entrancePicture/math/multiplication-table.png', ageBand: age(6, 8), difficulty: d('intermediate'), path: '/mathPages/mathDraw/mathDraw?id=multiplication-table', available: true }), + item({ + id: 'number-find', + title: '找数字,涂一涂', + subtitle: '在数字方阵中找出目标数字并涂色', + icon: '🔍', + previewImg: '/assets/entrancePicture/math/number-find.png', + ageBand: age(3, 5), + difficulty: 1, + path: '/mathPages/mathDraw/mathDraw?id=number-find', + available: true, + }), + item({ + id: 'number-write', + title: '看数字,写一写', + subtitle: '按笔画顺序练习书写数字', + icon: '✏️', + previewImg: '/assets/entrancePicture/math/number-write.png', + ageBand: age(3, 5), + difficulty: 1, + path: '/mathPages/mathDraw/mathDraw?id=number-write', + available: true, + }), + item({ + id: 'number-coloring', + title: '按数字,涂颜色', + subtitle: '按指定数字给对应圆圈涂色', + icon: '🎨', + previewImg: '/assets/entrancePicture/math/number-coloring.png', + ageBand: age(3, 5), + difficulty: 1, + path: '/mathPages/mathDraw/mathDraw?id=number-coloring', + available: true, + }), + item({ + id: 'counting-matching', + title: '数一数,连一连', + subtitle: '连线配对数字和对应数量图形', + icon: '🔗', + previewImg: '/assets/entrancePicture/math/count-match.png', + ageBand: age(3, 5), + difficulty: 1, + path: '/mathPages/mathDraw/mathDraw?id=counting-matching', + available: true, + }), + item({ + id: 'number-object-match', + title: '数物连线', + subtitle: '连线相同数量的物品和数字', + icon: '🔗', + previewImg: '/assets/entrancePicture/math/number-object-match.png', + ageBand: age(3, 5), + difficulty: 1, + path: '/mathPages/mathDraw/mathDraw?id=number-object-match', + available: true, + }), + item({ + id: 'number-object-fill', + title: '数物填写', + subtitle: '数物品数量,填写对应数字', + icon: '✏️', + previewImg: '/assets/entrancePicture/math/number-object-fill.png', + ageBand: age(3, 5), + difficulty: 1, + path: '/mathPages/mathDraw/mathDraw?id=number-object-fill', + available: true, + }), + item({ + id: 'counting-select', + title: '数一数,选一选', + subtitle: '数出物品数量,圈出正确答案', + icon: '✓', + previewImg: '/assets/entrancePicture/math/counting-select.png', + ageBand: age(3, 5), + difficulty: 1, + path: '/mathPages/mathDraw/mathDraw?id=counting-select', + available: true, + }), + item({ + id: 'counting-fill', + title: '数一数,填一填', + subtitle: '数出物品数量,填写数字', + icon: '✏️', + previewImg: '/assets/entrancePicture/math/counting-fill.png', + ageBand: age(3, 5), + difficulty: 1, + path: '/mathPages/mathDraw/mathDraw?id=counting-fill', + available: true, + }), + item({ + id: 'compare', + title: '数一数,比大小', + subtitle: '比较数量,填入 ><=', + icon: '⚖️', + previewImg: '/assets/entrancePicture/math/compare.png', + ageBand: age(4, 6), + difficulty: 2, + path: '/mathPages/mathDraw/mathDraw?id=compare', + available: true, + }), + item({ + id: 'number-sort', + title: '数字排序', + subtitle: '写出正确的数字顺序', + icon: '🔢', + previewImg: '/assets/entrancePicture/math/number-sort.png', + ageBand: age(4, 6), + difficulty: 2, + path: '/mathPages/mathDraw/mathDraw?id=number-sort', + available: true, + }), + item({ + id: 'missing-number', + title: '填上缺少的数字', + subtitle: '在数列中找出并填写缺失数字', + icon: '❓', + previewImg: '/assets/entrancePicture/math/missing-number.png', + ageBand: age(4, 6), + difficulty: 2, + path: '/mathPages/mathDraw/mathDraw?id=missing-number', + available: true, + }), + item({ + id: 'number-decompose', + title: '10以内数的分与合', + subtitle: '把数字分一分,合一合', + icon: '🔢', + previewImg: '/assets/entrancePicture/math/number-decompose.png', + ageBand: age(4, 6), + difficulty: 1, + path: '/mathPages/mathDraw/mathDraw?id=number-decompose', + available: true, + }), + item({ + id: 'number-decompose-20', + title: '20以内数的分与合', + subtitle: '把数字分一分,合一合', + icon: '🔢', + previewImg: '/assets/entrancePicture/math/number-decompose-20.png', + ageBand: age(5, 7), + difficulty: 2, + path: '/mathPages/mathDraw/mathDraw?id=number-decompose-20', + available: true, + }), + item({ + id: 'one-digit-addition', + title: '一位数加法', + subtitle: '通过圆点学习一位数加法运算', + icon: '➕', + previewImg: '/assets/entrancePicture/math/one-digit-addition.png', + ageBand: age(4, 6), + difficulty: 2, + path: '/mathPages/mathDraw/mathDraw?id=one-digit-addition', + available: true, + }), + item({ + id: 'addition-5', + title: '5以内加法', + subtitle: '图形化展示 5 以内加法', + icon: '➕', + previewImg: '/assets/entrancePicture/math/addition-5.png', + ageBand: age(3, 5), + difficulty: 1, + path: '/mathPages/mathDraw/mathDraw?id=addition-5', + available: true, + }), + item({ + id: 'addition-10', + title: '10以内加法', + subtitle: '图形化展示 10 以内加法', + icon: '➕', + previewImg: '/assets/entrancePicture/math/addition-10.png', + ageBand: age(4, 6), + difficulty: 2, + path: '/mathPages/mathDraw/mathDraw?id=addition-10', + available: true, + }), + item({ + id: 'subtraction-10', + title: '10以内减法', + subtitle: '图形化展示 10 以内减法', + icon: '➖', + previewImg: '/assets/entrancePicture/math/subtraction-10.png', + ageBand: age(4, 6), + difficulty: 2, + path: '/mathPages/mathDraw/mathDraw?id=subtraction-10', + available: true, + }), + item({ + id: 'addition-subtraction-10', + title: '10以内加减法', + subtitle: '加减法混合运算', + icon: '±', + previewImg: + '/assets/entrancePicture/math/addition-subtraction-10.png', + ageBand: age(5, 7), + difficulty: 2, + path: '/mathPages/mathDraw/mathDraw?id=addition-subtraction-10', + available: true, + }), + item({ + id: 'make-ten', + title: '凑十法练习', + subtitle: '20 以内进位加法', + icon: '➕', + previewImg: '/assets/entrancePicture/math/make-ten.png', + ageBand: age(5, 7), + difficulty: 3, + path: '/mathPages/mathDraw/mathDraw?id=make-ten', + available: true, + }), + item({ + id: 'break-ten', + title: '破十法练习', + subtitle: '20 以内退位减法', + icon: '➖', + previewImg: '/assets/entrancePicture/math/break-ten.png', + ageBand: age(5, 7), + difficulty: 3, + path: '/mathPages/mathDraw/mathDraw?id=break-ten', + available: true, + }), + item({ + id: 'flat-ten', + title: '平十法练习', + subtitle: '20 以内退位减法', + icon: '➖', + previewImg: '/assets/entrancePicture/math/flat-ten.png', + ageBand: age(5, 7), + difficulty: 3, + path: '/mathPages/mathDraw/mathDraw?id=flat-ten', + available: true, + }), + item({ + id: 'borrow-ten', + title: '借十法练习', + subtitle: '20 以上退位减法', + icon: '➖', + previewImg: '/assets/entrancePicture/math/borrow-ten.png', + ageBand: age(6, 8), + difficulty: 4, + path: '/mathPages/mathDraw/mathDraw?id=borrow-ten', + available: true, + }), + item({ + id: 'practice-addition', + title: '加法运算', + subtitle: '10/20/50/100 以内加法', + icon: '➕', + previewImg: '/assets/entrancePicture/math/practice-addition.png', + ageBand: age(5, 8), + difficulty: 3, + path: '/mathPages/mathDraw/mathDraw?id=practice-addition', + available: true, + }), + item({ + id: 'practice-subtraction', + title: '减法运算', + subtitle: '10/20/50/100 以内减法', + icon: '➖', + previewImg: '/assets/entrancePicture/math/practice-subtraction.png', + ageBand: age(5, 8), + difficulty: 3, + path: '/mathPages/mathDraw/mathDraw?id=practice-subtraction', + available: true, + }), + item({ + id: 'practice-mixed', + title: '混合运算', + subtitle: '10/20/50/100 以内加减法混合', + icon: '±', + previewImg: '/assets/entrancePicture/math/practice-mixed.png', + ageBand: age(5, 8), + difficulty: 3, + path: '/mathPages/mathDraw/mathDraw?id=practice-mixed', + available: true, + }), + item({ + id: 'multiplication-table', + title: '九九乘法表', + subtitle: '学习九九乘法口诀', + icon: '✖️', + previewImg: '/assets/entrancePicture/math/multiplication-table.png', + ageBand: age(6, 8), + difficulty: 3, + path: '/mathPages/mathDraw/mathDraw?id=multiplication-table', + available: true, + }), ], pinyin: [ - item({ id: 'pinyin-initials', title: '声母描红', subtitle: '23个声母认读与书写练习', icon: '🔤', ageBand: age(5, 7), difficulty: d('basic'), path: '', available: false }), - item({ id: 'pinyin-finals', title: '韵母描红', subtitle: '24个韵母认读与书写练习', icon: '🔤', ageBand: age(5, 7), difficulty: d('basic'), path: '', available: false }), - item({ id: 'pinyin-overall', title: '整体认读音节', subtitle: '16个整体认读音节练习', icon: '🔤', ageBand: age(5, 7), difficulty: d('intermediate'), path: '', available: false }), + item({ + id: 'pinyin-initials', + title: '声母描红', + subtitle: '23个声母认读与书写练习', + icon: '🔤', + ageBand: age(5, 7), + difficulty: 2, + path: '', + available: false, + }), + item({ + id: 'pinyin-finals', + title: '韵母描红', + subtitle: '24个韵母认读与书写练习', + icon: '🔤', + ageBand: age(5, 7), + difficulty: 2, + path: '', + available: false, + }), + item({ + id: 'pinyin-overall', + title: '整体认读音节', + subtitle: '16个整体认读音节练习', + icon: '🔤', + ageBand: age(5, 7), + difficulty: 3, + path: '', + available: false, + }), ], puzzle: [ - item({ id: 'color-shape-match', title: '根据颜色画图形', subtitle: '根据颜色画出对应图形', icon: '🎯', img: '/assets/entrancePicture/focus/color-shape-match.png', ageBand: age(4, 6), difficulty: d('basic'), path: '/focusPages/focusDraw/focusDraw?id=color-shape-match', available: true }), - item({ id: 'shape-symbol', title: '图形符号配对', subtitle: '根据图形画对应符号', icon: '🔗', img: '/assets/entrancePicture/focus/shape-symbol.png', ageBand: age(4, 6), difficulty: d('basic'), path: '/focusPages/focusDraw/focusDraw?id=shape-symbol', available: true }), - item({ id: 'shape-recognition', title: '识别形状', subtitle: '识别形状,涂一涂', icon: '🔍', img: '/assets/entrancePicture/focus/shape-recognition.png', ageBand: age(3, 5), difficulty: d('beginner'), path: '/focusPages/focusDraw/focusDraw?id=shape-recognition', available: true }), - item({ id: 'position-coloring', title: '方位涂涂乐', subtitle: '观察位置,在方格中涂色', icon: '📍', img: '/assets/entrancePicture/focus/position-coloring.png', ageBand: age(4, 6), difficulty: d('basic'), path: '/focusPages/focusDraw/focusDraw?id=position-coloring', available: true }), - item({ id: 'color-pattern', title: '颜色找规律', subtitle: '观察颜色规律,在空白图形中涂色', icon: '🎨', img: '/assets/entrancePicture/focus/color-pattern.png', ageBand: age(4, 6), difficulty: d('basic'), path: '/focusPages/focusDraw/focusDraw?id=color-pattern', available: true }), - item({ id: 'match-connect', title: '连连看', subtitle: '根据物品连一连', icon: '🔗', img: '/assets/entrancePicture/focus/match-connect.png', ageBand: age(3, 6), difficulty: d('beginner'), path: '/focusPages/focusDraw/focusDraw?id=match-connect', available: true }), - item({ id: 'line-recognition', title: '线条识别', subtitle: '认识不同线条,画出颜色对应的线条', icon: '📏', img: '/assets/entrancePicture/focus/line-recognition.png', ageBand: age(3, 5), difficulty: d('beginner'), path: '/focusPages/focusDraw/focusDraw?id=line-recognition', available: true }), - item({ id: 'grid-reasoning', title: '方格推理', subtitle: '推理出合并方格并连线', icon: '🧩', img: '/assets/entrancePicture/focus/grid-reasoning.png', ageBand: age(5, 7), difficulty: d('intermediate'), path: '/focusPages/focusDraw/focusDraw?id=grid-reasoning', available: true }), - item({ id: 'code-connect', title: '译码连线', subtitle: '按数字顺序将数字对应颜色连线', icon: '🔢', img: '/assets/entrancePicture/focus/code-connect.png', ageBand: age(5, 7), difficulty: d('intermediate'), path: '/focusPages/focusDraw/focusDraw?id=code-connect', available: true }), - item({ id: 'dot-connect', title: '数字点连线', subtitle: '按数字顺序连点成图', icon: '🔗', img: '/assets/entrancePicture/focus/dot-connect.png', ageBand: age(3, 6), difficulty: d('beginner'), path: '/focusPages/focusDraw/focusDraw?id=dot-connect', available: true }), - item({ id: 'grid-drawing-3x3', title: '格子仿画 3×3', subtitle: '简单有趣,培养专注力', icon: '🎨', img: '/assets/entrancePicture/focus/grid-drawing-3x3.png', ageBand: age(3, 5), difficulty: d('beginner'), path: '/focusPages/focusDraw/focusDraw?id=grid-drawing-3x3', available: true }), - item({ id: 'grid-drawing-5x5', title: '格子仿画 5×5', subtitle: '创意挑战,提升观察力', icon: '🎨', img: '/assets/entrancePicture/focus/grid-drawing-5x5.png', ageBand: age(4, 6), difficulty: d('basic'), path: '/focusPages/focusDraw/focusDraw?id=grid-drawing-5x5', available: true }), - item({ id: 'grid-drawing-7x7', title: '格子仿画 7×7', subtitle: '大师挑战,锻炼耐心', icon: '🎨', img: '/assets/entrancePicture/focus/grid-drawing-7x7.png', ageBand: age(5, 8), difficulty: d('intermediate'), path: '/focusPages/focusDraw/focusDraw?id=grid-drawing-7x7', available: true }), + item({ + id: 'color-shape-match', + title: '根据颜色画图形', + subtitle: '根据颜色画出对应图形', + icon: '🎯', + previewImg: '/assets/entrancePicture/focus/color-shape-match.png', + ageBand: age(4, 6), + difficulty: 2, + path: '/focusPages/focusDraw/focusDraw?id=color-shape-match', + available: true, + }), + item({ + id: 'shape-symbol', + title: '图形符号配对', + subtitle: '根据图形画对应符号', + icon: '🔗', + previewImg: '/assets/entrancePicture/focus/shape-symbol.png', + ageBand: age(4, 6), + difficulty: 2, + path: '/focusPages/focusDraw/focusDraw?id=shape-symbol', + available: true, + }), + item({ + id: 'shape-recognition', + title: '识别形状', + subtitle: '识别形状,涂一涂', + icon: '🔍', + previewImg: '/assets/entrancePicture/focus/shape-recognition.png', + ageBand: age(3, 5), + difficulty: 1, + path: '/focusPages/focusDraw/focusDraw?id=shape-recognition', + available: true, + }), + item({ + id: 'position-coloring', + title: '方位涂涂乐', + subtitle: '观察位置,在方格中涂色', + icon: '📍', + previewImg: '/assets/entrancePicture/focus/position-coloring.png', + ageBand: age(4, 6), + difficulty: 2, + path: '/focusPages/focusDraw/focusDraw?id=position-coloring', + available: true, + }), + item({ + id: 'color-pattern', + title: '颜色找规律', + subtitle: '观察颜色规律,在空白图形中涂色', + icon: '🎨', + previewImg: '/assets/entrancePicture/focus/color-pattern.png', + ageBand: age(4, 6), + difficulty: 2, + path: '/focusPages/focusDraw/focusDraw?id=color-pattern', + available: true, + }), + item({ + id: 'match-connect', + title: '连连看', + subtitle: '根据物品连一连', + icon: '🔗', + previewImg: '/assets/entrancePicture/focus/match-connect.png', + ageBand: age(3, 6), + difficulty: 1, + path: '/focusPages/focusDraw/focusDraw?id=match-connect', + available: true, + }), + item({ + id: 'line-recognition', + title: '线条识别', + subtitle: '认识不同线条,画出颜色对应的线条', + icon: '📏', + previewImg: '/assets/entrancePicture/focus/line-recognition.png', + ageBand: age(3, 5), + difficulty: 1, + path: '/focusPages/focusDraw/focusDraw?id=line-recognition', + available: true, + }), + item({ + id: 'grid-reasoning', + title: '方格推理', + subtitle: '推理出合并方格并连线', + icon: '🧩', + previewImg: '/assets/entrancePicture/focus/grid-reasoning.png', + ageBand: age(5, 7), + difficulty: 3, + path: '/focusPages/focusDraw/focusDraw?id=grid-reasoning', + available: true, + }), + item({ + id: 'code-connect', + title: '译码连线', + subtitle: '按数字顺序将数字对应颜色连线', + icon: '🔢', + previewImg: '/assets/entrancePicture/focus/code-connect.png', + ageBand: age(5, 7), + difficulty: 3, + path: '/focusPages/focusDraw/focusDraw?id=code-connect', + available: true, + }), + item({ + id: 'dot-connect', + title: '数字点连线', + subtitle: '按数字顺序连点成图', + icon: '🔗', + previewImg: '/assets/entrancePicture/focus/dot-connect.png', + ageBand: age(3, 6), + difficulty: 1, + path: '/focusPages/focusDraw/focusDraw?id=dot-connect', + available: true, + }), + item({ + id: 'grid-drawing-3x3', + title: '格子仿画 3×3', + subtitle: '简单有趣,培养专注力', + icon: '🎨', + previewImg: '/assets/entrancePicture/focus/grid-drawing-3x3.png', + ageBand: age(3, 5), + difficulty: 1, + path: '/focusPages/focusDraw/focusDraw?id=grid-drawing-3x3', + available: true, + }), + item({ + id: 'grid-drawing-5x5', + title: '格子仿画 5×5', + subtitle: '创意挑战,提升观察力', + icon: '🎨', + previewImg: '/assets/entrancePicture/focus/grid-drawing-5x5.png', + ageBand: age(4, 6), + difficulty: 2, + path: '/focusPages/focusDraw/focusDraw?id=grid-drawing-5x5', + available: true, + }), + item({ + id: 'grid-drawing-7x7', + title: '格子仿画 7×7', + subtitle: '大师挑战,锻炼耐心', + icon: '🎨', + previewImg: '/assets/entrancePicture/focus/grid-drawing-7x7.png', + ageBand: age(5, 8), + difficulty: 3, + path: '/focusPages/focusDraw/focusDraw?id=grid-drawing-7x7', + available: true, + }), ], chinese: [ - item({ id: 'word-recognition', title: '识字卡', subtitle: '输入或选字生成涂色识字卡', icon: '📖', ageBand: age(3, 6), difficulty: d('beginner'), path: '/pages/index/index', available: true }), - item({ id: 'copybook', title: '练字帖', subtitle: '选字生成田字格笔顺练字帖', icon: '✏️', ageBand: age(4, 7), difficulty: d('basic'), path: '/pages/copyBook/copyBook', available: true }), + item({ + id: 'word-recognition', + title: '识字卡', + subtitle: '输入或选字生成涂色识字卡', + icon: '📖', + ageBand: age(3, 6), + difficulty: 1, + path: '/pages/index/index', + available: true, + }), + item({ + id: 'copybook', + title: '练字帖', + subtitle: '选字生成田字格笔顺练字帖', + icon: '✏️', + ageBand: age(4, 7), + difficulty: 2, + path: '/pages/copyBook/copyBook', + available: true, + }), ], english: [ - item({ id: 'letter-tracing-single', title: '看图描红', subtitle: '单字母配图、例句与六行四线三格描红', icon: '🔠', img: '/assets/entrancePicture/english/letter-tracing-single.jpg', ageBand: age(4, 7), difficulty: d('beginner'), path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-single', available: true }), - item({ id: 'letter-tracing-single2', title: '看图描红', subtitle: '单字母配图、例句与六行四线三格描红', icon: '🔠', img: '/assets/entrancePicture/english/letter-tracing-single2.jpg', ageBand: age(4, 7), difficulty: d('beginner'), path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-single', available: true }), - item({ id: 'letter-tracing-upper-lower', title: '字母总览', subtitle: 'Uppercase / Lowercase 分区总览', icon: '🔡', img: '/assets/entrancePicture/english/letter-tracing-upper-lower.jpg', ageBand: age(4, 7), difficulty: d('beginner'), path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-upper-lower', available: true }), - item({ id: 'letter-tracing-case-pairing', title: '大小写练习', subtitle: '半组字母左大写右小写,逐行对照描红', icon: '✏️', img: '/assets/entrancePicture/english/letter-tracing-case-pairing.jpg', ageBand: age(4, 7), difficulty: d('beginner'), path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-case-pairing', available: true }), - item({ id: 'letter-tracing-two-column', title: '两列练习', subtitle: '左 A-M、右 N-Z,大小写配对描红', icon: '🔤', img: '/assets/entrancePicture/english/letter-tracing-two-column.jpg', ageBand: age(4, 7), difficulty: d('beginner'), path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-two-column', available: true }), - item({ id: 'letter-tracing-half', title: '单字母逐行', subtitle: '13 字母半表逐字练习', icon: '📝', img: '/assets/entrancePicture/english/letter-tracing-half.jpg', ageBand: age(4, 7), difficulty: d('basic'), path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-half', available: true }), - item({ id: 'letter-tracing-three', title: '三字母精练', subtitle: '每页 3 个字母,大写 + 小写深度书写', icon: '🔤', img: '/assets/entrancePicture/english/letter-tracing-three.jpg', ageBand: age(4, 7), difficulty: d('basic'), path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-three', available: true }), - item({ id: 'letter-tracing-daily-checkin', title: '每日打卡', subtitle: '四宫格每日字母打卡练习', icon: '🔤', img: '/assets/entrancePicture/english/letter-tracing-daily-checkin.jpg', ageBand: age(4, 7), difficulty: d('basic'), path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-daily-checkin', available: true }), + item({ + id: 'letter-tracing-single', + title: '看图描红', + subtitle: '单字母配图、例句与六行四线三格描红', + icon: '🔠', + previewImg: + '/assets/entrancePicture/english/letter-tracing-single.jpg', + ageBand: age(4, 7), + difficulty: 1, + path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-single', + available: true, + }), + item({ + id: 'letter-tracing-single2', + title: '看图描红', + subtitle: '单字母配图、例句与六行四线三格描红', + icon: '🔠', + previewImg: + '/assets/entrancePicture/english/letter-tracing-single2.jpg', + ageBand: age(4, 7), + difficulty: 1, + path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-single', + available: true, + }), + item({ + id: 'letter-tracing-upper-lower', + title: '字母总览', + subtitle: 'Uppercase / Lowercase 分区总览', + icon: '🔡', + previewImg: + '/assets/entrancePicture/english/letter-tracing-upper-lower.jpg', + ageBand: age(4, 7), + difficulty: 1, + path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-upper-lower', + available: true, + }), + item({ + id: 'letter-tracing-case-pairing', + title: '大小写练习', + subtitle: '半组字母左大写右小写,逐行对照描红', + icon: '✏️', + previewImg: + '/assets/entrancePicture/english/letter-tracing-case-pairing.jpg', + ageBand: age(4, 7), + difficulty: 1, + path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-case-pairing', + available: true, + }), + item({ + id: 'letter-tracing-two-column', + title: '两列练习', + subtitle: '左 A-M、右 N-Z,大小写配对描红', + icon: '🔤', + previewImg: + '/assets/entrancePicture/english/letter-tracing-two-column.jpg', + ageBand: age(4, 7), + difficulty: 1, + path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-two-column', + available: true, + }), + item({ + id: 'letter-tracing-half', + title: '单字母逐行', + subtitle: '13 字母半表逐字练习', + icon: '📝', + previewImg: + '/assets/entrancePicture/english/letter-tracing-half.jpg', + ageBand: age(4, 7), + difficulty: 2, + path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-half', + available: true, + }), + item({ + id: 'letter-tracing-three', + title: '三字母精练', + subtitle: '每页 3 个字母,大写 + 小写深度书写', + icon: '🔤', + previewImg: + '/assets/entrancePicture/english/letter-tracing-three.jpg', + ageBand: age(4, 7), + difficulty: 2, + path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-three', + available: true, + }), + item({ + id: 'letter-tracing-daily-checkin', + title: '每日打卡', + subtitle: '四宫格每日字母打卡练习', + icon: '🔤', + previewImg: + '/assets/entrancePicture/english/letter-tracing-daily-checkin.jpg', + ageBand: age(4, 7), + difficulty: 2, + path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-daily-checkin', + available: true, + }), ], craft: [ - item({ id: 'craft-coloring', title: '涂色卡', subtitle: '动物、交通、节日主题涂色', icon: '🎨', ageBand: age(3, 6), difficulty: d('beginner'), path: '', available: false }), - item({ id: 'craft-origami', title: '折纸模板', subtitle: '打印后即可折叠的趣味模板', icon: '🪭', ageBand: age(4, 7), difficulty: d('basic'), path: '', available: false }), - item({ id: 'craft-stickers', title: '贴纸打印', subtitle: '奖励贴纸与装饰贴纸', icon: '⭐', ageBand: age(3, 8), difficulty: d('beginner'), path: '', available: false }), + item({ + id: 'craft-coloring', + title: '涂色卡', + subtitle: '动物、交通、节日主题涂色', + icon: '🎨', + ageBand: age(3, 6), + difficulty: 1, + path: '', + available: false, + }), + item({ + id: 'craft-origami', + title: '折纸模板', + subtitle: '打印后即可折叠的趣味模板', + icon: '🪭', + ageBand: age(4, 7), + difficulty: 2, + path: '', + available: false, + }), + item({ + id: 'craft-stickers', + title: '贴纸打印', + subtitle: '奖励贴纸与装饰贴纸', + icon: '⭐', + ageBand: age(3, 8), + difficulty: 1, + path: '', + available: false, + }), ], }; diff --git a/miniprogram/pages/category/category.ts b/miniprogram/pages/category/category.ts index 600b470..7fa9962 100644 --- a/miniprogram/pages/category/category.ts +++ b/miniprogram/pages/category/category.ts @@ -7,11 +7,13 @@ type CategoryTab = { icon: string; }; -const SIDEBAR_TABS: CategoryTab[] = CATEGORY_LIST_WITH_ALL.map(({ id, name, icon }) => ({ - id, - name, - icon, -})); +const SIDEBAR_TABS: CategoryTab[] = CATEGORY_LIST_WITH_ALL.map( + ({ id, name, icon }) => ({ + id, + name, + icon, + }), +); function buildAllItems(): CategoryItem[] { const items: CategoryItem[] = []; diff --git a/miniprogram/pages/category/category.wxml b/miniprogram/pages/category/category.wxml index c9298d8..2c26eee 100644 --- a/miniprogram/pages/category/category.wxml +++ b/miniprogram/pages/category/category.wxml @@ -59,9 +59,9 @@ bindtap="onTapItem"> {{item.icon}}{{item.ageBand}} {{item.difficulty}}{{item.difficultyLabel}} diff --git a/miniprogram/pages/home/home.data.ts b/miniprogram/pages/home/home.data.ts index 0d0eabd..79338b6 100644 --- a/miniprogram/pages/home/home.data.ts +++ b/miniprogram/pages/home/home.data.ts @@ -1,12 +1,13 @@ import { CATEGORY_LIST_WITH_ALL } from '../../core/data/categories'; import { AGE_BANDS } from '../../core/data/difficulty'; +import { CategoryId } from '../../core/models/category'; export type HomeDisplayItem = { id: string; title: string; subtitle: string; description?: string; - category: 'math' | 'chinese' | 'english' | 'puzzle' | 'craft'; + category: CategoryId; ageBand: string; difficulty: '入门' | '基础' | '进阶' | '挑战'; icon: string; @@ -44,9 +45,8 @@ export type HomeDisplayDataset = { sections: HomeDisplaySection[]; }; -const HOME_CATEGORY_TABS: HomeDisplayDataset['categoryTabs'] = CATEGORY_LIST_WITH_ALL.map( - ({ id, name }) => ({ id, name }), -); +const HOME_CATEGORY_TABS: HomeDisplayDataset['categoryTabs'] = + CATEGORY_LIST_WITH_ALL.map(({ id, name }) => ({ id, name })); const HOME_AGE_BANDS = AGE_BANDS.map((item, index) => ({ key: item.key, diff --git a/miniprogram/utils/debugPublish.ts b/miniprogram/utils/debugPublish.ts new file mode 100644 index 0000000..66503c7 --- /dev/null +++ b/miniprogram/utils/debugPublish.ts @@ -0,0 +1,156 @@ +import type { + WorksheetCloudCategory, + WorksheetStatus, +} from '../core/models/worksheet'; + +/** + * 调试发布时的裁剪模式。 + * - header-footer: 同 node-tools 入口图脚本,默认裁掉页眉和页脚 + * - header-only: 仅裁掉页眉,保留底部品牌区 + * - none: 不裁剪,直接压缩整张 A4 预览图 + */ +export type DebugCropMode = 'header-footer' | 'header-only' | 'none'; + +/** + * 发布到 worksheets 集合的核心元数据。 + * 这里对齐《小程序云开发方案》中的题型字段设计,供页面侧生成 payload 使用。 + */ +export interface DebugPublishMeta { + id: string; + title: string; + subtitle: string; + category: WorksheetCloudCategory; + subcategory?: string; + path: string; + ageMin: number; + ageMax: number; + grade: number; + difficulty: 1 | 2 | 3 | 4; + previewImg?: string; + tags: string[]; + isNew: boolean; + isHot: boolean; + sortOrder?: number; + status: WorksheetStatus; +} + +/** 发布弹窗里可调的图片处理参数。 */ +export interface DebugPublishSettings { + cropMode: DebugCropMode; + quality: number; + width: number; + maxSizeKB: number; +} + +/** 弹窗确认后回传给页面的表单结果。 */ +export interface DebugPublishConfirmDetail { + meta: Pick; + settings: DebugPublishSettings; +} + +/** 图片处理组件收到的输入参数。 */ +export interface DebugProcessImageParams { + sourcePath: string; + settings: DebugPublishSettings; +} + +/** 图片处理完成后返回给页面的信息。 */ +export interface DebugProcessImageResult { + tempFilePath: string; + size: number; + width: number; + height: number; + quality: number; +} + +/** 默认压缩质量,与文档里的入口图建议值保持一致。 */ +export const DEBUG_PUBLISH_DEFAULT_QUALITY = 90; +/** 默认入口图宽度,沿用 node-tools 脚本的 600px。 */ +export const DEBUG_PUBLISH_DEFAULT_WIDTH = 600; +/** 默认体积上限,超过后会继续降质压缩。 */ +export const DEBUG_PUBLISH_MAX_SIZE_KB = 200; + +/** 页眉裁剪比例:110 / 842,来源于现有入口图脚本。 */ +export const DEBUG_CROP_TOP_RATIO = 110 / 842; +/** 页脚裁剪比例:52 / 842,来源于现有入口图脚本。 */ +export const DEBUG_CROP_BOTTOM_RATIO = 52 / 842; + +/** 仅开发版开放 Debug 发布能力。 */ +export function isDebugPublishEnabled(): boolean { + const accountInfo = wx.getAccountInfoSync(); + return accountInfo.miniProgram.envVersion === 'develop'; +} + +/** 限制压缩质量范围,避免过高或过低。 */ +export function clampPublishQuality(value: number): number { + return Math.min(95, Math.max(40, Math.round(value))); +} + +/** 限制输出宽度范围,防止导出过小或过大。 */ +export function clampPublishWidth(value: number): number { + return Math.min(1000, Math.max(400, Math.round(value))); +} + +/** 根据裁剪模式计算顶部 / 底部裁剪比例。 */ +export function getCropRatios(mode: DebugCropMode): { + topRatio: number; + bottomRatio: number; +} { + if (mode === 'header-only') { + return { + topRatio: DEBUG_CROP_TOP_RATIO, + bottomRatio: 0, + }; + } + + if (mode === 'none') { + return { + topRatio: 0, + bottomRatio: 0, + }; + } + + return { + topRatio: DEBUG_CROP_TOP_RATIO, + bottomRatio: DEBUG_CROP_BOTTOM_RATIO, + }; +} + +/** 统一构造预览图在云存储中的路径。 */ +export function buildWorksheetPreviewCloudPath( + category: WorksheetCloudCategory, + id: string, +): string { + return `assets/previews/${category}/${id}.jpg`; +} + +/** 将输入框中的标签文本拆成标签数组,兼容英文逗号、中文逗号、顿号和空白。 */ +export function normalizeTagsInput(raw: string): string[] { + return raw + .split(/[,,、\s]+/) + .map((item) => item.trim()) + .filter(Boolean); +} + +/** + * 由年龄段推断年级值。 + * 当前先用“年龄中心点 -> 年级”的简单映射,后续如有更细分规则可独立替换。 + */ +export function inferGradeFromAge(ageMin: number, ageMax: number): number { + const centerAge = Math.round((ageMin + ageMax) / 2); + const ageGradeMap: Record = { + 2: -4, + 3: -3, + 4: -2, + 5: -1, + 6: 0, + 7: 1, + 8: 2, + 9: 3, + 10: 4, + 11: 5, + 12: 6, + }; + + return ageGradeMap[centerAge] ?? 0; +}