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