diff --git a/docs/Debug发布方案.md b/docs/Debug发布方案.md new file mode 100644 index 0000000..7ee9b66 --- /dev/null +++ b/docs/Debug发布方案.md @@ -0,0 +1,596 @@ +# Doodle Mini — Debug 模式内容发布方案 + +> 版本:v1.0 +> 最后更新:2026-04-23 +> 配套文档:[技术架构设计文档](./技术架构设计文档.md) | [小程序云开发方案](./小程序云开发方案.md) | [产品设计文档](./产品设计文档.md) + +--- + +## 一、方案定位与动机 + +### 1.1 当前痛点 + +每新增一种可打印资料,从开发完成到上线需要经历以下**手动步骤**: + +``` +开发页面 + Draw 逻辑 + → ① 在预览中截图或导出入口图原图 + → ② 放入 node-tools/entranceInput/<分类>/ + → ③ 运行 processEntrancePicture.js 裁剪缩放 + → ④ 从 entranceOuput/ 拷贝到 miniprogram/assets/entrancePicture/ + → ⑤ 手动编辑 category.data.ts 添加元数据(title、subtitle、path、img 等) + → ⑥ 提交代码 → 审核 → 发版 +``` + +其中 ①~⑤ 是重复性机械劳动,且容易出错(路径拼错、忘记更新数据文件、图片尺寸不一致等)。 + +### 1.2 核心洞察 + +**当一个页面开发调试完成时,所有需要的信息已经就绪**: + +- **页面路径** — 就是当前正在调试的页面 URL +- **标题 / 副标题** — 已写在绘制逻辑或配置中 +- **入口图片** — 预览 Canvas 上正在渲染的就是 +- **分类 / 难度 / 年龄段 / 标签** — 已在页面配置中定义 + +在这个时刻一键上传,是**成本最低、数据最准确**的做法。 + +### 1.3 方案目标 + +在 **develop / devtools 环境**的预览页中增加「发布到云端」按钮,点击后自动: + +1. 从 Canvas 导出入口图 → 裁剪 → 压缩 +2. 上传图片到云存储 +3. 组装页面元数据 → 写入云数据库 `worksheets` 集合 +4. 线上小程序通过缓存策略拉取云端数据,新内容**无需发版即可上线** + +**不需要开发独立后台系统,不需要手动录入数据。** + +--- + +## 二、整体流程 + +``` +┌──────────────────────────────────────────────────────────────────┐ +│ Debug 模式发布流程 │ +│ │ +│ ① 开发者在 develop / devtools 环境下完成页面开发 │ +│ (绘制逻辑调试完毕,预览效果满意) │ +│ │ +│ ② 预览区底部出现 [📤 发布到云端] 按钮 │ +│ (仅 develop / devtools 环境可见,线上版本不显示) │ +│ │ +│ ③ 点击后弹出确认面板,预览即将上传的数据: │ +│ ┌─────────────────────────────────────────┐ │ +│ │ ID: letter-tracing-daily-checkin │ │ +│ │ 标题: 每日打卡 │ │ +│ │ 副标题: 四宫格每日字母打卡练习 │ │ +│ │ 分类: english │ │ +│ │ 路径: /englishPages/letterTracing/... │ │ +│ │ 难度: beginner │ │ +│ │ 年龄: 4-7岁 │ │ +│ │ 标签: [字母, 描红, 打卡] │ │ +│ │ [预览图缩略图] │ │ +│ │ │ │ +│ │ ─ 可编辑字段(允许微调) ─ │ │ +│ │ │ │ +│ │ 状态: ○ draft(默认) ○ active │ │ +│ │ │ │ +│ │ [取消] [确认发布] │ │ +│ └─────────────────────────────────────────┘ │ +│ │ +│ ④ 确认发布后,自动执行: │ +│ a. Canvas 导出 → 裁剪页眉页脚 → 压缩至 ≤200KB │ +│ b. 上传图片到云存储 │ +│ cloud://xxx/assets/previews//.jpg │ +│ c. 组装 WorksheetConfig → upsert 到云数据库 worksheets 集合 │ +│ d. 显示「发布成功 ✅」 │ +│ │ +│ ⑤ 线上小程序按缓存策略拉取新数据,内容自动出现 │ +│ (或在 debug 管理页手动将 status 从 draft 切为 active) │ +└──────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 三、技术设计 + +### 3.1 环境门控 + +发布功能**仅在开发环境**可见,**绝不暴露给普通用户**。 + +```typescript +/** + * 判断当前是否为开发环境,可执行 debug 发布操作。 + * 复用 downloadPrint.ts 中 isDevBypassLimits 的判断模式。 + */ +function isDebugPublishEnabled(): boolean { + const accountInfo = wx.getAccountInfoSync(); + const { envVersion } = accountInfo.miniProgram; + // develop: 开发版;trial: 体验版;release: 正式版 + return envVersion === 'develop'; +} +``` + +WXML 中条件渲染: + +```xml + + + + +``` + +### 3.2 页面元数据约定 + +每个 draw 页面需提供 `getPublishMeta()` 方法,返回标准化的发布元数据。 + +```typescript +/** + * 发布元数据接口 + * 各 draw 页面实现此接口,提供自身的配置信息 + */ +interface PublishMeta { + // ─── 必填字段 ─── + id: string; // 唯一标识,如 'letter-tracing-daily-checkin' + title: string; // 显示标题 + subtitle: string; // 显示副标题 + category: 'math' | 'chinese' | 'english' | 'puzzle' | 'craft'; + subcategory: string; // 子分类 + path: string; // 页面完整路径(含参数) + + // ─── 选填字段(有默认值)─── + 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; +} +``` + +在 `pageMixin` 中约定调用方式: + +```typescript +// pageMixin 中新增 +debugPublishMixin: { + getPublishMeta(): PublishMeta { + // 子类覆写此方法 + throw new Error('页面未实现 getPublishMeta()'); + } +} +``` + +各页面实现示例(letterTracing): + +```typescript +getPublishMeta(): PublishMeta { + const drawService = this.drawService; + return { + id: this.data.currentId, + title: drawService.title, + subtitle: drawService.subtitle, + category: 'english', + subcategory: 'letter-tracing', + path: `/englishPages/letterTracing/letterTracing?id=${this.data.currentId}`, + icon: '🔠', + ageRange: [4, 7], + difficulty: 'beginner', + tags: ['字母', '描红', '英语'], + }; +} +``` + +### 3.3 入口图导出与处理 + +复用已有的 `preview-card` 组件的 `exportToTempFile` 能力,再做裁剪处理。 + +```typescript +/** + * 从预览 Canvas 导出入口图 + * + * 处理流程: + * 1. 从 preview-card 导出完整 A4 临时文件 + * 2. 用离屏 Canvas 裁剪掉页眉/页脚区域(与 processEntrancePicture.js 逻辑对齐) + * 3. 缩放至入口图标准宽度(600px) + * 4. 导出为 JPEG(quality: 0.85) + */ +async function exportEntranceImage( + canvas: WechatMiniprogram.Canvas, + ctx: CanvasRenderingContext2D, + paperConfig: { headerHeight: number; footerHeight: number; width: number; height: number } +): Promise { + // 裁剪参数 — 与 node-tools/processEntrancePicture.js 保持一致 + const cropTop = paperConfig.headerHeight; + const cropBottom = paperConfig.footerHeight; + const sourceW = paperConfig.width; + const sourceH = paperConfig.height - cropTop - cropBottom; + + const ENTRANCE_WIDTH = 600; + const scale = ENTRANCE_WIDTH / sourceW; + const targetH = Math.round(sourceH * scale); + + // 调整 Canvas 尺寸用于裁剪输出 + canvas.width = ENTRANCE_WIDTH; + canvas.height = targetH; + ctx.drawImage( + canvas, // 自身作为源(需先 toDataURL 再 loadImage,实际实现需用临时文件中转) + 0, cropTop, sourceW, sourceH, // 源区域:去掉页眉页脚 + 0, 0, ENTRANCE_WIDTH, targetH // 目标区域:缩放到标准宽度 + ); + + const tempPath = await canvasToTempFilePath(canvas, { + fileType: 'jpg', + quality: 0.85, + destWidth: ENTRANCE_WIDTH, + destHeight: targetH, + }); + + return tempPath; +} +``` + +> **实际实现说明**:小程序 Canvas 不能直接自引用 `drawImage`,需要先导出为临时文件(`canvasToTempFilePath`),再用 `canvas.createImage()` 加载临时文件,然后在清空的 Canvas 上绘制裁剪区域。具体实现参考 `preview-card` 已有的 `exportToTempFile` 方法。 + +### 3.4 云端上传 + +#### 3.4.1 图片上传到云存储 + +```typescript +async function uploadEntranceImage( + tempFilePath: string, + category: string, + id: string +): Promise { + const cloudPath = `assets/previews/${category}/${id}.jpg`; + const res = await wx.cloud.uploadFile({ + cloudPath, + filePath: tempFilePath, + }); + return res.fileID; // cloud://doodle-xxx/assets/previews/english/letter-tracing-daily-checkin.jpg +} +``` + +#### 3.4.2 元数据写入云数据库 + +```typescript +async function publishWorksheet(meta: PublishMeta, imageFileID: string): Promise { + const db = wx.cloud.database(); + const collection = db.collection('worksheets'); + + const doc = { + ...meta, + previewImage: imageFileID, + status: meta.status || 'draft', + publishedAt: db.serverDate(), + updatedAt: db.serverDate(), + version: 1, + publishedBy: 'debug', // 标记为 debug 发布 + }; + + // upsert:如果已存在则更新,不存在则创建 + const existing = await collection.where({ id: meta.id }).get(); + if (existing.data.length > 0) { + const oldDoc = existing.data[0]; + await collection.doc(oldDoc._id).update({ + data: { + ...doc, + version: (oldDoc.version || 0) + 1, + updatedAt: db.serverDate(), + }, + }); + } else { + await collection.add({ data: doc }); + } +} +``` + +### 3.5 发布流程整合 + +```typescript +/** + * Debug 发布入口 — 挂载在 pageMixin 上 + * 由预览页的「发布到云端」按钮触发 + */ +async function onDebugPublish(this: any): Promise { + if (!isDebugPublishEnabled()) return; + + // 1. 获取页面元数据 + const meta: PublishMeta = this.getPublishMeta(); + + // 2. 弹出确认面板(展示即将发布的数据,允许微调) + const confirmed = await showPublishConfirmDialog(meta); + if (!confirmed) return; + + wx.showLoading({ title: '发布中...' }); + try { + // 3. 导出入口图 + const tempPath = await exportEntranceImage( + this.canvas, this.ctx, this.paperConfig + ); + + // 4. 上传图片到云存储 + const fileID = await uploadEntranceImage( + tempPath, meta.category, meta.id + ); + + // 5. 写入云数据库 + await publishWorksheet(meta, fileID); + + wx.hideLoading(); + wx.showToast({ title: '发布成功 ✅', icon: 'success' }); + } catch (err) { + wx.hideLoading(); + wx.showModal({ + title: '发布失败', + content: JSON.stringify(err), + showCancel: false, + }); + } +} +``` + +--- + +## 四、云端数据与现有架构的衔接 + +### 4.1 数据加载优先级(保持不变) + +与 [技术架构设计文档](./技术架构设计文档.md) 第 6.3 节、[小程序云开发方案](./小程序云开发方案.md) 第五节一致: + +``` +优先级 1: 本地缓存(wx.Storage) +优先级 2: 云端拉取(worksheets 集合) ← debug 发布的数据在此 +优先级 3: 前端内置兜底(category.data.ts)← 保持稳定兜底 +``` + +### 4.2 category.data.ts 的角色变化 + +| 阶段 | category.data.ts 的作用 | +|------|------------------------| +| **当前** | 唯一数据源(硬编码所有题型信息) | +| **方案实施后** | 兜底数据源 + 离线保障(云端不可用时生效) | +| **长期** | 通过 `syncFromCloud` 脚本自动同步,保持与云端一致 | + +### 4.3 worksheets 集合字段映射 + +debug 发布写入的字段,与 [小程序云开发方案](./小程序云开发方案.md) §3.3 `worksheets` 集合的字段**完全对齐**: + +| PublishMeta 字段 | worksheets 集合字段 | 说明 | +|-----------------|-------------------|------| +| `id` | `id` | 业务唯一标识 | +| `title` | `title` | 显示标题 | +| `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` | 版本号(递增) | + +--- + +## 五、云存储目录规划 + +与 [小程序云开发方案](./小程序云开发方案.md) §6 一致: + +``` +cloud://doodle-xxx/ +├── assets/ +│ └── previews/ ← debug 发布的入口图存放于此 +│ ├── math/ +│ │ ├── number-find.jpg +│ │ ├── addition-10.jpg +│ │ └── ... +│ ├── english/ +│ │ ├── letter-tracing-single.jpg +│ │ ├── letter-tracing-daily-checkin.jpg +│ │ └── ... +│ ├── puzzle/ +│ ├── chinese/ +│ └── craft/ +``` + +命名规则:`.jpg`,与 `PublishMeta.id` 一致,便于查找和管理。 + +--- + +## 六、安全与权限控制 + +### 6.1 客户端门控 + +```typescript +// 三重保障 +const canPublish = + isDebugPublishEnabled() // ① envVersion === 'develop' + && isDevBypassLimits() // ② 与下载绕过逻辑一致 + && wx.getStorageSync('enableDebug'); // ③ debug 页面手动开启 +``` + +### 6.2 云端安全规则 + +在云数据库安全规则中,`worksheets` 集合限制写入权限: + +```json +{ + "worksheets": { + ".write": false, + ".read": true + } +} +``` + +debug 发布通过**云函数**中转,云函数内校验 `openId` 白名单: + +```javascript +// 云函数 debugPublish +exports.main = async (event, context) => { + const { OPENID } = cloud.getWXContext(); + const ADMIN_OPENIDS = ['开发者的openid']; + + if (!ADMIN_OPENIDS.includes(OPENID)) { + throw new Error('无权限执行此操作'); + } + + // 执行 upsert 操作... +}; +``` + +### 6.3 数据保护 + +| 措施 | 说明 | +|------|------| +| 默认 draft 状态 | 发布后默认 `status: 'draft'`,需手动激活为 `active` | +| 版本递增 | 每次更新 `version + 1`,可追踪变更历史 | +| publishedBy 标记 | `publishedBy: 'debug'` 区分来源 | +| 时间戳 | `publishedAt` / `updatedAt` 记录操作时间 | + +--- + +## 七、辅助工具 + +### 7.1 syncFromCloud 脚本 + +在 `node-tools/` 中新增脚本,发版前从云数据库拉取最新数据,同步回 `category.data.ts` 作为兜底数据。 + +``` +Debug 发布 → 云端数据 → 线上可见 + ↓ + syncFromCloud.js ← 发版前运行 + ↓ + category.data.ts 更新 ← 兜底数据同步 + ↓ + 下次发版包含 +``` + +```javascript +// node-tools/src/syncFromCloud.js(伪代码) +// 通过云开发 HTTP API 或管理端 SDK 拉取 worksheets 集合 +// 按 category 分组 → 生成 TypeScript 代码 → 写入 category.data.ts +``` + +### 7.2 Debug 管理页扩展 + +在已有的 `supportPages/debug/debug` 页面中增加 tab,提供简易内容管理: + +``` +┌─────────────────────────────────────────────┐ +│ Debug 工具页 │ +│ │ +│ [调试配置] [已发布内容] [系统信息] │ +│ │ +│ ┌─────────────────────────────────────┐ │ +│ │ 已发布内容列表 │ │ +│ │ │ │ +│ │ ● letter-tracing-single │ │ +│ │ 状态: active 版本: 3 英语 │ │ +│ │ [编辑] [下架] │ │ +│ │ │ │ +│ │ ● letter-tracing-daily-checkin │ │ +│ │ 状态: draft 版本: 1 英语 │ │ +│ │ [激活] [编辑] [删除] │ │ +│ │ │ │ +│ │ ● addition-10 │ │ +│ │ 状态: active 版本: 5 数学 │ │ +│ │ [编辑] [下架] │ │ +│ └─────────────────────────────────────┘ │ +└─────────────────────────────────────────────┘ +``` + +--- + +## 八、实施计划 + +### Phase 1:基础发布能力(约 0.5~1 天) + +- [ ] `pageMixin` 中新增 `getPublishMeta()` 接口约定 +- [ ] 实现 `isDebugPublishEnabled()` 环境判断 +- [ ] 实现入口图导出 + 裁剪逻辑 +- [ ] 实现云存储上传 + 云数据库写入 +- [ ] 在 `preview-card` 或 `preview-footer-actions` 中添加发布按钮(条件渲染) + +### Phase 2:确认面板与安全(约 0.5 天) + +- [ ] 发布前确认弹窗(数据预览 + 可编辑字段) +- [ ] 云函数安全校验(openId 白名单) +- [ ] 默认 draft 状态 + 版本管理 + +### Phase 3:数据消费侧适配(约 0.5~1 天) + +- [ ] `category.ts`(或 `worksheet-service`)增加从云 DB 拉取逻辑 +- [ ] 实现缓存策略(本地缓存 → 云端 → 内置兜底) +- [ ] 入口图从云存储 fileID 获取临时 URL 展示 + +### Phase 4:辅助工具(约 0.5 天) + +- [ ] `syncFromCloud.js` 脚本 +- [ ] Debug 管理页扩展(列表 + 状态管理) + +**总计预估:2~3 天** + +--- + +## 九、与模板引擎的衔接(远期) + +当 [技术架构设计文档](./技术架构设计文档.md) 第五章所述模板引擎落地后,debug 发布方案可进一步升级: + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 模板引擎 + Debug 发布(远期形态) │ +│ │ +│ ① 在通用 worksheet 页面中: │ +│ 选择模板 + 配置生成器参数 → 实时预览 │ +│ │ +│ ② 效果满意后点击「发布」: │ +│ 自动上传: │ +│ • JSON 配置(template + generator + generatorConfig + layout)│ +│ • 入口预览图 │ +│ • 元数据(title、tags、ageRange 等) │ +│ │ +│ ③ 新题型无需任何前端代码改动即可上线 │ +│ (前提:使用已有的 TemplateRenderer + Generator 组合) │ +│ │ +│ ④ 配合 80% 新题型可动态上线的目标 │ +│ debug 发布成为主要的内容上线方式 │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 十、风险与应对 + +| 风险 | 影响 | 应对 | +|------|------|------| +| **误操作发布测试数据** | 污染线上列表 | 默认 `status: 'draft'`;确认面板二次确认 | +| **入口图质量不一致** | 不同设备/DPR 下渲染差异 | 统一使用开发者工具发布;导出时固定 `destWidth/destHeight` | +| **云端数据丢失** | 已发布内容消失 | `category.data.ts` 兜底;`syncFromCloud` 定期同步 | +| **安全:非授权上传** | 恶意写入数据 | 云函数白名单校验;客户端三重门控 | +| **双数据源不一致** | 本地兜底与云端数据冲突 | 云端优先,发版前 `syncFromCloud` 对齐 | +| **云存储额度** | 图片累积占用存储 | 入口图约 50~200KB/张,100 张仅 ~20MB,远低于免费额度 | + +--- + +## 附录 A:与现有代码的关系 + +| 现有模块 | 本方案的关联 | +|---------|------------| +| `preview-card` 组件 | 复用 `exportToTempFile`,新增裁剪逻辑 | +| `pageMixin.ts` | 新增 `getPublishMeta()` 约定和 `onDebugPublish()` 方法 | +| `downloadPrint.ts` / `isDevBypassLimits()` | 复用环境判断模式 | +| `category.data.ts` | 角色从「唯一数据源」变为「兜底数据源」 | +| `supportPages/debug/debug` | 扩展内容管理 tab | +| `node-tools/processEntrancePicture.js` | 裁剪参数对齐;流程被 debug 发布替代 | +| 云数据库 `worksheets` 集合 | 写入发布数据(字段与云开发方案对齐) | +| 云存储 `assets/previews/` | 存放入口图 | diff --git a/miniprogram/assets/entrancePicture/english/letter-tracing-case-pairing.jpg b/miniprogram/assets/entrancePicture/english/letter-tracing-case-pairing.jpg index a6c5ba8..acc9037 100644 Binary files a/miniprogram/assets/entrancePicture/english/letter-tracing-case-pairing.jpg and b/miniprogram/assets/entrancePicture/english/letter-tracing-case-pairing.jpg differ diff --git a/miniprogram/assets/entrancePicture/english/letter-tracing-case-pairing2.jpg b/miniprogram/assets/entrancePicture/english/letter-tracing-case-pairing2.jpg new file mode 100644 index 0000000..a6c5ba8 Binary files /dev/null and b/miniprogram/assets/entrancePicture/english/letter-tracing-case-pairing2.jpg differ diff --git a/miniprogram/assets/entrancePicture/english/letter-tracing-daily-checkin.jpg b/miniprogram/assets/entrancePicture/english/letter-tracing-daily-checkin.jpg new file mode 100644 index 0000000..dc1f2b1 Binary files /dev/null and b/miniprogram/assets/entrancePicture/english/letter-tracing-daily-checkin.jpg differ diff --git a/miniprogram/assets/entrancePicture/english/letter-tracing-single.jpg b/miniprogram/assets/entrancePicture/english/letter-tracing-single.jpg index af10b90..71b7954 100644 Binary files a/miniprogram/assets/entrancePicture/english/letter-tracing-single.jpg and b/miniprogram/assets/entrancePicture/english/letter-tracing-single.jpg differ diff --git a/miniprogram/assets/entrancePicture/english/letter-tracing-single2.jpg b/miniprogram/assets/entrancePicture/english/letter-tracing-single2.jpg new file mode 100644 index 0000000..af10b90 Binary files /dev/null and b/miniprogram/assets/entrancePicture/english/letter-tracing-single2.jpg differ diff --git a/miniprogram/core/data/categories.ts b/miniprogram/core/data/categories.ts index 093ff18..f989d04 100644 --- a/miniprogram/core/data/categories.ts +++ b/miniprogram/core/data/categories.ts @@ -1,52 +1,27 @@ import type { CategoryType } from '../models/category'; -export const CATEGORIES: CategoryType[] = [ - { - id: 'math', - name: '数感启蒙', - icon: '📐', - color: '#e3f2fd', - gradient: ['#fff9c4', '#fff176'], - }, - { - id: 'chinese', - name: '趣味识字', - icon: '✏️', - color: '#e8f5e9', - gradient: ['#c8e6c9', '#a5d6a7'], - }, - { - id: 'english', - name: '英语启蒙', - icon: '🔤', - color: '#e0f7fa', - gradient: ['#b2ebf2', '#80deea'], - }, - // { - // id: 'focus', - // name: '专注力', - // icon: '🧩', - // color: '#f3e5f5', - // gradient: ['#e1bee7', '#ce93d8'], - // }, - { - id: 'puzzle', - name: '益智游戏', - icon: '🧩', - color: '#fff3e0', - gradient: ['#ffe0b2', '#ffcc80'], - }, - { - id: 'craft', - name: '创意手工', - icon: '🌈', - color: '#fce4ec', - gradient: ['#f8bbd0', '#f48fb1'], - }, +export const ALL_CATEGORY: CategoryType = { + id: 'all', + name: '全部', + icon: '📋', +}; + +export const CATEGORY_LIST: CategoryType[] = [ + { id: 'math', name: '数感启蒙', icon: '📐' }, + { id: 'puzzle', name: '益智游戏', icon: '🧩' }, + { id: 'pinyin', name: '汉语拼音', icon: '🔤' }, + { id: 'chinese', name: '趣味识字', icon: '✏️' }, + { id: 'english', name: '英语启蒙', icon: '🔤' }, + { id: 'craft', name: '创意手工', icon: '🌈' }, +]; + +export const CATEGORY_LIST_WITH_ALL: CategoryType[] = [ + ALL_CATEGORY, + ...CATEGORY_LIST, ]; export function getCategoryById(id: string): CategoryType | undefined { - return CATEGORIES.find((c) => c.id === id); + return CATEGORY_LIST_WITH_ALL.find((c) => c.id === id); } export function getCategoryName(id: string): string { diff --git a/miniprogram/core/models/category.ts b/miniprogram/core/models/category.ts index 3afea08..23fd7e4 100644 --- a/miniprogram/core/models/category.ts +++ b/miniprogram/core/models/category.ts @@ -1,13 +1,11 @@ -import type { WorksheetCategory } from './worksheet'; - /** Tab / 首页用的分类展示模型 */ export interface CategoryType { - id: WorksheetCategory; + id: string; name: string; icon: string; - color: string; - /** 分类用于背景或装饰的渐变色,数组中为渐变起止的两个颜色值 */ - gradient: [string, string]; + img?: string; + color?: string; + sortOrder?: number; } /** 云集合 `categories` 文档(与小程序云开发方案 §3.4 对齐) */ diff --git a/miniprogram/englishPages/letterTracing/draw/dailyCheckinDraw.ts b/miniprogram/englishPages/letterTracing/draw/dailyCheckinDraw.ts index 57fb620..0c30e62 100644 --- a/miniprogram/englishPages/letterTracing/draw/dailyCheckinDraw.ts +++ b/miniprogram/englishPages/letterTracing/draw/dailyCheckinDraw.ts @@ -14,7 +14,6 @@ import { drawClosedFourLineGrid, drawLetterInFourLineGrid, loadLetterFont, - type LetterFontSizes, } from '../../shared/draw/drawTools'; type DailyCheckinData = Extract< @@ -34,7 +33,7 @@ const SECTION_PADDING_TOP = 12; const SECTION_PADDING_BOTTOM = 10; // 标题与上一元素的垂直间距(px) -const HEADER_TITLE_GAP = 24; +const HEADER_TITLE_GAP = 32; // 标题与元信息(如日期、学号等)的间距(px) const HEADER_META_GAP = 22; // 正文内容距标题/元信息顶部的间距(px) @@ -163,6 +162,7 @@ export default class DailyCheckinDraw extends BaseDrawService { const cx = rect.x + rect.w / 2; const titleY = rect.y + SECTION_PADDING_TOP + 12; const metaY = titleY + HEADER_TITLE_GAP; + const metaUnderlineY = metaY + 5; this.ctx.save(); this.ctx.fillStyle = '#322E25'; @@ -172,23 +172,21 @@ export default class DailyCheckinDraw extends BaseDrawService { this.ctx.font = '20px serif'; this.ctx.fillText('每日打卡', cx, titleY); - this.ctx.font = '14px serif'; - this.ctx.fillText('月', rect.x + rect.w * 0.34, metaY); - this.ctx.fillText('日', rect.x + rect.w * 0.48, metaY); - this.ctx.fillText('姓名:', rect.x + rect.w * 0.7, metaY); + this.ctx.font = '12px serif'; + this.ctx.fillText('月', rect.x + 76, metaY); + this.ctx.fillText('日', rect.x + 122, metaY); + this.ctx.fillText('姓名:', rect.x + 176, metaY); this.ctx.strokeStyle = '#8E897E'; this.ctx.lineWidth = 1; this.ctx.setLineDash([]); this.ctx.beginPath(); - this.ctx.moveTo(rect.x + rect.w * 0.14, metaY + 4); - this.ctx.lineTo(rect.x + rect.w * 0.3, metaY + 4); - this.ctx.moveTo(rect.x + rect.w * 0.38, metaY + 4); - this.ctx.lineTo(rect.x + rect.w * 0.44, metaY + 4); - this.ctx.moveTo(rect.x + rect.w * 0.52, metaY + 4); - this.ctx.lineTo(rect.x + rect.w * 0.58, metaY + 4); - this.ctx.moveTo(rect.x + rect.w * 0.78, metaY + 4); - this.ctx.lineTo(rect.x + rect.w * 0.94, metaY + 4); + this.ctx.moveTo(rect.x + 40, metaUnderlineY); + this.ctx.lineTo(rect.x + 68, metaUnderlineY); + this.ctx.moveTo(rect.x + 85, metaUnderlineY); + this.ctx.lineTo(rect.x + 113, metaUnderlineY); + this.ctx.moveTo(rect.x + 195, metaUnderlineY); + this.ctx.lineTo(rect.x + 248, metaUnderlineY); this.ctx.stroke(); this.ctx.restore(); @@ -237,13 +235,13 @@ export default class DailyCheckinDraw extends BaseDrawService { (contentWidth - GRID_COL_GAP * (GRID_COLS - 1)) / GRID_COLS; const cellH = (contentHeight - GRID_ROW_GAP * (GRID_ROWS - 1)) / GRID_ROWS; - const pairGap = Math.min(16, cellW * 0.28); + const pairGap = 16; const regularSizes = calcLetterFontSizes( - cellH * 0.86, + cellH * 0.98, PRINT_CLEARLY_REGULAR, ); const dashedSizes = calcLetterFontSizes( - cellH * 0.86, + cellH * 0.98, PRINT_CLEARLY_DASHED, ); diff --git a/miniprogram/englishPages/letterTracing/letterTracing.wxml b/miniprogram/englishPages/letterTracing/letterTracing.wxml index 9da66ef..f477542 100644 --- a/miniprogram/englishPages/letterTracing/letterTracing.wxml +++ b/miniprogram/englishPages/letterTracing/letterTracing.wxml @@ -10,29 +10,6 @@ bind:canvas-ready="onCanvasReady" bind:favorite="onPreviewFavorite" /> - - - 练习类型 - - - - {{item.label}} - - - - 选择字母 @@ -123,6 +100,30 @@ custom-class="lt-shuffle-btn__icon" /> 换一批 + + + + 练习类型 + + + + {{item.label}} + + + + diff --git a/miniprogram/pages/category/category.data.ts b/miniprogram/pages/category/category.data.ts index a065861..e03b575 100644 --- a/miniprogram/pages/category/category.data.ts +++ b/miniprogram/pages/category/category.data.ts @@ -1,3 +1,5 @@ +import { CATEGORY_LIST } from '../../core/data/categories'; + export type CategoryItem = { id: string; title: string; @@ -58,92 +60,80 @@ function item(input: ItemInput): CategoryItem { return { ...input, likes, downloads, date: TODAY }; } -// prettier-ignore -export const CATEGORY_DATA: CategoryDataset = { - searchPlaceholder: '搜索练习纸...', - categories: [ - { - id: 'math', name: '数感启蒙', icon: '📐', - items: [ - 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 }), - ], - }, - { - id: 'pinyin', name: '汉语拼音', icon: '🔤', - items: [ - 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 }), - ], - }, - { - id: 'puzzle', name: '益智游戏', icon: '🧩', - items: [ - 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 }), - ], - }, - { - id: 'chinese', name: '趣味识字', icon: '✏️', - items: [ - 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 }), - ], - }, - { - id: 'english', name: '英语启蒙', icon: '🔤', - items: [ - 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-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 }), - ], - }, - { - id: 'craft', name: '创意手工', icon: '🌈', - items: [ - 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 }), - ], - }, +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 }), + ], + 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 }), + ], + 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 }), + ], + 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 }), + ], + 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 }), + ], + 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 }), ], }; + +export const CATEGORY_DATA: CategoryDataset = { + searchPlaceholder: '搜索练习纸...', + categories: CATEGORY_LIST.map((category) => ({ + ...category, + items: CATEGORY_ITEMS_BY_ID[category.id] ?? [], + })), +}; diff --git a/miniprogram/pages/category/category.less b/miniprogram/pages/category/category.less index 332ba48..b92dc81 100644 --- a/miniprogram/pages/category/category.less +++ b/miniprogram/pages/category/category.less @@ -134,8 +134,8 @@ } .cat-card__thumb { - width: 160rpx; - height: 180rpx; + width: 168rpx; + height: 210rpx; flex-shrink: 0; border-radius: 10rpx; display: flex; @@ -143,6 +143,7 @@ justify-content: center; overflow: hidden; border: 1rpx solid #bbb; + background: #f0f0f0; } .cat-card__thumb-img { diff --git a/miniprogram/pages/category/category.ts b/miniprogram/pages/category/category.ts index 9da57f5..600b470 100644 --- a/miniprogram/pages/category/category.ts +++ b/miniprogram/pages/category/category.ts @@ -1,3 +1,4 @@ +import { CATEGORY_LIST_WITH_ALL } from '../../core/data/categories'; import { CATEGORY_DATA, type CategoryItem } from './category.data'; type CategoryTab = { @@ -6,54 +7,37 @@ type CategoryTab = { icon: string; }; -type DisplayItem = CategoryItem & { - thumbBg: string; -}; +const SIDEBAR_TABS: CategoryTab[] = CATEGORY_LIST_WITH_ALL.map(({ id, name, icon }) => ({ + id, + name, + icon, +})); -const CATEGORY_BG: Record = { - math: '#fff3c4', - pinyin: '#d8f0d0', - puzzle: '#f5e6dc', - chinese: '#d8f0d0', - english: '#e8f4ff', - craft: '#fce4ec', -}; - -const ALL_TAB: CategoryTab = { id: 'all', name: '全部', icon: '📋' }; - -const SIDEBAR_TABS: CategoryTab[] = [ - ALL_TAB, - ...CATEGORY_DATA.categories.map((c) => ({ - id: c.id, - name: c.name, - icon: c.icon, - })), -]; - -function buildAllItems(): DisplayItem[] { - const items: DisplayItem[] = []; +function buildAllItems(): CategoryItem[] { + const items: CategoryItem[] = []; for (const group of CATEGORY_DATA.categories) { for (const item of group.items) { items.push({ ...item, - thumbBg: CATEGORY_BG[group.id] ?? '#f0e7d6', }); } } return items; } -function getItemsByCategory(categoryId: string): DisplayItem[] { +function getItemsByCategory(categoryId: string): CategoryItem[] { if (categoryId === 'all') return buildAllItems(); const group = CATEGORY_DATA.categories.find((c) => c.id === categoryId); if (!group) return []; return group.items.map((item) => ({ ...item, - thumbBg: CATEGORY_BG[group.id] ?? '#f0e7d6', })); } -function filterByKeyword(items: DisplayItem[], keyword: string): DisplayItem[] { +function filterByKeyword( + items: CategoryItem[], + keyword: string, +): CategoryItem[] { if (!keyword) return items; const kw = keyword.toLowerCase(); return items.filter( @@ -77,7 +61,7 @@ Page({ searchKeyword: '', sidebarTabs: SIDEBAR_TABS, activeCategoryId: 'all', - displayItems: buildAllItems() as DisplayItem[], + displayItems: buildAllItems() as CategoryItem[], scrollIntoViewId: '', }, diff --git a/miniprogram/pages/category/category.wxml b/miniprogram/pages/category/category.wxml index f0da4f2..c9298d8 100644 --- a/miniprogram/pages/category/category.wxml +++ b/miniprogram/pages/category/category.wxml @@ -57,9 +57,7 @@ data-title="{{item.title}}" data-available="{{item.available}}" bindtap="onTapItem"> - + ({ id, name }), +); const HOME_AGE_BANDS = AGE_BANDS.map((item, index) => ({ key: item.key,