feat: 分类数据统一

This commit is contained in:
R524809
2026-04-23 10:54:22 +08:00
parent 2a1dbad164
commit b2ab123b4f
15 changed files with 757 additions and 221 deletions
+596
View File
@@ -0,0 +1,596 @@
# Doodle Mini — Debug 模式内容发布方案
> 版本:v1.0
> 最后更新:2026-04-23
> 配套文档:[技术架构设计文档](./技术架构设计文档.md) | [小程序云开发方案](./小程序云开发方案.md) | [产品设计文档](./产品设计文档.md)
---
## 一、方案定位与动机
### 1.1 当前痛点
每新增一种可打印资料,从开发完成到上线需要经历以下**手动步骤**:
```
开发页面 + Draw 逻辑
→ ① 在预览中截图或导出入口图原图
→ ② 放入 node-tools/entranceInput/<分类>/
→ ③ 运行 processEntrancePicture.js 裁剪缩放
→ ④ 从 entranceOuput/ 拷贝到 miniprogram/assets/entrancePicture/
→ ⑤ 手动编辑 category.data.ts 添加元数据(title、subtitle、path、img 等)
→ ⑥ 提交代码 → 审核 → 发版
```
其中 ①~⑤ 是重复性机械劳动,且容易出错(路径拼错、忘记更新数据文件、图片尺寸不一致等)。
### 1.2 核心洞察
**当一个页面开发调试完成时,所有需要的信息已经就绪**
- **页面路径** — 就是当前正在调试的页面 URL
- **标题 / 副标题** — 已写在绘制逻辑或配置中
- **入口图片** — 预览 Canvas 上正在渲染的就是
- **分类 / 难度 / 年龄段 / 标签** — 已在页面配置中定义
在这个时刻一键上传,是**成本最低、数据最准确**的做法。
### 1.3 方案目标
在 **develop / devtools 环境**的预览页中增加「发布到云端」按钮,点击后自动:
1. 从 Canvas 导出入口图 → 裁剪 → 压缩
2. 上传图片到云存储
3. 组装页面元数据 → 写入云数据库 `worksheets` 集合
4. 线上小程序通过缓存策略拉取云端数据,新内容**无需发版即可上线**
**不需要开发独立后台系统,不需要手动录入数据。**
---
## 二、整体流程
```
┌──────────────────────────────────────────────────────────────────┐
│ Debug 模式发布流程 │
│ │
│ ① 开发者在 develop / devtools 环境下完成页面开发 │
│ (绘制逻辑调试完毕,预览效果满意) │
│ │
│ ② 预览区底部出现 [📤 发布到云端] 按钮 │
│ (仅 develop / devtools 环境可见,线上版本不显示) │
│ │
│ ③ 点击后弹出确认面板,预览即将上传的数据: │
│ ┌─────────────────────────────────────────┐ │
│ │ ID: letter-tracing-daily-checkin │ │
│ │ 标题: 每日打卡 │ │
│ │ 副标题: 四宫格每日字母打卡练习 │ │
│ │ 分类: english │ │
│ │ 路径: /englishPages/letterTracing/... │ │
│ │ 难度: beginner │ │
│ │ 年龄: 4-7岁 │ │
│ │ 标签: [字母, 描红, 打卡] │ │
│ │ [预览图缩略图] │ │
│ │ │ │
│ │ ─ 可编辑字段(允许微调) ─ │ │
│ │ │ │
│ │ 状态: ○ draft(默认) ○ active │ │
│ │ │ │
│ │ [取消] [确认发布] │ │
│ └─────────────────────────────────────────┘ │
│ │
│ ④ 确认发布后,自动执行: │
│ a. Canvas 导出 → 裁剪页眉页脚 → 压缩至 ≤200KB │
│ b. 上传图片到云存储 │
│ cloud://xxx/assets/previews/<category>/<id>.jpg │
│ c. 组装 WorksheetConfig → upsert 到云数据库 worksheets 集合 │
│ d. 显示「发布成功 ✅」 │
│ │
│ ⑤ 线上小程序按缓存策略拉取新数据,内容自动出现 │
│ (或在 debug 管理页手动将 status 从 draft 切为 active
└──────────────────────────────────────────────────────────────────┘
```
---
## 三、技术设计
### 3.1 环境门控
发布功能**仅在开发环境**可见,**绝不暴露给普通用户**。
```typescript
/**
* 判断当前是否为开发环境,可执行 debug 发布操作。
* 复用 downloadPrint.ts 中 isDevBypassLimits 的判断模式。
*/
function isDebugPublishEnabled(): boolean {
const accountInfo = wx.getAccountInfoSync();
const { envVersion } = accountInfo.miniProgram;
// develop: 开发版;trial: 体验版;release: 正式版
return envVersion === 'develop';
}
```
WXML 中条件渲染:
```xml
<!-- 仅开发环境显示发布按钮 -->
<view wx:if="{{isDevEnv}}" class="debug-publish-bar">
<button bind:tap="onDebugPublish">📤 发布到云端</button>
</view>
```
### 3.2 页面元数据约定
每个 draw 页面需提供 `getPublishMeta()` 方法,返回标准化的发布元数据。
```typescript
/**
* 发布元数据接口
* 各 draw 页面实现此接口,提供自身的配置信息
*/
interface PublishMeta {
// ─── 必填字段 ───
id: string; // 唯一标识,如 'letter-tracing-daily-checkin'
title: string; // 显示标题
subtitle: string; // 显示副标题
category: 'math' | 'chinese' | 'english' | 'puzzle' | 'craft';
subcategory: string; // 子分类
path: string; // 页面完整路径(含参数)
// ─── 选填字段(有默认值)───
icon?: string; // Emoji 图标
ageRange?: [number, number]; // 适用年龄,默认 [3, 8]
difficulty?: 'beginner' | 'basic' | 'intermediate' | 'advanced';
tags?: string[]; // 搜索标签
sortOrder?: number; // 排序权重
status?: 'draft' | 'active' | 'hidden'; // 默认 'draft'
// ─── 模板引擎相关(可选,未来扩展)───
template?: string;
generator?: string;
generatorConfig?: Record<string, any>;
layoutConfig?: Record<string, any>;
}
```
`pageMixin` 中约定调用方式:
```typescript
// pageMixin 中新增
debugPublishMixin: {
getPublishMeta(): PublishMeta {
// 子类覆写此方法
throw new Error('页面未实现 getPublishMeta()');
}
}
```
各页面实现示例(letterTracing):
```typescript
getPublishMeta(): PublishMeta {
const drawService = this.drawService;
return {
id: this.data.currentId,
title: drawService.title,
subtitle: drawService.subtitle,
category: 'english',
subcategory: 'letter-tracing',
path: `/englishPages/letterTracing/letterTracing?id=${this.data.currentId}`,
icon: '🔠',
ageRange: [4, 7],
difficulty: 'beginner',
tags: ['字母', '描红', '英语'],
};
}
```
### 3.3 入口图导出与处理
复用已有的 `preview-card` 组件的 `exportToTempFile` 能力,再做裁剪处理。
```typescript
/**
* 从预览 Canvas 导出入口图
*
* 处理流程:
* 1. 从 preview-card 导出完整 A4 临时文件
* 2. 用离屏 Canvas 裁剪掉页眉/页脚区域(与 processEntrancePicture.js 逻辑对齐)
* 3. 缩放至入口图标准宽度(600px)
* 4. 导出为 JPEGquality: 0.85
*/
async function exportEntranceImage(
canvas: WechatMiniprogram.Canvas,
ctx: CanvasRenderingContext2D,
paperConfig: { headerHeight: number; footerHeight: number; width: number; height: number }
): Promise<string> {
// 裁剪参数 — 与 node-tools/processEntrancePicture.js 保持一致
const cropTop = paperConfig.headerHeight;
const cropBottom = paperConfig.footerHeight;
const sourceW = paperConfig.width;
const sourceH = paperConfig.height - cropTop - cropBottom;
const ENTRANCE_WIDTH = 600;
const scale = ENTRANCE_WIDTH / sourceW;
const targetH = Math.round(sourceH * scale);
// 调整 Canvas 尺寸用于裁剪输出
canvas.width = ENTRANCE_WIDTH;
canvas.height = targetH;
ctx.drawImage(
canvas, // 自身作为源(需先 toDataURL 再 loadImage,实际实现需用临时文件中转)
0, cropTop, sourceW, sourceH, // 源区域:去掉页眉页脚
0, 0, ENTRANCE_WIDTH, targetH // 目标区域:缩放到标准宽度
);
const tempPath = await canvasToTempFilePath(canvas, {
fileType: 'jpg',
quality: 0.85,
destWidth: ENTRANCE_WIDTH,
destHeight: targetH,
});
return tempPath;
}
```
> **实际实现说明**:小程序 Canvas 不能直接自引用 `drawImage`,需要先导出为临时文件(`canvasToTempFilePath`),再用 `canvas.createImage()` 加载临时文件,然后在清空的 Canvas 上绘制裁剪区域。具体实现参考 `preview-card` 已有的 `exportToTempFile` 方法。
### 3.4 云端上传
#### 3.4.1 图片上传到云存储
```typescript
async function uploadEntranceImage(
tempFilePath: string,
category: string,
id: string
): Promise<string> {
const cloudPath = `assets/previews/${category}/${id}.jpg`;
const res = await wx.cloud.uploadFile({
cloudPath,
filePath: tempFilePath,
});
return res.fileID; // cloud://doodle-xxx/assets/previews/english/letter-tracing-daily-checkin.jpg
}
```
#### 3.4.2 元数据写入云数据库
```typescript
async function publishWorksheet(meta: PublishMeta, imageFileID: string): Promise<void> {
const db = wx.cloud.database();
const collection = db.collection('worksheets');
const doc = {
...meta,
previewImage: imageFileID,
status: meta.status || 'draft',
publishedAt: db.serverDate(),
updatedAt: db.serverDate(),
version: 1,
publishedBy: 'debug', // 标记为 debug 发布
};
// upsert:如果已存在则更新,不存在则创建
const existing = await collection.where({ id: meta.id }).get();
if (existing.data.length > 0) {
const oldDoc = existing.data[0];
await collection.doc(oldDoc._id).update({
data: {
...doc,
version: (oldDoc.version || 0) + 1,
updatedAt: db.serverDate(),
},
});
} else {
await collection.add({ data: doc });
}
}
```
### 3.5 发布流程整合
```typescript
/**
* Debug 发布入口 — 挂载在 pageMixin 上
* 由预览页的「发布到云端」按钮触发
*/
async function onDebugPublish(this: any): Promise<void> {
if (!isDebugPublishEnabled()) return;
// 1. 获取页面元数据
const meta: PublishMeta = this.getPublishMeta();
// 2. 弹出确认面板(展示即将发布的数据,允许微调)
const confirmed = await showPublishConfirmDialog(meta);
if (!confirmed) return;
wx.showLoading({ title: '发布中...' });
try {
// 3. 导出入口图
const tempPath = await exportEntranceImage(
this.canvas, this.ctx, this.paperConfig
);
// 4. 上传图片到云存储
const fileID = await uploadEntranceImage(
tempPath, meta.category, meta.id
);
// 5. 写入云数据库
await publishWorksheet(meta, fileID);
wx.hideLoading();
wx.showToast({ title: '发布成功 ✅', icon: 'success' });
} catch (err) {
wx.hideLoading();
wx.showModal({
title: '发布失败',
content: JSON.stringify(err),
showCancel: false,
});
}
}
```
---
## 四、云端数据与现有架构的衔接
### 4.1 数据加载优先级(保持不变)
与 [技术架构设计文档](./技术架构设计文档.md) 第 6.3 节、[小程序云开发方案](./小程序云开发方案.md) 第五节一致:
```
优先级 1: 本地缓存(wx.Storage
优先级 2: 云端拉取(worksheets 集合) ← debug 发布的数据在此
优先级 3: 前端内置兜底(category.data.ts)← 保持稳定兜底
```
### 4.2 category.data.ts 的角色变化
| 阶段 | category.data.ts 的作用 |
|------|------------------------|
| **当前** | 唯一数据源(硬编码所有题型信息) |
| **方案实施后** | 兜底数据源 + 离线保障(云端不可用时生效) |
| **长期** | 通过 `syncFromCloud` 脚本自动同步,保持与云端一致 |
### 4.3 worksheets 集合字段映射
debug 发布写入的字段,与 [小程序云开发方案](./小程序云开发方案.md) §3.3 `worksheets` 集合的字段**完全对齐**
| PublishMeta 字段 | worksheets 集合字段 | 说明 |
|-----------------|-------------------|------|
| `id` | `id` | 业务唯一标识 |
| `title` | `title` | 显示标题 |
| `subtitle` | `desc` | 显示描述 |
| `category` | `category` | 所属大类 |
| `subcategory` | `subcategory` | 子分类 |
| `path` | 新增字段 `pagePath` | 小程序页面路径 |
| `icon` | 新增字段 `icon` | Emoji 图标 |
| `ageRange` | `ageRange` | 适用年龄段 |
| `difficulty` | `difficulty` | 难度级别 |
| `tags` | `tags` | 搜索标签 |
| `sortOrder` | `sortOrder` | 排列顺序 |
| `status` | `status` | 上架状态 |
| — | `previewImage` | 云存储 fileID |
| — | `publishedAt` | 发布时间 |
| — | `version` | 版本号(递增) |
---
## 五、云存储目录规划
与 [小程序云开发方案](./小程序云开发方案.md) §6 一致:
```
cloud://doodle-xxx/
├── assets/
│ └── previews/ ← debug 发布的入口图存放于此
│ ├── math/
│ │ ├── number-find.jpg
│ │ ├── addition-10.jpg
│ │ └── ...
│ ├── english/
│ │ ├── letter-tracing-single.jpg
│ │ ├── letter-tracing-daily-checkin.jpg
│ │ └── ...
│ ├── puzzle/
│ ├── chinese/
│ └── craft/
```
命名规则:`<id>.jpg`,与 `PublishMeta.id` 一致,便于查找和管理。
---
## 六、安全与权限控制
### 6.1 客户端门控
```typescript
// 三重保障
const canPublish =
isDebugPublishEnabled() // ① envVersion === 'develop'
&& isDevBypassLimits() // ② 与下载绕过逻辑一致
&& wx.getStorageSync('enableDebug'); // ③ debug 页面手动开启
```
### 6.2 云端安全规则
在云数据库安全规则中,`worksheets` 集合限制写入权限:
```json
{
"worksheets": {
".write": false,
".read": true
}
}
```
debug 发布通过**云函数**中转,云函数内校验 `openId` 白名单:
```javascript
// 云函数 debugPublish
exports.main = async (event, context) => {
const { OPENID } = cloud.getWXContext();
const ADMIN_OPENIDS = ['开发者的openid'];
if (!ADMIN_OPENIDS.includes(OPENID)) {
throw new Error('无权限执行此操作');
}
// 执行 upsert 操作...
};
```
### 6.3 数据保护
| 措施 | 说明 |
|------|------|
| 默认 draft 状态 | 发布后默认 `status: 'draft'`,需手动激活为 `active` |
| 版本递增 | 每次更新 `version + 1`,可追踪变更历史 |
| publishedBy 标记 | `publishedBy: 'debug'` 区分来源 |
| 时间戳 | `publishedAt` / `updatedAt` 记录操作时间 |
---
## 七、辅助工具
### 7.1 syncFromCloud 脚本
`node-tools/` 中新增脚本,发版前从云数据库拉取最新数据,同步回 `category.data.ts` 作为兜底数据。
```
Debug 发布 → 云端数据 → 线上可见
syncFromCloud.js ← 发版前运行
category.data.ts 更新 ← 兜底数据同步
下次发版包含
```
```javascript
// node-tools/src/syncFromCloud.js(伪代码)
// 通过云开发 HTTP API 或管理端 SDK 拉取 worksheets 集合
// 按 category 分组 → 生成 TypeScript 代码 → 写入 category.data.ts
```
### 7.2 Debug 管理页扩展
在已有的 `supportPages/debug/debug` 页面中增加 tab,提供简易内容管理:
```
┌─────────────────────────────────────────────┐
│ Debug 工具页 │
│ │
│ [调试配置] [已发布内容] [系统信息] │
│ │
│ ┌─────────────────────────────────────┐ │
│ │ 已发布内容列表 │ │
│ │ │ │
│ │ ● letter-tracing-single │ │
│ │ 状态: active 版本: 3 英语 │ │
│ │ [编辑] [下架] │ │
│ │ │ │
│ │ ● letter-tracing-daily-checkin │ │
│ │ 状态: draft 版本: 1 英语 │ │
│ │ [激活] [编辑] [删除] │ │
│ │ │ │
│ │ ● addition-10 │ │
│ │ 状态: active 版本: 5 数学 │ │
│ │ [编辑] [下架] │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
```
---
## 八、实施计划
### Phase 1:基础发布能力(约 0.5~1 天)
- [ ] `pageMixin` 中新增 `getPublishMeta()` 接口约定
- [ ] 实现 `isDebugPublishEnabled()` 环境判断
- [ ] 实现入口图导出 + 裁剪逻辑
- [ ] 实现云存储上传 + 云数据库写入
- [ ]`preview-card``preview-footer-actions` 中添加发布按钮(条件渲染)
### Phase 2:确认面板与安全(约 0.5 天)
- [ ] 发布前确认弹窗(数据预览 + 可编辑字段)
- [ ] 云函数安全校验(openId 白名单)
- [ ] 默认 draft 状态 + 版本管理
### Phase 3:数据消费侧适配(约 0.5~1 天)
- [ ] `category.ts`(或 `worksheet-service`)增加从云 DB 拉取逻辑
- [ ] 实现缓存策略(本地缓存 → 云端 → 内置兜底)
- [ ] 入口图从云存储 fileID 获取临时 URL 展示
### Phase 4:辅助工具(约 0.5 天)
- [ ] `syncFromCloud.js` 脚本
- [ ] Debug 管理页扩展(列表 + 状态管理)
**总计预估:2~3 天**
---
## 九、与模板引擎的衔接(远期)
当 [技术架构设计文档](./技术架构设计文档.md) 第五章所述模板引擎落地后,debug 发布方案可进一步升级:
```
┌─────────────────────────────────────────────────────────────────┐
│ 模板引擎 + Debug 发布(远期形态) │
│ │
│ ① 在通用 worksheet 页面中: │
│ 选择模板 + 配置生成器参数 → 实时预览 │
│ │
│ ② 效果满意后点击「发布」: │
│ 自动上传: │
│ • JSON 配置(template + generator + generatorConfig + layout)│
│ • 入口预览图 │
│ • 元数据(title、tags、ageRange 等) │
│ │
│ ③ 新题型无需任何前端代码改动即可上线 │
│ (前提:使用已有的 TemplateRenderer + Generator 组合) │
│ │
│ ④ 配合 80% 新题型可动态上线的目标 │
│ debug 发布成为主要的内容上线方式 │
└─────────────────────────────────────────────────────────────────┘
```
---
## 十、风险与应对
| 风险 | 影响 | 应对 |
|------|------|------|
| **误操作发布测试数据** | 污染线上列表 | 默认 `status: 'draft'`;确认面板二次确认 |
| **入口图质量不一致** | 不同设备/DPR 下渲染差异 | 统一使用开发者工具发布;导出时固定 `destWidth/destHeight` |
| **云端数据丢失** | 已发布内容消失 | `category.data.ts` 兜底;`syncFromCloud` 定期同步 |
| **安全:非授权上传** | 恶意写入数据 | 云函数白名单校验;客户端三重门控 |
| **双数据源不一致** | 本地兜底与云端数据冲突 | 云端优先,发版前 `syncFromCloud` 对齐 |
| **云存储额度** | 图片累积占用存储 | 入口图约 50~200KB/张,100 张仅 ~20MB,远低于免费额度 |
---
## 附录 A:与现有代码的关系
| 现有模块 | 本方案的关联 |
|---------|------------|
| `preview-card` 组件 | 复用 `exportToTempFile`,新增裁剪逻辑 |
| `pageMixin.ts` | 新增 `getPublishMeta()` 约定和 `onDebugPublish()` 方法 |
| `downloadPrint.ts` / `isDevBypassLimits()` | 复用环境判断模式 |
| `category.data.ts` | 角色从「唯一数据源」变为「兜底数据源」 |
| `supportPages/debug/debug` | 扩展内容管理 tab |
| `node-tools/processEntrancePicture.js` | 裁剪参数对齐;流程被 debug 发布替代 |
| 云数据库 `worksheets` 集合 | 写入发布数据(字段与云开发方案对齐) |
| 云存储 `assets/previews/` | 存放入口图 |
Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

+19 -44
View File
@@ -1,52 +1,27 @@
import type { CategoryType } from '../models/category'; import type { CategoryType } from '../models/category';
export const CATEGORIES: CategoryType[] = [ export const ALL_CATEGORY: CategoryType = {
{ id: 'all',
id: 'math', name: '全部',
name: '数感启蒙', icon: '📋',
icon: '📐', };
color: '#e3f2fd',
gradient: ['#fff9c4', '#fff176'], export const CATEGORY_LIST: CategoryType[] = [
}, { id: 'math', name: '数感启蒙', icon: '📐' },
{ { id: 'puzzle', name: '益智游戏', icon: '🧩' },
id: 'chinese', { id: 'pinyin', name: '汉语拼音', icon: '🔤' },
name: '趣味识字', { id: 'chinese', name: '趣味识字', icon: '✏️' },
icon: '✏️', { id: 'english', name: '英语启蒙', icon: '🔤' },
color: '#e8f5e9', { id: 'craft', name: '创意手工', icon: '🌈' },
gradient: ['#c8e6c9', '#a5d6a7'], ];
},
{ export const CATEGORY_LIST_WITH_ALL: CategoryType[] = [
id: 'english', ALL_CATEGORY,
name: '英语启蒙', ...CATEGORY_LIST,
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 { export function getCategoryById(id: string): CategoryType | undefined {
return CATEGORIES.find((c) => c.id === id); return CATEGORY_LIST_WITH_ALL.find((c) => c.id === id);
} }
export function getCategoryName(id: string): string { export function getCategoryName(id: string): string {
+4 -6
View File
@@ -1,13 +1,11 @@
import type { WorksheetCategory } from './worksheet';
/** Tab / 首页用的分类展示模型 */ /** Tab / 首页用的分类展示模型 */
export interface CategoryType { export interface CategoryType {
id: WorksheetCategory; id: string;
name: string; name: string;
icon: string; icon: string;
color: string; img?: string;
/** 分类用于背景或装饰的渐变色,数组中为渐变起止的两个颜色值 */ color?: string;
gradient: [string, string]; sortOrder?: number;
} }
/** 云集合 `categories` 文档(与小程序云开发方案 §3.4 对齐) */ /** 云集合 `categories` 文档(与小程序云开发方案 §3.4 对齐) */
@@ -14,7 +14,6 @@ import {
drawClosedFourLineGrid, drawClosedFourLineGrid,
drawLetterInFourLineGrid, drawLetterInFourLineGrid,
loadLetterFont, loadLetterFont,
type LetterFontSizes,
} from '../../shared/draw/drawTools'; } from '../../shared/draw/drawTools';
type DailyCheckinData = Extract< type DailyCheckinData = Extract<
@@ -34,7 +33,7 @@ const SECTION_PADDING_TOP = 12;
const SECTION_PADDING_BOTTOM = 10; const SECTION_PADDING_BOTTOM = 10;
// 标题与上一元素的垂直间距(px) // 标题与上一元素的垂直间距(px)
const HEADER_TITLE_GAP = 24; const HEADER_TITLE_GAP = 32;
// 标题与元信息(如日期、学号等)的间距(px) // 标题与元信息(如日期、学号等)的间距(px)
const HEADER_META_GAP = 22; const HEADER_META_GAP = 22;
// 正文内容距标题/元信息顶部的间距(px) // 正文内容距标题/元信息顶部的间距(px)
@@ -163,6 +162,7 @@ export default class DailyCheckinDraw extends BaseDrawService {
const cx = rect.x + rect.w / 2; const cx = rect.x + rect.w / 2;
const titleY = rect.y + SECTION_PADDING_TOP + 12; const titleY = rect.y + SECTION_PADDING_TOP + 12;
const metaY = titleY + HEADER_TITLE_GAP; const metaY = titleY + HEADER_TITLE_GAP;
const metaUnderlineY = metaY + 5;
this.ctx.save(); this.ctx.save();
this.ctx.fillStyle = '#322E25'; this.ctx.fillStyle = '#322E25';
@@ -172,23 +172,21 @@ export default class DailyCheckinDraw extends BaseDrawService {
this.ctx.font = '20px serif'; this.ctx.font = '20px serif';
this.ctx.fillText('每日打卡', cx, titleY); this.ctx.fillText('每日打卡', cx, titleY);
this.ctx.font = '14px serif'; this.ctx.font = '12px serif';
this.ctx.fillText('月', rect.x + rect.w * 0.34, metaY); this.ctx.fillText('月', rect.x + 76, metaY);
this.ctx.fillText('日', rect.x + rect.w * 0.48, metaY); this.ctx.fillText('日', rect.x + 122, metaY);
this.ctx.fillText('姓名:', rect.x + rect.w * 0.7, metaY); this.ctx.fillText('姓名:', rect.x + 176, metaY);
this.ctx.strokeStyle = '#8E897E'; this.ctx.strokeStyle = '#8E897E';
this.ctx.lineWidth = 1; this.ctx.lineWidth = 1;
this.ctx.setLineDash([]); this.ctx.setLineDash([]);
this.ctx.beginPath(); this.ctx.beginPath();
this.ctx.moveTo(rect.x + rect.w * 0.14, metaY + 4); this.ctx.moveTo(rect.x + 40, metaUnderlineY);
this.ctx.lineTo(rect.x + rect.w * 0.3, metaY + 4); this.ctx.lineTo(rect.x + 68, metaUnderlineY);
this.ctx.moveTo(rect.x + rect.w * 0.38, metaY + 4); this.ctx.moveTo(rect.x + 85, metaUnderlineY);
this.ctx.lineTo(rect.x + rect.w * 0.44, metaY + 4); this.ctx.lineTo(rect.x + 113, metaUnderlineY);
this.ctx.moveTo(rect.x + rect.w * 0.52, metaY + 4); this.ctx.moveTo(rect.x + 195, metaUnderlineY);
this.ctx.lineTo(rect.x + rect.w * 0.58, metaY + 4); this.ctx.lineTo(rect.x + 248, metaUnderlineY);
this.ctx.moveTo(rect.x + rect.w * 0.78, metaY + 4);
this.ctx.lineTo(rect.x + rect.w * 0.94, metaY + 4);
this.ctx.stroke(); this.ctx.stroke();
this.ctx.restore(); this.ctx.restore();
@@ -237,13 +235,13 @@ export default class DailyCheckinDraw extends BaseDrawService {
(contentWidth - GRID_COL_GAP * (GRID_COLS - 1)) / GRID_COLS; (contentWidth - GRID_COL_GAP * (GRID_COLS - 1)) / GRID_COLS;
const cellH = const cellH =
(contentHeight - GRID_ROW_GAP * (GRID_ROWS - 1)) / GRID_ROWS; (contentHeight - GRID_ROW_GAP * (GRID_ROWS - 1)) / GRID_ROWS;
const pairGap = Math.min(16, cellW * 0.28); const pairGap = 16;
const regularSizes = calcLetterFontSizes( const regularSizes = calcLetterFontSizes(
cellH * 0.86, cellH * 0.98,
PRINT_CLEARLY_REGULAR, PRINT_CLEARLY_REGULAR,
); );
const dashedSizes = calcLetterFontSizes( const dashedSizes = calcLetterFontSizes(
cellH * 0.86, cellH * 0.98,
PRINT_CLEARLY_DASHED, PRINT_CLEARLY_DASHED,
); );
@@ -10,29 +10,6 @@
bind:canvas-ready="onCanvasReady" bind:canvas-ready="onCanvasReady"
bind:favorite="onPreviewFavorite" /> bind:favorite="onPreviewFavorite" />
<!-- 练习类型(2列卡片网格) -->
<view class="lt-section">
<text class="lt-section-title">练习类型</text>
<view class="lt-mode-grid">
<view
wx:for="{{modeOptions}}"
wx:key="id"
class="lt-mode-card {{worksheetId === item.id ? 'lt-mode-card--active' : ''}}"
data-id="{{item.id}}"
hover-class="lt-mode-card--pressed"
hover-start-time="0"
hover-stay-time="70"
bindtap="onSelectMode">
<toy-icon
name="{{item.icon}}"
size="42rpx"
color="{{worksheetId === item.id ? '#453900' : '#605b50'}}"
custom-class="lt-mode-card__icon" />
<text class="lt-mode-card__label">{{item.label}}</text>
</view>
</view>
</view>
<!-- 选择字母(默认字帖模式) --> <!-- 选择字母(默认字帖模式) -->
<view wx:if="{{showLetterPicker}}" class="lt-section"> <view wx:if="{{showLetterPicker}}" class="lt-section">
<text class="lt-section-title">选择字母</text> <text class="lt-section-title">选择字母</text>
@@ -123,6 +100,30 @@
custom-class="lt-shuffle-btn__icon" /> custom-class="lt-shuffle-btn__icon" />
<text class="lt-shuffle-btn__text">换一批</text> <text class="lt-shuffle-btn__text">换一批</text>
</view> </view>
<!-- 练习类型(2列卡片网格) -->
<view class="lt-section">
<text class="lt-section-title">练习类型</text>
<view class="lt-mode-grid">
<view
wx:for="{{modeOptions}}"
wx:key="id"
class="lt-mode-card {{worksheetId === item.id ? 'lt-mode-card--active' : ''}}"
data-id="{{item.id}}"
hover-class="lt-mode-card--pressed"
hover-start-time="0"
hover-stay-time="70"
bindtap="onSelectMode">
<toy-icon
name="{{item.icon}}"
size="42rpx"
color="{{worksheetId === item.id ? '#453900' : '#605b50'}}"
custom-class="lt-mode-card__icon" />
<text class="lt-mode-card__label">{{item.label}}</text>
</view>
</view>
</view>
<draw-ad type="mathDraw"></draw-ad> <draw-ad type="mathDraw"></draw-ad>
</view> </view>
</view> </view>
+20 -30
View File
@@ -1,3 +1,5 @@
import { CATEGORY_LIST } from '../../core/data/categories';
export type CategoryItem = { export type CategoryItem = {
id: string; id: string;
title: string; title: string;
@@ -58,13 +60,8 @@ function item(input: ItemInput): CategoryItem {
return { ...input, likes, downloads, date: TODAY }; return { ...input, likes, downloads, date: TODAY };
} }
// prettier-ignore const CATEGORY_ITEMS_BY_ID: Record<string, CategoryItem[]> = {
export const CATEGORY_DATA: CategoryDataset = { math: [
searchPlaceholder: '搜索练习纸...',
categories: [
{
id: 'math', name: '数感启蒙', icon: '📐',
items: [
item({ id: 'number-find', title: '找数字,涂一涂', subtitle: '在数字方阵中找出目标数字并涂色', icon: '🔍', img: '/assets/entrancePicture/math/number-find.png', ageBand: age(3, 5), difficulty: d('beginner'), path: '/mathPages/mathDraw/mathDraw?id=number-find', available: true }), item({ id: 'number-find', title: '找数字,涂一涂', subtitle: '在数字方阵中找出目标数字并涂色', icon: '🔍', img: '/assets/entrancePicture/math/number-find.png', ageBand: age(3, 5), difficulty: d('beginner'), path: '/mathPages/mathDraw/mathDraw?id=number-find', available: true }),
item({ id: 'number-write', title: '看数字,写一写', subtitle: '按笔画顺序练习书写数字', icon: '✏️', img: '/assets/entrancePicture/math/number-write.png', ageBand: age(3, 5), difficulty: d('beginner'), path: '/mathPages/mathDraw/mathDraw?id=number-write', available: true }), item({ id: 'number-write', title: '看数字,写一写', subtitle: '按笔画顺序练习书写数字', icon: '✏️', img: '/assets/entrancePicture/math/number-write.png', ageBand: age(3, 5), difficulty: d('beginner'), path: '/mathPages/mathDraw/mathDraw?id=number-write', available: true }),
item({ id: 'number-coloring', title: '按数字,涂颜色', subtitle: '按指定数字给对应圆圈涂色', icon: '🎨', img: '/assets/entrancePicture/math/number-coloring.png', ageBand: age(3, 5), difficulty: d('beginner'), path: '/mathPages/mathDraw/mathDraw?id=number-coloring', available: true }), item({ id: 'number-coloring', title: '按数字,涂颜色', subtitle: '按指定数字给对应圆圈涂色', icon: '🎨', img: '/assets/entrancePicture/math/number-coloring.png', ageBand: age(3, 5), difficulty: d('beginner'), path: '/mathPages/mathDraw/mathDraw?id=number-coloring', available: true }),
@@ -92,18 +89,12 @@ export const CATEGORY_DATA: CategoryDataset = {
item({ id: 'practice-mixed', title: '混合运算', subtitle: '10/20/50/100 以内加减法混合', icon: '±', img: '/assets/entrancePicture/math/practice-mixed.png', ageBand: age(5, 8), difficulty: d('intermediate'), path: '/mathPages/mathDraw/mathDraw?id=practice-mixed', available: true }), item({ id: 'practice-mixed', title: '混合运算', subtitle: '10/20/50/100 以内加减法混合', icon: '±', img: '/assets/entrancePicture/math/practice-mixed.png', ageBand: age(5, 8), difficulty: d('intermediate'), path: '/mathPages/mathDraw/mathDraw?id=practice-mixed', available: true }),
item({ id: 'multiplication-table', title: '九九乘法表', subtitle: '学习九九乘法口诀', icon: '✖️', img: '/assets/entrancePicture/math/multiplication-table.png', ageBand: age(6, 8), difficulty: d('intermediate'), path: '/mathPages/mathDraw/mathDraw?id=multiplication-table', available: true }), item({ id: 'multiplication-table', title: '九九乘法表', subtitle: '学习九九乘法口诀', icon: '✖️', img: '/assets/entrancePicture/math/multiplication-table.png', ageBand: age(6, 8), difficulty: d('intermediate'), path: '/mathPages/mathDraw/mathDraw?id=multiplication-table', available: true }),
], ],
}, pinyin: [
{
id: 'pinyin', name: '汉语拼音', icon: '🔤',
items: [
item({ id: 'pinyin-initials', title: '声母描红', subtitle: '23个声母认读与书写练习', icon: '🔤', ageBand: age(5, 7), difficulty: d('basic'), path: '', available: false }), item({ id: 'pinyin-initials', title: '声母描红', subtitle: '23个声母认读与书写练习', icon: '🔤', ageBand: age(5, 7), difficulty: d('basic'), path: '', available: false }),
item({ id: 'pinyin-finals', title: '韵母描红', subtitle: '24个韵母认读与书写练习', icon: '🔤', ageBand: age(5, 7), difficulty: d('basic'), path: '', available: false }), item({ id: 'pinyin-finals', title: '韵母描红', subtitle: '24个韵母认读与书写练习', icon: '🔤', ageBand: age(5, 7), difficulty: d('basic'), path: '', available: false }),
item({ id: 'pinyin-overall', title: '整体认读音节', subtitle: '16个整体认读音节练习', icon: '🔤', ageBand: age(5, 7), difficulty: d('intermediate'), path: '', available: false }), item({ id: 'pinyin-overall', title: '整体认读音节', subtitle: '16个整体认读音节练习', icon: '🔤', ageBand: age(5, 7), difficulty: d('intermediate'), path: '', available: false }),
], ],
}, puzzle: [
{
id: 'puzzle', name: '益智游戏', icon: '🧩',
items: [
item({ id: 'color-shape-match', title: '根据颜色画图形', subtitle: '根据颜色画出对应图形', icon: '🎯', img: '/assets/entrancePicture/focus/color-shape-match.png', ageBand: age(4, 6), difficulty: d('basic'), path: '/focusPages/focusDraw/focusDraw?id=color-shape-match', available: true }), item({ id: 'color-shape-match', title: '根据颜色画图形', subtitle: '根据颜色画出对应图形', icon: '🎯', img: '/assets/entrancePicture/focus/color-shape-match.png', ageBand: age(4, 6), difficulty: d('basic'), path: '/focusPages/focusDraw/focusDraw?id=color-shape-match', available: true }),
item({ id: 'shape-symbol', title: '图形符号配对', subtitle: '根据图形画对应符号', icon: '🔗', img: '/assets/entrancePicture/focus/shape-symbol.png', ageBand: age(4, 6), difficulty: d('basic'), path: '/focusPages/focusDraw/focusDraw?id=shape-symbol', available: true }), item({ id: 'shape-symbol', title: '图形符号配对', subtitle: '根据图形画对应符号', icon: '🔗', img: '/assets/entrancePicture/focus/shape-symbol.png', ageBand: age(4, 6), difficulty: d('basic'), path: '/focusPages/focusDraw/focusDraw?id=shape-symbol', available: true }),
item({ id: 'shape-recognition', title: '识别形状', subtitle: '识别形状,涂一涂', icon: '🔍', img: '/assets/entrancePicture/focus/shape-recognition.png', ageBand: age(3, 5), difficulty: d('beginner'), path: '/focusPages/focusDraw/focusDraw?id=shape-recognition', available: true }), item({ id: 'shape-recognition', title: '识别形状', subtitle: '识别形状,涂一涂', icon: '🔍', img: '/assets/entrancePicture/focus/shape-recognition.png', ageBand: age(3, 5), difficulty: d('beginner'), path: '/focusPages/focusDraw/focusDraw?id=shape-recognition', available: true }),
@@ -118,32 +109,31 @@ export const CATEGORY_DATA: CategoryDataset = {
item({ id: 'grid-drawing-5x5', title: '格子仿画 5×5', subtitle: '创意挑战,提升观察力', icon: '🎨', img: '/assets/entrancePicture/focus/grid-drawing-5x5.png', ageBand: age(4, 6), difficulty: d('basic'), path: '/focusPages/focusDraw/focusDraw?id=grid-drawing-5x5', available: true }), item({ id: 'grid-drawing-5x5', title: '格子仿画 5×5', subtitle: '创意挑战,提升观察力', icon: '🎨', img: '/assets/entrancePicture/focus/grid-drawing-5x5.png', ageBand: age(4, 6), difficulty: d('basic'), path: '/focusPages/focusDraw/focusDraw?id=grid-drawing-5x5', available: true }),
item({ id: 'grid-drawing-7x7', title: '格子仿画 7×7', subtitle: '大师挑战,锻炼耐心', icon: '🎨', img: '/assets/entrancePicture/focus/grid-drawing-7x7.png', ageBand: age(5, 8), difficulty: d('intermediate'), path: '/focusPages/focusDraw/focusDraw?id=grid-drawing-7x7', available: true }), item({ id: 'grid-drawing-7x7', title: '格子仿画 7×7', subtitle: '大师挑战,锻炼耐心', icon: '🎨', img: '/assets/entrancePicture/focus/grid-drawing-7x7.png', ageBand: age(5, 8), difficulty: d('intermediate'), path: '/focusPages/focusDraw/focusDraw?id=grid-drawing-7x7', available: true }),
], ],
}, chinese: [
{
id: 'chinese', name: '趣味识字', icon: '✏️',
items: [
item({ id: 'word-recognition', title: '识字卡', subtitle: '输入或选字生成涂色识字卡', icon: '📖', ageBand: age(3, 6), difficulty: d('beginner'), path: '/pages/index/index', available: true }), item({ id: 'word-recognition', title: '识字卡', subtitle: '输入或选字生成涂色识字卡', icon: '📖', ageBand: age(3, 6), difficulty: d('beginner'), path: '/pages/index/index', available: true }),
item({ id: 'copybook', title: '练字帖', subtitle: '选字生成田字格笔顺练字帖', icon: '✏️', ageBand: age(4, 7), difficulty: d('basic'), path: '/pages/copyBook/copyBook', available: true }), item({ id: 'copybook', title: '练字帖', subtitle: '选字生成田字格笔顺练字帖', icon: '✏️', ageBand: age(4, 7), difficulty: d('basic'), path: '/pages/copyBook/copyBook', available: true }),
], ],
}, english: [
{
id: 'english', name: '英语启蒙', icon: '🔤',
items: [
item({ id: 'letter-tracing-single', title: '看图描红', subtitle: '单字母配图、例句与六行四线三格描红', icon: '🔠', img: '/assets/entrancePicture/english/letter-tracing-single.jpg', ageBand: age(4, 7), difficulty: d('beginner'), path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-single', available: true }), item({ id: 'letter-tracing-single', title: '看图描红', subtitle: '单字母配图、例句与六行四线三格描红', icon: '🔠', img: '/assets/entrancePicture/english/letter-tracing-single.jpg', ageBand: age(4, 7), difficulty: d('beginner'), path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-single', available: true }),
item({ id: 'letter-tracing-single2', title: '看图描红', subtitle: '单字母配图、例句与六行四线三格描红', icon: '🔠', img: '/assets/entrancePicture/english/letter-tracing-single2.jpg', ageBand: age(4, 7), difficulty: d('beginner'), path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-single', available: true }),
item({ id: 'letter-tracing-upper-lower', title: '字母总览', subtitle: 'Uppercase / Lowercase 分区总览', icon: '🔡', img: '/assets/entrancePicture/english/letter-tracing-upper-lower.jpg', ageBand: age(4, 7), difficulty: d('beginner'), path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-upper-lower', available: true }), item({ id: 'letter-tracing-upper-lower', title: '字母总览', subtitle: 'Uppercase / Lowercase 分区总览', icon: '🔡', img: '/assets/entrancePicture/english/letter-tracing-upper-lower.jpg', ageBand: age(4, 7), difficulty: d('beginner'), path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-upper-lower', available: true }),
item({ id: 'letter-tracing-case-pairing', title: '大小写练习', subtitle: '半组字母左大写右小写,逐行对照描红', icon: '✏️', img: '/assets/entrancePicture/english/letter-tracing-case-pairing.jpg', ageBand: age(4, 7), difficulty: d('beginner'), path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-case-pairing', available: true }), item({ id: 'letter-tracing-case-pairing', title: '大小写练习', subtitle: '半组字母左大写右小写,逐行对照描红', icon: '✏️', img: '/assets/entrancePicture/english/letter-tracing-case-pairing.jpg', ageBand: age(4, 7), difficulty: d('beginner'), path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-case-pairing', available: true }),
item({ id: 'letter-tracing-two-column', title: '两列练习', subtitle: '左 AM、右 NZ,大小写配对描红', icon: '🔤', img: '/assets/entrancePicture/english/letter-tracing-two-column.jpg', ageBand: age(4, 7), difficulty: d('beginner'), path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-two-column', available: true }), item({ id: 'letter-tracing-two-column', title: '两列练习', subtitle: '左 A-M、右 N-Z,大小写配对描红', icon: '🔤', img: '/assets/entrancePicture/english/letter-tracing-two-column.jpg', ageBand: age(4, 7), difficulty: d('beginner'), path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-two-column', available: true }),
item({ id: 'letter-tracing-half', title: '单字母逐行', subtitle: '13 字母半表逐字练习', icon: '📝', img: '/assets/entrancePicture/english/letter-tracing-half.jpg', ageBand: age(4, 7), difficulty: d('basic'), path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-half', available: true }), item({ id: 'letter-tracing-half', title: '单字母逐行', subtitle: '13 字母半表逐字练习', icon: '📝', img: '/assets/entrancePicture/english/letter-tracing-half.jpg', ageBand: age(4, 7), difficulty: d('basic'), path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-half', available: true }),
item({ id: 'letter-tracing-three', title: '三字母精练', subtitle: '每页 3 个字母,大写 + 小写深度书写', icon: '🔤', img: '/assets/entrancePicture/english/letter-tracing-three.jpg', ageBand: age(4, 7), difficulty: d('basic'), path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-three', available: true }), item({ id: 'letter-tracing-three', title: '三字母精练', subtitle: '每页 3 个字母,大写 + 小写深度书写', icon: '🔤', img: '/assets/entrancePicture/english/letter-tracing-three.jpg', ageBand: age(4, 7), difficulty: d('basic'), path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-three', available: true }),
item({ id: 'letter-tracing-daily-checkin', title: '每日打卡', subtitle: '四宫格每日字母打卡练习', icon: '🔤', img: '/assets/entrancePicture/english/letter-tracing-daily-checkin.jpg', ageBand: age(4, 7), difficulty: d('basic'), path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-daily-checkin', available: true }),
], ],
}, craft: [
{
id: 'craft', name: '创意手工', icon: '🌈',
items: [
item({ id: 'craft-coloring', title: '涂色卡', subtitle: '动物、交通、节日主题涂色', icon: '🎨', ageBand: age(3, 6), difficulty: d('beginner'), path: '', available: false }), item({ id: 'craft-coloring', title: '涂色卡', subtitle: '动物、交通、节日主题涂色', icon: '🎨', ageBand: age(3, 6), difficulty: d('beginner'), path: '', available: false }),
item({ id: 'craft-origami', title: '折纸模板', subtitle: '打印后即可折叠的趣味模板', icon: '🪭', ageBand: age(4, 7), difficulty: d('basic'), path: '', available: false }), item({ id: 'craft-origami', title: '折纸模板', subtitle: '打印后即可折叠的趣味模板', icon: '🪭', ageBand: age(4, 7), difficulty: d('basic'), path: '', available: false }),
item({ id: 'craft-stickers', title: '贴纸打印', subtitle: '奖励贴纸与装饰贴纸', icon: '⭐', ageBand: age(3, 8), difficulty: d('beginner'), path: '', available: false }), item({ id: 'craft-stickers', title: '贴纸打印', subtitle: '奖励贴纸与装饰贴纸', icon: '⭐', ageBand: age(3, 8), difficulty: d('beginner'), path: '', available: false }),
], ],
}, };
],
export const CATEGORY_DATA: CategoryDataset = {
searchPlaceholder: '搜索练习纸...',
categories: CATEGORY_LIST.map((category) => ({
...category,
items: CATEGORY_ITEMS_BY_ID[category.id] ?? [],
})),
}; };
+3 -2
View File
@@ -134,8 +134,8 @@
} }
.cat-card__thumb { .cat-card__thumb {
width: 160rpx; width: 168rpx;
height: 180rpx; height: 210rpx;
flex-shrink: 0; flex-shrink: 0;
border-radius: 10rpx; border-radius: 10rpx;
display: flex; display: flex;
@@ -143,6 +143,7 @@
justify-content: center; justify-content: center;
overflow: hidden; overflow: hidden;
border: 1rpx solid #bbb; border: 1rpx solid #bbb;
background: #f0f0f0;
} }
.cat-card__thumb-img { .cat-card__thumb-img {
+14 -30
View File
@@ -1,3 +1,4 @@
import { CATEGORY_LIST_WITH_ALL } from '../../core/data/categories';
import { CATEGORY_DATA, type CategoryItem } from './category.data'; import { CATEGORY_DATA, type CategoryItem } from './category.data';
type CategoryTab = { type CategoryTab = {
@@ -6,54 +7,37 @@ type CategoryTab = {
icon: string; icon: string;
}; };
type DisplayItem = CategoryItem & { const SIDEBAR_TABS: CategoryTab[] = CATEGORY_LIST_WITH_ALL.map(({ id, name, icon }) => ({
thumbBg: string; id,
}; name,
icon,
}));
const CATEGORY_BG: Record<string, string> = { function buildAllItems(): CategoryItem[] {
math: '#fff3c4', const items: CategoryItem[] = [];
pinyin: '#d8f0d0',
puzzle: '#f5e6dc',
chinese: '#d8f0d0',
english: '#e8f4ff',
craft: '#fce4ec',
};
const ALL_TAB: CategoryTab = { id: 'all', name: '全部', icon: '📋' };
const SIDEBAR_TABS: CategoryTab[] = [
ALL_TAB,
...CATEGORY_DATA.categories.map((c) => ({
id: c.id,
name: c.name,
icon: c.icon,
})),
];
function buildAllItems(): DisplayItem[] {
const items: DisplayItem[] = [];
for (const group of CATEGORY_DATA.categories) { for (const group of CATEGORY_DATA.categories) {
for (const item of group.items) { for (const item of group.items) {
items.push({ items.push({
...item, ...item,
thumbBg: CATEGORY_BG[group.id] ?? '#f0e7d6',
}); });
} }
} }
return items; return items;
} }
function getItemsByCategory(categoryId: string): DisplayItem[] { function getItemsByCategory(categoryId: string): CategoryItem[] {
if (categoryId === 'all') return buildAllItems(); if (categoryId === 'all') return buildAllItems();
const group = CATEGORY_DATA.categories.find((c) => c.id === categoryId); const group = CATEGORY_DATA.categories.find((c) => c.id === categoryId);
if (!group) return []; if (!group) return [];
return group.items.map((item) => ({ return group.items.map((item) => ({
...item, ...item,
thumbBg: CATEGORY_BG[group.id] ?? '#f0e7d6',
})); }));
} }
function filterByKeyword(items: DisplayItem[], keyword: string): DisplayItem[] { function filterByKeyword(
items: CategoryItem[],
keyword: string,
): CategoryItem[] {
if (!keyword) return items; if (!keyword) return items;
const kw = keyword.toLowerCase(); const kw = keyword.toLowerCase();
return items.filter( return items.filter(
@@ -77,7 +61,7 @@ Page({
searchKeyword: '', searchKeyword: '',
sidebarTabs: SIDEBAR_TABS, sidebarTabs: SIDEBAR_TABS,
activeCategoryId: 'all', activeCategoryId: 'all',
displayItems: buildAllItems() as DisplayItem[], displayItems: buildAllItems() as CategoryItem[],
scrollIntoViewId: '', scrollIntoViewId: '',
}, },
+1 -3
View File
@@ -57,9 +57,7 @@
data-title="{{item.title}}" data-title="{{item.title}}"
data-available="{{item.available}}" data-available="{{item.available}}"
bindtap="onTapItem"> bindtap="onTapItem">
<view <view class="cat-card__thumb">
class="cat-card__thumb"
style="background-color: {{item.thumbBg}}">
<image <image
wx:if="{{item.img}}" wx:if="{{item.img}}"
class="cat-card__thumb-img" class="cat-card__thumb-img"
+4 -9
View File
@@ -1,3 +1,4 @@
import { CATEGORY_LIST_WITH_ALL } from '../../core/data/categories';
import { AGE_BANDS } from '../../core/data/difficulty'; import { AGE_BANDS } from '../../core/data/difficulty';
export type HomeDisplayItem = { export type HomeDisplayItem = {
@@ -43,15 +44,9 @@ export type HomeDisplayDataset = {
sections: HomeDisplaySection[]; sections: HomeDisplaySection[];
}; };
const HOME_CATEGORY_TABS = [ const HOME_CATEGORY_TABS: HomeDisplayDataset['categoryTabs'] = CATEGORY_LIST_WITH_ALL.map(
{ id: 'all', name: '全部' }, ({ id, name }) => ({ id, name }),
{ id: 'math', name: '数感启蒙' }, );
{ id: 'pinyin', name: '汉语拼音' },
{ id: 'puzzle', name: '益智游戏' },
{ id: 'chinese', name: '趣味识字' },
{ id: 'english', name: '英语启蒙' },
{ id: 'craft', name: '创意手工' },
] as const;
const HOME_AGE_BANDS = AGE_BANDS.map((item, index) => ({ const HOME_AGE_BANDS = AGE_BANDS.map((item, index) => ({
key: item.key, key: item.key,