From dc5e057cb391b6c61d1ec4ca5dd257751ecdc477 Mon Sep 17 00:00:00 2001 From: R524809 Date: Thu, 26 Mar 2026 17:33:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E7=94=9F=E4=BA=A7=E5=88=9D=E7=89=88?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cloudfunctions/doodle/index.js | 47 +-- docs/产品设计文档.md | 282 ++++++++++++++++-- miniprogram/app.json | 61 ++-- miniprogram/app.ts | 5 + miniprogram/assets/tabBar/icon-age-active.png | Bin 0 -> 191 bytes miniprogram/assets/tabBar/icon-age.png | Bin 0 -> 191 bytes miniprogram/assets/tabBar/icon-fav-active.png | Bin 0 -> 229 bytes miniprogram/assets/tabBar/icon-fav.png | Bin 0 -> 229 bytes .../assets/tabBar/icon-home-active.png | Bin 0 -> 219 bytes miniprogram/assets/tabBar/icon-home.png | Bin 0 -> 219 bytes .../assets/tabBar/icon-profile-active.png | Bin 0 -> 238 bytes miniprogram/assets/tabBar/icon-profile.png | Bin 0 -> 238 bytes miniprogram/base/callCloud.ts | 56 +--- .../shared/category-tabs/category-tabs.json | 4 + .../shared/category-tabs/category-tabs.less | 36 +++ .../shared/category-tabs/category-tabs.ts | 22 ++ .../shared/category-tabs/category-tabs.wxml | 15 + .../shared/detail-card/detail-card.json | 4 + .../shared/detail-card/detail-card.less | 104 +++++++ .../shared/detail-card/detail-card.ts | 39 +++ .../shared/detail-card/detail-card.wxml | 22 ++ .../shared/worksheet-card/worksheet-card.json | 4 + .../shared/worksheet-card/worksheet-card.less | 65 ++++ .../shared/worksheet-card/worksheet-card.ts | 36 +++ .../shared/worksheet-card/worksheet-card.wxml | 21 ++ miniprogram/constants/focusFunctions.ts | 139 +-------- miniprogram/constants/mathFunctions.ts | 232 +------------- miniprogram/core/data/categories.ts | 54 ++++ miniprogram/core/data/worksheets.ts | 156 ++++++++++ miniprogram/core/models/print-config.ts | 7 + miniprogram/core/models/worksheet.ts | 26 ++ miniprogram/core/utils/random.ts | 33 ++ miniprogram/pages/age/age.json | 8 + miniprogram/pages/age/age.less | 108 +++++++ miniprogram/pages/age/age.ts | 44 +++ miniprogram/pages/age/age.wxml | 31 ++ miniprogram/pages/category/category.json | 10 + miniprogram/pages/category/category.less | 81 +++++ miniprogram/pages/category/category.ts | 160 ++++++++++ miniprogram/pages/category/category.wxml | 58 ++++ miniprogram/pages/debug/debug.wxml | 6 - miniprogram/pages/favorites/favorites.json | 6 + miniprogram/pages/favorites/favorites.less | 88 ++++++ miniprogram/pages/favorites/favorites.ts | 27 ++ miniprogram/pages/favorites/favorites.wxml | 38 +++ miniprogram/pages/guide/guide.json | 6 + miniprogram/pages/guide/guide.less | 150 ++++++++++ miniprogram/pages/guide/guide.ts | 7 + miniprogram/pages/guide/guide.wxml | 63 ++++ miniprogram/pages/home/home.json | 11 + miniprogram/pages/home/home.less | 172 +++++++++++ miniprogram/pages/home/home.ts | 88 ++++++ miniprogram/pages/home/home.wxml | 105 +++++++ miniprogram/pages/profile/profile.json | 6 + miniprogram/pages/profile/profile.less | 115 +++++++ miniprogram/pages/profile/profile.ts | 46 +++ miniprogram/pages/profile/profile.wxml | 62 ++++ miniprogram/platform/canvas-adapter.ts | 92 ++++++ miniprogram/platform/cloud-adapter.ts | 70 +++++ miniprogram/platform/image-adapter.ts | 49 +++ miniprogram/platform/storage-adapter.ts | 29 ++ miniprogram/style/theme.less | 45 ++- tools/generate-tabbar-icons.js | 232 ++++++++++++++ 63 files changed, 2940 insertions(+), 543 deletions(-) create mode 100644 miniprogram/assets/tabBar/icon-age-active.png create mode 100644 miniprogram/assets/tabBar/icon-age.png create mode 100644 miniprogram/assets/tabBar/icon-fav-active.png create mode 100644 miniprogram/assets/tabBar/icon-fav.png create mode 100644 miniprogram/assets/tabBar/icon-home-active.png create mode 100644 miniprogram/assets/tabBar/icon-home.png create mode 100644 miniprogram/assets/tabBar/icon-profile-active.png create mode 100644 miniprogram/assets/tabBar/icon-profile.png create mode 100644 miniprogram/components/shared/category-tabs/category-tabs.json create mode 100644 miniprogram/components/shared/category-tabs/category-tabs.less create mode 100644 miniprogram/components/shared/category-tabs/category-tabs.ts create mode 100644 miniprogram/components/shared/category-tabs/category-tabs.wxml create mode 100644 miniprogram/components/shared/detail-card/detail-card.json create mode 100644 miniprogram/components/shared/detail-card/detail-card.less create mode 100644 miniprogram/components/shared/detail-card/detail-card.ts create mode 100644 miniprogram/components/shared/detail-card/detail-card.wxml create mode 100644 miniprogram/components/shared/worksheet-card/worksheet-card.json create mode 100644 miniprogram/components/shared/worksheet-card/worksheet-card.less create mode 100644 miniprogram/components/shared/worksheet-card/worksheet-card.ts create mode 100644 miniprogram/components/shared/worksheet-card/worksheet-card.wxml create mode 100644 miniprogram/core/data/categories.ts create mode 100644 miniprogram/core/data/worksheets.ts create mode 100644 miniprogram/core/models/print-config.ts create mode 100644 miniprogram/core/models/worksheet.ts create mode 100644 miniprogram/core/utils/random.ts create mode 100644 miniprogram/pages/age/age.json create mode 100644 miniprogram/pages/age/age.less create mode 100644 miniprogram/pages/age/age.ts create mode 100644 miniprogram/pages/age/age.wxml create mode 100644 miniprogram/pages/category/category.json create mode 100644 miniprogram/pages/category/category.less create mode 100644 miniprogram/pages/category/category.ts create mode 100644 miniprogram/pages/category/category.wxml create mode 100644 miniprogram/pages/favorites/favorites.json create mode 100644 miniprogram/pages/favorites/favorites.less create mode 100644 miniprogram/pages/favorites/favorites.ts create mode 100644 miniprogram/pages/favorites/favorites.wxml create mode 100644 miniprogram/pages/guide/guide.json create mode 100644 miniprogram/pages/guide/guide.less create mode 100644 miniprogram/pages/guide/guide.ts create mode 100644 miniprogram/pages/guide/guide.wxml create mode 100644 miniprogram/pages/home/home.json create mode 100644 miniprogram/pages/home/home.less create mode 100644 miniprogram/pages/home/home.ts create mode 100644 miniprogram/pages/home/home.wxml create mode 100644 miniprogram/pages/profile/profile.json create mode 100644 miniprogram/pages/profile/profile.less create mode 100644 miniprogram/pages/profile/profile.ts create mode 100644 miniprogram/pages/profile/profile.wxml create mode 100644 miniprogram/platform/canvas-adapter.ts create mode 100644 miniprogram/platform/cloud-adapter.ts create mode 100644 miniprogram/platform/image-adapter.ts create mode 100644 miniprogram/platform/storage-adapter.ts create mode 100644 tools/generate-tabbar-icons.js diff --git a/cloudfunctions/doodle/index.js b/cloudfunctions/doodle/index.js index 2f2ebcc..03e6ee0 100644 --- a/cloudfunctions/doodle/index.js +++ b/cloudfunctions/doodle/index.js @@ -1,7 +1,5 @@ const cloud = require('wx-server-sdk'); const getOpenId = require('./src/user/getOpenId'); -const { createArea } = require('./src/station/area'); -const { createStation } = require('./src/station/station'); const { createPDF } = require('./src/test/createPDF'); cloud.init({ @@ -15,12 +13,6 @@ exports.main = async (event, context) => { const res = await getOpenId(event, context); return res; } - case 'createArea': { - return await createArea(); - } - case 'createStation': { - return await createStation(); - } case 'createPDF': { return await createPDF(); } @@ -32,44 +24,7 @@ exports.main = async (event, context) => { }; } } catch (error) { - console.error('Error in user cloud Function:', error); + console.error('Error in cloud function:', error); return { code: 1, message: '内部服务器错误', data: {} }; } }; - -// const getOpenIdHandler = async (event, context) => { -// const wxContext = cloud.getWXContext(); -// return { openid: wxContext.OPENID }; -// }; - -// exports.main = responseMiddleware(getOpenId); -// exports.main = async (event, context) => { -// console.log('event.type-------:', event.type); -// debugger; -// switch (event.type) { -// case 'getOpenId': { -// debugger; -// return getOpenId(event, context); -// } -// } -// }; - -/*exports.main = async (event, context) => { - console.log('event.type-------:', event.type); - try { - switch (event.type) { - case 'getOpenId': - try { - const res = await getOpenId(event, context); - console.log('---------res-:', res); - return res; - } catch (error) { - console.error('Error in getOpenId:', error); - return { code: 1, message: '内部服务器错误', data: {} }; - } - default: - return { code: 1, message: '未知的事件类型', data: {} }; - } - } catch (error) {} -}; -*/ diff --git a/docs/产品设计文档.md b/docs/产品设计文档.md index 894ba91..a9ee78e 100644 --- a/docs/产品设计文档.md +++ b/docs/产品设计文档.md @@ -14,13 +14,13 @@ ### 1.1 内容层面 -| 问题 | 现状 | 影响 | -| -------- | ---------------------------------------------- | -------------------------------------------------------- | -| 品类单一 | 仅有数学计算、专注力训练、识字、练字四类 | 缺少英语启蒙、科学认知、艺术创造、生活常识等早教核心板块 | -| 题型趋同 | 数学 26 种题型多为数字计算变体,缺少生活化数学 | 用户容易审美疲劳,低龄段(3-5岁)可用内容少 | -| 缺乏体系 | 题型平铺排列,无年龄分级/难度递进 | 家长无法按孩子年龄/水平选择合适内容 | -| 无趣味化 | 缺少迷宫、涂色卡、手工折纸等高趣味性内容 | 与竞品(kiddoworksheets)差距大 | -| 内容静态 | 所有内容均为代码写死,每种题型需独立编写绘制代码 | 新增题型需要发版(1-2 周),无法快速响应热点/节日内容 | +| 问题 | 现状 | 影响 | +| -------- | ------------------------------------------------ | -------------------------------------------------------- | +| 品类单一 | 仅有数学计算、专注力训练、识字、练字四类 | 缺少英语启蒙、科学认知、艺术创造、生活常识等早教核心板块 | +| 题型趋同 | 数学 26 种题型多为数字计算变体,缺少生活化数学 | 用户容易审美疲劳,低龄段(3-5岁)可用内容少 | +| 缺乏体系 | 题型平铺排列,无年龄分级/难度递进 | 家长无法按孩子年龄/水平选择合适内容 | +| 无趣味化 | 缺少迷宫、涂色卡、手工折纸等高趣味性内容 | 与竞品(kiddoworksheets)差距大 | +| 内容静态 | 所有内容均为代码写死,每种题型需独立编写绘制代码 | 新增题型需要发版(1-2 周),无法快速响应热点/节日内容 | ### 1.2 用户体验层面 @@ -268,7 +268,235 @@ └─────────────────────────────────────┘ ``` -### 5.3 生成/预览页重设计 +### 5.3 分龄页「按年龄学」设计 + +**设计原则**:分龄页不是简单的年龄段入口页——那等同于给首页加了一层无意义的中间跳转。分龄页的核心价值是**帮家长回答「我家孩子该练什么」**,提供一张按发展阶段编排的「学习地图」。 + +**与首页的区别**: + +- 首页「发现」= **按内容品类浏览**(数学 / 语文 / 益智…),适合「我想找某类练习」的场景 +- 分龄「按年龄学」= **按孩子成长阶段推荐**(能力发展 + 学习路线),适合「不知道该练什么」的场景 + +#### 页面结构 + +``` +┌─────────────────────────────────────┐ +│ 🦆 按年龄学 │ +├─────────────────────────────────────┤ +│ │ +│ 👶 选择孩子年龄 │ +│ ┌─────┐ ┌─────┐ ┌─────┐ │ +│ │ 3-4 │ │ 4-5 │ │[5-6]│ ←── 横滑 │ +│ │ 岁 │ │ 岁 │ │ 岁 │ 选中高亮 │ +│ └─────┘ └─────┘ └─────┘ │ +│ ┌─────┐ ┌─────┐ │ +│ │ 6-7 │ │ 7-8 │ │ +│ │ 岁 │ │ 岁 │ │ +│ └─────┘ └─────┘ │ +│ │ +├── 以下为选中年龄段「5-6 岁」的内容 ──┤ +│ │ +│ 📋 这个阶段的能力目标 │ +│ ┌────────────────────────────┐ │ +│ │ 🔢 数感:20以内加减法、凑十破十 │ │ +│ │ ✏️ 书写:笔画描红、简单汉字 │ │ +│ │ 🧩 思维:方格推理、颜色规律 │ │ +│ │ 🎨 创造:格子仿画、涂色 │ │ +│ └────────────────────────────┘ │ +│ │ +│ 📌 推荐学习路线 │ ← 核心差异化 +│ ┌────────────────────────────┐ │ +│ │ 第 1 周 数学基础 │ │ +│ │ ├── 10以内分与合 [去练习 →] │ │ +│ │ ├── 10以内加法 [去练习 →] │ │ +│ │ └── 数一数连一连 [去练习 →] │ │ +│ │ │ │ +│ │ 第 2 周 进阶计算 │ │ +│ │ ├── 凑十法练习 [去练习 →] │ │ +│ │ ├── 比大小 [去练习 →] │ │ +│ │ └── 填缺少的数字 [去练习 →] │ │ +│ │ │ │ +│ │ 第 3 周 专注力训练 │ │ +│ │ ├── 颜色找规律 [去练习 →] │ │ +│ │ ├── 方格推理 [去练习 →] │ │ +│ │ └── 格子仿画 5x5 [去练习 →] │ │ +│ │ │ │ +│ │ 第 4 周 综合巩固 │ │ +│ │ ├── 10以内加减法 [去练习 →] │ │ +│ │ ├── 识字卡 [去练习 →] │ │ +│ │ └── 连连看 [去练习 →] │ │ +│ └────────────────────────────┘ │ +│ │ +│ 📚 该年龄段全部练习 │ ← 兜底浏览 +│ ┌──────────────────────────┐ │ +│ │ [数学] 共 15 项 ▾ │ │ +│ │ ┌─────┐ ┌─────┐ ┌─────┐│ │ +│ │ │10以内│ │凑十法│ │比大小││ │ +│ │ │ 加法 │ │ 练习 │ │ ││ │ +│ │ └─────┘ └─────┘ └─────┘│ │ +│ │ │ │ +│ │ [专注力] 共 8 项 ▾ │ │ +│ │ ┌─────┐ ┌─────┐ ┌─────┐│ │ +│ │ │颜色找│ │方格 │ │格子仿││ │ +│ │ │ 规律 │ │ 推理 │ │ 画 ││ │ +│ │ └─────┘ └─────┘ └─────┘│ │ +│ │ │ │ +│ │ [语文] 共 2 项 ▾ │ │ +│ │ ┌─────┐ ┌─────┐ │ │ +│ │ │识字卡│ │练字帖│ │ │ +│ │ └─────┘ └─────┘ │ │ +│ └──────────────────────────┘ │ +│ │ +└─────────────────────────────────────┘ +``` + +#### 页面三层信息架构 + +| 层次 | 模块 | 作用 | 数据来源 | +| ----------- | ---------------------- | -------------------------------------------------------- | -------------------------------------- | +| **第 1 层** | 年龄选择器 | 一目了然地切换年龄段,卡片式带可爱插画 | 固定 5 档:3-4 / 4-5 / 5-6 / 6-7 / 7-8 | +| **第 2 层** | 能力目标 + 学习路线 | 告诉家长「这个年龄该练什么、按什么顺序练」,解决选择焦虑 | 运营配置,按年龄段维护 4 周路线 | +| **第 3 层** | 全部练习(按品类折叠) | 想自由浏览的家长可以看到该年龄段所有可用内容 | `ALL_WORKSHEETS` 按 ageRange 筛选 | + +#### 各年龄段能力目标参考 + +| 年龄段 | 数学 | 语文 | 专注力/益智 | 创意 | +| ---------- | --------------------------------------- | ------------------- | -------------------------------- | ---------------- | +| **3-4 岁** | 数字认知 1-10、数物对应、形状认知 | 识字卡启蒙 | 连连看、线条识别、格子仿画 3x3 | 涂色卡 | +| **4-5 岁** | 数字排序、5 以内加法、比大小 | 识字卡、练字帖入门 | 颜色找规律、方位涂涂乐、图形配对 | 涂色卡、简单折纸 | +| **5-6 岁** | 10 以内加减法、凑十法、分与合 | 练字帖、拼音入门 | 方格推理、格子仿画 5x5、译码连线 | 涂色卡、手工 | +| **6-7 岁** | 20 以内加减法、破十法、100 以内运算入门 | 笔画/偏旁、看图说话 | 数独入门、迷宫、找不同 | 折纸、手工 | +| **7-8 岁** | 100 以内加减法、乘法表、混合运算 | 成语/古诗 | 高阶迷宫、数独 | 自制绘本 | + +#### 「学习路线」设计要点 + +1. **以 4 周为一个循环**:每周 3 个练习项,共 12 个。不求覆盖全部,而是给出一条**有节奏的推荐路径** +2. **跨品类编排**:不能只推数学,应穿插专注力、语文、创意,保持孩子兴趣 +3. **由易到难**:第 1 周入门、第 2 周进阶、第 3 周拓展、第 4 周综合巩固 +4. **运营可配置**:学习路线以 JSON 配置存储,运营可根据上新内容调整推荐 +5. **可扩展为「打印计划」**:未来结合用户体系,可记录完成进度,变成每日/每周打印计划 + +#### 学习路线数据结构 + +```typescript +interface AgeLearningPlan { + ageRange: [number, number]; // 如 [5, 6] + ageLabel: string; // "5-6 岁" + milestones: string[]; // 能力目标描述 + weeks: WeekPlan[]; // 4 周学习路线 +} + +interface WeekPlan { + week: number; // 1-4 + theme: string; // "数学基础" / "进阶计算" + worksheetIds: string[]; // 推荐的 worksheet id 列表 +} +``` + +#### 与其他页面的联动 + +| 场景 | 行为 | +| ------------------------ | ------------------------------------------ | +| 点击路线中的「去练习」 | 直接跳转到对应 worksheet 的生成/预览页 | +| 点击底部全部练习中的卡片 | 同上 | +| 首页分龄筛选 | 跳转到分龄页并自动选中对应年龄段 | +| 「我的」页面孩子信息 | 如已设置孩子年龄,分龄页自动选中对应年龄段 | + +#### 为什么不只做年龄入口? + +如果分龄页只是展示 5 个年龄段按钮,点进去看到一堆卡片列表——那它与「首页加一个年龄筛选器」完全等价,没有独占一个 Tab 的必要。**学习路线 + 能力目标**才是分龄页的不可替代价值: + +- 新用户打开 → 看到「5-6 岁该练凑十法、识字卡」→ 不再迷茫 +- 持续用户打开 → 按照 4 周路线逐步推进 → 形成使用习惯 +- 家长分享价值 → 「推荐你用这个,有按年龄的学习计划」→ 口碑传播 + +### 5.4 收藏页设计(Tab:收藏) + +**定位**:对应 TabBar 第三项「收藏」,是用户在**发现 / 分龄**之外最常用的「个人书架」:**一类**是还想再练、想对比的题型(收藏);**一类**是**已经生成并保存过**的练习记录(下载历史)。二者合在一页、用顶部 **Segment 切换**,避免「收藏在 A 页、历史在 B 页」的割裂感;与「我的」页的关系见下文联动说明。 + +**设计原则**: + +1. **一屏两件事,主次清晰**:默认优先展示「收藏的题型」(强意向);「下载历史」作为第二 Tab,满足「上次打的是哪个」的溯源需求。 +2. **列表即入口**:每一行都能 **一键回到该题型的生成/预览页**(带默认参数或上次参数,见技术实现约定),减少二次搜索。 +3. **轻量管理**:支持**取消收藏**、历史项**删除/清空**(需二次确认);不提供复杂文件夹,避免早教场景下的操作负担。 +4. **空状态友好**:用鸭丫 IP + 一句话引导去「发现」或「分龄」,与品牌文档中的空状态规范一致。 + +#### 页面结构(线框) + +``` +┌─────────────────────────────────────┐ +│ 我的收藏 │ ← 导航标题 +├─────────────────────────────────────┤ +│ ┌───────────────┬───────────────┐ │ +│ │ 收藏的题型 ✓ │ 下载历史 │ │ ← 分段控制(Segment) +│ └───────────────┴───────────────┘ │ +├─────────────────────────────────────┤ +│ [收藏的题型] Tab 下: │ +│ │ +│ ┌────────────────────────────────┐ │ +│ │ [缩略] 10以内加法 │ │ +│ │ 数学启蒙 · 4-6岁 · ⭐⭐ │ │ +│ │ 收藏于 3 天前 ❤️ │ │ ← 实心=已收藏,点此取消 +│ └────────────────────────────────┘ │ +│ ┌────────────────────────────────┐ │ +│ │ [缩略] 走迷宫 │ │ +│ │ 益智游戏 · 3-7岁 │ │ +│ │ 收藏于 5 天前 ❤️ │ │ +│ └────────────────────────────────┘ │ +│ │ +├─────────────────────────────────────┤ +│ [下载历史] Tab 下: │ +│ │ +│ ┌────────────────────────────────┐ │ +│ │ [缩略] 拼音描红 │ │ +│ │ 今天 14:32 已保存相册 │ │ +│ └────────────────────────────────┘ │ +│ ┌────────────────────────────────┐ │ +│ │ [缩略] 10以内加法 │ │ +│ │ 昨天 09:15 已保存相册 │ │ +│ └────────────────────────────────┘ │ +│ ··· 按时间倒序,可「清空」 │ │ +└─────────────────────────────────────┘ +``` + +#### 双 Tab 信息架构 + +| Tab | 模块 | 用户目标 | 列表字段建议 | 主操作 | +| -------------- | -------- | -------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------ | +| **收藏的题型** | 收藏列表 | 快速找回「标过心的」练习 | 预览缩略、标题、品类、适龄、难度、收藏时间 | 点击进入**生成/预览页**;点击 ❤ 取消收藏 | +| **下载历史** | 历史记录 | 复用「上次打过的那一张」的配置感 | 预览缩略、标题、下载时间、可选:题型元信息 | 点击进入同一题型的生成页;侧滑或菜单**删除单条**;顶部或底部**清空历史** | + +#### 交互与规则细化 + +| 场景 | 行为 | +| ---------------------- | ----------------------------------------------------------------------------------- | +| 从收藏进入生成页 | 跳转 `worksheetId` 对应页面;若产品已支持「记住上次参数」,则预填题量/范围等 | +| 从历史进入生成页 | 同上;可选展示轻提示「已为您加载上次常用设置」 | +| 取消收藏 | **即时反馈**(列表项移除或红心变空),必要时 toast「已取消收藏」 | +| 删除历史项 | 单条左滑删除或行内「删除」;清空历史需**弹窗确认** | +| 题型已下架/ id 失效 | 灰显条目,副文案「内容已更新,看看相似推荐」,提供跳转发现页或同类筛选 | +| 未登录(若未来接账号) | Phase 1 可仅本地缓存;接用户体系后收藏/历史**跨设备同步**,冲突策略以服务端时间为准 | + +#### 空状态 + +| Tab | 文案方向 | 主按钮 | +| ---------- | ------------------------------------------------------------ | ------------------------------ | +| 收藏的题型 | 「鸭丫还没找到收藏呢,去发现好玩的练习纸吧~」 | 跳转「发现」Tab | +| 下载历史 | 「还没有保存记录,生成一张练习纸并保存到相册后会出现在这里」 | 跳转「发现」或保留上次浏览分类 | + +#### 与「我的」页的关系(避免重复心智) + +- **Tab「收藏」**:完整功能页,含双 Segment + 列表,是**主入口**。 +- **「我的」中的「下载历史 / 我的收藏」**:建议作为**快捷入口**,点击后 **跳转到本 Tab 并自动选中对应 Segment**,而不是再维护一套独立列表页,降低开发与认知成本。 + +#### 数据要点(产品侧) + +| 实体 | 关键字段 | 说明 | +| ------ | -------------------------------------------------- | ---------------------------------------------------------------------------- | +| 收藏项 | `worksheetId`、`favoritedAt` | 与内容库 id 对齐,便于展示元数据 | +| 历史项 | `worksheetId`、`savedAt`、可选 `localAssetId`/快照 | 以「最近一次成功保存到相册」为准触发记录;是否存缩略图视包体积与隐私策略而定 | + +### 5.5 生成/预览页重设计 ``` ┌─────────────────────────────────────┐ @@ -301,7 +529,7 @@ └─────────────────────────────────────┘ ``` -### 5.4 「我的」页面(新增) +### 5.6 「我的」页面(新增) ``` ┌─────────────────────────────────────┐ @@ -436,13 +664,13 @@ **有了模板引擎后:** -| 场景 | 之前 | 之后 | -|------|------|------| -| 新增「50以内减法」 | 写代码 → 发版 | JSON 配置一条记录 → **即时上线** | +| 场景 | 之前 | 之后 | +| ------------------ | ----------------- | ---------------------------------------- | +| 新增「50以内减法」 | 写代码 → 发版 | JSON 配置一条记录 → **即时上线** | | 新增「恐龙涂色卡」 | 写绘制代码 → 发版 | 上传 SVG 素材 + JSON 配置 → **即时上线** | -| 新增「字母描红」 | 写代码 → 发版 | JSON 配置 + 字母数据 → **即时上线** | -| 新增「迷宫」 | 写迷宫算法 → 发版 | 需写新生成器 → **仍需发版** | -| 六一主题涂色卡 | 来不及开发 | 上传素材 + 配置 → **当天上线** | +| 新增「字母描红」 | 写代码 → 发版 | JSON 配置 + 字母数据 → **即时上线** | +| 新增「迷宫」 | 写迷宫算法 → 发版 | 需写新生成器 → **仍需发版** | +| 六一主题涂色卡 | 来不及开发 | 上传素材 + 配置 → **当天上线** | #### 对产品运营的影响 @@ -462,12 +690,12 @@ #### 内容运营节奏建议 -| 运营方式 | 频率 | 说明 | -|---------|------|------| -| 常规上新 | 每周 2-3 种 | 通过 JSON 配置新增题型变体或素材型内容 | -| 主题上新 | 每月 1 次 | 节日/季节主题内容包(涂色卡、手工模板等) | -| 热点响应 | 随时 | 追节日/热点快速上线应景内容 | -| 版本迭代 | 每 2-4 周 | 新排版模板、新生成算法、UI 改进等需发版内容 | +| 运营方式 | 频率 | 说明 | +| -------- | ----------- | ------------------------------------------- | +| 常规上新 | 每周 2-3 种 | 通过 JSON 配置新增题型变体或素材型内容 | +| 主题上新 | 每月 1 次 | 节日/季节主题内容包(涂色卡、手工模板等) | +| 热点响应 | 随时 | 追节日/热点快速上线应景内容 | +| 版本迭代 | 每 2-4 周 | 新排版模板、新生成算法、UI 改进等需发版内容 | ### 7.3 分包优化 @@ -506,14 +734,14 @@ interface WorksheetConfig { status: 'active' | 'draft' | 'hidden'; // ─── 模板引擎配置(核心新增)─── - template: TemplateType; // 使用哪个排版模板 - generator: GeneratorType; // 使用哪个数据生成器 - generatorConfig: Record;// 生成器参数 - layoutConfig: LayoutConfig; // 排版参数 - userConfigurable?: UserConfigField[];// 用户可调整的参数(渲染到设置面板) + template: TemplateType; // 使用哪个排版模板 + generator: GeneratorType; // 使用哪个数据生成器 + generatorConfig: Record; // 生成器参数 + layoutConfig: LayoutConfig; // 排版参数 + userConfigurable?: UserConfigField[]; // 用户可调整的参数(渲染到设置面板) // ─── 兼容旧架构 ─── - legacyPage?: string; // 旧页面路径(迁移过渡期使用) + legacyPage?: string; // 旧页面路径(迁移过渡期使用) } ``` diff --git a/miniprogram/app.json b/miniprogram/app.json index 872492a..dc0cce9 100644 --- a/miniprogram/app.json +++ b/miniprogram/app.json @@ -1,9 +1,15 @@ { "pages": [ - "pages/mathIndex/mathIndex", - "pages/focusIndex/focusIndex", + "pages/home/home", + "pages/age/age", + "pages/favorites/favorites", + "pages/profile/profile", + "pages/category/category", + "pages/guide/guide", "pages/index/index", "pages/copyBook/copyBook", + "pages/mathIndex/mathIndex", + "pages/focusIndex/focusIndex", "pages/debug/debug" ], "subPackages": [ @@ -50,19 +56,11 @@ } ], "preloadRule": { - "pages/index/index": { + "pages/home/home": { "network": "all", "packages": ["mathPages", "focusPages"] }, - "pages/mathIndex/mathIndex": { - "network": "all", - "packages": ["mathPages", "focusPages"] - }, - "pages/focusIndex/focusIndex": { - "network": "all", - "packages": ["mathPages", "focusPages"] - }, - "pages/copyBook/copyBook": { + "pages/category/category": { "network": "all", "packages": ["mathPages", "focusPages"] } @@ -87,33 +85,34 @@ } }, "tabBar": { - "color": "#323232", - "selectedColor": "#F5BC00", - "backgroundColor": "#fff", + "color": "#999999", + "selectedColor": "#333333", + "backgroundColor": "#ffffff", + "borderStyle": "white", "list": [ { - "pagePath": "pages/mathIndex/mathIndex", - "iconPath": "assets/tabBar/icon-math.png", - "selectedIconPath": "assets/tabBar/icon-math-active.png", - "text": "数感启蒙" + "pagePath": "pages/home/home", + "iconPath": "assets/tabBar/icon-home.png", + "selectedIconPath": "assets/tabBar/icon-home-active.png", + "text": "发现" }, { - "pagePath": "pages/focusIndex/focusIndex", - "iconPath": "assets/tabBar/icon-shape.png", - "selectedIconPath": "assets/tabBar/icon-shape-active.png", - "text": "专注力培养" + "pagePath": "pages/age/age", + "iconPath": "assets/tabBar/icon-age.png", + "selectedIconPath": "assets/tabBar/icon-age-active.png", + "text": "分龄" }, { - "pagePath": "pages/index/index", - "iconPath": "assets/tabBar/icon-word.png", - "selectedIconPath": "assets/tabBar/icon-word-active.png", - "text": "识字" + "pagePath": "pages/favorites/favorites", + "iconPath": "assets/tabBar/icon-fav.png", + "selectedIconPath": "assets/tabBar/icon-fav-active.png", + "text": "收藏" }, { - "pagePath": "pages/copyBook/copyBook", - "iconPath": "assets/tabBar/icon-edit.png", - "selectedIconPath": "assets/tabBar/icon-edit-active.png", - "text": "练字" + "pagePath": "pages/profile/profile", + "iconPath": "assets/tabBar/icon-profile.png", + "selectedIconPath": "assets/tabBar/icon-profile-active.png", + "text": "我的" } ] } diff --git a/miniprogram/app.ts b/miniprogram/app.ts index aab5995..b267f41 100644 --- a/miniprogram/app.ts +++ b/miniprogram/app.ts @@ -9,6 +9,11 @@ App({ printConfig: defaultPrintConfig, }, onLaunch() { + wx.cloud.init({ + env: 'cloud1-9gifs7a2756e2c87', + traceUser: true, + }); + const accountInfo = wx.getAccountInfoSync(); const env = accountInfo.miniProgram.envVersion || 'release'; /** diff --git a/miniprogram/assets/tabBar/icon-age-active.png b/miniprogram/assets/tabBar/icon-age-active.png new file mode 100644 index 0000000000000000000000000000000000000000..0d76788904b6e04a0b098ceeb314d92668131175 GIT binary patch literal 191 zcmeAS@N?(olHy`uVBq!ia0vp^fgsGm1|(PYdzk^LHcuDFkcwMxZ!~f;C~zbIRzAlpUC&7>K(sQ(C5MZ1%k2QI2M2T5SZ=a#2tATH@Lo~Tsf9yeiEj&s zppuKeSVvu<15-`2r)Hzq!Nf+8Scd>e1rCNP#B9$=ODbjKE#{p)CcDoA=pqJBS3j3^ HP62-Wr}`{jlR|obPmg z!^hZSA#G&E_Cevq41oiOCK#GluqwP%Ra~iRpsA|(QpF%sMN!jX3fO9p1D2eNz0T^K Uc>Rn{GSJ-&p00i_>zopr0CRd#SpWb4 literal 0 HcmV?d00001 diff --git a/miniprogram/assets/tabBar/icon-fav.png b/miniprogram/assets/tabBar/icon-fav.png new file mode 100644 index 0000000000000000000000000000000000000000..3d856bb1b877ecf4c1e1ade2b8f612fed7fd084f GIT binary patch literal 229 zcmeAS@N?(olHy`uVBq!ia0vp^fgsGm1|(PYdzk^LHJ&bxAr-gY-ZbPoV!*@T`1e11 z-P@(UlbaRTQ!>S_aM{d#%i%dm1%!4-XaAkI?9jgCkN23~+_@_BUjD$j*kp$H?e}+P z%RCTgF62r|J&>%R=)`h^Nl3}1LqO4~g+oxu#aygIz_y*Sh2x_AX@dh@6O#YWRTOLL z;bUyEkT$Yn`=D@QhQNVC6AVo&SQTEXDy~#D&{S1?sbY|+qNwRG1#C6Q0ZY!sW+j!r UoXEb85$J9PPgg&ebxsLQ0QaR(X8-^I literal 0 HcmV?d00001 diff --git a/miniprogram/assets/tabBar/icon-home-active.png b/miniprogram/assets/tabBar/icon-home-active.png new file mode 100644 index 0000000000000000000000000000000000000000..403d9af65c52bdf5701e2075738ac0b5bbc67fe5 GIT binary patch literal 219 zcmeAS@N?(olHy`uVBq!ia0vp^fgsGm1|(PYdzk^LMV>B>Ar-gYUfC#kK!JzFv1-x( zTW?N?Dm1HnOWW7TTgT%$Nd<%?XBFFth%xTJ7{S6K$--g8A&?@Va6-Xhf@6b1GlL@= zV;r|+XSwRL`3>O??-+7;cM5M%eZqMlY=Sytv_l$$4)09P4Js*a2ZAQdWISE3qSGOu p=+wd?sN{l;p$fGWXs_25?yYjux}VOT^B(A022WQ%mvv4FO#qRSLqY%m literal 0 HcmV?d00001 diff --git a/miniprogram/assets/tabBar/icon-home.png b/miniprogram/assets/tabBar/icon-home.png new file mode 100644 index 0000000000000000000000000000000000000000..db2ad2738ca60b459df920bf605572cee5ad9ce8 GIT binary patch literal 219 zcmeAS@N?(olHy`uVBq!ia0vp^fgsGm1|(PYdzk^LMV>B>Ar-gYUfC#kK!JzF@z?u? zthXmb6`EDPrR{6vt>f{Wqyj>cvx@CR#2EKqj9_7rWZ^L45J(YFIHBM$!LdQ1nZc2b zF^*fZvt0Gr{DyFccMLhaJB2r>KH)qNHbI>++98cWhj%9D29*@I13?pJGM=ti(diIS pbZX%cRC2+_P=#6wwAbqjcPx8*75fJt6`*SwJYD@<);T3K0RUUDK<)ql literal 0 HcmV?d00001 diff --git a/miniprogram/assets/tabBar/icon-profile-active.png b/miniprogram/assets/tabBar/icon-profile-active.png new file mode 100644 index 0000000000000000000000000000000000000000..687a52fad80e2065eb2bd59186d028b66a27aca0 GIT binary patch literal 238 zcmeAS@N?(olHy`uVBq!ia0vp^fgsGm1|(PYdzk^Lt)4E9Ar-gY-s;`%`+l{7iT?`z^s0f(5w_#t)|+KGPGqJ+>N(F7^Wesl_B)D#J8t@1y9jQ3d*Pvi~PC&6~%Y$uSnKQbWFBM!fWVBgcdv5Mu hMn$koR5bUoKYVz6W$FTBA)p5sJYD@<);T3K0RX0VR8{~0 literal 0 HcmV?d00001 diff --git a/miniprogram/assets/tabBar/icon-profile.png b/miniprogram/assets/tabBar/icon-profile.png new file mode 100644 index 0000000000000000000000000000000000000000..ba0d958f3bf5af9273703882779b52d7e098e0e9 GIT binary patch literal 238 zcmeAS@N?(olHy`uVBq!ia0vp^fgsGm1|(PYdzk^Lt)4E9Ar-gY-sH59xEz3wQvY5 zu@+Qv=@57up)l)F1IrHf$(LBPSUo@a{V6?NekQz{{g&Vg!Gc@|KGTJP!Jva9+ gqaxTPDw_M)EAwK@#4m5X4)g$nr>mdKI;Vst0E9GC=>Px# literal 0 HcmV?d00001 diff --git a/miniprogram/base/callCloud.ts b/miniprogram/base/callCloud.ts index 27282ea..d22cb9d 100644 --- a/miniprogram/base/callCloud.ts +++ b/miniprogram/base/callCloud.ts @@ -1,54 +1,6 @@ /** - * 调用云函数 - * @param funcName - * @param options - * @param typeName - * @returns + * 桥接文件 - 保持向后兼容 + * 实际实现已迁移到 platform/cloud-adapter.ts */ -export default async function callCloud( - action: string, - options?: Record, - // actionName?: string, -) { - try { - // const name = funcName - // const action = actionName || (options && options.action) || ''; - // if (funcName.indexOf('.') > -1) { - // const nameArr = funcName.split('.') || []; - // name = nameArr[0]; - // action = nameArr[1]; - // } - const name = 'robotaxi'; - - const { errMsg, result } = await wx.cloud.callFunction({ - name, - data: { - action, - ...options, - }, - }); - if (!/ok/.test(errMsg)) { - wx.showToast({ - title: '服务器错误', - icon: 'none', - duration: 2000, - }); - return { - code: '500', - info: '服务器繁忙', - message: errMsg, - }; - } else { - // const { code, info, data } = result; - // if (code === '401') { - // wx.showToast({ - // title: info, - // duration: 2000, - // }); - // } - return result; - } - } catch (e: any) { - return { code: '500', info: e.message || '服务器繁忙' }; - } -} +import { callCloud } from '../platform/cloud-adapter'; +export default callCloud; diff --git a/miniprogram/components/shared/category-tabs/category-tabs.json b/miniprogram/components/shared/category-tabs/category-tabs.json new file mode 100644 index 0000000..59a1db6 --- /dev/null +++ b/miniprogram/components/shared/category-tabs/category-tabs.json @@ -0,0 +1,4 @@ +{ + "component": true, + "usingComponents": {} +} diff --git a/miniprogram/components/shared/category-tabs/category-tabs.less b/miniprogram/components/shared/category-tabs/category-tabs.less new file mode 100644 index 0000000..151c82d --- /dev/null +++ b/miniprogram/components/shared/category-tabs/category-tabs.less @@ -0,0 +1,36 @@ +.category-tabs { + background: #fff; + width: 100%; +} + +.category-tabs__scroll { + width: 100%; + white-space: nowrap; +} + +.category-tabs__inner { + display: inline-flex; + flex-direction: row; + align-items: center; + gap: 16rpx; + padding: 16rpx 24rpx; + box-sizing: border-box; +} + +.category-tabs__pill { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 12rpx 32rpx; + border-radius: 32rpx; + background: #f5f5f5; + font-size: 28rpx; + color: rgba(0, 0, 0, 0.65); + flex-shrink: 0; +} + +.category-tabs__pill--active { + background: #f7ee47; + font-weight: 600; + color: rgba(0, 0, 0, 0.88); +} diff --git a/miniprogram/components/shared/category-tabs/category-tabs.ts b/miniprogram/components/shared/category-tabs/category-tabs.ts new file mode 100644 index 0000000..ee34094 --- /dev/null +++ b/miniprogram/components/shared/category-tabs/category-tabs.ts @@ -0,0 +1,22 @@ +Component({ + options: { + styleIsolation: 'apply-shared', + }, + properties: { + tabs: { + type: Array, + value: [], + }, + activeTab: { + type: String, + value: '', + }, + }, + methods: { + onTabTap(e: WechatMiniprogram.TouchEvent) { + const id = (e.currentTarget.dataset.id as string) || ''; + if (!id) return; + this.triggerEvent('change', { id }); + }, + }, +}); diff --git a/miniprogram/components/shared/category-tabs/category-tabs.wxml b/miniprogram/components/shared/category-tabs/category-tabs.wxml new file mode 100644 index 0000000..aa473f1 --- /dev/null +++ b/miniprogram/components/shared/category-tabs/category-tabs.wxml @@ -0,0 +1,15 @@ + + + + + {{item.name}} + + + + diff --git a/miniprogram/components/shared/detail-card/detail-card.json b/miniprogram/components/shared/detail-card/detail-card.json new file mode 100644 index 0000000..59a1db6 --- /dev/null +++ b/miniprogram/components/shared/detail-card/detail-card.json @@ -0,0 +1,4 @@ +{ + "component": true, + "usingComponents": {} +} diff --git a/miniprogram/components/shared/detail-card/detail-card.less b/miniprogram/components/shared/detail-card/detail-card.less new file mode 100644 index 0000000..aa3388f --- /dev/null +++ b/miniprogram/components/shared/detail-card/detail-card.less @@ -0,0 +1,104 @@ +.detail-card { + display: flex; + flex-direction: row; + align-items: stretch; + background: #fff; + border-radius: 32rpx; + box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.08); + overflow: hidden; + transition: transform 0.15s ease; +} + +.detail-card--active { + transform: scale(0.98); +} + +.detail-card__preview { + width: 240rpx; + min-height: 200rpx; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; +} + +.detail-card__preview-text { + font-size: 56rpx; + line-height: 1.2; + text-align: center; + padding: 16rpx; + color: rgba(0, 0, 0, 0.45); +} + +.detail-card__body { + flex: 1; + min-width: 0; + padding: 24rpx 24rpx 24rpx 16rpx; + display: flex; + flex-direction: column; + justify-content: center; + gap: 12rpx; +} + +.detail-card__title { + font-size: 28rpx; + font-weight: 600; + color: rgba(0, 0, 0, 0.88); + line-height: 1.4; +} + +.detail-card__meta { + display: flex; + flex-wrap: wrap; + gap: 8rpx; +} + +.detail-card__badge { + font-size: 20rpx; + line-height: 1.4; + padding: 4rpx 12rpx; + border-radius: 8rpx; +} + +.detail-card__badge--age { + background: #e3f2fd; + color: #1976d2; +} + +.detail-card__badge--diff { + background: #fff8e1; + color: #f57f17; +} + +.detail-card__stats { + font-size: 22rpx; + color: rgba(0, 0, 0, 0.45); +} + +.detail-card__actions { + display: flex; + flex-direction: row; + align-items: center; + gap: 16rpx; + margin-top: 8rpx; +} + +.detail-card__btn { + font-size: 24rpx; + line-height: 1.4; + padding: 12rpx 28rpx; + border-radius: 40rpx; + text-align: center; +} + +.detail-card__btn--primary { + background: #f7ee47; + color: rgba(0, 0, 0, 0.88); + font-weight: 600; +} + +.detail-card__btn--ghost { + background: #f5f5f5; + color: rgba(0, 0, 0, 0.65); +} diff --git a/miniprogram/components/shared/detail-card/detail-card.ts b/miniprogram/components/shared/detail-card/detail-card.ts new file mode 100644 index 0000000..db2edec --- /dev/null +++ b/miniprogram/components/shared/detail-card/detail-card.ts @@ -0,0 +1,39 @@ +Component({ + properties: { + title: { + type: String, + value: '', + }, + previewBg: { + type: String, + value: '#f5f5f5', + }, + previewText: { + type: String, + value: '', + }, + ageRange: { + type: String, + value: '', + }, + difficulty: { + type: String, + value: '', + }, + downloads: { + type: String, + value: '', + }, + }, + methods: { + onTap() { + this.triggerEvent('tap'); + }, + onGenerate() { + this.triggerEvent('generate'); + }, + onFav() { + this.triggerEvent('fav'); + }, + }, +}); diff --git a/miniprogram/components/shared/detail-card/detail-card.wxml b/miniprogram/components/shared/detail-card/detail-card.wxml new file mode 100644 index 0000000..67e69c3 --- /dev/null +++ b/miniprogram/components/shared/detail-card/detail-card.wxml @@ -0,0 +1,22 @@ + + + {{previewText}} + + + {{title}} + + {{ageRange}} + {{difficulty}} + + {{downloads}} + + 生成 + 收藏 + + + diff --git a/miniprogram/components/shared/worksheet-card/worksheet-card.json b/miniprogram/components/shared/worksheet-card/worksheet-card.json new file mode 100644 index 0000000..59a1db6 --- /dev/null +++ b/miniprogram/components/shared/worksheet-card/worksheet-card.json @@ -0,0 +1,4 @@ +{ + "component": true, + "usingComponents": {} +} diff --git a/miniprogram/components/shared/worksheet-card/worksheet-card.less b/miniprogram/components/shared/worksheet-card/worksheet-card.less new file mode 100644 index 0000000..79af27b --- /dev/null +++ b/miniprogram/components/shared/worksheet-card/worksheet-card.less @@ -0,0 +1,65 @@ +.worksheet-card { + background: #fff; + border-radius: 24rpx; + box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.08); + overflow: hidden; + transition: transform 0.15s ease; +} + +.worksheet-card--active { + transform: scale(0.96); +} + +.worksheet-card__preview { + position: relative; + height: 160rpx; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; +} + +.worksheet-card__img { + width: 100%; + height: 100%; +} + +.worksheet-card__preview-text { + font-size: 48rpx; + line-height: 1.2; + text-align: center; + padding: 0 24rpx; + color: rgba(0, 0, 0, 0.65); +} + +.worksheet-card__title { + padding: 20rpx 24rpx 12rpx; + font-size: 24rpx; + font-weight: 600; + color: rgba(0, 0, 0, 0.88); + line-height: 1.4; +} + +.worksheet-card__tags { + display: flex; + flex-wrap: wrap; + gap: 12rpx; + padding: 0 24rpx 24rpx; +} + +.worksheet-card__tag { + font-size: 20rpx; + line-height: 1.4; + padding: 6rpx 16rpx; + border-radius: 8rpx; +} + +.worksheet-card__tag--age { + background: #e3f2fd; + color: #1976d2; +} + +.worksheet-card__tag--diff { + background: #fff8e1; + color: #f57f17; +} diff --git a/miniprogram/components/shared/worksheet-card/worksheet-card.ts b/miniprogram/components/shared/worksheet-card/worksheet-card.ts new file mode 100644 index 0000000..0fccc7f --- /dev/null +++ b/miniprogram/components/shared/worksheet-card/worksheet-card.ts @@ -0,0 +1,36 @@ +Component({ + options: { + styleIsolation: 'apply-shared', + }, + properties: { + title: { + type: String, + value: '', + }, + previewBg: { + type: String, + value: '#f5f5f5', + }, + previewText: { + type: String, + value: '', + }, + ageRange: { + type: String, + value: '', + }, + difficulty: { + type: String, + value: '', + }, + img: { + type: String, + value: '', + }, + }, + methods: { + onTap() { + this.triggerEvent('tap'); + }, + }, +}); diff --git a/miniprogram/components/shared/worksheet-card/worksheet-card.wxml b/miniprogram/components/shared/worksheet-card/worksheet-card.wxml new file mode 100644 index 0000000..61f0d05 --- /dev/null +++ b/miniprogram/components/shared/worksheet-card/worksheet-card.wxml @@ -0,0 +1,21 @@ + + + + {{previewText}} + + {{title}} + + {{ageRange}} + {{difficulty}} + + diff --git a/miniprogram/constants/focusFunctions.ts b/miniprogram/constants/focusFunctions.ts index 7570b23..207d2a7 100644 --- a/miniprogram/constants/focusFunctions.ts +++ b/miniprogram/constants/focusFunctions.ts @@ -1,131 +1,8 @@ -export interface FocusFunctionType { - id: string; - page?: string; - title: string; - desc: string; - icon: string; - mode?: string; // 用于传递默认模式参数,如 '3x3', '5x5', '7x7' - img?: string; -} - -export const FOCUS_FUNCTION_TYPES: FocusFunctionType[] = [ - // 根据颜色画图形 - { - id: 'color-shape-match', - page: 'colorShapeMatch', - title: '根据颜色画图形', - desc: '根据颜色画出对应的图形', - icon: '🎯', - img: '/assets/focusEntrance/color-shape-match.png', - }, - // 图形符号配对 - { - id: 'shape-symbol', - page: 'shapeSymbol', - title: '图形符号配对', - desc: '根据图形画对应的符号', - icon: '🔗', - img: '/assets/focusEntrance/shape-symbol.png', - }, - // 形状识别 - { - id: 'shape-recognition', - page: 'shape', - title: '识别形状', - desc: '识别形状,涂一涂', - icon: '🔍', - img: '/assets/focusEntrance/shape-recognition.png', - }, - // 方位涂涂乐 - { - id: 'position-coloring', - page: 'positionColoring', - title: '方位涂涂乐', - desc: '观察卡片位置,在对应的方格中涂上颜色', - icon: '📍', - img: '/assets/focusEntrance/position-coloring.png', - }, - // 颜色找规律 - { - id: 'color-pattern', - page: 'colorPattern', - title: '颜色找规律', - desc: '观察颜色规律,在空白图形中涂上颜色', - icon: '🎨', - img: '/assets/focusEntrance/color-pattern.png', - }, - // 连连看 - { - id: 'match-connect', - page: 'matchConnect', - title: '连连看', - desc: '快来根据物品连一连吧!', - icon: '🔗', - img: '/assets/focusEntrance/match-connect.png', - }, - - // 线条识别 - { - id: 'line-recognition', - page: 'lineRecognition', - title: '线条识别', - desc: '认识不同线条,画出颜色对应的线条', - icon: '📏', - img: '/assets/focusEntrance/line-recognition.png', - }, - // 方格推理 - { - id: 'grid-reasoning', - page: 'gridReasoning', - title: '方格推理', - desc: '仔细观察,推理出合并方格并连线', - icon: '🧩', - img: '/assets/focusEntrance/grid-reasoning.png', - }, - // 译码连线 - { - id: 'code-connect', - page: 'codeConnect', - title: '译码连线', - desc: '按照数字顺序,将数字对应的颜色连线', - icon: '🔢', - img: '/assets/focusEntrance/code-connect.png', - }, - // 数字点连线 - { - id: 'dot-connect', - page: 'dotConnect', - title: '数字点连线', - desc: '按数字顺序连点', - icon: '🔗', - img: '/assets/focusEntrance/dot-connect.png', - }, - // 格子仿画 - { - id: 'grid-drawing-3x3', - page: 'gridDrawing', - title: '格子仿画 3x3', - desc: '🎯 简单有趣,培养专注力', - icon: '🎨', - mode: '3x3', - img: '/assets/focusEntrance/grid-drawing-3x3.png', - }, - { - id: 'grid-drawing-5x5', - page: 'gridDrawing', - title: '格子仿画 5x5', - desc: '✨ 创意挑战,提升观察力', - icon: '🎨', - mode: '5x5', - img: '/assets/focusEntrance/grid-drawing-5x5.png', - }, - { - id: 'grid-drawing-7x7', - page: 'gridDrawing', - title: '格子仿画 7x7', - desc: '🌟 大师挑战,锻炼耐心', - icon: '🎨', - mode: '7x7', - img: '/assets/focusEntrance/grid-drawing-7x7.png', - }, -]; +/** + * 桥接文件 - 保持向后兼容 + * 实际数据已迁移到 core/data/worksheets.ts + */ +export { + FOCUS_FUNCTION_TYPES, + type FocusFunctionType, +} from '../core/data/worksheets'; diff --git a/miniprogram/constants/mathFunctions.ts b/miniprogram/constants/mathFunctions.ts index adfe312..544b898 100644 --- a/miniprogram/constants/mathFunctions.ts +++ b/miniprogram/constants/mathFunctions.ts @@ -1,227 +1,5 @@ -export interface MathFunctionType { - id: string; - page?: string; - title: string; - desc: string; - icon?: string; - img?: string; -} - -export const MATH_FUNCTION_TYPES: MathFunctionType[] = [ - { - id: 'number-sort', - page: 'numberSort', - title: '数字排序', - desc: '数字排序,写出正确顺序', - icon: '🔢', - img: '/assets/mathEntrance/number-sort.png', - }, - { - id: 'one-digit-addition', - page: 'oneDigitAddition', - title: '一位数加法', - desc: '通过圆点学习一位数加法运算', - icon: '➕', - img: '/assets/mathEntrance/one-digit-addition.png', - }, - { - id: 'make-ten', - page: 'makeTen', - title: '凑十法练习', - desc: '通过凑十法学习20以内进位加法', - icon: '➕', - img: '/assets/mathEntrance/make-ten.png', - }, - { - id: 'break-ten', - page: 'breakTen', - title: '破十法练习', - desc: '通过破十法学习20以内退位减法', - icon: '➖', - img: '/assets/mathEntrance/break-ten.png', - }, - { - id: 'flat-ten', - page: 'flatTen', - title: '平十法练习', - desc: '通过平十法学习20以内退位减法', - icon: '➖', - img: '/assets/mathEntrance/flat-ten.png', - }, - { - id: 'borrow-ten', - page: 'borrowTen', - title: '借十法练习', - desc: '通过借十法学习20以上退位减法', - icon: '➖', - img: '/assets/mathEntrance/borrow-ten.png', - }, - { - id: 'number-decompose-20', - page: 'numberDecompose', - title: '20以内数的分与合', - desc: '把数字分一分,合一合', - icon: '🔢', - img: '/assets/mathEntrance/number-decompose-20.png', - }, - { - id: 'number-decompose', - page: 'numberDecompose', - title: '10以内数的分与合', - desc: '把数字分一分,合一合', - icon: '🔢', - img: '/assets/mathEntrance/number-decompose.png', - }, - // 第一阶段:认识数字(最基础) - { - id: 'number-find', - page: 'numberFind', - title: '找数字,涂一涂', - desc: '找一找下面相同的数字,涂上颜色', - // icon: '🔍', - img: '/assets/mathEntrance/number-find.png', - }, - { - id: 'number-write', - page: 'numberFind', - title: '看数字,写一写', - desc: '看数字,按笔画顺序写一写', - // icon: '✏️', - img: '/assets/mathEntrance/number-write.png', - }, - { - id: 'number-coloring', - page: 'countMatch', - title: '按数字,涂颜色', - desc: '按数字给相应的圆圈涂上颜色', - icon: '🎨', - img: '/assets/mathEntrance/number-coloring.png', - }, - // 第二阶段:数数与计数 - { - id: 'counting-matching', - page: 'countMatch', - title: '数一数,连一连', - desc: '通过连线配对数字和对应的数量图形', - icon: '🔗', - img: '/assets/mathEntrance/count-match.png', - }, - // 数物对应 - { - id: 'number-object-match', - page: 'numberObjectMatch', - title: '数物连线', - desc: '连线相同数量的物品和数字', - icon: '🔗', - img: '/assets/mathEntrance/number-object-match.png', - }, - { - id: 'number-object-fill', - page: 'numberObjectMatch', - title: '数物填写', - desc: '数一数物品数量,填写对应的数字', - icon: '✏️', - img: '/assets/mathEntrance/number-object-fill.png', - }, - { - id: 'counting-select', - page: 'countingSelect', - title: '数一数,选一选', - desc: '数出物品数量,圈出正确答案', - icon: '✓', - img: '/assets/mathEntrance/counting-select.png', - }, - { - id: 'counting-fill', - page: 'countingSelect', - title: '数一数,填一填', - desc: '数出物品数量,填写对应的数字', - icon: '✏️', - img: '/assets/mathEntrance/counting-fill.png', - }, - { - id: 'compare', - page: 'compare', - title: '数一数,比大小', - desc: '比较数量大小,在 ⭕️ 中填入>、<、=', - icon: '⚖️', - img: '/assets/mathEntrance/compare.png', - }, - // 第三阶段:数字序列 - { - id: 'missing-number', - page: 'missingNumber', - title: '填上缺少的数字', - desc: '在数字序列中找出并填写缺失的数字', - icon: '❓', - img: '/assets/mathEntrance/missing-number.png', - }, - - // 第四阶段:简单运算(从易到难) - { - id: 'addition-5', - page: 'addition', - title: '5以内加法', - desc: '练习5以内的加法运算,图形化展示', - icon: '➕', - img: '/assets/mathEntrance/addition-5.png', - }, - { - id: 'addition-10', - page: 'addition', - title: '10以内加法', - desc: '练习10以内的加法运算,图形化展示', - icon: '➕', - img: '/assets/mathEntrance/addition-10.png', - }, - { - id: 'subtraction-10', - page: 'addition', - title: '10以内减法', - desc: '练习10以内的减法运算,图形化展示', - icon: '➖', - img: '/assets/mathEntrance/subtraction-10.png', - }, - { - id: 'addition-subtraction-10', - page: 'addition', - title: '10以内加减法', - desc: '练习10以内的加法和减法混合运算', - icon: '±', - img: '/assets/mathEntrance/addition-subtraction-10.png', - }, - // 计算练习题 - { - id: 'practice-addition', - page: 'calculationPractice', - title: '加法运算', - desc: '练习10/20/50/100以内加法运算', - icon: '➕', - img: '/assets/mathEntrance/practice-addition.png', - }, - { - id: 'practice-subtraction', - page: 'calculationPractice', - title: '减法运算', - desc: '练习10/20/50/100以内减法运算', - icon: '➖', - img: '/assets/mathEntrance/practice-subtraction.png', - }, - { - id: 'practice-mixed', - page: 'calculationPractice', - title: '混合运算', - desc: '练习10/20/50/100以内加减法混合运算', - icon: '±', - img: '/assets/mathEntrance/practice-mixed.png', - }, - // 九九乘法表 - { - id: 'multiplication-table', - page: 'multiplicationTable', - title: '九九乘法表', - desc: '学习九九乘法口诀', - icon: '✖️', - img: '/assets/mathEntrance/multiplication-table.png', - }, -]; +/** + * 桥接文件 - 保持向后兼容 + * 实际数据已迁移到 core/data/worksheets.ts + */ +export { MATH_FUNCTION_TYPES, type MathFunctionType } from '../core/data/worksheets'; diff --git a/miniprogram/core/data/categories.ts b/miniprogram/core/data/categories.ts new file mode 100644 index 0000000..6eb62c7 --- /dev/null +++ b/miniprogram/core/data/categories.ts @@ -0,0 +1,54 @@ +import { CategoryType } from '../models/worksheet'; + +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 function getCategoryById(id: string): CategoryType | undefined { + return CATEGORIES.find((c) => c.id === id); +} + +export function getCategoryName(id: string): string { + return getCategoryById(id)?.name || id; +} diff --git a/miniprogram/core/data/worksheets.ts b/miniprogram/core/data/worksheets.ts new file mode 100644 index 0000000..8cab68b --- /dev/null +++ b/miniprogram/core/data/worksheets.ts @@ -0,0 +1,156 @@ +import { WorksheetType } from '../models/worksheet'; + +/** + * MathFunctionType - 保持向后兼容 + */ +export interface MathFunctionType { + id: string; + page?: string; + title: string; + desc: string; + icon?: string; + img?: string; +} + +/** + * FocusFunctionType - 保持向后兼容 + */ +export interface FocusFunctionType { + id: string; + page?: string; + title: string; + desc: string; + icon: string; + mode?: string; + img?: string; +} + +// prettier-ignore +export const MATH_FUNCTION_TYPES: MathFunctionType[] = [ + { id: 'number-sort', page: 'numberSort', title: '数字排序', desc: '数字排序,写出正确顺序', icon: '🔢', img: '/assets/mathEntrance/number-sort.png' }, + { id: 'one-digit-addition', page: 'oneDigitAddition', title: '一位数加法', desc: '通过圆点学习一位数加法运算', icon: '➕', img: '/assets/mathEntrance/one-digit-addition.png' }, + { id: 'make-ten', page: 'makeTen', title: '凑十法练习', desc: '通过凑十法学习20以内进位加法', icon: '➕', img: '/assets/mathEntrance/make-ten.png' }, + { id: 'break-ten', page: 'breakTen', title: '破十法练习', desc: '通过破十法学习20以内退位减法', icon: '➖', img: '/assets/mathEntrance/break-ten.png' }, + { id: 'flat-ten', page: 'flatTen', title: '平十法练习', desc: '通过平十法学习20以内退位减法', icon: '➖', img: '/assets/mathEntrance/flat-ten.png' }, + { id: 'borrow-ten', page: 'borrowTen', title: '借十法练习', desc: '通过借十法学习20以上退位减法', icon: '➖', img: '/assets/mathEntrance/borrow-ten.png' }, + { id: 'number-decompose-20', page: 'numberDecompose', title: '20以内数的分与合', desc: '把数字分一分,合一合', icon: '🔢', img: '/assets/mathEntrance/number-decompose-20.png' }, + { id: 'number-decompose', page: 'numberDecompose', title: '10以内数的分与合', desc: '把数字分一分,合一合', icon: '🔢', img: '/assets/mathEntrance/number-decompose.png' }, + { id: 'number-find', page: 'numberFind', title: '找数字,涂一涂', desc: '找一找下面相同的数字,涂上颜色', img: '/assets/mathEntrance/number-find.png' }, + { id: 'number-write', page: 'numberFind', title: '看数字,写一写', desc: '看数字,按笔画顺序写一写', img: '/assets/mathEntrance/number-write.png' }, + { id: 'number-coloring', page: 'countMatch', title: '按数字,涂颜色', desc: '按数字给相应的圆圈涂上颜色', icon: '🎨', img: '/assets/mathEntrance/number-coloring.png' }, + { id: 'counting-matching', page: 'countMatch', title: '数一数,连一连', desc: '通过连线配对数字和对应的数量图形', icon: '🔗', img: '/assets/mathEntrance/count-match.png' }, + { id: 'number-object-match', page: 'numberObjectMatch', title: '数物连线', desc: '连线相同数量的物品和数字', icon: '🔗', img: '/assets/mathEntrance/number-object-match.png' }, + { id: 'number-object-fill', page: 'numberObjectMatch', title: '数物填写', desc: '数一数物品数量,填写对应的数字', icon: '✏️', img: '/assets/mathEntrance/number-object-fill.png' }, + { id: 'counting-select', page: 'countingSelect', title: '数一数,选一选', desc: '数出物品数量,圈出正确答案', icon: '✓', img: '/assets/mathEntrance/counting-select.png' }, + { id: 'counting-fill', page: 'countingSelect', title: '数一数,填一填', desc: '数出物品数量,填写对应的数字', icon: '✏️', img: '/assets/mathEntrance/counting-fill.png' }, + { id: 'compare', page: 'compare', title: '数一数,比大小', desc: '比较数量大小,在 ⭕️ 中填入>、<、=', icon: '⚖️', img: '/assets/mathEntrance/compare.png' }, + { id: 'missing-number', page: 'missingNumber', title: '填上缺少的数字', desc: '在数字序列中找出并填写缺失的数字', icon: '❓', img: '/assets/mathEntrance/missing-number.png' }, + { id: 'addition-5', page: 'addition', title: '5以内加法', desc: '练习5以内的加法运算,图形化展示', icon: '➕', img: '/assets/mathEntrance/addition-5.png' }, + { id: 'addition-10', page: 'addition', title: '10以内加法', desc: '练习10以内的加法运算,图形化展示', icon: '➕', img: '/assets/mathEntrance/addition-10.png' }, + { id: 'subtraction-10', page: 'addition', title: '10以内减法', desc: '练习10以内的减法运算,图形化展示', icon: '➖', img: '/assets/mathEntrance/subtraction-10.png' }, + { id: 'addition-subtraction-10', page: 'addition', title: '10以内加减法', desc: '练习10以内的加法和减法混合运算', icon: '±', img: '/assets/mathEntrance/addition-subtraction-10.png' }, + { id: 'practice-addition', page: 'calculationPractice', title: '加法运算', desc: '练习10/20/50/100以内加法运算', icon: '➕', img: '/assets/mathEntrance/practice-addition.png' }, + { id: 'practice-subtraction', page: 'calculationPractice', title: '减法运算', desc: '练习10/20/50/100以内减法运算', icon: '➖', img: '/assets/mathEntrance/practice-subtraction.png' }, + { id: 'practice-mixed', page: 'calculationPractice', title: '混合运算', desc: '练习10/20/50/100以内加减法混合运算', icon: '±', img: '/assets/mathEntrance/practice-mixed.png' }, + { id: 'multiplication-table', page: 'multiplicationTable', title: '九九乘法表', desc: '学习九九乘法口诀', icon: '✖️', img: '/assets/mathEntrance/multiplication-table.png' }, +]; + +// prettier-ignore +export const FOCUS_FUNCTION_TYPES: FocusFunctionType[] = [ + { id: 'color-shape-match', page: 'colorShapeMatch', title: '根据颜色画图形', desc: '根据颜色画出对应的图形', icon: '🎯', img: '/assets/focusEntrance/color-shape-match.png' }, + { id: 'shape-symbol', page: 'shapeSymbol', title: '图形符号配对', desc: '根据图形画对应的符号', icon: '🔗', img: '/assets/focusEntrance/shape-symbol.png' }, + { id: 'shape-recognition', page: 'shape', title: '识别形状', desc: '识别形状,涂一涂', icon: '🔍', img: '/assets/focusEntrance/shape-recognition.png' }, + { id: 'position-coloring', page: 'positionColoring', title: '方位涂涂乐', desc: '观察卡片位置,在对应的方格中涂上颜色', icon: '📍', img: '/assets/focusEntrance/position-coloring.png' }, + { id: 'color-pattern', page: 'colorPattern', title: '颜色找规律', desc: '观察颜色规律,在空白图形中涂上颜色', icon: '🎨', img: '/assets/focusEntrance/color-pattern.png' }, + { id: 'match-connect', page: 'matchConnect', title: '连连看', desc: '快来根据物品连一连吧!', icon: '🔗', img: '/assets/focusEntrance/match-connect.png' }, + { id: 'line-recognition', page: 'lineRecognition', title: '线条识别', desc: '认识不同线条,画出颜色对应的线条', icon: '📏', img: '/assets/focusEntrance/line-recognition.png' }, + { id: 'grid-reasoning', page: 'gridReasoning', title: '方格推理', desc: '仔细观察,推理出合并方格并连线', icon: '🧩', img: '/assets/focusEntrance/grid-reasoning.png' }, + { id: 'code-connect', page: 'codeConnect', title: '译码连线', desc: '按照数字顺序,将数字对应的颜色连线', icon: '🔢', img: '/assets/focusEntrance/code-connect.png' }, + { id: 'dot-connect', page: 'dotConnect', title: '数字点连线', desc: '按数字顺序连点', icon: '🔗', img: '/assets/focusEntrance/dot-connect.png' }, + { id: 'grid-drawing-3x3', page: 'gridDrawing', title: '格子仿画 3x3', desc: '🎯 简单有趣,培养专注力', icon: '🎨', mode: '3x3', img: '/assets/focusEntrance/grid-drawing-3x3.png' }, + { id: 'grid-drawing-5x5', page: 'gridDrawing', title: '格子仿画 5x5', desc: '✨ 创意挑战,提升观察力', icon: '🎨', mode: '5x5', img: '/assets/focusEntrance/grid-drawing-5x5.png' }, + { id: 'grid-drawing-7x7', page: 'gridDrawing', title: '格子仿画 7x7', desc: '🌟 大师挑战,锻炼耐心', icon: '🎨', mode: '7x7', img: '/assets/focusEntrance/grid-drawing-7x7.png' }, +]; + +/** + * 统一的 WorksheetType 列表 - 合并所有分类 + * 用于新版首页和分类页展示 + */ +// prettier-ignore +export const ALL_WORKSHEETS: WorksheetType[] = [ + // === 数学 === + { id: 'number-sort', title: '数字排序', desc: '数字排序,写出正确顺序', category: 'math', page: 'numberSort', subpackage: 'mathPages', icon: '🔢', img: '/assets/mathEntrance/number-sort.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#fff3e0', previewText: '1→2→3' }, + { id: 'one-digit-addition', title: '一位数加法', desc: '通过圆点学习一位数加法运算', category: 'math', page: 'oneDigitAddition', subpackage: 'mathPages', icon: '➕', img: '/assets/mathEntrance/one-digit-addition.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e3f2fd', previewText: '2+3=?' }, + { id: 'make-ten', title: '凑十法练习', desc: '通过凑十法学习20以内进位加法', category: 'math', page: 'makeTen', subpackage: 'mathPages', icon: '➕', img: '/assets/mathEntrance/make-ten.png', ageRange: [5, 7], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '7+?=10' }, + { id: 'break-ten', title: '破十法练习', desc: '通过破十法学习20以内退位减法', category: 'math', page: 'breakTen', subpackage: 'mathPages', icon: '➖', img: '/assets/mathEntrance/break-ten.png', ageRange: [5, 7], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '15-8=?' }, + { id: 'flat-ten', title: '平十法练习', desc: '通过平十法学习20以内退位减法', category: 'math', page: 'flatTen', subpackage: 'mathPages', icon: '➖', img: '/assets/mathEntrance/flat-ten.png', ageRange: [5, 7], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '14-6=?' }, + { id: 'borrow-ten', title: '借十法练习', desc: '通过借十法学习20以上退位减法', category: 'math', page: 'borrowTen', subpackage: 'mathPages', icon: '➖', img: '/assets/mathEntrance/borrow-ten.png', ageRange: [6, 8], difficulty: 'advanced', previewBg: '#e3f2fd', previewText: '32-8=?' }, + { id: 'number-decompose-20', title: '20以内分与合', desc: '把数字分一分,合一合', category: 'math', page: 'numberDecompose', subpackage: 'mathPages', icon: '🔢', img: '/assets/mathEntrance/number-decompose-20.png', ageRange: [5, 7], difficulty: 'basic', previewBg: '#fff3e0', previewText: '15=?+?' }, + { id: 'number-decompose', title: '10以内分与合', desc: '把数字分一分,合一合', category: 'math', page: 'numberDecompose', subpackage: 'mathPages', icon: '🔢', img: '/assets/mathEntrance/number-decompose.png', ageRange: [4, 6], difficulty: 'beginner', previewBg: '#fff3e0', previewText: '8=?+?' }, + { id: 'number-find', title: '找数字涂一涂', desc: '找一找下面相同的数字,涂上颜色', category: 'math', page: 'numberFind', subpackage: 'mathPages', img: '/assets/mathEntrance/number-find.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#fff3e0', previewText: '🔍 1 2 3' }, + { id: 'number-write', title: '看数字写一写', desc: '看数字,按笔画顺序写一写', category: 'math', page: 'numberFind', subpackage: 'mathPages', img: '/assets/mathEntrance/number-write.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#fff3e0', previewText: '1 2 3' }, + { id: 'number-coloring', title: '按数字涂颜色', desc: '按数字给相应的圆圈涂上颜色', category: 'math', page: 'countMatch', subpackage: 'mathPages', icon: '🎨', img: '/assets/mathEntrance/number-coloring.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#fce4ec', previewText: '🎨' }, + { id: 'counting-matching', title: '数一数连一连', desc: '通过连线配对数字和对应的数量图形', category: 'math', page: 'countMatch', subpackage: 'mathPages', icon: '🔗', img: '/assets/mathEntrance/count-match.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e8f5e9', previewText: '3↔🍎🍎🍎' }, + { id: 'number-object-match', title: '数物连线', desc: '连线相同数量的物品和数字', category: 'math', page: 'numberObjectMatch', subpackage: 'mathPages', icon: '🔗', img: '/assets/mathEntrance/number-object-match.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e8f5e9', previewText: '🔗' }, + { id: 'number-object-fill', title: '数物填写', desc: '数一数物品数量,填写对应的数字', category: 'math', page: 'numberObjectMatch', subpackage: 'mathPages', icon: '✏️', img: '/assets/mathEntrance/number-object-fill.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e8f5e9', previewText: '✏️' }, + { id: 'counting-select', title: '数一数选一选', desc: '数出物品数量,圈出正确答案', category: 'math', page: 'countingSelect', subpackage: 'mathPages', icon: '✓', img: '/assets/mathEntrance/counting-select.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e8f5e9', previewText: '✓' }, + { id: 'counting-fill', title: '数一数填一填', desc: '数出物品数量,填写对应的数字', category: 'math', page: 'countingSelect', subpackage: 'mathPages', icon: '✏️', img: '/assets/mathEntrance/counting-fill.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e8f5e9', previewText: '✏️' }, + { id: 'compare', title: '比大小', desc: '比较数量大小,在 ⭕️ 中填入>、<、=', category: 'math', page: 'compare', subpackage: 'mathPages', icon: '⚖️', img: '/assets/mathEntrance/compare.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e3f2fd', previewText: '3 ○ 5' }, + { id: 'missing-number', title: '填缺少的数字', desc: '在数字序列中找出并填写缺失的数字', category: 'math', page: 'missingNumber', subpackage: 'mathPages', icon: '❓', img: '/assets/mathEntrance/missing-number.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#fff3e0', previewText: '1_3_5' }, + { id: 'addition-5', title: '5以内加法', desc: '练习5以内的加法运算', category: 'math', page: 'addition', subpackage: 'mathPages', icon: '➕', img: '/assets/mathEntrance/addition-5.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e3f2fd', previewText: '2+1=?' }, + { id: 'addition-10', title: '10以内加法', desc: '练习10以内的加法运算', category: 'math', page: 'addition', subpackage: 'mathPages', icon: '➕', img: '/assets/mathEntrance/addition-10.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e3f2fd', previewText: '3+5=?' }, + { id: 'subtraction-10', title: '10以内减法', desc: '练习10以内的减法运算', category: 'math', page: 'addition', subpackage: 'mathPages', icon: '➖', img: '/assets/mathEntrance/subtraction-10.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e3f2fd', previewText: '8-3=?' }, + { id: 'addition-subtraction-10', title: '10以内加减法', desc: '练习加法和减法混合运算', category: 'math', page: 'addition', subpackage: 'mathPages', icon: '±', img: '/assets/mathEntrance/addition-subtraction-10.png', ageRange: [5, 7], difficulty: 'basic', previewBg: '#e3f2fd', previewText: '±' }, + { id: 'practice-addition', title: '加法运算', desc: '练习10/20/50/100以内加法运算', category: 'math', page: 'calculationPractice', subpackage: 'mathPages', icon: '➕', img: '/assets/mathEntrance/practice-addition.png', ageRange: [5, 8], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '25+18=?' }, + { id: 'practice-subtraction', title: '减法运算', desc: '练习10/20/50/100以内减法运算', category: 'math', page: 'calculationPractice', subpackage: 'mathPages', icon: '➖', img: '/assets/mathEntrance/practice-subtraction.png', ageRange: [5, 8], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '43-17=?' }, + { id: 'practice-mixed', title: '混合运算', desc: '练习10/20/50/100以内加减法混合运算', category: 'math', page: 'calculationPractice', subpackage: 'mathPages', icon: '±', img: '/assets/mathEntrance/practice-mixed.png', ageRange: [5, 8], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '±' }, + { id: 'multiplication-table', title: '九九乘法表', desc: '学习九九乘法口诀', category: 'math', page: 'multiplicationTable', subpackage: 'mathPages', icon: '✖️', img: '/assets/mathEntrance/multiplication-table.png', ageRange: [6, 8], difficulty: 'intermediate', previewBg: '#f3e5f5', previewText: '3×4=12' }, + + // === 专注力 === + { id: 'color-shape-match', title: '颜色画图形', desc: '根据颜色画出对应的图形', category: 'focus', page: 'colorShapeMatch', subpackage: 'focusPages', icon: '🎯', img: '/assets/focusEntrance/color-shape-match.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#f3e5f5', previewText: '🎯' }, + { id: 'shape-symbol', title: '图形符号配对', desc: '根据图形画对应的符号', category: 'focus', page: 'shapeSymbol', subpackage: 'focusPages', icon: '🔗', img: '/assets/focusEntrance/shape-symbol.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e8eaf6', previewText: '△→♠' }, + { id: 'shape-recognition', title: '识别形状', desc: '识别形状,涂一涂', category: 'focus', page: 'shape', subpackage: 'focusPages', icon: '🔍', img: '/assets/focusEntrance/shape-recognition.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#fce4ec', previewText: '△ ○ □' }, + { id: 'position-coloring', title: '方位涂涂乐', desc: '观察卡片位置,在对应的方格中涂上颜色', category: 'focus', page: 'positionColoring', subpackage: 'focusPages', icon: '📍', img: '/assets/focusEntrance/position-coloring.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e8f5e9', previewText: '📍' }, + { id: 'color-pattern', title: '颜色找规律', desc: '观察颜色规律,在空白图形中涂上颜色', category: 'focus', page: 'colorPattern', subpackage: 'focusPages', icon: '🎨', img: '/assets/focusEntrance/color-pattern.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#fff8e1', previewText: '🎨' }, + { id: 'match-connect', title: '连连看', desc: '快来根据物品连一连吧!', category: 'focus', page: 'matchConnect', subpackage: 'focusPages', icon: '🔗', img: '/assets/focusEntrance/match-connect.png', ageRange: [3, 6], difficulty: 'beginner', previewBg: '#e0f7fa', previewText: '🔗' }, + { id: 'line-recognition', title: '线条识别', desc: '认识不同线条,画出颜色对应的线条', category: 'focus', page: 'lineRecognition', subpackage: 'focusPages', icon: '📏', img: '/assets/focusEntrance/line-recognition.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#f1f8e9', previewText: '〰️' }, + { id: 'grid-reasoning', title: '方格推理', desc: '仔细观察,推理出合并方格并连线', category: 'focus', page: 'gridReasoning', subpackage: 'focusPages', icon: '🧩', img: '/assets/focusEntrance/grid-reasoning.png', ageRange: [5, 7], difficulty: 'intermediate', previewBg: '#e8eaf6', previewText: '🧩' }, + { id: 'code-connect', title: '译码连线', desc: '按照数字顺序,将数字对应的颜色连线', category: 'focus', page: 'codeConnect', subpackage: 'focusPages', icon: '🔢', img: '/assets/focusEntrance/code-connect.png', ageRange: [5, 7], difficulty: 'intermediate', previewBg: '#fff3e0', previewText: '🔢' }, + { id: 'dot-connect', title: '数字点连线', desc: '按数字顺序连点', category: 'focus', page: 'dotConnect', subpackage: 'focusPages', icon: '🔗', img: '/assets/focusEntrance/dot-connect.png', ageRange: [3, 6], difficulty: 'beginner', previewBg: '#e3f2fd', previewText: '1→2→3' }, + { id: 'grid-drawing-3x3', title: '格子仿画 3x3', desc: '简单有趣,培养专注力', category: 'focus', page: 'gridDrawing', subpackage: 'focusPages', icon: '🎨', mode: '3x3', img: '/assets/focusEntrance/grid-drawing-3x3.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#fce4ec', previewText: '▦' }, + { id: 'grid-drawing-5x5', title: '格子仿画 5x5', desc: '创意挑战,提升观察力', category: 'focus', page: 'gridDrawing', subpackage: 'focusPages', icon: '🎨', mode: '5x5', img: '/assets/focusEntrance/grid-drawing-5x5.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#fce4ec', previewText: '▦' }, + { id: 'grid-drawing-7x7', title: '格子仿画 7x7', desc: '大师挑战,锻炼耐心', category: 'focus', page: 'gridDrawing', subpackage: 'focusPages', icon: '🎨', mode: '7x7', img: '/assets/focusEntrance/grid-drawing-7x7.png', ageRange: [5, 8], difficulty: 'intermediate', previewBg: '#fce4ec', previewText: '▦' }, + + // === 语文 === + { id: 'word-recognition', title: '识字卡', desc: '认识汉字,选字生成识字卡', category: 'chinese', page: 'index', subpackage: '', icon: '📖', ageRange: [3, 6], difficulty: 'beginner', previewBg: '#fce4ec', previewText: '大 小' }, + { id: 'copybook', title: '练字帖', desc: '选字生成田字格练字帖', category: 'chinese', page: 'copyBook', subpackage: '', icon: '✏️', ageRange: [4, 7], difficulty: 'basic', previewBg: '#fff8e1', previewText: '横竖撇' }, +]; + +/** + * 按分类获取题型列表 + */ +export function getWorksheetsByCategory(category: string): WorksheetType[] { + if (category === 'all') return ALL_WORKSHEETS; + return ALL_WORKSHEETS.filter((w) => w.category === category); +} + +/** + * 按年龄范围筛选 + */ +export function getWorksheetsByAge(worksheets: WorksheetType[], age: number): WorksheetType[] { + 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}`; +} diff --git a/miniprogram/core/models/print-config.ts b/miniprogram/core/models/print-config.ts new file mode 100644 index 0000000..bd295d0 --- /dev/null +++ b/miniprogram/core/models/print-config.ts @@ -0,0 +1,7 @@ +export type PrintHeader = 'wechat' | 'noLogoImage' | 'LogoImage' | 'minimal'; + +export interface PrintConfig { + header: PrintHeader; + appName: string; + appHint: string; +} diff --git a/miniprogram/core/models/worksheet.ts b/miniprogram/core/models/worksheet.ts new file mode 100644 index 0000000..957685c --- /dev/null +++ b/miniprogram/core/models/worksheet.ts @@ -0,0 +1,26 @@ +export type WorksheetCategory = 'math' | 'focus' | 'chinese' | 'english' | 'puzzle' | 'craft'; +export type DifficultyLevel = 'beginner' | 'basic' | 'intermediate' | 'advanced'; + +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; +} + +export interface CategoryType { + id: WorksheetCategory; + name: string; + icon: string; + color: string; + gradient: [string, string]; +} diff --git a/miniprogram/core/utils/random.ts b/miniprogram/core/utils/random.ts new file mode 100644 index 0000000..0afe743 --- /dev/null +++ b/miniprogram/core/utils/random.ts @@ -0,0 +1,33 @@ +/** + * 生成指定范围内的随机整数 [min, max] + */ +export function randomInt(min: number, max: number): number { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +/** + * 从数组中随机选取一个元素 + */ +export function randomPick(arr: T[]): T { + return arr[Math.floor(Math.random() * arr.length)]; +} + +/** + * 从数组中随机选取 n 个不重复元素 + */ +export function randomPickN(arr: T[], n: number): T[] { + const shuffled = [...arr].sort(() => Math.random() - 0.5); + return shuffled.slice(0, Math.min(n, arr.length)); +} + +/** + * 打乱数组(Fisher-Yates) + */ +export function shuffle(arr: T[]): T[] { + const result = [...arr]; + for (let i = result.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [result[i], result[j]] = [result[j], result[i]]; + } + return result; +} diff --git a/miniprogram/pages/age/age.json b/miniprogram/pages/age/age.json new file mode 100644 index 0000000..a7351f1 --- /dev/null +++ b/miniprogram/pages/age/age.json @@ -0,0 +1,8 @@ +{ + "navigationBarTitleText": "分龄推荐", + "navigationBarTextStyle": "black", + "navigationBarBackgroundColor": "#ffffff", + "backgroundColor": "#F5F5F5", + "enablePullDownRefresh": false, + "usingComponents": {} +} diff --git a/miniprogram/pages/age/age.less b/miniprogram/pages/age/age.less new file mode 100644 index 0000000..3fc56f4 --- /dev/null +++ b/miniprogram/pages/age/age.less @@ -0,0 +1,108 @@ +.age-page { + min-height: 100vh; + background: #f5f5f5; + padding-bottom: 48rpx; + box-sizing: border-box; +} + +.age-hero { + background: #fff; + padding: 40rpx 32rpx 36rpx; + margin-bottom: 24rpx; +} + +.age-hero__title { + display: block; + font-size: 44rpx; + font-weight: 700; + color: rgba(0, 0, 0, 0.88); + margin-bottom: 12rpx; +} + +.age-hero__subtitle { + display: block; + font-size: 26rpx; + line-height: 1.5; + color: #999; +} + +.age-grid { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 24rpx; + padding: 0 24rpx; + box-sizing: border-box; +} + +.age-card { + width: calc((100% - 24rpx) / 2); + background: #fff; + border-radius: 32rpx; + overflow: hidden; + box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.06); + box-sizing: border-box; +} + +.age-card__art { + height: 160rpx; + display: flex; + align-items: center; + justify-content: center; +} + +.age-card__emoji { + font-size: 72rpx; + line-height: 1; +} + +.age-card__body { + padding: 24rpx 24rpx 28rpx; +} + +.age-card__label { + display: block; + font-size: 32rpx; + font-weight: 700; + color: rgba(0, 0, 0, 0.88); + margin-bottom: 8rpx; +} + +.age-card__desc { + display: block; + font-size: 24rpx; + color: #999; + margin-bottom: 12rpx; +} + +.age-card__count { + display: block; + font-size: 22rpx; + color: #ccc; +} + +.age-tip { + margin: 40rpx 24rpx 0; + background: #fff; + border-radius: 24rpx; + padding: 28rpx 24rpx; + display: flex; + flex-direction: row; + align-items: flex-start; + gap: 20rpx; + box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.04); + box-sizing: border-box; +} + +.age-tip__duck { + font-size: 64rpx; + line-height: 1; + flex-shrink: 0; +} + +.age-tip__text { + flex: 1; + font-size: 26rpx; + line-height: 1.55; + color: rgba(0, 0, 0, 0.65); +} diff --git a/miniprogram/pages/age/age.ts b/miniprogram/pages/age/age.ts new file mode 100644 index 0000000..fe4973c --- /dev/null +++ b/miniprogram/pages/age/age.ts @@ -0,0 +1,44 @@ +import { ALL_WORKSHEETS, getWorksheetsByAge } from '../../core/data/worksheets'; + +interface AgeGroup { + label: string; + desc: string; + emoji: string; + minAge: number; + maxAge: number; + count: number; + gradient: string; +} + +Page({ + data: { + ageGroups: [] as AgeGroup[], + }, + + onLoad() { + const groups = [ + { label: '3-4 岁', desc: '启蒙认知', emoji: '👶', minAge: 3, maxAge: 4, gradient: 'linear-gradient(135deg, #fff9c4, #fff176)' }, + { label: '4-5 岁', desc: '基础练习', emoji: '🧒', minAge: 4, maxAge: 5, gradient: 'linear-gradient(135deg, #c8e6c9, #81c784)' }, + { label: '5-6 岁', desc: '能力提升', emoji: '👦', minAge: 5, maxAge: 6, gradient: 'linear-gradient(135deg, #bbdefb, #64b5f6)' }, + { label: '6-7 岁', desc: '幼小衔接', emoji: '👧', minAge: 6, maxAge: 7, gradient: 'linear-gradient(135deg, #e1bee7, #ba68c8)' }, + { label: '7-8 岁', desc: '巩固挑战', emoji: '🎒', minAge: 7, maxAge: 8, gradient: 'linear-gradient(135deg, #ffccbc, #ff8a65)' }, + ]; + + const ageGroups = groups.map((g) => ({ + ...g, + count: getWorksheetsByAge(ALL_WORKSHEETS, g.minAge).length, + })); + + this.setData({ ageGroups }); + }, + + onAgeCardTap(e: WechatMiniprogram.TouchEvent) { + const minAge = Number(e.currentTarget.dataset.minAge); + const maxAge = Number(e.currentTarget.dataset.maxAge); + if (Number.isNaN(minAge) || Number.isNaN(maxAge)) return; + const title = `${minAge}-${maxAge}岁推荐`; + wx.navigateTo({ + url: `/pages/category/category?minAge=${minAge}&maxAge=${maxAge}&title=${encodeURIComponent(title)}`, + }); + }, +}); diff --git a/miniprogram/pages/age/age.wxml b/miniprogram/pages/age/age.wxml new file mode 100644 index 0000000..c6dcfb5 --- /dev/null +++ b/miniprogram/pages/age/age.wxml @@ -0,0 +1,31 @@ + + + 分龄推荐 + 按年龄段挑选适合的练习单,循序渐进更省心 + + + + + + {{item.emoji}} + + + {{item.label}} + {{item.desc}} + {{item.count}} 个练习 + + + + + + 🦆 + 建议家长根据孩子当前能力微调难度,不必严格按年龄划分哦~ + + diff --git a/miniprogram/pages/category/category.json b/miniprogram/pages/category/category.json new file mode 100644 index 0000000..c5b0815 --- /dev/null +++ b/miniprogram/pages/category/category.json @@ -0,0 +1,10 @@ +{ + "navigationBarTitleText": "分类", + "navigationBarTextStyle": "black", + "navigationBarBackgroundColor": "#ffffff", + "backgroundColor": "#F6F6F6", + "usingComponents": { + "worksheet-card": "../../components/shared/worksheet-card/worksheet-card", + "detail-card": "../../components/shared/detail-card/detail-card" + } +} diff --git a/miniprogram/pages/category/category.less b/miniprogram/pages/category/category.less new file mode 100644 index 0000000..968968a --- /dev/null +++ b/miniprogram/pages/category/category.less @@ -0,0 +1,81 @@ +.category-page { + min-height: 100vh; + background: #f6f6f6; + padding-bottom: 48rpx; +} + +.category-page__header { + padding: 24rpx 32rpx 16rpx; + background: #fff; +} + +.category-page__header-title { + font-size: 34rpx; + font-weight: 600; + color: #333; +} + +.category-page__filters { + background: #fff; + padding: 24rpx; + margin-bottom: 16rpx; +} + +.category-page__filter-row { + display: flex; + flex-direction: row; + align-items: flex-start; + margin-bottom: 20rpx; +} + +.category-page__filter-row:last-child { + margin-bottom: 0; +} + +.category-page__filter-label { + flex-shrink: 0; + width: 72rpx; + font-size: 26rpx; + color: #999; + line-height: 56rpx; +} + +.category-page__pills { + flex: 1; + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 16rpx; +} + +.category-page__pill { + font-size: 24rpx; + padding: 8rpx 20rpx; + border-radius: 24rpx; + background: #f5f5f5; + color: #333; +} + +.category-page__pill--active { + background: #f7ee47; + color: #333; +} + +.category-page__list { + display: flex; + flex-direction: column; +} + +.category-page__card-wrap { + margin: 0 24rpx 24rpx; +} + +.category-page__empty { + padding: 120rpx 48rpx; + text-align: center; +} + +.category-page__empty-text { + font-size: 28rpx; + color: #999; +} diff --git a/miniprogram/pages/category/category.ts b/miniprogram/pages/category/category.ts new file mode 100644 index 0000000..99e0e1d --- /dev/null +++ b/miniprogram/pages/category/category.ts @@ -0,0 +1,160 @@ +import type { WorksheetType } from '../../core/models/worksheet'; +import { + getWorksheetPath, + getWorksheetsByAge, + getWorksheetsByCategory, +} from '../../core/data/worksheets'; + +type AgeFilterKey = 'all' | '3-4' | '4-5' | '5-6' | '6-7' | '7-8'; +type DiffFilterKey = 'all' | 'beginner' | 'basic' | 'intermediate'; + +const AGE_BAND: Record, [number, number]> = { + '3-4': [3, 4], + '4-5': [4, 5], + '5-6': [5, 6], + '6-7': [6, 7], + '7-8': [7, 8], +}; + +const DIFF_LABEL: Record = { + beginner: '入门', + basic: '基础', + intermediate: '进阶', + advanced: '进阶+', +}; + +type DisplayItem = WorksheetType & { + ageRangeLabel: string; + difficultyLabel: string; +}; + +function filterByAgeBand( + list: WorksheetType[], + band: AgeFilterKey, +): WorksheetType[] { + if (band === 'all') return list; + const [lo, hi] = AGE_BAND[band]; + const map = new Map(); + for (let age = lo; age <= hi; age += 1) { + getWorksheetsByAge(list, age).forEach((w) => map.set(w.id, w)); + } + return Array.from(map.values()); +} + +function filterByDifficulty( + list: WorksheetType[], + diff: DiffFilterKey, +): WorksheetType[] { + if (diff === 'all') return list; + return list.filter((w) => w.difficulty === diff); +} + +function toDisplayList(list: WorksheetType[]): DisplayItem[] { + return list.map((w) => ({ + ...w, + ageRangeLabel: + w.ageRange && w.ageRange.length === 2 + ? `${w.ageRange[0]}-${w.ageRange[1]}岁` + : '', + difficultyLabel: w.difficulty ? DIFF_LABEL[w.difficulty] || '' : '', + })); +} + +Page({ + data: { + title: '分类', + sourceItems: [] as WorksheetType[], + items: [] as DisplayItem[], + ageFilters: [ + { key: 'all', label: '全部' }, + { key: '3-4', label: '3-4岁' }, + { key: '4-5', label: '4-5岁' }, + { key: '5-6', label: '5-6岁' }, + { key: '6-7', label: '6-7岁' }, + { key: '7-8', label: '7-8岁' }, + ], + diffFilters: [ + { key: 'all', label: '全部' }, + { key: 'beginner', label: '⭐ 入门' }, + { key: 'basic', label: '⭐⭐ 基础' }, + { key: 'intermediate', label: '⭐⭐⭐ 进阶' }, + ], + activeAge: 'all' as AgeFilterKey, + activeDiff: 'all' as DiffFilterKey, + }, + + onLoad(options: { + category?: string; + minAge?: string; + maxAge?: string; + title?: string; + }) { + const category = options.category || 'all'; + let base = getWorksheetsByCategory(category); + + const minAge = options.minAge != null ? Number(options.minAge) : NaN; + const maxAge = options.maxAge != null ? Number(options.maxAge) : NaN; + if (!Number.isNaN(minAge) && !Number.isNaN(maxAge)) { + const map = new Map(); + const lo = Math.min(minAge, maxAge); + const hi = Math.max(minAge, maxAge); + for (let age = lo; age <= hi; age += 1) { + getWorksheetsByAge(base, age).forEach((w) => + map.set(w.id, w), + ); + } + base = Array.from(map.values()); + } + + const title = options.title + ? decodeURIComponent(options.title) + : '分类'; + + this.setData({ + title, + sourceItems: base, + }); + wx.setNavigationBarTitle({ title }); + this.applyFilters(); + }, + + applyFilters() { + const { sourceItems, activeAge, activeDiff } = this.data; + let next = filterByAgeBand(sourceItems, activeAge); + next = filterByDifficulty(next, activeDiff); + this.setData({ items: toDisplayList(next) }); + }, + + onAgeFilter(e: WechatMiniprogram.TouchEvent) { + const key = e.currentTarget.dataset.key as AgeFilterKey; + if (!key) return; + this.setData({ activeAge: key }, () => this.applyFilters()); + }, + + onDiffFilter(e: WechatMiniprogram.TouchEvent) { + const key = e.currentTarget.dataset.key as DiffFilterKey; + if (!key) return; + this.setData({ activeDiff: key }, () => this.applyFilters()); + }, + + buildWorksheetUrl(w: WorksheetType): string { + let url = getWorksheetPath(w); + if (!url) return ''; + if (w.mode) { + url += url.includes('?') ? `&mode=${w.mode}` : `?mode=${w.mode}`; + } + return url; + }, + + onCardTap(e: WechatMiniprogram.TouchEvent) { + const id = e.currentTarget.dataset.id as string; + const w = this.data.items.find((item) => item.id === id); + if (!w) return; + const url = this.buildWorksheetUrl(w); + if (!url) { + wx.showToast({ title: '暂无法打开', icon: 'none' }); + return; + } + wx.navigateTo({ url }); + }, +}); diff --git a/miniprogram/pages/category/category.wxml b/miniprogram/pages/category/category.wxml new file mode 100644 index 0000000..75f5793 --- /dev/null +++ b/miniprogram/pages/category/category.wxml @@ -0,0 +1,58 @@ + + + {{title}} + + + + + 年龄 + + + {{item.label}} + + + + + 难度 + + + {{item.label}} + + + + + + + + + + + + + 暂无符合条件的练习纸 + + diff --git a/miniprogram/pages/debug/debug.wxml b/miniprogram/pages/debug/debug.wxml index 8144add..3e722ef 100644 --- a/miniprogram/pages/debug/debug.wxml +++ b/miniprogram/pages/debug/debug.wxml @@ -74,11 +74,5 @@ title="测试页面入口" size="large" custom-class="centered-title" /> - diff --git a/miniprogram/pages/favorites/favorites.json b/miniprogram/pages/favorites/favorites.json new file mode 100644 index 0000000..e0adf8d --- /dev/null +++ b/miniprogram/pages/favorites/favorites.json @@ -0,0 +1,6 @@ +{ + "navigationBarTitleText": "我的收藏", + "navigationBarTextStyle": "black", + "navigationBarBackgroundColor": "#ffffff", + "backgroundColor": "#F6F6F6" +} diff --git a/miniprogram/pages/favorites/favorites.less b/miniprogram/pages/favorites/favorites.less new file mode 100644 index 0000000..0a686bc --- /dev/null +++ b/miniprogram/pages/favorites/favorites.less @@ -0,0 +1,88 @@ +.favorites-page { + min-height: 100vh; + background: #f6f6f6; +} + +.favorites-page__title { + padding: 32rpx 32rpx 24rpx; + font-size: 40rpx; + font-weight: 600; + color: #333; + background: #fff; +} + +.favorites-page__tabs { + display: flex; + flex-direction: row; + background: #fff; + border-bottom: 1rpx solid #eee; +} + +.favorites-page__tab { + flex: 1; + text-align: center; + padding: 24rpx 0; + font-size: 30rpx; + color: #666; + position: relative; +} + +.favorites-page__tab--active { + color: #333; + font-weight: 600; +} + +.favorites-page__tab--active::after { + content: ''; + position: absolute; + left: 50%; + bottom: 0; + transform: translateX(-50%); + width: 48rpx; + height: 6rpx; + background: #f7ee47; + border-radius: 3rpx; +} + +.favorites-page__empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 120rpx 48rpx 0; +} + +.favorites-page__emoji { + font-size: 96rpx; + line-height: 1.2; + margin-bottom: 32rpx; +} + +.favorites-page__empty-msg { + margin-bottom: 48rpx; +} + +.favorites-page__empty-line { + display: block; + font-size: 28rpx; + color: #666; + text-align: center; + line-height: 1.6; + margin-bottom: 8rpx; +} + +.favorites-page__btn { + width: 280rpx; + height: 80rpx; + line-height: 80rpx; + background: #f7ee47; + color: #333; + font-size: 30rpx; + font-weight: 600; + border-radius: 40rpx; + border: none; +} + +.favorites-page__btn::after { + border: none; +} diff --git a/miniprogram/pages/favorites/favorites.ts b/miniprogram/pages/favorites/favorites.ts new file mode 100644 index 0000000..78f8855 --- /dev/null +++ b/miniprogram/pages/favorites/favorites.ts @@ -0,0 +1,27 @@ +type FavoritesTab = 'favorites' | 'downloads'; + +Page({ + data: { + activeTab: 'favorites' as FavoritesTab, + favoriteList: [] as unknown[], + downloadList: [] as unknown[], + }, + + onShow() { + const tab = wx.getStorageSync('favorites_active_tab') as FavoritesTab; + if (tab === 'downloads' || tab === 'favorites') { + this.setData({ activeTab: tab }); + wx.removeStorageSync('favorites_active_tab'); + } + }, + + onTabSwitch(e: WechatMiniprogram.TouchEvent) { + const tab = e.currentTarget.dataset.tab as FavoritesTab; + if (tab !== 'favorites' && tab !== 'downloads') return; + this.setData({ activeTab: tab }); + }, + + onDiscoverTap() { + wx.switchTab({ url: '/pages/home/home' }); + }, +}); diff --git a/miniprogram/pages/favorites/favorites.wxml b/miniprogram/pages/favorites/favorites.wxml new file mode 100644 index 0000000..0b37144 --- /dev/null +++ b/miniprogram/pages/favorites/favorites.wxml @@ -0,0 +1,38 @@ + + 我的收藏 + + + + 收藏的题型 + + + 下载历史 + + + + + 🦆 + + 鸭丫还没找到收藏呢 + 去发现好玩的练习纸吧~ + + + + + + 🦆 + + 还没有下载记录哦 + 快去生成练习纸吧~ + + + + diff --git a/miniprogram/pages/guide/guide.json b/miniprogram/pages/guide/guide.json new file mode 100644 index 0000000..1e5d362 --- /dev/null +++ b/miniprogram/pages/guide/guide.json @@ -0,0 +1,6 @@ +{ + "navigationBarTitleText": "打印指南", + "navigationBarTextStyle": "black", + "navigationBarBackgroundColor": "#f7ee47", + "backgroundColor": "#F6F6F6" +} diff --git a/miniprogram/pages/guide/guide.less b/miniprogram/pages/guide/guide.less new file mode 100644 index 0000000..a737437 --- /dev/null +++ b/miniprogram/pages/guide/guide.less @@ -0,0 +1,150 @@ +.guide-page { + min-height: 100vh; + background: #f6f6f6; + padding-bottom: 48rpx; +} + +.guide-page__hero { + background: linear-gradient(180deg, #f7ee47 0%, #e6d520 100%); + padding: 60rpx 40rpx; + display: flex; + flex-direction: column; + align-items: center; +} + +.guide-page__hero-icon { + font-size: 80rpx; + margin-bottom: 24rpx; +} + +.guide-page__hero-title { + font-size: 40rpx; + font-weight: 700; + color: #333; +} + +.guide-page__steps { + position: relative; + padding: 40rpx 32rpx 24rpx 32rpx; + margin: 0 24rpx; + margin-top: -24rpx; + background: #fff; + border-radius: 24rpx; + box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.06); +} + +.guide-page__timeline-line { + position: absolute; + left: 62rpx; + top: 100rpx; + bottom: 100rpx; + width: 4rpx; + background: #eee; + z-index: 0; +} + +.guide-page__step { + position: relative; + z-index: 1; + display: flex; + flex-direction: row; + margin-bottom: 48rpx; +} + +.guide-page__step--last { + margin-bottom: 0; +} + +.guide-page__step-num-wrap { + flex-shrink: 0; + width: 64rpx; + margin-right: 24rpx; + display: flex; + justify-content: center; +} + +.guide-page__step-num { + width: 60rpx; + height: 60rpx; + border-radius: 50%; + background: #f7ee47; + font-size: 28rpx; + font-weight: 700; + color: #333; + display: flex; + align-items: center; + justify-content: center; +} + +.guide-page__step-body { + flex: 1; + padding-top: 4rpx; +} + +.guide-page__step-title { + display: block; + font-size: 32rpx; + font-weight: 600; + color: #333; + margin-bottom: 12rpx; +} + +.guide-page__step-desc { + display: block; + font-size: 28rpx; + color: #666; + line-height: 1.55; + margin-bottom: 16rpx; +} + +.guide-page__tip { + background: #fff8e1; + border-radius: 16rpx; + padding: 20rpx 24rpx; + font-size: 26rpx; + color: #666; + line-height: 1.5; +} + +.guide-page__tip-line { + display: block; + margin-bottom: 8rpx; +} + +.guide-page__tip-line:last-child { + margin-bottom: 0; +} + +.guide-page__faq-title { + font-size: 32rpx; + font-weight: 700; + color: #333; + padding: 40rpx 32rpx 16rpx; +} + +.guide-page__faq { + padding: 0 24rpx; +} + +.guide-page__faq-item { + background: #fff; + border-radius: 16rpx; + padding: 28rpx 32rpx; + margin-bottom: 20rpx; + box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04); +} + +.guide-page__faq-q { + display: block; + font-size: 28rpx; + font-weight: 700; + color: #333; + margin-bottom: 12rpx; +} + +.guide-page__faq-a { + display: block; + font-size: 26rpx; + color: #999; + line-height: 1.55; +} diff --git a/miniprogram/pages/guide/guide.ts b/miniprogram/pages/guide/guide.ts new file mode 100644 index 0000000..7cf30bb --- /dev/null +++ b/miniprogram/pages/guide/guide.ts @@ -0,0 +1,7 @@ +Page({ + data: {}, + + onLoad() { + // 静态说明页,无额外逻辑 + }, +}); diff --git a/miniprogram/pages/guide/guide.wxml b/miniprogram/pages/guide/guide.wxml new file mode 100644 index 0000000..321ea7b --- /dev/null +++ b/miniprogram/pages/guide/guide.wxml @@ -0,0 +1,63 @@ + + + 🖨️ + 3 步完成打印 + + + + + + + + 1 + + + 保存到相册 + 在练习纸预览页点击「保存」或「保存到相册」,授权后即可保存图片。 + 提示:建议先关闭深色模式截图,打印对比度更好。 + + + + + + 2 + + + 连接打印机 + 确保打印机与手机处于同一 Wi-Fi,或按打印机说明完成蓝牙配对。 + 家用喷墨 / 激光打印机均可;A4 纸竖向打印即可。 + + + + + + 3 + + + 从相册打印 + 打开手机相册,选中刚保存的练习纸图片,点击分享或打印。 + + iPhone:分享 → 打印 → 选择打印机。 + Android:更多 → 打印 或 使用系统相册自带的打印入口。 + + + + + + 常见问题 + + + + 保存失败怎么办? + 请在小程序设置中开启相册写入权限,并检查手机存储空间是否充足。 + + + 打印出来太小/太大? + 在打印预览中选择「适应页面」或调整缩放比例,通常 100% 即可。 + + + 没有打印机? + 可将图片发到电脑或拷贝至 U 盘,在打印店完成打印。 + + + diff --git a/miniprogram/pages/home/home.json b/miniprogram/pages/home/home.json new file mode 100644 index 0000000..7dbf850 --- /dev/null +++ b/miniprogram/pages/home/home.json @@ -0,0 +1,11 @@ +{ + "navigationBarTitleText": "涂鸦丫", + "navigationBarTextStyle": "black", + "navigationBarBackgroundColor": "#FEF6E7", + "backgroundColor": "#FEF6E7", + "enablePullDownRefresh": false, + "usingComponents": { + "worksheet-card": "/components/shared/worksheet-card/worksheet-card", + "category-tabs": "/components/shared/category-tabs/category-tabs" + } +} diff --git a/miniprogram/pages/home/home.less b/miniprogram/pages/home/home.less new file mode 100644 index 0000000..91bbbbe --- /dev/null +++ b/miniprogram/pages/home/home.less @@ -0,0 +1,172 @@ +/* 首页体验配色:主题 #FFD709 / 正文 #605B50 / 底 #FEF6E7 / 按钮字 #322E25 / 标签 #91F78E */ + +.home-page { + min-height: 100vh; + background: #fef6e7; + padding-bottom: 48rpx; + box-sizing: border-box; + color: #605b50; +} + +.home-header { + background: #fef6e7; + padding: 24rpx 24rpx 20rpx; +} + +.home-brand { + display: flex; + flex-direction: row; + align-items: center; + gap: 16rpx; + margin-bottom: 24rpx; +} + +.home-brand__duck { + font-size: 48rpx; + line-height: 1; +} + +.home-brand__name { + font-size: 36rpx; + font-weight: 700; + color: #322e25; +} + +.home-search { + background: rgba(255, 255, 255, 0.72); + border-radius: 40rpx; + padding: 22rpx 32rpx; + border: 1rpx solid rgba(255, 215, 9, 0.35); +} + +.home-search__placeholder { + font-size: 28rpx; + color: rgba(96, 91, 80, 0.55); +} + +.home-body { + padding: 24rpx 24rpx 0; +} + +.home-section { + margin-bottom: 40rpx; +} + +.home-section--hot { + margin-bottom: 32rpx; +} + +.home-section__title-row { + margin-bottom: 20rpx; +} + +.home-section__title { + font-size: 32rpx; + font-weight: 700; + color: #605b50; +} + +.home-section__head { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + margin-bottom: 20rpx; +} + +.home-section__head-left { + display: flex; + flex-direction: row; + align-items: center; + gap: 12rpx; +} + +.home-section__icon { + font-size: 36rpx; + line-height: 1; +} + +.home-section__name { + font-size: 32rpx; + font-weight: 700; + color: #605b50; +} + +.home-section__more { + font-size: 26rpx; + color: #322e25; + font-weight: 500; +} + +.home-hot__scroll { + width: 100%; + white-space: nowrap; +} + +.home-hot__row { + display: inline-flex; + flex-direction: row; + gap: 24rpx; + padding-bottom: 8rpx; +} + +.home-hot__item { + display: inline-block; + width: 240rpx; + vertical-align: top; +} + +.home-hot__card-wrap { + width: 240rpx; + padding: 4rpx; + border-radius: 28rpx; + box-sizing: border-box; + background: linear-gradient(145deg, rgba(255, 215, 9, 0.85), rgba(254, 246, 231, 0.95)); +} + +/* category-tabs:仅首页通过 apply-shared + .home-page 生效 */ +.home-page .category-tabs { + background: #fef6e7; +} + +.home-page .category-tabs__pill { + background: rgba(96, 91, 80, 0.08); + color: #605b50; +} + +.home-page .category-tabs__pill--active { + background: #ffd709; + color: #322e25; + font-weight: 600; +} + +/* worksheet-card:仅首页 */ +.home-page .worksheet-card { + box-shadow: 0 8rpx 28rpx rgba(50, 46, 37, 0.08); +} + +.home-page .worksheet-card__title { + color: #605b50; +} + +.home-page .worksheet-card__preview-text { + color: #605b50; +} + +.home-page .worksheet-card__tag--age, +.home-page .worksheet-card__tag--diff { + background: #91f78e; + color: #322e25; +} + +.home-grid { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 20rpx; +} + +.home-grid__cell { + width: calc((100% - 40rpx) / 3); + box-sizing: border-box; +} diff --git a/miniprogram/pages/home/home.ts b/miniprogram/pages/home/home.ts new file mode 100644 index 0000000..07e2a39 --- /dev/null +++ b/miniprogram/pages/home/home.ts @@ -0,0 +1,88 @@ +import { ALL_WORKSHEETS, getWorksheetsByCategory, getWorksheetPath } from '../../core/data/worksheets'; +import { CATEGORIES } from '../../core/data/categories'; +import { WorksheetType } from '../../core/models/worksheet'; + +const CATEGORY_TABS = [ + { id: 'all', name: '全部' }, + ...CATEGORIES.map((c) => ({ id: c.id, name: c.name })), +]; + +Page({ + data: { + tabs: CATEGORY_TABS, + activeTab: 'all', + sections: [] as Array<{ title: string; icon: string; category: string; items: WorksheetType[] }>, + hotItems: [] as WorksheetType[], + filteredItems: null as WorksheetType[] | null, + }, + + onLoad() { + const sections = CATEGORIES.filter((cat) => { + const items = getWorksheetsByCategory(cat.id); + return items.length > 0; + }).map((cat) => ({ + title: cat.name, + icon: cat.icon, + category: cat.id, + items: getWorksheetsByCategory(cat.id).slice(0, 3), + })); + + const hotItems = ALL_WORKSHEETS.filter((w) => w.page).slice(0, 4); + + this.setData({ sections, hotItems }); + }, + + onTabChange(e: WechatMiniprogram.CustomEvent) { + const activeTab = e.detail.id; + if (activeTab === 'all') { + this.setData({ activeTab, filteredItems: null }); + } else { + const filteredItems = getWorksheetsByCategory(activeTab); + this.setData({ activeTab, filteredItems }); + } + }, + + onCardTap(e: WechatMiniprogram.TouchEvent) { + const id = e.currentTarget.dataset.id as string | undefined; + if (!id) return; + const item = ALL_WORKSHEETS.find((w) => w.id === id); + if (!item) return; + const path = getWorksheetPath(item); + if (path) { + wx.navigateTo({ url: path }); + } + }, + + onHotCardTap(e: WechatMiniprogram.TouchEvent) { + const id = e.currentTarget.dataset.id as string | undefined; + if (!id) return; + const item = ALL_WORKSHEETS.find((w) => w.id === id); + if (!item) return; + const path = getWorksheetPath(item); + if (path) { + wx.navigateTo({ url: path }); + } + }, + + onSeeMore(e: WechatMiniprogram.TouchEvent) { + const { category } = e.currentTarget.dataset; + wx.navigateTo({ + url: `/pages/category/category?category=${category}`, + }); + }, + + onShareAppMessage() { + return { + title: '涂鸦丫 - 快来生成宝宝的专属学习卡', + path: '/pages/home/home', + imageUrl: 'https://cdn.joeyone.cn/doodle/share-img/index-share-v2.png', + }; + }, + + onShareTimeline() { + return { + title: '涂鸦丫 - 快来生成宝宝的专属学习卡', + query: '', + }; + }, +}); diff --git a/miniprogram/pages/home/home.wxml b/miniprogram/pages/home/home.wxml new file mode 100644 index 0000000..a43d4ce --- /dev/null +++ b/miniprogram/pages/home/home.wxml @@ -0,0 +1,105 @@ + +function ageLabel(arr) { + if (!arr || arr.length < 2) { + return ''; + } + return arr[0] + '-' + arr[1] + '岁'; +} +module.exports.ageLabel = ageLabel; + + + + + + 🦆 + 涂鸦丫 + + + 搜索练习单(即将上线) + + + + + + + + + + + + + + + + + + 🔥 热门下载 + + + + + + + + + + + + + + + + {{item.icon}} + {{item.title}} + + 更多 + + + + + + + + + + diff --git a/miniprogram/pages/profile/profile.json b/miniprogram/pages/profile/profile.json new file mode 100644 index 0000000..6eaa5bd --- /dev/null +++ b/miniprogram/pages/profile/profile.json @@ -0,0 +1,6 @@ +{ + "navigationBarTitleText": "我的", + "navigationBarTextStyle": "black", + "navigationBarBackgroundColor": "#ffffff", + "backgroundColor": "#F6F6F6" +} diff --git a/miniprogram/pages/profile/profile.less b/miniprogram/pages/profile/profile.less new file mode 100644 index 0000000..c04679a --- /dev/null +++ b/miniprogram/pages/profile/profile.less @@ -0,0 +1,115 @@ +.profile-page { + min-height: 100vh; + background: #f6f6f6; + padding-bottom: 48rpx; +} + +.profile-page__header { + display: flex; + flex-direction: row; + align-items: center; + background: #f7ee47; + padding: 48rpx 32rpx; +} + +.profile-page__avatar { + width: 112rpx; + height: 112rpx; + border-radius: 50%; + background: #fff; + display: flex; + align-items: center; + justify-content: center; + font-size: 56rpx; + flex-shrink: 0; +} + +.profile-page__header-info { + margin-left: 28rpx; + display: flex; + flex-direction: column; + justify-content: center; +} + +.profile-page__name { + font-size: 36rpx; + font-weight: 700; + color: #333; + margin-bottom: 12rpx; +} + +.profile-page__stat { + font-size: 26rpx; + color: #555; +} + +.profile-page__group { + background: #fff; + border-radius: 32rpx; + margin: 24rpx 24rpx 0; + box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.06); + overflow: hidden; +} + +.profile-page__item { + display: flex; + flex-direction: row; + align-items: center; + padding: 28rpx 32rpx; + border-bottom: 1rpx solid #f0f0f0; +} + +.profile-page__item--last { + border-bottom: none; +} + +.profile-page__icon { + font-size: 36rpx; + margin-right: 20rpx; + width: 44rpx; + text-align: center; +} + +.profile-page__label { + flex: 1; + font-size: 30rpx; + color: #333; +} + +.profile-page__badge { + font-size: 20rpx; + color: #fff; + background: #ff4757; + padding: 4rpx 12rpx; + border-radius: 8rpx; + margin-right: 12rpx; +} + +.profile-page__arrow { + font-size: 36rpx; + color: #ccc; + font-weight: 300; +} + +.profile-page__footer { + display: flex; + flex-direction: column; + align-items: center; + padding: 64rpx 32rpx 32rpx; +} + +.profile-page__footer-emoji { + font-size: 72rpx; + margin-bottom: 16rpx; +} + +.profile-page__footer-slogan { + font-size: 26rpx; + color: #999; + margin-bottom: 12rpx; +} + +.profile-page__footer-version { + font-size: 22rpx; + color: #ccc; +} diff --git a/miniprogram/pages/profile/profile.ts b/miniprogram/pages/profile/profile.ts new file mode 100644 index 0000000..f3f3df3 --- /dev/null +++ b/miniprogram/pages/profile/profile.ts @@ -0,0 +1,46 @@ +Page({ + data: { + generateCount: 0, + version: '', + }, + + onLoad() { + const info = wx.getAccountInfoSync(); + const version = info.miniProgram.version || '开发版'; + this.setData({ version }); + }, + + onGoToDownloads() { + wx.setStorageSync('favorites_active_tab', 'downloads'); + wx.switchTab({ url: '/pages/favorites/favorites' }); + }, + + onGoToFavorites() { + wx.setStorageSync('favorites_active_tab', 'favorites'); + wx.switchTab({ url: '/pages/favorites/favorites' }); + }, + + onGoToPrintPlan() { + wx.showToast({ title: '功能开发中', icon: 'none' }); + }, + + onGoToStats() { + wx.showToast({ title: '功能开发中', icon: 'none' }); + }, + + onGoToGuide() { + wx.navigateTo({ url: '/pages/guide/guide' }); + }, + + onGoToPrintSettings() { + wx.showToast({ title: '功能开发中', icon: 'none' }); + }, + + onGoToFeedback() { + wx.showToast({ title: '功能开发中', icon: 'none' }); + }, + + onGoToAbout() { + wx.showToast({ title: '涂鸦丫练习纸', icon: 'none' }); + }, +}); diff --git a/miniprogram/pages/profile/profile.wxml b/miniprogram/pages/profile/profile.wxml new file mode 100644 index 0000000..0ffb4da --- /dev/null +++ b/miniprogram/pages/profile/profile.wxml @@ -0,0 +1,62 @@ + + + 🦆 + + 微信用户 + 已生成 {{generateCount}} 张练习纸 + + + + + + 📥 + 下载历史 + + + + ❤️ + 我的收藏 + + + + 📋 + 每日打印计划 + 即将上线 + + + + 📊 + 学习统计 + + + + + + + 🖨️ + 打印指南 + + + + ⚙️ + 打印设置 + + + + 💬 + 意见反馈 + + + + ℹ️ + 关于涂鸦丫 + + + + + + 🦆 + 陪孩子一起,把学习变成游戏 + v{{version}} + + diff --git a/miniprogram/platform/canvas-adapter.ts b/miniprogram/platform/canvas-adapter.ts new file mode 100644 index 0000000..27c749b --- /dev/null +++ b/miniprogram/platform/canvas-adapter.ts @@ -0,0 +1,92 @@ +/** + * Canvas 平台适配器 + * 封装微信小程序 Canvas API,为未来 Web 平台迁移做准备 + */ + +export interface CanvasInitResult { + canvas: WechatMiniprogram.Canvas; + ctx: CanvasRenderingContext2D; + width: number; + height: number; +} + +/** + * 初始化 Canvas 节点 + * @param selector Canvas 选择器 (如 '#canvasContent') + * @param wrapperSelector 容器选择器 (如 '#canvasWrapper') + * @param aspectRatio 宽高比 (width / height) + */ +export function initCanvas( + selector: string, + wrapperSelector: string, + aspectRatio: number, +): Promise { + return new Promise((resolve, reject) => { + const query = wx.createSelectorQuery(); + query + .select(wrapperSelector) + .boundingClientRect((rect) => { + if (!rect) { + reject(new Error('Canvas wrapper not found')); + return; + } + + const boxWidth = rect.width; + const boxHeight = boxWidth / aspectRatio; + + wx.createSelectorQuery() + .select(selector) + .fields({ node: true, size: true }) + .exec((res) => { + if (!res[0]) { + reject(new Error('Canvas node not found')); + return; + } + + const canvas = res[0].node; + const ctx = canvas.getContext('2d'); + const dpr = wx.getSystemInfoSync().pixelRatio; + + canvas.width = boxWidth * dpr; + canvas.height = boxHeight * dpr; + ctx.scale(dpr, dpr); + + resolve({ canvas, ctx, width: boxWidth, height: boxHeight }); + }); + }) + .exec(); + }); +} + +/** + * Canvas 导出为临时文件路径 + */ +export function canvasToTempFilePath( + canvas: WechatMiniprogram.Canvas, + options?: { quality?: number; fileType?: 'png' | 'jpg' }, +): Promise { + return new Promise((resolve, reject) => { + wx.canvasToTempFilePath({ + canvas, + fileType: options?.fileType || 'png', + quality: options?.quality || 1, + success: (res) => resolve(res.tempFilePath), + fail: reject, + }); + }); +} + +/** + * 加载图片到 Canvas + */ +export function loadImage( + canvas: WechatMiniprogram.Canvas, + src: string, +): Promise { + return new Promise((resolve, reject) => { + const img = canvas.createImage(); + img.onload = () => resolve(img); + img.onerror = reject; + img.src = src; + }); +} diff --git a/miniprogram/platform/cloud-adapter.ts b/miniprogram/platform/cloud-adapter.ts new file mode 100644 index 0000000..3596f82 --- /dev/null +++ b/miniprogram/platform/cloud-adapter.ts @@ -0,0 +1,70 @@ +/** + * 云服务平台适配器 + * 封装微信云开发 API + */ + +export interface CloudResult { + code?: string | number; + message?: string; + info?: string; + data?: any; + [key: string]: any; +} + +/** + * 调用云函数 + */ +export async function callCloud( + action: string, + options?: Record, +): Promise { + try { + const name = 'doodle'; + + const { errMsg, result } = await wx.cloud.callFunction({ + name, + data: { + action, + ...options, + }, + }); + if (!/ok/.test(errMsg)) { + wx.showToast({ + title: '服务器错误', + icon: 'none', + duration: 2000, + }); + return { + code: '500', + info: '服务器繁忙', + message: errMsg, + }; + } + return result as CloudResult; + } catch (e: any) { + return { code: '500', info: e.message || '服务器繁忙' }; + } +} + +/** + * 查询云数据库集合 + */ +export async function queryCollection( + collection: string, + where?: Record, + limit: number = 20, + orderBy?: { field: string; order: 'asc' | 'desc' }, +): Promise { + try { + const db = wx.cloud.database(); + let query = db.collection(collection).where(where || {}).limit(limit); + if (orderBy) { + query = query.orderBy(orderBy.field, orderBy.order); + } + const { data } = await query.get(); + return data; + } catch (e) { + console.error('queryCollection failed:', collection, e); + return []; + } +} diff --git a/miniprogram/platform/image-adapter.ts b/miniprogram/platform/image-adapter.ts new file mode 100644 index 0000000..960c5d1 --- /dev/null +++ b/miniprogram/platform/image-adapter.ts @@ -0,0 +1,49 @@ +/** + * 图片平台适配器 + * 封装微信小程序图片加载和保存 API + */ + +/** + * 保存图片到相册 + */ +export function saveImageToAlbum(filePath: string): Promise { + return new Promise((resolve, reject) => { + wx.saveImageToPhotosAlbum({ + filePath, + success: () => resolve(), + fail: (err) => { + if ( + err.errMsg.includes('auth deny') || + err.errMsg.includes('authorize') + ) { + wx.showModal({ + title: '提示', + content: '需要授权保存图片到相册', + confirmText: '去设置', + success: (res) => { + if (res.confirm) { + wx.openSetting(); + } + }, + }); + } + reject(err); + }, + }); + }); +} + +/** + * 获取图片信息 + */ +export function getImageInfo( + src: string, +): Promise { + return new Promise((resolve, reject) => { + wx.getImageInfo({ + src, + success: resolve, + fail: reject, + }); + }); +} diff --git a/miniprogram/platform/storage-adapter.ts b/miniprogram/platform/storage-adapter.ts new file mode 100644 index 0000000..8a19224 --- /dev/null +++ b/miniprogram/platform/storage-adapter.ts @@ -0,0 +1,29 @@ +/** + * 存储平台适配器 + * 封装微信小程序 Storage API + */ + +export function getStorage(key: string, defaultValue?: T): T | undefined { + try { + const value = wx.getStorageSync(key); + return value !== '' ? value : defaultValue; + } catch { + return defaultValue; + } +} + +export function setStorage(key: string, value: any): void { + try { + wx.setStorageSync(key, value); + } catch (e) { + console.error('setStorage failed:', key, e); + } +} + +export function removeStorage(key: string): void { + try { + wx.removeStorageSync(key); + } catch (e) { + console.error('removeStorage failed:', key, e); + } +} diff --git a/miniprogram/style/theme.less b/miniprogram/style/theme.less index fa46a28..b6c2965 100644 --- a/miniprogram/style/theme.less +++ b/miniprogram/style/theme.less @@ -1,41 +1,40 @@ -// 主题色 +// v3.0 品牌色系 +@brand: #f7ee47; +@brand-dark: #e6d520; +@brand-light: #fffef0; + +// 主题色(保持向后兼容) @primary-color: #798154; @select-color: #f7ee47; @link-color: #2794f4; @wechat-color: #1fe000; // 文本颜色 -@text-default: #3c454b; -@text-primary: #595236; +@text-default: #333333; +@text-primary: #333333; +@text-secondary: #999999; +@text-light: #cccccc; @text-gray: #9da0a3; @text-white: #fff; @text-disable: #ccc; +// 边框 @border: 1rpx solid rgba(221, 221, 221, 0.5); +@border-color: #eeeeee; // 背景色 -@bg-default: #e8eddb; +@bg-default: #f5f5f5; +@bg-white: #ffffff; @bg-gray: #fef6db; -@bg-white: #fff; -// // 文本大小 -// @font-size-base: 24rpx; -// @font-size-sub: 28rpx; -// @font-size-title: 32rpx; +// 圆角 +@radius-sm: 16rpx; +@radius: 24rpx; +@radius-lg: 32rpx; -// // 按钮字体大小 -// @font-size-btn-block: 36rpx; -// @font-size-btn-large: 30rpx; -// @font-size-btn-default: 26rpx; +// 阴影 +@shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06); +@shadow-lg: 0 8rpx 32rpx rgba(0, 0, 0, 0.1); -// // 圆角 -// @border-radius-base: 8rpx; -// @border-radius-m: 16rpx; -// @border-radius-lg: 24rpx; - -// // btn -// @btn-border: 1rpx solid #c1c1c1; -// @btn-border-radius: 12rpx; - -// // icon +// icon @font-size-icon: 40rpx; diff --git a/tools/generate-tabbar-icons.js b/tools/generate-tabbar-icons.js new file mode 100644 index 0000000..dc03af1 --- /dev/null +++ b/tools/generate-tabbar-icons.js @@ -0,0 +1,232 @@ +/** + * Generates 81×81 RGBA PNG tab bar icons (no npm deps — zlib + raw PNG). + * Run: node tools/generate-tabbar-icons.js + */ + +import { createWriteStream, mkdirSync } from "fs"; +import { dirname, join } from "path"; +import { fileURLToPath } from "url"; +import { deflateSync } from "zlib"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +const W = 81; +const H = 81; + +const OUT_DIR = join(__dirname, "../miniprogram/assets/tabBar"); + +const PNG_SIG = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); + +/** PNG CRC-32 (polynomial 0xedb88320) */ +function crc32(buf) { + let c = 0xffffffff; + for (let i = 0; i < buf.length; i++) { + c ^= buf[i]; + for (let k = 0; k < 8; k++) { + c = (c >>> 1) ^ (0xedb88320 & -(c & 1)); + } + } + return (c ^ 0xffffffff) >>> 0; +} + +function chunk(type, data) { + const t = Buffer.from(type, "ascii"); + const len = Buffer.allocUnsafe(4); + len.writeUInt32BE(data.length, 0); + const crc = Buffer.allocUnsafe(4); + crc.writeUInt32BE(crc32(Buffer.concat([t, data])), 0); + return Buffer.concat([len, t, data, crc]); +} + +function encodePng(rgba) { + const rowLen = 1 + W * 4; + const raw = Buffer.allocUnsafe(H * rowLen); + let o = 0; + for (let y = 0; y < H; y++) { + raw[o++] = 0; // filter: None + const rowStart = y * W * 4; // top row first (y = 0) + rgba.copy(raw, o, rowStart, rowStart + W * 4); + o += W * 4; + } + + const ihdr = Buffer.allocUnsafe(13); + ihdr.writeUInt32BE(W, 0); + ihdr.writeUInt32BE(H, 4); + ihdr[8] = 8; // bit depth + ihdr[9] = 6; // RGBA + ihdr[10] = 0; + ihdr[11] = 0; + ihdr[12] = 0; + + const idat = deflateSync(raw, { level: 9 }); + return Buffer.concat([PNG_SIG, chunk("IHDR", ihdr), chunk("IDAT", idat), chunk("IEND", Buffer.alloc(0))]); +} + +function parseHexRgb(hex) { + const h = hex.replace("#", ""); + return { + r: parseInt(h.slice(0, 2), 16), + g: parseInt(h.slice(2, 4), 16), + b: parseInt(h.slice(4, 6), 16), + }; +} + +function setPixel(rgba, x, y, r, g, b, a) { + if (x < 0 || x >= W || y < 0 || y >= H) return; + const i = (y * W + x) * 4; + rgba[i] = r; + rgba[i + 1] = g; + rgba[i + 2] = b; + rgba[i + 3] = a; +} + +function fillTriangle(rgba, x0, y0, x1, y1, x2, y2, r, g, b, a) { + const ymin = Math.max(0, Math.min(y0, y1, y2)); + const ymax = Math.min(H - 1, Math.max(y0, y1, y2)); + for (let y = ymin; y <= ymax; y++) { + const xs = []; + const edge = (xa, ya, xb, yb) => { + if (ya === yb) { + if (y === ya) { + const lo = Math.min(xa, xb); + const hi = Math.max(xa, xb); + xs.push(lo, hi); + } + return; + } + if (y < Math.min(ya, yb) || y > Math.max(ya, yb)) return; + const t = (y - ya) / (yb - ya); + xs.push(Math.round(xa + t * (xb - xa))); + }; + edge(x0, y0, x1, y1); + edge(x1, y1, x2, y2); + edge(x2, y2, x0, y0); + if (xs.length < 2) continue; + const xMin = Math.max(0, Math.min(...xs)); + const xMax = Math.min(W - 1, Math.max(...xs)); + for (let x = xMin; x <= xMax; x++) setPixel(rgba, x, y, r, g, b, a); + } +} + +function fillRect(rgba, x0, y0, x1, y1, r, g, b, a) { + const xa = Math.max(0, Math.min(x0, x1)); + const xb = Math.min(W - 1, Math.max(x0, x1)); + const ya = Math.max(0, Math.min(y0, y1)); + const yb = Math.min(H - 1, Math.max(y0, y1)); + for (let y = ya; y <= yb; y++) { + for (let x = xa; x <= xb; x++) setPixel(rgba, x, y, r, g, b, a); + } +} + +function fillCircle(rgba, cx, cy, rad, r, g, b, a) { + const r2 = rad * rad; + const x0 = Math.max(0, Math.floor(cx - rad - 1)); + const x1 = Math.min(W - 1, Math.ceil(cx + rad + 1)); + const y0 = Math.max(0, Math.floor(cy - rad - 1)); + const y1 = Math.min(H - 1, Math.ceil(cy + rad + 1)); + for (let y = y0; y <= y1; y++) { + for (let x = x0; x <= x1; x++) { + const dx = x - cx; + const dy = y - cy; + if (dx * dx + dy * dy <= r2) setPixel(rgba, x, y, r, g, b, a); + } + } +} + +function fillEllipse(rgba, cx, cy, rx, ry, r, g, b, a, yMin = 0, yMax = H - 1) { + const x0 = Math.max(0, Math.floor(cx - rx - 1)); + const x1 = Math.min(W - 1, Math.ceil(cx + rx + 1)); + const ya = Math.max(0, yMin); + const yb = Math.min(H - 1, yMax); + const rx2 = rx * rx; + const ry2 = ry * ry; + for (let y = ya; y <= yb; y++) { + for (let x = x0; x <= x1; x++) { + const dx = x - cx; + const dy = y - cy; + if (dx * dx * ry2 + dy * dy * rx2 <= rx2 * ry2) setPixel(rgba, x, y, r, g, b, a); + } + } +} + +function drawHome(rgba, rgb) { + const { r, g, b } = rgb; + const a = 255; + // Roof: triangle + fillTriangle(rgba, 40, 15, 20, 34, 60, 34, r, g, b, a); + // Body + fillRect(rgba, 26, 34, 54, 56, r, g, b, a); +} + +function drawAge(rgba, rgb) { + const { r, g, b } = rgb; + const a = 255; + fillCircle(rgba, 40, 24, 10, r, g, b, a); + fillRect(rgba, 32, 35, 48, 59, r, g, b, a); +} + +function drawHeart(rgba, rgb) { + const { r, g, b } = rgb; + const a = 255; + const cx = 40; + const cy = 38; + const s = 19; + for (let py = 0; py < H; py++) { + for (let px = 0; px < W; px++) { + const x = (px - cx) / s; + const y = -(py - cy) / s; + const x2 = x * x; + const y2 = y * y; + const t = x2 + y2 - 1; + if (t * t * t - x2 * y * y * y <= 0) setPixel(rgba, px, py, r, g, b, a); + } + } +} + +function drawProfile(rgba, rgb) { + const { r, g, b } = rgb; + const a = 255; + fillCircle(rgba, 40, 25, 10, r, g, b, a); + // Wider shoulder ellipse (lower half reads as arc) + fillEllipse(rgba, 40, 52, 27, 13, r, g, b, a, 38, H - 1); +} + +function writePng(path, png) { + mkdirSync(dirname(path), { recursive: true }); + return new Promise((resolve, reject) => { + const ws = createWriteStream(path); + ws.on("error", reject); + ws.on("finish", resolve); + ws.end(png); + }); +} + +const COLOR_NORMAL = parseHexRgb("#999999"); +const COLOR_ACTIVE = parseHexRgb("#333333"); + +const jobs = [ + { name: "icon-home.png", draw: drawHome, color: COLOR_NORMAL }, + { name: "icon-home-active.png", draw: drawHome, color: COLOR_ACTIVE }, + { name: "icon-age.png", draw: drawAge, color: COLOR_NORMAL }, + { name: "icon-age-active.png", draw: drawAge, color: COLOR_ACTIVE }, + { name: "icon-fav.png", draw: drawHeart, color: COLOR_NORMAL }, + { name: "icon-fav-active.png", draw: drawHeart, color: COLOR_ACTIVE }, + { name: "icon-profile.png", draw: drawProfile, color: COLOR_NORMAL }, + { name: "icon-profile-active.png", draw: drawProfile, color: COLOR_ACTIVE }, +]; + +async function main() { + for (const { name, draw, color } of jobs) { + const rgba = Buffer.alloc(W * H * 4, 0); + draw(rgba, color); + const png = encodePng(rgba); + const out = join(OUT_DIR, name); + await writePng(out, png); + console.log("Wrote", out, `(${png.length} bytes)`); + } +} + +main().catch((e) => { + console.error(e); + process.exit(1); +});