feat:生产初版页面
@@ -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) {}
|
||||
};
|
||||
*/
|
||||
|
||||
@@ -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<string, any>;// 生成器参数
|
||||
layoutConfig: LayoutConfig; // 排版参数
|
||||
userConfigurable?: UserConfigField[];// 用户可调整的参数(渲染到设置面板)
|
||||
template: TemplateType; // 使用哪个排版模板
|
||||
generator: GeneratorType; // 使用哪个数据生成器
|
||||
generatorConfig: Record<string, any>; // 生成器参数
|
||||
layoutConfig: LayoutConfig; // 排版参数
|
||||
userConfigurable?: UserConfigField[]; // 用户可调整的参数(渲染到设置面板)
|
||||
|
||||
// ─── 兼容旧架构 ───
|
||||
legacyPage?: string; // 旧页面路径(迁移过渡期使用)
|
||||
legacyPage?: string; // 旧页面路径(迁移过渡期使用)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -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": "我的"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,6 +9,11 @@ App<IAppOption>({
|
||||
printConfig: defaultPrintConfig,
|
||||
},
|
||||
onLaunch() {
|
||||
wx.cloud.init({
|
||||
env: 'cloud1-9gifs7a2756e2c87',
|
||||
traceUser: true,
|
||||
});
|
||||
|
||||
const accountInfo = wx.getAccountInfoSync();
|
||||
const env = accountInfo.miniProgram.envVersion || 'release';
|
||||
/**
|
||||
|
||||
|
After Width: | Height: | Size: 191 B |
|
After Width: | Height: | Size: 191 B |
|
After Width: | Height: | Size: 229 B |
|
After Width: | Height: | Size: 229 B |
|
After Width: | Height: | Size: 219 B |
|
After Width: | Height: | Size: 219 B |
|
After Width: | Height: | Size: 238 B |
|
After Width: | Height: | Size: 238 B |
@@ -1,54 +1,6 @@
|
||||
/**
|
||||
* 调用云函数
|
||||
* @param funcName
|
||||
* @param options
|
||||
* @param typeName
|
||||
* @returns
|
||||
* 桥接文件 - 保持向后兼容
|
||||
* 实际实现已迁移到 platform/cloud-adapter.ts
|
||||
*/
|
||||
export default async function callCloud(
|
||||
action: string,
|
||||
options?: Record<string, any>,
|
||||
// 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;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 });
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
<view class="category-tabs">
|
||||
<scroll-view class="category-tabs__scroll" scroll-x>
|
||||
<view class="category-tabs__inner">
|
||||
<view
|
||||
wx:for="{{tabs}}"
|
||||
wx:key="id"
|
||||
class="category-tabs__pill {{activeTab === item.id ? 'category-tabs__pill--active' : ''}}"
|
||||
data-id="{{item.id}}"
|
||||
bindtap="onTabTap"
|
||||
>
|
||||
{{item.name}}
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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');
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
<view
|
||||
class="detail-card"
|
||||
hover-class="detail-card--active"
|
||||
hover-stay-time="100"
|
||||
bindtap="onTap"
|
||||
>
|
||||
<view class="detail-card__preview" style="background-color: {{previewBg}}">
|
||||
<text class="detail-card__preview-text">{{previewText}}</text>
|
||||
</view>
|
||||
<view class="detail-card__body">
|
||||
<view class="detail-card__title">{{title}}</view>
|
||||
<view class="detail-card__meta">
|
||||
<view wx:if="{{ageRange}}" class="detail-card__badge detail-card__badge--age">{{ageRange}}</view>
|
||||
<view wx:if="{{difficulty}}" class="detail-card__badge detail-card__badge--diff">{{difficulty}}</view>
|
||||
</view>
|
||||
<view wx:if="{{downloads}}" class="detail-card__stats">{{downloads}}</view>
|
||||
<view class="detail-card__actions">
|
||||
<view class="detail-card__btn detail-card__btn--primary" catchtap="onGenerate">生成</view>
|
||||
<view class="detail-card__btn detail-card__btn--ghost" catchtap="onFav">收藏</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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');
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
<view
|
||||
class="worksheet-card"
|
||||
hover-class="worksheet-card--active"
|
||||
hover-stay-time="100"
|
||||
bindtap="onTap"
|
||||
>
|
||||
<view class="worksheet-card__preview" style="background-color: {{previewBg}}">
|
||||
<image
|
||||
wx:if="{{img}}"
|
||||
class="worksheet-card__img"
|
||||
src="{{img}}"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<text wx:else class="worksheet-card__preview-text">{{previewText}}</text>
|
||||
</view>
|
||||
<view class="worksheet-card__title">{{title}}</view>
|
||||
<view class="worksheet-card__tags">
|
||||
<view wx:if="{{ageRange}}" class="worksheet-card__tag worksheet-card__tag--age">{{ageRange}}</view>
|
||||
<view wx:if="{{difficulty}}" class="worksheet-card__tag worksheet-card__tag--diff">{{difficulty}}</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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}`;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export type PrintHeader = 'wechat' | 'noLogoImage' | 'LogoImage' | 'minimal';
|
||||
|
||||
export interface PrintConfig {
|
||||
header: PrintHeader;
|
||||
appName: string;
|
||||
appHint: string;
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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<T>(arr: T[]): T {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数组中随机选取 n 个不重复元素
|
||||
*/
|
||||
export function randomPickN<T>(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<T>(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;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"navigationBarTitleText": "分龄推荐",
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarBackgroundColor": "#ffffff",
|
||||
"backgroundColor": "#F5F5F5",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)}`,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
<view class="age-page">
|
||||
<view class="age-hero">
|
||||
<text class="age-hero__title">分龄推荐</text>
|
||||
<text class="age-hero__subtitle">按年龄段挑选适合的练习单,循序渐进更省心</text>
|
||||
</view>
|
||||
|
||||
<view class="age-grid">
|
||||
<view
|
||||
wx:for="{{ageGroups}}"
|
||||
wx:key="label"
|
||||
class="age-card"
|
||||
bindtap="onAgeCardTap"
|
||||
data-min-age="{{item.minAge}}"
|
||||
data-max-age="{{item.maxAge}}"
|
||||
>
|
||||
<view class="age-card__art" style="background: {{item.gradient}}">
|
||||
<text class="age-card__emoji">{{item.emoji}}</text>
|
||||
</view>
|
||||
<view class="age-card__body">
|
||||
<text class="age-card__label">{{item.label}}</text>
|
||||
<text class="age-card__desc">{{item.desc}}</text>
|
||||
<text class="age-card__count">{{item.count}} 个练习</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="age-tip">
|
||||
<text class="age-tip__duck">🦆</text>
|
||||
<text class="age-tip__text">建议家长根据孩子当前能力微调难度,不必严格按年龄划分哦~</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<Exclude<AgeFilterKey, 'all'>, [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<string, string> = {
|
||||
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<string, WorksheetType>();
|
||||
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<string, WorksheetType>();
|
||||
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 });
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
<view class="category-page">
|
||||
<view class="category-page__header">
|
||||
<text class="category-page__header-title">{{title}}</text>
|
||||
</view>
|
||||
|
||||
<view class="category-page__filters">
|
||||
<view class="category-page__filter-row">
|
||||
<text class="category-page__filter-label">年龄</text>
|
||||
<view class="category-page__pills">
|
||||
<view
|
||||
wx:for="{{ageFilters}}"
|
||||
wx:key="key"
|
||||
class="category-page__pill {{activeAge === item.key ? 'category-page__pill--active' : ''}}"
|
||||
data-key="{{item.key}}"
|
||||
bindtap="onAgeFilter"
|
||||
>
|
||||
{{item.label}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="category-page__filter-row">
|
||||
<text class="category-page__filter-label">难度</text>
|
||||
<view class="category-page__pills">
|
||||
<view
|
||||
wx:for="{{diffFilters}}"
|
||||
wx:key="key"
|
||||
class="category-page__pill {{activeDiff === item.key ? 'category-page__pill--active' : ''}}"
|
||||
data-key="{{item.key}}"
|
||||
bindtap="onDiffFilter"
|
||||
>
|
||||
{{item.label}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{items.length}}" class="category-page__list">
|
||||
<view
|
||||
wx:for="{{items}}"
|
||||
wx:key="id"
|
||||
class="category-page__card-wrap"
|
||||
>
|
||||
<detail-card
|
||||
title="{{item.title}}"
|
||||
preview-bg="{{item.previewBg}}"
|
||||
preview-text="{{item.previewText}}"
|
||||
age-range="{{item.ageRangeLabel}}"
|
||||
difficulty="{{item.difficultyLabel}}"
|
||||
data-id="{{item.id}}"
|
||||
bind:tap="onCardTap"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:else class="category-page__empty">
|
||||
<text class="category-page__empty-text">暂无符合条件的练习纸</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -74,11 +74,5 @@
|
||||
title="测试页面入口"
|
||||
size="large"
|
||||
custom-class="centered-title" />
|
||||
<van-cell
|
||||
title="形状打印"
|
||||
clickable
|
||||
data-name="shapePrint"
|
||||
is-link
|
||||
url="/demoPages/shapePrint/index" />
|
||||
</van-cell-group>
|
||||
</view>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"navigationBarTitleText": "我的收藏",
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarBackgroundColor": "#ffffff",
|
||||
"backgroundColor": "#F6F6F6"
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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' });
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
<view class="favorites-page">
|
||||
<view class="favorites-page__title">我的收藏</view>
|
||||
|
||||
<view class="favorites-page__tabs">
|
||||
<view
|
||||
class="favorites-page__tab {{activeTab === 'favorites' ? 'favorites-page__tab--active' : ''}}"
|
||||
data-tab="favorites"
|
||||
bindtap="onTabSwitch"
|
||||
>
|
||||
收藏的题型
|
||||
</view>
|
||||
<view
|
||||
class="favorites-page__tab {{activeTab === 'downloads' ? 'favorites-page__tab--active' : ''}}"
|
||||
data-tab="downloads"
|
||||
bindtap="onTabSwitch"
|
||||
>
|
||||
下载历史
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{activeTab === 'favorites' && favoriteList.length === 0}}" class="favorites-page__empty">
|
||||
<text class="favorites-page__emoji">🦆</text>
|
||||
<view class="favorites-page__empty-msg">
|
||||
<text class="favorites-page__empty-line">鸭丫还没找到收藏呢</text>
|
||||
<text class="favorites-page__empty-line">去发现好玩的练习纸吧~</text>
|
||||
</view>
|
||||
<button class="favorites-page__btn" bindtap="onDiscoverTap">去发现</button>
|
||||
</view>
|
||||
|
||||
<view wx:elif="{{activeTab === 'downloads' && downloadList.length === 0}}" class="favorites-page__empty">
|
||||
<text class="favorites-page__emoji">🦆</text>
|
||||
<view class="favorites-page__empty-msg">
|
||||
<text class="favorites-page__empty-line">还没有下载记录哦</text>
|
||||
<text class="favorites-page__empty-line">快去生成练习纸吧~</text>
|
||||
</view>
|
||||
<button class="favorites-page__btn" bindtap="onDiscoverTap">去发现</button>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"navigationBarTitleText": "打印指南",
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarBackgroundColor": "#f7ee47",
|
||||
"backgroundColor": "#F6F6F6"
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
Page({
|
||||
data: {},
|
||||
|
||||
onLoad() {
|
||||
// 静态说明页,无额外逻辑
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
<view class="guide-page">
|
||||
<view class="guide-page__hero">
|
||||
<text class="guide-page__hero-icon">🖨️</text>
|
||||
<text class="guide-page__hero-title">3 步完成打印</text>
|
||||
</view>
|
||||
|
||||
<view class="guide-page__steps">
|
||||
<view class="guide-page__timeline-line" />
|
||||
|
||||
<view class="guide-page__step">
|
||||
<view class="guide-page__step-num-wrap">
|
||||
<view class="guide-page__step-num">1</view>
|
||||
</view>
|
||||
<view class="guide-page__step-body">
|
||||
<text class="guide-page__step-title">保存到相册</text>
|
||||
<text class="guide-page__step-desc">在练习纸预览页点击「保存」或「保存到相册」,授权后即可保存图片。</text>
|
||||
<view class="guide-page__tip">提示:建议先关闭深色模式截图,打印对比度更好。</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="guide-page__step">
|
||||
<view class="guide-page__step-num-wrap">
|
||||
<view class="guide-page__step-num">2</view>
|
||||
</view>
|
||||
<view class="guide-page__step-body">
|
||||
<text class="guide-page__step-title">连接打印机</text>
|
||||
<text class="guide-page__step-desc">确保打印机与手机处于同一 Wi-Fi,或按打印机说明完成蓝牙配对。</text>
|
||||
<view class="guide-page__tip">家用喷墨 / 激光打印机均可;A4 纸竖向打印即可。</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="guide-page__step guide-page__step--last">
|
||||
<view class="guide-page__step-num-wrap">
|
||||
<view class="guide-page__step-num">3</view>
|
||||
</view>
|
||||
<view class="guide-page__step-body">
|
||||
<text class="guide-page__step-title">从相册打印</text>
|
||||
<text class="guide-page__step-desc">打开手机相册,选中刚保存的练习纸图片,点击分享或打印。</text>
|
||||
<view class="guide-page__tip">
|
||||
<text class="guide-page__tip-line">iPhone:分享 → 打印 → 选择打印机。</text>
|
||||
<text class="guide-page__tip-line">Android:更多 → 打印 或 使用系统相册自带的打印入口。</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="guide-page__faq-title">常见问题</view>
|
||||
|
||||
<view class="guide-page__faq">
|
||||
<view class="guide-page__faq-item">
|
||||
<text class="guide-page__faq-q">保存失败怎么办?</text>
|
||||
<text class="guide-page__faq-a">请在小程序设置中开启相册写入权限,并检查手机存储空间是否充足。</text>
|
||||
</view>
|
||||
<view class="guide-page__faq-item">
|
||||
<text class="guide-page__faq-q">打印出来太小/太大?</text>
|
||||
<text class="guide-page__faq-a">在打印预览中选择「适应页面」或调整缩放比例,通常 100% 即可。</text>
|
||||
</view>
|
||||
<view class="guide-page__faq-item">
|
||||
<text class="guide-page__faq-q">没有打印机?</text>
|
||||
<text class="guide-page__faq-a">可将图片发到电脑或拷贝至 U 盘,在打印店完成打印。</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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: '',
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
<wxs module="fmt">
|
||||
function ageLabel(arr) {
|
||||
if (!arr || arr.length < 2) {
|
||||
return '';
|
||||
}
|
||||
return arr[0] + '-' + arr[1] + '岁';
|
||||
}
|
||||
module.exports.ageLabel = ageLabel;
|
||||
</wxs>
|
||||
|
||||
<view class="home-page">
|
||||
<view class="home-header">
|
||||
<view class="home-brand">
|
||||
<text class="home-brand__duck">🦆</text>
|
||||
<text class="home-brand__name">涂鸦丫</text>
|
||||
</view>
|
||||
<view class="home-search">
|
||||
<text class="home-search__placeholder">搜索练习单(即将上线)</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<category-tabs tabs="{{tabs}}" active-tab="{{activeTab}}" bind:change="onTabChange" />
|
||||
|
||||
<view class="home-body">
|
||||
<block wx:if="{{activeTab != 'all'}}">
|
||||
<view class="home-grid">
|
||||
<view
|
||||
wx:for="{{filteredItems}}"
|
||||
wx:key="id"
|
||||
class="home-grid__cell"
|
||||
bindtap="onCardTap"
|
||||
data-id="{{item.id}}"
|
||||
>
|
||||
<worksheet-card
|
||||
title="{{item.title}}"
|
||||
preview-bg="{{item.previewBg}}"
|
||||
preview-text="{{item.previewText}}"
|
||||
img="{{item.img}}"
|
||||
age-range="{{fmt.ageLabel(item.ageRange)}}"
|
||||
difficulty="{{item.difficulty}}"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<block wx:else>
|
||||
<view class="home-section home-section--hot">
|
||||
<view class="home-section__title-row">
|
||||
<text class="home-section__title">🔥 热门下载</text>
|
||||
</view>
|
||||
<scroll-view class="home-hot__scroll" scroll-x enable-flex>
|
||||
<view class="home-hot__row">
|
||||
<view
|
||||
wx:for="{{hotItems}}"
|
||||
wx:key="id"
|
||||
class="home-hot__item"
|
||||
bindtap="onHotCardTap"
|
||||
data-id="{{item.id}}"
|
||||
>
|
||||
<view class="home-hot__card-wrap">
|
||||
<worksheet-card
|
||||
title="{{item.title}}"
|
||||
preview-bg="{{item.previewBg}}"
|
||||
preview-text="{{item.previewText}}"
|
||||
img="{{item.img}}"
|
||||
age-range="{{fmt.ageLabel(item.ageRange)}}"
|
||||
difficulty="{{item.difficulty}}"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<view wx:for="{{sections}}" wx:key="category" class="home-section">
|
||||
<view class="home-section__head">
|
||||
<view class="home-section__head-left">
|
||||
<text class="home-section__icon">{{item.icon}}</text>
|
||||
<text class="home-section__name">{{item.title}}</text>
|
||||
</view>
|
||||
<text class="home-section__more" bindtap="onSeeMore" data-category="{{item.category}}">更多</text>
|
||||
</view>
|
||||
<view class="home-grid">
|
||||
<view
|
||||
wx:for="{{item.items}}"
|
||||
wx:for-item="w"
|
||||
wx:key="id"
|
||||
class="home-grid__cell"
|
||||
bindtap="onCardTap"
|
||||
data-id="{{w.id}}"
|
||||
>
|
||||
<worksheet-card
|
||||
title="{{w.title}}"
|
||||
preview-bg="{{w.previewBg}}"
|
||||
preview-text="{{w.previewText}}"
|
||||
img="{{w.img}}"
|
||||
age-range="{{fmt.ageLabel(w.ageRange)}}"
|
||||
difficulty="{{w.difficulty}}"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"navigationBarTitleText": "我的",
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarBackgroundColor": "#ffffff",
|
||||
"backgroundColor": "#F6F6F6"
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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' });
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
<view class="profile-page">
|
||||
<view class="profile-page__header">
|
||||
<view class="profile-page__avatar">🦆</view>
|
||||
<view class="profile-page__header-info">
|
||||
<text class="profile-page__name">微信用户</text>
|
||||
<text class="profile-page__stat">已生成 {{generateCount}} 张练习纸</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="profile-page__group">
|
||||
<view class="profile-page__item" bindtap="onGoToDownloads">
|
||||
<text class="profile-page__icon">📥</text>
|
||||
<text class="profile-page__label">下载历史</text>
|
||||
<text class="profile-page__arrow">›</text>
|
||||
</view>
|
||||
<view class="profile-page__item" bindtap="onGoToFavorites">
|
||||
<text class="profile-page__icon">❤️</text>
|
||||
<text class="profile-page__label">我的收藏</text>
|
||||
<text class="profile-page__arrow">›</text>
|
||||
</view>
|
||||
<view class="profile-page__item" bindtap="onGoToPrintPlan">
|
||||
<text class="profile-page__icon">📋</text>
|
||||
<text class="profile-page__label">每日打印计划</text>
|
||||
<view class="profile-page__badge">即将上线</view>
|
||||
<text class="profile-page__arrow">›</text>
|
||||
</view>
|
||||
<view class="profile-page__item profile-page__item--last" bindtap="onGoToStats">
|
||||
<text class="profile-page__icon">📊</text>
|
||||
<text class="profile-page__label">学习统计</text>
|
||||
<text class="profile-page__arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="profile-page__group">
|
||||
<view class="profile-page__item" bindtap="onGoToGuide">
|
||||
<text class="profile-page__icon">🖨️</text>
|
||||
<text class="profile-page__label">打印指南</text>
|
||||
<text class="profile-page__arrow">›</text>
|
||||
</view>
|
||||
<view class="profile-page__item" bindtap="onGoToPrintSettings">
|
||||
<text class="profile-page__icon">⚙️</text>
|
||||
<text class="profile-page__label">打印设置</text>
|
||||
<text class="profile-page__arrow">›</text>
|
||||
</view>
|
||||
<view class="profile-page__item" bindtap="onGoToFeedback">
|
||||
<text class="profile-page__icon">💬</text>
|
||||
<text class="profile-page__label">意见反馈</text>
|
||||
<text class="profile-page__arrow">›</text>
|
||||
</view>
|
||||
<view class="profile-page__item profile-page__item--last" bindtap="onGoToAbout">
|
||||
<text class="profile-page__icon">ℹ️</text>
|
||||
<text class="profile-page__label">关于涂鸦丫</text>
|
||||
<text class="profile-page__arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="profile-page__footer">
|
||||
<text class="profile-page__footer-emoji">🦆</text>
|
||||
<text class="profile-page__footer-slogan">陪孩子一起,把学习变成游戏</text>
|
||||
<text class="profile-page__footer-version">v{{version}}</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -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<CanvasInitResult> {
|
||||
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<string> {
|
||||
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<WechatMiniprogram.Image> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = canvas.createImage();
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = reject;
|
||||
img.src = src;
|
||||
});
|
||||
}
|
||||
@@ -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<string, any>,
|
||||
): Promise<CloudResult> {
|
||||
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<string, any>,
|
||||
limit: number = 20,
|
||||
orderBy?: { field: string; order: 'asc' | 'desc' },
|
||||
): Promise<any[]> {
|
||||
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 [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* 图片平台适配器
|
||||
* 封装微信小程序图片加载和保存 API
|
||||
*/
|
||||
|
||||
/**
|
||||
* 保存图片到相册
|
||||
*/
|
||||
export function saveImageToAlbum(filePath: string): Promise<void> {
|
||||
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<WechatMiniprogram.GetImageInfoSuccessCallbackResult> {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.getImageInfo({
|
||||
src,
|
||||
success: resolve,
|
||||
fail: reject,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 存储平台适配器
|
||||
* 封装微信小程序 Storage API
|
||||
*/
|
||||
|
||||
export function getStorage<T>(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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||