feat: 完成分龄页开发
This commit is contained in:
@@ -19,6 +19,9 @@ lerna-debug.log*
|
|||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# AI Skill 临时缓存(从云存储下载的数据文件)
|
||||||
|
.cache/
|
||||||
|
|
||||||
# Tests
|
# Tests
|
||||||
/coverage
|
/coverage
|
||||||
/.nyc_output
|
/.nyc_output
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
const cloud = require('wx-server-sdk');
|
||||||
|
|
||||||
|
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV });
|
||||||
|
|
||||||
|
const AGE_BANDS = {
|
||||||
|
'3-4': { minAge: 3, maxAge: 4 },
|
||||||
|
'4-5': { minAge: 4, maxAge: 5 },
|
||||||
|
'5-6': { minAge: 5, maxAge: 6 },
|
||||||
|
'6-7': { minAge: 6, maxAge: 7 },
|
||||||
|
'7-8': { minAge: 7, maxAge: 8 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const DIFFICULTY_LABELS = {
|
||||||
|
1: '入门',
|
||||||
|
2: '基础',
|
||||||
|
3: '进阶',
|
||||||
|
4: '挑战',
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeAgeKey(ageKey) {
|
||||||
|
const key = String(ageKey || '').trim();
|
||||||
|
return AGE_BANDS[key] ? key : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ageBand(min, max) {
|
||||||
|
const minAge = Number(min) || 0;
|
||||||
|
const maxAge = Number(max) || 0;
|
||||||
|
return minAge && maxAge ? `${minAge}-${maxAge}岁` : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function difficultyLabel(difficulty) {
|
||||||
|
if (typeof difficulty === 'string') {
|
||||||
|
return difficulty;
|
||||||
|
}
|
||||||
|
return DIFFICULTY_LABELS[difficulty] || '基础';
|
||||||
|
}
|
||||||
|
|
||||||
|
function toRecommendedItem(ws) {
|
||||||
|
return {
|
||||||
|
worksheetId: ws._id,
|
||||||
|
title: ws.title || '',
|
||||||
|
subtitle: ws.subtitle || '',
|
||||||
|
previewImg: ws.previewImg || '',
|
||||||
|
ageBand: ageBand(ws.ageMin, ws.ageMax),
|
||||||
|
ageMin: Number(ws.ageMin) || 0,
|
||||||
|
ageMax: Number(ws.ageMax) || 0,
|
||||||
|
difficulty: difficultyLabel(ws.difficulty),
|
||||||
|
difficultyLabel: difficultyLabel(ws.difficulty),
|
||||||
|
path: ws.path || '',
|
||||||
|
downloads: Number(ws.downloads) || 0,
|
||||||
|
likes: Number(ws.likes) || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAgeMin(ws) {
|
||||||
|
return Number(ws.ageMin) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAgeMax(ws) {
|
||||||
|
return Number(ws.ageMax) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function coversAgeBand(ws, band) {
|
||||||
|
return getAgeMin(ws) === band.minAge && getAgeMax(ws) >= band.maxAge;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortByField(items, field) {
|
||||||
|
return [...items].sort((a, b) => {
|
||||||
|
const fieldDiff = (Number(b[field]) || 0) - (Number(a[field]) || 0);
|
||||||
|
if (fieldDiff !== 0) return fieldDiff;
|
||||||
|
const aTime = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
|
||||||
|
const bTime = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
|
||||||
|
return bTime - aTime;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickTop(items, field, limit, excludeIds) {
|
||||||
|
if (limit <= 0) return [];
|
||||||
|
|
||||||
|
const picked = [];
|
||||||
|
for (const ws of sortByField(items, field)) {
|
||||||
|
if (excludeIds.has(ws._id)) continue;
|
||||||
|
picked.push(ws);
|
||||||
|
excludeIds.add(ws._id);
|
||||||
|
if (picked.length >= limit) break;
|
||||||
|
}
|
||||||
|
return picked;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function queryActiveWorksheets(db) {
|
||||||
|
const { data } = await db
|
||||||
|
.collection('worksheets')
|
||||||
|
.where({
|
||||||
|
status: 'active',
|
||||||
|
})
|
||||||
|
.orderBy('downloads', 'desc')
|
||||||
|
.orderBy('updatedAt', 'desc')
|
||||||
|
.limit(100)
|
||||||
|
.get();
|
||||||
|
|
||||||
|
return data || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 入参:
|
||||||
|
* - ageKey: '3-4' | '4-5' | '5-6' | '6-7' | '7-8'
|
||||||
|
*
|
||||||
|
* 返回:
|
||||||
|
* - items: 优先返回 ageMin 等于目标下限且 ageMax 覆盖目标上限的 downloads Top3 + likes Top3,不足 6 个用全站 downloads Top 补足
|
||||||
|
*/
|
||||||
|
exports.main = async (event) => {
|
||||||
|
try {
|
||||||
|
const ageKey = normalizeAgeKey(event && event.ageKey);
|
||||||
|
if (!ageKey) {
|
||||||
|
return { success: false, message: 'ageKey 不合法' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const band = AGE_BANDS[ageKey];
|
||||||
|
const db = cloud.database();
|
||||||
|
const activeWorksheets = await queryActiveWorksheets(db);
|
||||||
|
const matchedWorksheets = activeWorksheets.filter((ws) =>
|
||||||
|
coversAgeBand(ws, band),
|
||||||
|
);
|
||||||
|
|
||||||
|
const excludeIds = new Set();
|
||||||
|
const topDownloads = pickTop(
|
||||||
|
matchedWorksheets,
|
||||||
|
'downloads',
|
||||||
|
3,
|
||||||
|
excludeIds,
|
||||||
|
);
|
||||||
|
const topLikes = pickTop(matchedWorksheets, 'likes', 3, excludeIds);
|
||||||
|
const fallbackDownloads = pickTop(
|
||||||
|
activeWorksheets,
|
||||||
|
'downloads',
|
||||||
|
Math.max(0, 6 - topDownloads.length - topLikes.length),
|
||||||
|
excludeIds,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
ageKey,
|
||||||
|
items: [...topDownloads, ...topLikes, ...fallbackDownloads]
|
||||||
|
.slice(0, 6)
|
||||||
|
.map(toRecommendedItem),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message:
|
||||||
|
error instanceof Error ? error.message : '获取分龄推荐失败',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "age-recommended-query",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"dependencies": {
|
||||||
|
"wx-server-sdk": "^3.0.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"timeout": 20
|
||||||
|
}
|
||||||
@@ -214,17 +214,58 @@ async function buildHomeData(db, event) {
|
|||||||
return { homeData };
|
return { homeData };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeIdArray(value) {
|
||||||
|
if (!Array.isArray(value)) return [];
|
||||||
|
return value.map((x) => String(x || '').trim()).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractHomeSeedFromCurrentConfig(home) {
|
||||||
|
const featuredIds = normalizeIdArray(home?.featured?.map((x) => x?.id));
|
||||||
|
const hotIds = normalizeIdArray(home?.hot?.map((x) => x?.id));
|
||||||
|
const sections = Array.isArray(home?.sections)
|
||||||
|
? home.sections
|
||||||
|
.map((s) => ({
|
||||||
|
id: String(s?.id || '').trim(),
|
||||||
|
subtitle: String(s?.subtitle || '').trim(),
|
||||||
|
morePath: String(s?.morePath || '').trim(),
|
||||||
|
worksheetIds: normalizeIdArray(s?.items?.map((it) => it?.id)),
|
||||||
|
}))
|
||||||
|
.filter((s) => s.id && Array.isArray(s.worksheetIds))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return { featuredIds, hotIds, sections };
|
||||||
|
}
|
||||||
|
|
||||||
exports.main = async (event) => {
|
exports.main = async (event) => {
|
||||||
try {
|
try {
|
||||||
const db = cloud.database();
|
const db = cloud.database();
|
||||||
const page = String(event.page || '').trim();
|
const page = String(event.page || '').trim();
|
||||||
|
|
||||||
if (page !== 'category' && page !== 'home') {
|
if (page !== 'category' && page !== 'home' && page !== 'all') {
|
||||||
throw new Error('page 参数不合法,支持 category / home');
|
throw new Error('page 参数不合法,支持 category / home / all');
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await readCurrentConfig();
|
const config = await readCurrentConfig();
|
||||||
|
|
||||||
|
// 当未提供 featuredIds/hotIds/sections 时,默认从现有 home 配置反推出 worksheetIds
|
||||||
|
// 用于“只想重建展示数据(比如 ageBand/previewImg/difficultyLabel),但不想手工维护 id 列表”的场景。
|
||||||
|
const homeSeed = extractHomeSeedFromCurrentConfig(config.home || {});
|
||||||
|
const homeEvent = {
|
||||||
|
...event,
|
||||||
|
featuredIds:
|
||||||
|
Array.isArray(event.featuredIds) && event.featuredIds.length > 0
|
||||||
|
? event.featuredIds
|
||||||
|
: homeSeed.featuredIds,
|
||||||
|
hotIds:
|
||||||
|
Array.isArray(event.hotIds) && event.hotIds.length > 0
|
||||||
|
? event.hotIds
|
||||||
|
: homeSeed.hotIds,
|
||||||
|
sections:
|
||||||
|
Array.isArray(event.sections) && event.sections.length > 0
|
||||||
|
? event.sections
|
||||||
|
: homeSeed.sections,
|
||||||
|
};
|
||||||
|
|
||||||
if (page === 'category') {
|
if (page === 'category') {
|
||||||
const { categoryData, stats } = await buildCategoryData(db);
|
const { categoryData, stats } = await buildCategoryData(db);
|
||||||
config.category = categoryData;
|
config.category = categoryData;
|
||||||
@@ -244,7 +285,7 @@ exports.main = async (event) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (page === 'home') {
|
if (page === 'home') {
|
||||||
const { homeData } = await buildHomeData(db, event);
|
const { homeData } = await buildHomeData(db, homeEvent);
|
||||||
config.home = homeData;
|
config.home = homeData;
|
||||||
config.version = (config.version || 0) + 1;
|
config.version = (config.version || 0) + 1;
|
||||||
config.updatedAt = new Date().toISOString();
|
config.updatedAt = new Date().toISOString();
|
||||||
@@ -262,6 +303,29 @@ exports.main = async (event) => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (page === 'all') {
|
||||||
|
const { categoryData, stats } = await buildCategoryData(db);
|
||||||
|
const { homeData } = await buildHomeData(db, homeEvent);
|
||||||
|
config.category = categoryData;
|
||||||
|
config.home = homeData;
|
||||||
|
config.version = (config.version || 0) + 1;
|
||||||
|
config.updatedAt = new Date().toISOString();
|
||||||
|
|
||||||
|
await saveConfig(config);
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
version: config.version,
|
||||||
|
updatedAt: config.updatedAt,
|
||||||
|
stats,
|
||||||
|
featured: homeData.featured.length,
|
||||||
|
hot: homeData.hot.length,
|
||||||
|
sections: homeData.sections.length,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"timeout": 20
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
const cloud = require('wx-server-sdk');
|
||||||
|
|
||||||
|
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV });
|
||||||
|
|
||||||
|
const BATCH_SIZE = 10;
|
||||||
|
|
||||||
|
function toInt(value, fallback = 0) {
|
||||||
|
const n = Number(value);
|
||||||
|
if (!Number.isFinite(n)) return fallback;
|
||||||
|
return Math.round(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 与 miniprogram/utils/debugPublish.inferGradeFromAge 保持一致 */
|
||||||
|
function inferGradeFromAge(ageMin, ageMax) {
|
||||||
|
const centerAge = Math.round((ageMin + ageMax) / 2);
|
||||||
|
const ageGradeMap = {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeTags(value) {
|
||||||
|
if (!Array.isArray(value)) return null;
|
||||||
|
return value.map((t) => String(t || '').trim()).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildUpdateData(db, patch) {
|
||||||
|
const id = String(patch.id || '').trim();
|
||||||
|
if (!id) return { id: null, ok: false, error: '缺少 id' };
|
||||||
|
|
||||||
|
const data = { updatedAt: db.serverDate() };
|
||||||
|
let hasField = false;
|
||||||
|
|
||||||
|
const hasAgeMin = patch.ageMin !== undefined && patch.ageMin !== null;
|
||||||
|
const hasAgeMax = patch.ageMax !== undefined && patch.ageMax !== null;
|
||||||
|
if (hasAgeMin !== hasAgeMax) {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
ok: false,
|
||||||
|
error: 'ageMin 与 ageMax 必须同时提供',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (hasAgeMin && hasAgeMax) {
|
||||||
|
const ageMin = toInt(patch.ageMin);
|
||||||
|
const ageMax = toInt(patch.ageMax);
|
||||||
|
if (ageMin < 0 || ageMax < ageMin) {
|
||||||
|
return { id, ok: false, error: '年龄范围不合法' };
|
||||||
|
}
|
||||||
|
data.ageMin = ageMin;
|
||||||
|
data.ageMax = ageMax;
|
||||||
|
data.grade =
|
||||||
|
patch.grade !== undefined && patch.grade !== null
|
||||||
|
? toInt(patch.grade)
|
||||||
|
: inferGradeFromAge(ageMin, ageMax);
|
||||||
|
hasField = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (patch.tags !== undefined) {
|
||||||
|
const tags = normalizeTags(patch.tags);
|
||||||
|
if (tags === null) {
|
||||||
|
return { id, ok: false, error: 'tags 须为数组' };
|
||||||
|
}
|
||||||
|
data.tags = tags;
|
||||||
|
hasField = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasField) {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
ok: false,
|
||||||
|
error: '未包含可更新字段(ageMin/ageMax 或 tags)',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { id, ok: true, data };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runInBatches(items, batchSize, worker) {
|
||||||
|
const results = [];
|
||||||
|
for (let i = 0; i < items.length; i += batchSize) {
|
||||||
|
const chunk = items.slice(i, i + batchSize);
|
||||||
|
results.push(...(await Promise.all(chunk.map(worker))));
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.main = async (event) => {
|
||||||
|
try {
|
||||||
|
const db = cloud.database();
|
||||||
|
const patches = Array.isArray(event.patches) ? event.patches : [];
|
||||||
|
const dryRun = !!event.dryRun;
|
||||||
|
const invalidResults = [];
|
||||||
|
const validUpdates = [];
|
||||||
|
|
||||||
|
for (const p of patches) {
|
||||||
|
const update = buildUpdateData(db, p);
|
||||||
|
|
||||||
|
if (dryRun) {
|
||||||
|
invalidResults.push(
|
||||||
|
update.ok
|
||||||
|
? {
|
||||||
|
id: update.id,
|
||||||
|
ok: true,
|
||||||
|
dryRun: true,
|
||||||
|
wouldSet: {
|
||||||
|
...update.data,
|
||||||
|
updatedAt: '[serverDate]',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: update,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!update.ok) {
|
||||||
|
invalidResults.push(update);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
validUpdates.push(update);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateResults = dryRun
|
||||||
|
? []
|
||||||
|
: await runInBatches(
|
||||||
|
validUpdates,
|
||||||
|
BATCH_SIZE,
|
||||||
|
async ({ id, data }) => {
|
||||||
|
try {
|
||||||
|
await db
|
||||||
|
.collection('worksheets')
|
||||||
|
.doc(id)
|
||||||
|
.update({ data });
|
||||||
|
return { id, ok: true };
|
||||||
|
} catch (error) {
|
||||||
|
const message =
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: '更新失败';
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
ok: false,
|
||||||
|
error: /document.*not.*exist|not.*found/i.test(
|
||||||
|
message,
|
||||||
|
)
|
||||||
|
? 'worksheet 不存在'
|
||||||
|
: message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const results = [...invalidResults, ...updateResults];
|
||||||
|
const failed = results.filter((r) => !r.ok).length;
|
||||||
|
return {
|
||||||
|
success: failed === 0,
|
||||||
|
dryRun,
|
||||||
|
total: results.length,
|
||||||
|
failed,
|
||||||
|
results,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error instanceof Error ? error.message : '批量更新失败',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "worksheets-batch-patch",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"dependencies": {
|
||||||
|
"wx-server-sdk": "^3.0.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
const cloud = require('wx-server-sdk');
|
||||||
|
|
||||||
|
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV });
|
||||||
|
|
||||||
|
const VALID_FIELDS = new Set(['likes', 'downloads']);
|
||||||
|
|
||||||
|
exports.main = async (event) => {
|
||||||
|
try {
|
||||||
|
const db = cloud.database();
|
||||||
|
const command = db.command;
|
||||||
|
const collection = db.collection('worksheets');
|
||||||
|
|
||||||
|
const id = String(event.id || '').trim();
|
||||||
|
const field = String(event.field || '').trim();
|
||||||
|
|
||||||
|
if (!id) throw new Error('worksheet id 不能为空');
|
||||||
|
if (!VALID_FIELDS.has(field)) throw new Error('统计字段不合法');
|
||||||
|
|
||||||
|
await collection.doc(id).update({
|
||||||
|
data: {
|
||||||
|
[field]: command.inc(1),
|
||||||
|
updatedAt: db.serverDate(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: { _id: id, field },
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message:
|
||||||
|
error instanceof Error ? error.message : '更新统计失败',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "worksheets-stats-update",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"dependencies": {
|
||||||
|
"wx-server-sdk": "^3.0.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,396 @@
|
|||||||
|
# 分龄页内容管理方案
|
||||||
|
|
||||||
|
> 版本:v1.0 | 最后更新:2026-05-06
|
||||||
|
> 配套文档:[页面内容管理方案](./页面内容管理方案.md) | [小程序云开发方案](./小程序云开发方案.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、背景与目标
|
||||||
|
|
||||||
|
分龄页(`pages/age/age`)按年龄段展示学习内容,目前数据写死在 `age.ts` 内部 Mock。页面内容可拆分为**四个部分**,变更频率各不相同:
|
||||||
|
|
||||||
|
| 部分 | 名称 | 变更频率 | 数据来源 |
|
||||||
|
| -------- | ---------------- | ----------------------- | ---------------------------------------------------------- |
|
||||||
|
| 第一部分 | 年龄段设置 | 几乎不变 | 写死在 `age.config.ts` |
|
||||||
|
| 第二部分 | 能力目标 | 低频变更 | AI Skill 生成 → 写入 `age.config.ts` |
|
||||||
|
| 第三部分 | 4 周推荐学习路线 | 随 worksheet 增加而更新 | AI Skill 生成 → 写入 `age.config.ts` |
|
||||||
|
| 第四部分 | 为你推荐 | 动态 | 运行时调用云函数生成:同年龄段 downloads Top3 + likes Top3 |
|
||||||
|
|
||||||
|
**目标:**
|
||||||
|
|
||||||
|
1. 将分龄页配置抽取到独立的 `age.config.ts` 中,分离静态配置与动态数据
|
||||||
|
2. 通过 AI Skill(Cursor 本地技能),结合最新 worksheet 数据,以早教专家视角自动生成第二部分(能力目标)和第三部分(学习路线)
|
||||||
|
3. 第四部分「为你推荐」在运行时通过云函数生成:同年龄段 **downloads Top3 + likes Top3**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、页面四部分详解
|
||||||
|
|
||||||
|
### 2.1 第一部分:年龄段设置(静态)
|
||||||
|
|
||||||
|
年龄段划分几乎不变,直接写死在配置中。
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// miniprogram/pages/age/age.config.ts
|
||||||
|
|
||||||
|
export const AGE_TAB_SUB: Record<AgeBandKey, string> = {
|
||||||
|
'3-4': '启蒙认知',
|
||||||
|
'4-5': '基础练习',
|
||||||
|
'5-6': '能力提升',
|
||||||
|
'6-7': '幼小衔接',
|
||||||
|
'7-8': '知识拓展',
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
数据来源:`core/data/difficulty.ts` 中的 `AGE_BANDS` 定义了 5 个年龄段 `3-4 | 4-5 | 5-6 | 6-7 | 7-8`。
|
||||||
|
|
||||||
|
### 2.2 第二部分:能力目标(AI 生成,低频更新)
|
||||||
|
|
||||||
|
每个年龄段 3 个能力维度(数感、书写、思维),描述该年龄段应达到的核心能力。
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type AbilityItem = {
|
||||||
|
icon: string; // emoji 图标
|
||||||
|
title: string; // 能力维度名称
|
||||||
|
desc: string; // 能力描述
|
||||||
|
};
|
||||||
|
|
||||||
|
// age.config.ts 中的结构
|
||||||
|
export const AGE_ABILITIES: Record<AgeBandKey, AbilityItem[]> = {
|
||||||
|
'3-4': [
|
||||||
|
{ icon: '🔢', title: '数感', desc: '认读 1-5、点数对应' },
|
||||||
|
{ icon: '✏️', title: '书写', desc: '涂鸦线条、简单描红' },
|
||||||
|
{ icon: '🧩', title: '思维', desc: '找相同、简单配对' },
|
||||||
|
],
|
||||||
|
// ... 其他年龄段
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**更新方式:** 运行 AI Skill `generate-age-abilities`,基于当前所有 worksheet 内容,以早教专家视角重新生成各年龄段的能力目标,并更新 `age.config.ts`。
|
||||||
|
|
||||||
|
### 2.3 第三部分:4 周推荐学习路线(AI 生成,中频更新)
|
||||||
|
|
||||||
|
每个年龄段 4 周学习计划,每周一个主题,包含 3 个推荐练习。练习需关联到实际存在的 worksheet。
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type WeekExercise = {
|
||||||
|
_id: string; // 云 worksheets._id,与 page-config category.items[].id 一致
|
||||||
|
title: string; // worksheet 标题
|
||||||
|
subtitle: string; // 副标题(列表展示)
|
||||||
|
path: string; // 小程序跳转路径,可含 query;点击后 navigateTo / switchTab
|
||||||
|
};
|
||||||
|
|
||||||
|
type WeekPlan = {
|
||||||
|
week: number; // 第几周 1-4
|
||||||
|
theme: string; // 周主题
|
||||||
|
exercises: WeekExercise[]; // 3 个练习
|
||||||
|
};
|
||||||
|
|
||||||
|
// age.config.ts 中的结构
|
||||||
|
export const AGE_WEEK_PLANS: Record<AgeBandKey, WeekPlan[]> = {
|
||||||
|
'3-4': [
|
||||||
|
{
|
||||||
|
week: 1,
|
||||||
|
theme: '数感启蒙',
|
||||||
|
exercises: [
|
||||||
|
{
|
||||||
|
_id: 'ws_001',
|
||||||
|
title: '找数字,涂一涂',
|
||||||
|
subtitle: '在数字方阵中找出目标数字并涂色',
|
||||||
|
path: '/mathPages/mathDraw/mathDraw?id=number-find',
|
||||||
|
},
|
||||||
|
// ...
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// ... week 2-4
|
||||||
|
],
|
||||||
|
// ... 其他年龄段
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**更新方式:** 运行 AI Skill `generate-age-weekly-plans`,基于当前所有 worksheet 内容,以早教专家视角为每个年龄段编排 4 周学习路线,并更新 `age.config.ts`。
|
||||||
|
|
||||||
|
**交互:** 列表展示 `title` + `subtitle`;用户点击「去练习」时,使用 `path` 调用 `wx.navigateTo`(TabBar 页面则 `wx.switchTab` 并不带 query)。
|
||||||
|
|
||||||
|
### 2.4 第四部分:为你推荐(动态,云函数)
|
||||||
|
|
||||||
|
展示当前年龄段的 6 个推荐 worksheet,规则固定为:
|
||||||
|
|
||||||
|
- **前 3 个**:同年龄段 `downloads` 最高的 3 个(downloads Top3)
|
||||||
|
- **后 3 个**:同年龄段 `likes` 最高的 3 个(likes Top3)
|
||||||
|
|
||||||
|
> 若两者出现重复,likes Top3 会自动向后补齐,确保总数为 6(在 worksheet 数量充足的前提下)。
|
||||||
|
|
||||||
|
实现方式:
|
||||||
|
|
||||||
|
- **云函数**:`cloudfunctions/ageRecommendedQuery`,入参 `ageKey`(如 `5-6`),从 `worksheets` 集合查询 `status=active 且 ageMin/ageMax 匹配` 的数据并返回 6 条
|
||||||
|
- **分龄页运行时**:在切换年龄段时调用该云函数,拿到 `title + previewImg + worksheetId` 渲染
|
||||||
|
- **兜底**:云函数失败或无数据时,前端使用本地占位推荐(后续可替换为固定兜底表)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、`age.config.ts` 完整结构
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// miniprogram/pages/age/age.config.ts
|
||||||
|
|
||||||
|
import { AgeBandKey } from '../../core/data/difficulty';
|
||||||
|
|
||||||
|
// ─── 类型定义 ─────────────────────────────────
|
||||||
|
export type AbilityItem = {
|
||||||
|
icon: string;
|
||||||
|
title: string;
|
||||||
|
desc: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WeekExercise = {
|
||||||
|
_id: string;
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WeekPlan = {
|
||||||
|
week: number;
|
||||||
|
theme: string;
|
||||||
|
exercises: WeekExercise[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AgeBandConfig = {
|
||||||
|
key: AgeBandKey;
|
||||||
|
label: string;
|
||||||
|
subLabel: string;
|
||||||
|
abilities: AbilityItem[];
|
||||||
|
weeks: WeekPlan[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── 第一部分:年龄段设置(手动维护,几乎不变)────────
|
||||||
|
export const AGE_TAB_SUB: Record<AgeBandKey, string> = {
|
||||||
|
'3-4': '启蒙认知',
|
||||||
|
'4-5': '基础练习',
|
||||||
|
'5-6': '能力提升',
|
||||||
|
'6-7': '幼小衔接',
|
||||||
|
'7-8': '知识拓展',
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── 第二部分:能力目标(由 AI Skill generate-age-abilities 生成)────────
|
||||||
|
export const AGE_ABILITIES: Record<AgeBandKey, AbilityItem[]> = {
|
||||||
|
'3-4': [
|
||||||
|
/* AI 生成内容 */
|
||||||
|
],
|
||||||
|
'4-5': [
|
||||||
|
/* AI 生成内容 */
|
||||||
|
],
|
||||||
|
'5-6': [
|
||||||
|
/* AI 生成内容 */
|
||||||
|
],
|
||||||
|
'6-7': [
|
||||||
|
/* AI 生成内容 */
|
||||||
|
],
|
||||||
|
'7-8': [
|
||||||
|
/* AI 生成内容 */
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── 第三部分:4 周学习路线(由 AI Skill generate-age-weekly-plans 生成)────────
|
||||||
|
export const AGE_WEEK_PLANS: Record<AgeBandKey, WeekPlan[]> = {
|
||||||
|
'3-4': [
|
||||||
|
/* AI 生成内容 */
|
||||||
|
],
|
||||||
|
'4-5': [
|
||||||
|
/* AI 生成内容 */
|
||||||
|
],
|
||||||
|
'5-6': [
|
||||||
|
/* AI 生成内容 */
|
||||||
|
],
|
||||||
|
'6-7': [
|
||||||
|
/* AI 生成内容 */
|
||||||
|
],
|
||||||
|
'7-8': [
|
||||||
|
/* AI 生成内容 */
|
||||||
|
],
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、AI Skill 方案
|
||||||
|
|
||||||
|
### 4.1 设计思路
|
||||||
|
|
||||||
|
通过两个 Cursor AI Skill 分别管理第二部分和第三部分的内容生成:
|
||||||
|
|
||||||
|
```
|
||||||
|
doodle-mini/skills/
|
||||||
|
├── generate-age-abilities/
|
||||||
|
│ └── SKILL.md # Skill 1:生成能力目标
|
||||||
|
└── generate-age-weekly-plans/
|
||||||
|
└── SKILL.md # Skill 2:生成 4 周学习路线
|
||||||
|
```
|
||||||
|
|
||||||
|
两个 Skill 的共同点:
|
||||||
|
|
||||||
|
1. **数据输入**:`page-config.json` 存储在微信云存储(路径:`/content/page-config.json`),本地不存在。运行 Skill 前需先将其下载到 `.cache/page-config.json`(通过微信开发者工具或 `tcb` CLI),Skill 从本地缓存读取 `category` 下所有 worksheet 的标题、描述和内容
|
||||||
|
2. **专家视角**:以儿童早教专家(3-8 岁)的专业视角进行内容编排
|
||||||
|
3. **输出目标**:生成符合 `age.config.ts` 数据结构的 TypeScript 代码,直接更新配置文件
|
||||||
|
|
||||||
|
### 4.2 Skill 1:`generate-age-abilities`(生成能力目标)
|
||||||
|
|
||||||
|
**触发场景:** 当 worksheet 内容体系发生较大变化(如新增学科分类、调整难度体系)时运行
|
||||||
|
|
||||||
|
**生成逻辑:**
|
||||||
|
|
||||||
|
1. 读取所有 active worksheet 的分类、年龄段、难度、标题和描述
|
||||||
|
2. 按年龄段分组,分析该年龄段覆盖的知识领域和技能点
|
||||||
|
3. 以早教专家视角,为每个年龄段总结 3 个核心能力维度(数感/书写/思维 或根据实际内容调整)
|
||||||
|
4. 每个能力维度用一句话描述该年龄段应达到的水平
|
||||||
|
5. 更新 `age.config.ts` 中的 `AGE_ABILITIES`
|
||||||
|
|
||||||
|
### 4.3 Skill 2:`generate-age-weekly-plans`(生成 4 周学习路线)
|
||||||
|
|
||||||
|
**触发场景:** 每次有新 worksheet 发布上线后运行,确保学习路线推荐最新内容
|
||||||
|
|
||||||
|
**生成逻辑:**
|
||||||
|
|
||||||
|
1. 读取所有 active worksheet 的完整信息(标题、描述、分类、年龄段、难度)
|
||||||
|
2. 按年龄段分组,筛选适合该年龄段的 worksheet
|
||||||
|
3. 以早教专家视角,设计 4 周主题式学习路线:
|
||||||
|
|
||||||
|
- 遵循由易到难、由基础到综合的编排原则
|
||||||
|
- 每周一个主题,覆盖不同能力维度
|
||||||
|
- 每周 3 个练习,从实际存在的 worksheet 中选取
|
||||||
|
- 确保 4 周内容覆盖该年龄段的主要学科和技能
|
||||||
|
|
||||||
|
4. 生成 `WeekPlan[]` 数据,关联实际 worksheet ID
|
||||||
|
5. 更新 `age.config.ts` 中的 `AGE_WEEK_PLANS`
|
||||||
|
|
||||||
|
### 4.4 数据获取方式
|
||||||
|
|
||||||
|
`page-config.json` 存储在微信云存储中(路径:`/content/page-config.json`),Skill 运行在本地 Cursor 环境中,无法直接访问云存储。需先将文件下载到本地:
|
||||||
|
|
||||||
|
**前置步骤:下载 page-config.json 到本地缓存**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p .cache
|
||||||
|
```
|
||||||
|
|
||||||
|
下载方式(任选其一):
|
||||||
|
|
||||||
|
1. **微信开发者工具**:云开发控制台 → 存储 → 找到 `/content/page-config.json` → 下载 → 保存到 `.cache/page-config.json`
|
||||||
|
2. **tcb CLI**:`tcb storage:download /content/page-config.json .cache/page-config.json`
|
||||||
|
|
||||||
|
**Skill 读取本地缓存**
|
||||||
|
|
||||||
|
```
|
||||||
|
读取 .cache/page-config.json
|
||||||
|
→ 解析 category.categories[].items[]
|
||||||
|
→ 提取每个 worksheet 的 id、title、description、category、ageBand、difficulty
|
||||||
|
```
|
||||||
|
|
||||||
|
> `.cache/` 目录应加入 `.gitignore`,仅作为 Skill 运行时的临时数据源。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、分龄页管理(Debug 管理页)
|
||||||
|
|
||||||
|
### 5.1 管理页入口
|
||||||
|
|
||||||
|
`supportPages/ageContentManage/ageContentManage`(待开发)
|
||||||
|
|
||||||
|
### 5.2 管理 UI
|
||||||
|
|
||||||
|
分龄管理页按年龄段切换:`3-4 岁 | 4-5 岁 | 5-6 岁 | 6-7 岁 | 7-8 岁`
|
||||||
|
|
||||||
|
| 模块 | 操作 | 说明 |
|
||||||
|
| -------- | -------- | ---------------------------------------------------------- |
|
||||||
|
| 能力目标 | 只读展示 | 来自 `age.config.ts` 的 `AGE_ABILITIES`,由 AI Skill 维护 |
|
||||||
|
| 四周路线 | 只读展示 | 来自 `age.config.ts` 的 `AGE_WEEK_PLANS`,由 AI Skill 维护 |
|
||||||
|
| 推荐内容 | 自动计算 | 运行时取收藏/下载最高的 6 个 worksheet |
|
||||||
|
|
||||||
|
> 注:第二、三部分改为由 AI Skill 在开发时生成并提交代码,管理页仅作预览展示,不再需要手动选择 worksheet。
|
||||||
|
|
||||||
|
### 5.3 更新流程
|
||||||
|
|
||||||
|
```
|
||||||
|
AI Skill 生成流程(开发时):
|
||||||
|
0. 从云存储下载 /content/page-config.json → 保存到 .cache/page-config.json
|
||||||
|
1. 开发者在 Cursor 中运行 AI Skill
|
||||||
|
2. Skill 读取 .cache/page-config.json 中的 worksheet 数据
|
||||||
|
3. 以早教专家视角生成能力目标 / 学习路线
|
||||||
|
4. 自动更新 age.config.ts
|
||||||
|
5. 开发者 review 后提交代码
|
||||||
|
|
||||||
|
运行时数据流:
|
||||||
|
分龄页 onLoad / 切换年龄段
|
||||||
|
→ 从 age.config.ts 读取能力目标 + 学习路线(静态)
|
||||||
|
→ 调用云函数 ageRecommendedQuery(ageKey) 获取推荐(动态)
|
||||||
|
→ 渲染页面
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、数据结构
|
||||||
|
|
||||||
|
### 6.1 分龄页完整数据结构
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type AgePageData = {
|
||||||
|
ageTabs: Array<{
|
||||||
|
key: AgeBandKey;
|
||||||
|
rangeText: string; // '3-4'
|
||||||
|
subLabel: string; // '启蒙认知'
|
||||||
|
}>;
|
||||||
|
bands: Record<
|
||||||
|
AgeBandKey,
|
||||||
|
{
|
||||||
|
abilities: AbilityItem[];
|
||||||
|
weeks: WeekPlan[];
|
||||||
|
recommended: RecommendedItem[]; // 运行时动态计算
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RecommendedItem = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
image: string;
|
||||||
|
worksheetId: string;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、实施计划
|
||||||
|
|
||||||
|
### Phase 1:配置文件重构
|
||||||
|
|
||||||
|
1. 创建 `miniprogram/pages/age/age.config.ts`,从 `age.ts` 中抽取类型定义和静态数据
|
||||||
|
2. 重构 `age.ts`,改为引用 `age.config.ts` 中的配置
|
||||||
|
|
||||||
|
### Phase 2:AI Skill 开发
|
||||||
|
|
||||||
|
1. 创建 `skills/generate-age-abilities/SKILL.md`
|
||||||
|
2. 创建 `skills/generate-age-weekly-plans/SKILL.md`
|
||||||
|
3. 运行 Skill 生成初始内容,验证数据结构正确性
|
||||||
|
|
||||||
|
### Phase 3:动态推荐
|
||||||
|
|
||||||
|
1. 新增云函数 `ageRecommendedQuery`(downloads Top3 + likes Top3)
|
||||||
|
2. 分龄页调用云函数并渲染「为你推荐」
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、关键文件索引
|
||||||
|
|
||||||
|
| 文件 / 模块 | 职责 |
|
||||||
|
| ------------------------------------------- | --------------------------------------------------- |
|
||||||
|
| `pages/age/age.config.ts` | 分龄页配置(类型定义 + 静态数据 + AI 生成数据) |
|
||||||
|
| `pages/age/age.ts` | 分龄页逻辑,引用 age.config.ts |
|
||||||
|
| `skills/generate-age-abilities/SKILL.md` | AI Skill:生成各年龄段能力目标 |
|
||||||
|
| `skills/generate-age-weekly-plans/SKILL.md` | AI Skill:生成 4 周学习路线 |
|
||||||
|
| `core/data/difficulty.ts` | AGE_BANDS 年龄段定义 |
|
||||||
|
| `core/data/categories.ts` | CATEGORY_LIST 分类定义 |
|
||||||
|
| `.cache/page-config.json` | worksheet 数据本地缓存(运行 Skill 前从云存储下载) |
|
||||||
|
| 云存储 `/content/page-config.json` | worksheet 数据源(Source of Truth) |
|
||||||
|
| `cloudfunctions/worksheetsQuery/` | 查询 worksheet 的云函数 |
|
||||||
|
| `cloudfunctions/ageRecommendedQuery/` | 分龄页推荐:同年龄段 downloads Top3 + likes Top3 |
|
||||||
@@ -145,9 +145,9 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 3.6 集合:`favorites`(收藏)
|
### 3.6 集合:`favorites`(收藏日志)
|
||||||
|
|
||||||
对应 Prisma `Favorite`。
|
对应 Prisma `FavoriteLog`。
|
||||||
|
|
||||||
| 字段 | BSON 类型 | 必填 | 说明 |
|
| 字段 | BSON 类型 | 必填 | 说明 |
|
||||||
| ------------- | --------- | ---- | ----------------------- |
|
| ------------- | --------- | ---- | ----------------------- |
|
||||||
@@ -172,11 +172,11 @@
|
|||||||
对应 Prisma `DownloadLog`。
|
对应 Prisma `DownloadLog`。
|
||||||
|
|
||||||
| 字段 | BSON 类型 | 必填 | 说明 |
|
| 字段 | BSON 类型 | 必填 | 说明 |
|
||||||
| ------------- | -------------- | ---- | ----------------------- |
|
| ------------- | -------------- | ---- | --------------------------------- |
|
||||||
| `_id` | string | 是 | 主键。 |
|
| `_id` | string | 是 | 主键。 |
|
||||||
| `userId` | string | 是 | 关联 `users._id`。 |
|
| `userId` | string | 是 | 关联 `users._id`。 |
|
||||||
| `worksheetId` | string | 是 | 关联 `worksheets._id`。 |
|
| `worksheetId` | string | 是 | 关联 `worksheets._id`。 |
|
||||||
| `params` | object \| null | 否 | 生成参数快照(JSON)。 |
|
| `params` | object \| null | 否 | 生成参数快照(JSON)。 暂时不使用 |
|
||||||
| `createdAt` | date | 是 | 下载时间。 |
|
| `createdAt` | date | 是 | 下载时间。 |
|
||||||
|
|
||||||
**索引建议**:`{ userId: 1, createdAt: -1 }`;`{ worksheetId: 1, createdAt: -1 }`(统计/运营)。
|
**索引建议**:`{ userId: 1, createdAt: -1 }`;`{ worksheetId: 1, createdAt: -1 }`(统计/运营)。
|
||||||
+17
-36
@@ -368,41 +368,18 @@ type CategoryPageData = {
|
|||||||
|
|
||||||
## 五、分龄页内容管理
|
## 五、分龄页内容管理
|
||||||
|
|
||||||
### 5.1 新增 `age.config.ts`
|
> 详细方案已独立为 [分龄页内容管理方案](./分龄页内容管理方案.md)
|
||||||
|
|
||||||
在 `miniprogram/pages/age/` 下新增 `age.config.ts`,将年龄段划分、能力目标、默认 worksheet 写死在配置中,作为兜底数据和管理页的基础结构。
|
分龄页内容分为四部分,通过 `age.config.ts` 统一管理:
|
||||||
|
|
||||||
```ts
|
| 部分 | 名称 | 管理方式 |
|
||||||
export type AgeBandConfig = {
|
| --- | --- | --- |
|
||||||
key: AgeBandKey;
|
| 第一部分 | 年龄段设置 | 写死在 `age.config.ts`,几乎不变 |
|
||||||
label: string;
|
| 第二部分 | 能力目标 | AI Skill `generate-age-abilities` 生成 |
|
||||||
subLabel: string;
|
| 第三部分 | 4 周学习路线 | AI Skill `generate-age-weekly-plans` 生成 |
|
||||||
abilities: AbilityItem[];
|
| 第四部分 | 为你推荐 | 运行时动态计算(取收藏/下载最高的 6 个 worksheet) |
|
||||||
weeks: WeekPlan[];
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
> `worksheetIds` 初始为空数组,由分龄管理页选择后填入。`worksheetTitles` 作为兜底展示。
|
AI Skill 位于 `skills/` 目录,在开发时通过 Cursor 运行,以早教专家视角结合最新 worksheet 数据生成内容。
|
||||||
|
|
||||||
### 5.2 管理 UI
|
|
||||||
|
|
||||||
分龄管理按年龄段切换:`3-4 岁 | 4-5 岁 | 5-6 岁 | 6-7 岁 | 7-8 岁`
|
|
||||||
|
|
||||||
| 模块 | 操作 | 说明 |
|
|
||||||
| ---- | ------------ | -------------------------------- |
|
|
||||||
| 能力目标 | 只读展示 | 来自 `age.config.ts`,不需要在管理页修改 |
|
|
||||||
| 四周路线 | 选择 worksheet | 每周选择 4 个 worksheet,主题来自 config |
|
|
||||||
| 推荐内容 | 选择 worksheet | 动态数据,展示 likes 数量最高的 6 个worksheet |
|
|
||||||
|
|
||||||
### 5.3 更新流程
|
|
||||||
|
|
||||||
```
|
|
||||||
分龄管理 → 选择年龄段 → 为每周选择 worksheet → 选择推荐内容
|
|
||||||
→ 点击「更新分龄数据」
|
|
||||||
→ 合并 age.config.ts 的能力目标 + 管理页选择的 worksheet 详情
|
|
||||||
→ 调用 pageContentBuild({ page: 'age', data: ... })
|
|
||||||
→ 云函数更新 page_configs/current 中的 age 字段 + 同步云存储
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -443,9 +420,11 @@ supportPages/debug/debug
|
|||||||
|
|
||||||
### Phase 4:分龄页管理(待开发)
|
### Phase 4:分龄页管理(待开发)
|
||||||
|
|
||||||
1. 新增 `age.config.ts` 配置文件
|
> 详见 [分龄页内容管理方案](./分龄页内容管理方案.md)
|
||||||
2. 实现分龄管理页(年龄段切换、周 worksheet 选择)
|
|
||||||
3. 实现「更新分龄数据」→ 合并 config + worksheet 详情
|
1. 新增 `age.config.ts` 配置文件,将静态配置与 AI 生成数据分离
|
||||||
|
2. 开发 AI Skill(`generate-age-abilities` + `generate-age-weekly-plans`)
|
||||||
|
3. 分龄页接入 pageConfig 数据源,实现「为你推荐」动态计算
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -475,7 +454,9 @@ supportPages/debug/debug
|
|||||||
| `cloudfunctions/worksheetsQuery/` | 查询 worksheet 列表 |
|
| `cloudfunctions/worksheetsQuery/` | 查询 worksheet 列表 |
|
||||||
| `cloudfunctions/worksheetsUpdateStatus/` | 更新 worksheet 状态 |
|
| `cloudfunctions/worksheetsUpdateStatus/` | 更新 worksheet 状态 |
|
||||||
| `pages/category/category.ts` | 分类页,支持 `?id=xxx` 参数选中分类 |
|
| `pages/category/category.ts` | 分类页,支持 `?id=xxx` 参数选中分类 |
|
||||||
| `pages/age/age.config.ts` | 分龄页兜底配置 |
|
| `pages/age/age.config.ts` | 分龄页配置(详见 [分龄页方案](./分龄页内容管理方案.md)) |
|
||||||
|
| `skills/generate-age-abilities/SKILL.md` | AI Skill:生成分龄页能力目标 |
|
||||||
|
| `skills/generate-age-weekly-plans/SKILL.md` | AI Skill:生成分龄页学习路线 |
|
||||||
| `pages/home/home.data.ts` | 首页兜底数据 |
|
| `pages/home/home.data.ts` | 首页兜底数据 |
|
||||||
| `pages/category/category.data.ts` | 分类页兜底数据 |
|
| `pages/category/category.data.ts` | 分类页兜底数据 |
|
||||||
| `core/data/categories.ts` | CATEGORY_LIST 分类定义(id/name/icon/path) |
|
| `core/data/categories.ts` | CATEGORY_LIST 分类定义(id/name/icon/path) |
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"pages": [
|
"pages": [
|
||||||
"guide/guide",
|
"guide/guide",
|
||||||
"debug/debug",
|
"debug/debug",
|
||||||
|
"worksheetSync/worksheetSync",
|
||||||
"categoryManage/categoryManage",
|
"categoryManage/categoryManage",
|
||||||
"categoryContentManage/categoryContentManage",
|
"categoryContentManage/categoryContentManage",
|
||||||
"homeContentManage/homeContentManage",
|
"homeContentManage/homeContentManage",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,6 +5,13 @@
|
|||||||
"css_prefix_text": "icon-",
|
"css_prefix_text": "icon-",
|
||||||
"description": "",
|
"description": "",
|
||||||
"glyphs": [
|
"glyphs": [
|
||||||
|
{
|
||||||
|
"icon_id": "47501281",
|
||||||
|
"name": "loading",
|
||||||
|
"font_class": "loading",
|
||||||
|
"unicode": "e62e",
|
||||||
|
"unicode_decimal": 58926
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"icon_id": "47465822",
|
"icon_id": "47465822",
|
||||||
"name": "task-o",
|
"name": "task-o",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -25,6 +25,7 @@ export interface PageCanvasInstance {
|
|||||||
getShareOptions(): ShareOptions;
|
getShareOptions(): ShareOptions;
|
||||||
selectComponent(selector: string): any;
|
selectComponent(selector: string): any;
|
||||||
getPublishMeta?(): DebugPublishMeta;
|
getPublishMeta?(): DebugPublishMeta;
|
||||||
|
getWorksheetStatsId?(): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -225,10 +226,15 @@ export function getPageCommonMethods(config: PageCommonMethodsConfig = {}) {
|
|||||||
* 导出打印
|
* 导出打印
|
||||||
*/
|
*/
|
||||||
async exportToPrint(this: PageCanvasInstance) {
|
async exportToPrint(this: PageCanvasInstance) {
|
||||||
|
const worksheetId =
|
||||||
|
typeof this.getWorksheetStatsId === 'function'
|
||||||
|
? this.getWorksheetStatsId()
|
||||||
|
: this.data.functionId;
|
||||||
await downloadPrint(this.canvas, {
|
await downloadPrint(this.canvas, {
|
||||||
errorToast: '请先生成内容',
|
errorToast: '请先生成内容',
|
||||||
trackerName: this.data.pageTitle,
|
trackerName: this.data.pageTitle,
|
||||||
trackerMode: this.data.currentMode,
|
trackerMode: this.data.currentMode,
|
||||||
|
worksheetId,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -278,11 +284,16 @@ export function getPageCommonMethods(config: PageCommonMethodsConfig = {}) {
|
|||||||
async onShareSuccess(this: PageCanvasInstance) {
|
async onShareSuccess(this: PageCanvasInstance) {
|
||||||
this.setData({ showShareDialog: false });
|
this.setData({ showShareDialog: false });
|
||||||
if (this.canvas) {
|
if (this.canvas) {
|
||||||
|
const worksheetId =
|
||||||
|
typeof this.getWorksheetStatsId === 'function'
|
||||||
|
? this.getWorksheetStatsId()
|
||||||
|
: this.data.functionId;
|
||||||
// 分享成功后下载(不限制次数)
|
// 分享成功后下载(不限制次数)
|
||||||
await downloadPrint(this.canvas, {
|
await downloadPrint(this.canvas, {
|
||||||
errorToast: '请先生成内容',
|
errorToast: '请先生成内容',
|
||||||
trackerName: this.data.pageTitle,
|
trackerName: this.data.pageTitle,
|
||||||
trackerMode: this.data.currentMode,
|
trackerMode: this.data.currentMode,
|
||||||
|
worksheetId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
@import '../../style/theme.less';
|
||||||
|
@import '../../style/tags.less';
|
||||||
|
|
||||||
.worksheet-card {
|
.worksheet-card {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 24rpx;
|
border-radius: 24rpx;
|
||||||
@@ -63,3 +66,80 @@
|
|||||||
background: #fff8e1;
|
background: #fff8e1;
|
||||||
color: #f57f17;
|
color: #f57f17;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── variant: track(对齐首页 home-track-card) ── */
|
||||||
|
.worksheet-card--track {
|
||||||
|
box-shadow: 0 6rpx 20rpx rgba(50, 46, 37, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.worksheet-card--track.worksheet-card--active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.worksheet-card__top {
|
||||||
|
height: 270rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
border-bottom: 1rpx solid rgba(50, 46, 37, 0.08);
|
||||||
|
background: #f5edd8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.worksheet-card__cover {
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.worksheet-card__icon {
|
||||||
|
font-size: 72rpx;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.worksheet-card__tags-track {
|
||||||
|
position: absolute;
|
||||||
|
right: 12rpx;
|
||||||
|
bottom: 12rpx;
|
||||||
|
display: flex;
|
||||||
|
gap: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.worksheet-card__body {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
padding: 20rpx @page-padding-inner-x 22rpx;
|
||||||
|
background: #ffffff;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.worksheet-card__title-track {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: @text-title;
|
||||||
|
line-height: 1.35;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
line-clamp: 2;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
overflow: hidden;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.worksheet-card__sub {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 20rpx;
|
||||||
|
color: @text-gray;
|
||||||
|
line-height: 1.45;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
line-clamp: 2;
|
||||||
|
overflow: hidden;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
@@ -3,10 +3,18 @@ Component({
|
|||||||
styleIsolation: 'apply-shared',
|
styleIsolation: 'apply-shared',
|
||||||
},
|
},
|
||||||
properties: {
|
properties: {
|
||||||
|
variant: {
|
||||||
|
type: String,
|
||||||
|
value: 'classic',
|
||||||
|
},
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
value: '',
|
value: '',
|
||||||
},
|
},
|
||||||
|
subtitle: {
|
||||||
|
type: String,
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
previewBg: {
|
previewBg: {
|
||||||
type: String,
|
type: String,
|
||||||
value: '#f5f5f5',
|
value: '#f5f5f5',
|
||||||
@@ -19,6 +27,10 @@ Component({
|
|||||||
type: String,
|
type: String,
|
||||||
value: '',
|
value: '',
|
||||||
},
|
},
|
||||||
|
ageBand: {
|
||||||
|
type: String,
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
difficulty: {
|
difficulty: {
|
||||||
type: String,
|
type: String,
|
||||||
value: '',
|
value: '',
|
||||||
@@ -27,10 +39,25 @@ Component({
|
|||||||
type: String,
|
type: String,
|
||||||
value: '',
|
value: '',
|
||||||
},
|
},
|
||||||
|
previewImg: {
|
||||||
|
type: String,
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
path: {
|
||||||
|
type: String,
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onTap() {
|
onTap() {
|
||||||
this.triggerEvent('tap');
|
this.triggerEvent('tap', {
|
||||||
|
title: this.data.title,
|
||||||
|
path: this.data.path,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,21 +1,62 @@
|
|||||||
<view
|
<view
|
||||||
class="worksheet-card"
|
class="worksheet-card worksheet-card--{{variant}}"
|
||||||
hover-class="worksheet-card--active"
|
hover-class="worksheet-card--active"
|
||||||
hover-stay-time="100"
|
hover-stay-time="100"
|
||||||
bindtap="onTap"
|
bindtap="onTap">
|
||||||
>
|
<block wx:if="{{variant === 'track'}}">
|
||||||
<view class="worksheet-card__preview" style="background-color: {{previewBg}}">
|
<view class="worksheet-card__top">
|
||||||
|
<image
|
||||||
|
wx:if="{{previewImg || img}}"
|
||||||
|
class="worksheet-card__cover"
|
||||||
|
src="{{previewImg || img}}"
|
||||||
|
mode="widthFix" />
|
||||||
|
<text wx:else class="worksheet-card__icon">{{icon}}</text>
|
||||||
|
|
||||||
|
<view
|
||||||
|
wx:if="{{ageBand || ageRange || difficulty}}"
|
||||||
|
class="worksheet-card__tags-track">
|
||||||
|
<text wx:if="{{ageBand || ageRange}}" class="tag tag--age"
|
||||||
|
>{{ageBand || ageRange}}</text
|
||||||
|
>
|
||||||
|
<text wx:if="{{difficulty}}" class="tag tag--diff"
|
||||||
|
>{{difficulty}}</text
|
||||||
|
>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="worksheet-card__body">
|
||||||
|
<text class="worksheet-card__title-track">{{title}}</text>
|
||||||
|
<text wx:if="{{subtitle}}" class="worksheet-card__sub"
|
||||||
|
>{{subtitle}}</text
|
||||||
|
>
|
||||||
|
</view>
|
||||||
|
</block>
|
||||||
|
|
||||||
|
<block wx:else>
|
||||||
|
<view
|
||||||
|
class="worksheet-card__preview"
|
||||||
|
style="background-color: {{previewBg}}">
|
||||||
<image
|
<image
|
||||||
wx:if="{{img}}"
|
wx:if="{{img}}"
|
||||||
class="worksheet-card__img"
|
class="worksheet-card__img"
|
||||||
src="{{img}}"
|
src="{{img}}"
|
||||||
mode="aspectFill"
|
mode="aspectFill" />
|
||||||
/>
|
<text wx:else class="worksheet-card__preview-text"
|
||||||
<text wx:else class="worksheet-card__preview-text">{{previewText}}</text>
|
>{{previewText}}</text
|
||||||
|
>
|
||||||
</view>
|
</view>
|
||||||
<view class="worksheet-card__title">{{title}}</view>
|
<view class="worksheet-card__title">{{title}}</view>
|
||||||
<view class="worksheet-card__tags">
|
<view class="worksheet-card__tags">
|
||||||
<view wx:if="{{ageRange}}" class="worksheet-card__tag worksheet-card__tag--age">{{ageRange}}</view>
|
<view
|
||||||
<view wx:if="{{difficulty}}" class="worksheet-card__tag worksheet-card__tag--diff">{{difficulty}}</view>
|
wx:if="{{ageRange}}"
|
||||||
|
class="worksheet-card__tag worksheet-card__tag--age"
|
||||||
|
>{{ageRange}}</view
|
||||||
|
>
|
||||||
|
<view
|
||||||
|
wx:if="{{difficulty}}"
|
||||||
|
class="worksheet-card__tag worksheet-card__tag--diff"
|
||||||
|
>{{difficulty}}</view
|
||||||
|
>
|
||||||
</view>
|
</view>
|
||||||
|
</block>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -2,22 +2,27 @@ import type { LetterTracingMode } from './generators/letter-tracing-generator';
|
|||||||
import type { DebugPublishMeta } from '../../utils/debugPublish';
|
import type { DebugPublishMeta } from '../../utils/debugPublish';
|
||||||
import { inferGradeFromAge } from '../../utils/debugPublish';
|
import { inferGradeFromAge } from '../../utils/debugPublish';
|
||||||
|
|
||||||
interface ModeDefinition {
|
/** 本地 worksheet 元数据(与云库 `worksheets` 文档 `_id` 对齐) */
|
||||||
|
interface LetterTracingWorksheetDefinition {
|
||||||
id: LetterTracingMode;
|
id: LetterTracingMode;
|
||||||
icon: string;
|
icon: string;
|
||||||
title: string;
|
title: string;
|
||||||
subtitle: string;
|
subtitle: string;
|
||||||
|
ageMin: number;
|
||||||
|
ageMax: number;
|
||||||
difficulty: 1 | 2 | 3 | 4;
|
difficulty: 1 | 2 | 3 | 4;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MODES = [
|
export const LETTER_TRACING_WORKSHEET_DEFINITIONS = [
|
||||||
{
|
{
|
||||||
id: 'letter-tracing-single',
|
id: 'letter-tracing-single',
|
||||||
icon: 'start-a',
|
icon: 'start-a',
|
||||||
title: '默认字帖',
|
title: '默认字帖',
|
||||||
subtitle: '配图、例句与描红',
|
subtitle: '配图、例句与描红',
|
||||||
|
ageMin: 4,
|
||||||
|
ageMax: 7,
|
||||||
difficulty: 1,
|
difficulty: 1,
|
||||||
tags: ['字母', '描红', '看图描红'],
|
tags: ['字母', '描红', '看图描红'],
|
||||||
sortOrder: 42,
|
sortOrder: 42,
|
||||||
@@ -27,6 +32,8 @@ const MODES = [
|
|||||||
icon: 'draw-o',
|
icon: 'draw-o',
|
||||||
title: '基础描红',
|
title: '基础描红',
|
||||||
subtitle: 'Uppercase / Lowercase 总览',
|
subtitle: 'Uppercase / Lowercase 总览',
|
||||||
|
ageMin: 5,
|
||||||
|
ageMax: 7,
|
||||||
difficulty: 1,
|
difficulty: 1,
|
||||||
tags: ['字母', '描红', '字母总览'],
|
tags: ['字母', '描红', '字母总览'],
|
||||||
sortOrder: 44,
|
sortOrder: 44,
|
||||||
@@ -36,6 +43,8 @@ const MODES = [
|
|||||||
icon: 'font-size',
|
icon: 'font-size',
|
||||||
title: '大小写对照',
|
title: '大小写对照',
|
||||||
subtitle: '半组字母左大写右小写',
|
subtitle: '半组字母左大写右小写',
|
||||||
|
ageMin: 5,
|
||||||
|
ageMax: 7,
|
||||||
difficulty: 1,
|
difficulty: 1,
|
||||||
tags: ['字母', '描红', '大小写练习'],
|
tags: ['字母', '描红', '大小写练习'],
|
||||||
sortOrder: 45,
|
sortOrder: 45,
|
||||||
@@ -45,6 +54,8 @@ const MODES = [
|
|||||||
icon: 'two-columns',
|
icon: 'two-columns',
|
||||||
title: '两列描红',
|
title: '两列描红',
|
||||||
subtitle: '左 A–M、右 N–Z 配对描红',
|
subtitle: '左 A–M、右 N–Z 配对描红',
|
||||||
|
ageMin: 5,
|
||||||
|
ageMax: 7,
|
||||||
difficulty: 1,
|
difficulty: 1,
|
||||||
tags: ['字母', '描红', '两列练习'],
|
tags: ['字母', '描红', '两列练习'],
|
||||||
sortOrder: 43,
|
sortOrder: 43,
|
||||||
@@ -54,6 +65,8 @@ const MODES = [
|
|||||||
icon: 'square-half',
|
icon: 'square-half',
|
||||||
title: '13字母半表',
|
title: '13字母半表',
|
||||||
subtitle: '每行一个字母,13 字母半表',
|
subtitle: '每行一个字母,13 字母半表',
|
||||||
|
ageMin: 5,
|
||||||
|
ageMax: 8,
|
||||||
difficulty: 2,
|
difficulty: 2,
|
||||||
tags: ['字母', '描红', '半表练习'],
|
tags: ['字母', '描红', '半表练习'],
|
||||||
sortOrder: 47,
|
sortOrder: 47,
|
||||||
@@ -63,6 +76,8 @@ const MODES = [
|
|||||||
icon: 'ABC-list',
|
icon: 'ABC-list',
|
||||||
title: '三字母精练',
|
title: '三字母精练',
|
||||||
subtitle: '每页聚焦 3 个字母深度书写',
|
subtitle: '每页聚焦 3 个字母深度书写',
|
||||||
|
ageMin: 5,
|
||||||
|
ageMax: 7,
|
||||||
difficulty: 2,
|
difficulty: 2,
|
||||||
tags: ['字母', '描红', '三字母精练'],
|
tags: ['字母', '描红', '三字母精练'],
|
||||||
sortOrder: 46,
|
sortOrder: 46,
|
||||||
@@ -72,36 +87,37 @@ const MODES = [
|
|||||||
icon: 'task-o',
|
icon: 'task-o',
|
||||||
title: '每日打卡',
|
title: '每日打卡',
|
||||||
subtitle: '四宫格每日字母打卡练习',
|
subtitle: '四宫格每日字母打卡练习',
|
||||||
|
ageMin: 6,
|
||||||
|
ageMax: 8,
|
||||||
difficulty: 2,
|
difficulty: 2,
|
||||||
tags: ['字母', '描红', '每日打卡'],
|
tags: ['字母', '描红', '每日打卡'],
|
||||||
sortOrder: 48,
|
sortOrder: 48,
|
||||||
},
|
},
|
||||||
] as const satisfies ReadonlyArray<ModeDefinition>;
|
] as const satisfies ReadonlyArray<LetterTracingWorksheetDefinition>;
|
||||||
|
|
||||||
type Mode = (typeof MODES)[number];
|
type LetterTracingWorksheetRow = (typeof LETTER_TRACING_WORKSHEET_DEFINITIONS)[number];
|
||||||
|
|
||||||
const MODE_BY_ID = Object.fromEntries(MODES.map((m) => [m.id, m])) as Record<
|
const LETTER_TRACING_WORKSHEET_BY_ID = Object.fromEntries(
|
||||||
string,
|
LETTER_TRACING_WORKSHEET_DEFINITIONS.map((m) => [m.id, m]),
|
||||||
Mode
|
) as Record<string, LetterTracingWorksheetRow>;
|
||||||
>;
|
|
||||||
|
|
||||||
/** 页面渲染用:模式选择器列表 */
|
/** 页面渲染用:模式选择器列表(与 `LETTER_TRACING_WORKSHEET_DEFINITIONS` 同义) */
|
||||||
export const LETTER_TRACING_MODE_OPTIONS = MODES;
|
export const LETTER_TRACING_MODE_OPTIONS = LETTER_TRACING_WORKSHEET_DEFINITIONS;
|
||||||
|
|
||||||
/** 页面 pageInfoLookup 用 */
|
/** 页面 pageInfoLookup 用 */
|
||||||
export function getModeInfo(id: string) {
|
export function getModeInfo(id: string) {
|
||||||
const m = MODE_BY_ID[id];
|
const m = LETTER_TRACING_WORKSHEET_BY_ID[id];
|
||||||
return m ? { title: m.title, desc: m.subtitle } : undefined;
|
return m ? { title: m.title, desc: m.subtitle } : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 判断 id 是否有效 */
|
/** 判断 id 是否有效 */
|
||||||
export function isValidMode(id: string): boolean {
|
export function isValidMode(id: string): boolean {
|
||||||
return id in MODE_BY_ID;
|
return id in LETTER_TRACING_WORKSHEET_BY_ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 发布用:从当前 mode 生成 DebugPublishMeta */
|
/** 发布用:从当前 mode 生成 DebugPublishMeta */
|
||||||
export function getPublishMetaByMode(id: string): DebugPublishMeta | null {
|
export function getPublishMetaByMode(id: string): DebugPublishMeta | null {
|
||||||
const m = MODE_BY_ID[id];
|
const m = LETTER_TRACING_WORKSHEET_BY_ID[id];
|
||||||
if (!m) return null;
|
if (!m) return null;
|
||||||
return {
|
return {
|
||||||
id: m.id,
|
id: m.id,
|
||||||
@@ -110,9 +126,9 @@ export function getPublishMetaByMode(id: string): DebugPublishMeta | null {
|
|||||||
category: 'english',
|
category: 'english',
|
||||||
subcategory: 'letter-tracing',
|
subcategory: 'letter-tracing',
|
||||||
path: `/englishPages/letterTracing/letterTracing?id=${m.id}`,
|
path: `/englishPages/letterTracing/letterTracing?id=${m.id}`,
|
||||||
ageMin: 4,
|
ageMin: m.ageMin,
|
||||||
ageMax: 7,
|
ageMax: m.ageMax,
|
||||||
grade: inferGradeFromAge(4, 7),
|
grade: inferGradeFromAge(m.ageMin, m.ageMax),
|
||||||
difficulty: m.difficulty,
|
difficulty: m.difficulty,
|
||||||
previewImg: '',
|
previewImg: '',
|
||||||
tags: [...m.tags],
|
tags: [...m.tags],
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
} 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';
|
import type { DebugPublishMeta } from '../../utils/debugPublish';
|
||||||
|
import { incrementWorksheetLikes } from '../../utils/worksheetStats';
|
||||||
|
|
||||||
/** 三字母精练分组:26 字母每 3 个一组 */
|
/** 三字母精练分组:26 字母每 3 个一组 */
|
||||||
const TRIPLE_GROUPS: { title: string; letters: string[] }[] = [];
|
const TRIPLE_GROUPS: { title: string; letters: string[] }[] = [];
|
||||||
@@ -271,9 +272,12 @@ createPage(
|
|||||||
this.drawCanvas();
|
this.drawCanvas();
|
||||||
},
|
},
|
||||||
|
|
||||||
onPreviewFavorite() {
|
async onPreviewFavorite() {
|
||||||
const next = !this.data.isPreviewFavorite;
|
const next = !this.data.isPreviewFavorite;
|
||||||
this.setData({ isPreviewFavorite: next });
|
this.setData({ isPreviewFavorite: next });
|
||||||
|
if (next) {
|
||||||
|
incrementWorksheetLikes(this.data.worksheetId);
|
||||||
|
}
|
||||||
wx.showToast({
|
wx.showToast({
|
||||||
title: next ? '收藏成功' : '已取消收藏',
|
title: next ? '收藏成功' : '已取消收藏',
|
||||||
icon: 'none',
|
icon: 'none',
|
||||||
@@ -354,6 +358,7 @@ createPage(
|
|||||||
worksheetId,
|
worksheetId,
|
||||||
functionId: worksheetId,
|
functionId: worksheetId,
|
||||||
traceMode,
|
traceMode,
|
||||||
|
isPreviewFavorite: false,
|
||||||
selectedLetter,
|
selectedLetter,
|
||||||
selectedLetterPair: `${selectedLetter}${selectedLetter.toLowerCase()}`,
|
selectedLetterPair: `${selectedLetter}${selectedLetter.toLowerCase()}`,
|
||||||
letterCaseLower,
|
letterCaseLower,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import type { DebugPublishMeta } from '../../utils/debugPublish';
|
import type { DebugPublishMeta } from '../../utils/debugPublish';
|
||||||
import { inferGradeFromAge } from '../../utils/debugPublish';
|
import { inferGradeFromAge } from '../../utils/debugPublish';
|
||||||
|
|
||||||
interface FocusDrawDefinition {
|
/** 本地 worksheet 元数据(与云库 `worksheets` 文档 `_id` 对齐) */
|
||||||
|
interface FocusWorksheetDefinition {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
subtitle: string;
|
subtitle: string;
|
||||||
@@ -12,7 +13,7 @@ interface FocusDrawDefinition {
|
|||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FOCUS_DRAW_DEFINITIONS = [
|
export const FOCUS_WORKSHEET_DEFINITIONS = [
|
||||||
{
|
{
|
||||||
id: 'color-shape-match',
|
id: 'color-shape-match',
|
||||||
title: '根据颜色画图形',
|
title: '根据颜色画图形',
|
||||||
@@ -68,7 +69,7 @@ const FOCUS_DRAW_DEFINITIONS = [
|
|||||||
title: '线条识别',
|
title: '线条识别',
|
||||||
subtitle: '认识不同线条,画对应线条',
|
subtitle: '认识不同线条,画对应线条',
|
||||||
ageMin: 3,
|
ageMin: 3,
|
||||||
ageMax: 5,
|
ageMax: 6,
|
||||||
difficulty: 1,
|
difficulty: 1,
|
||||||
tags: ['专注力', '线条', '识别', '控笔'],
|
tags: ['专注力', '线条', '识别', '控笔'],
|
||||||
sortOrder: 206,
|
sortOrder: 206,
|
||||||
@@ -97,7 +98,7 @@ const FOCUS_DRAW_DEFINITIONS = [
|
|||||||
id: 'dot-connect',
|
id: 'dot-connect',
|
||||||
title: '数字点连线',
|
title: '数字点连线',
|
||||||
subtitle: '按数字顺序连点成图',
|
subtitle: '按数字顺序连点成图',
|
||||||
ageMin: 3,
|
ageMin: 4,
|
||||||
ageMax: 6,
|
ageMax: 6,
|
||||||
difficulty: 1,
|
difficulty: 1,
|
||||||
tags: ['专注力', '数字', '连线'],
|
tags: ['专注力', '数字', '连线'],
|
||||||
@@ -127,19 +128,19 @@ const FOCUS_DRAW_DEFINITIONS = [
|
|||||||
id: 'grid-drawing-7x7',
|
id: 'grid-drawing-7x7',
|
||||||
title: '格子仿画 7×7',
|
title: '格子仿画 7×7',
|
||||||
subtitle: '大师挑战,锻炼耐心',
|
subtitle: '大师挑战,锻炼耐心',
|
||||||
ageMin: 5,
|
ageMin: 6,
|
||||||
ageMax: 8,
|
ageMax: 8,
|
||||||
difficulty: 3,
|
difficulty: 3,
|
||||||
tags: ['专注力', '格子仿画', '耐心'],
|
tags: ['专注力', '格子仿画', '耐心'],
|
||||||
sortOrder: 212,
|
sortOrder: 212,
|
||||||
},
|
},
|
||||||
] as const satisfies ReadonlyArray<FocusDrawDefinition>;
|
] as const satisfies ReadonlyArray<FocusWorksheetDefinition>;
|
||||||
|
|
||||||
type FocusDrawDefinitionItem = (typeof FOCUS_DRAW_DEFINITIONS)[number];
|
type FocusWorksheetDefinitionItem = (typeof FOCUS_WORKSHEET_DEFINITIONS)[number];
|
||||||
|
|
||||||
const FOCUS_DRAW_BY_ID = Object.fromEntries(
|
const FOCUS_WORKSHEET_BY_ID = Object.fromEntries(
|
||||||
FOCUS_DRAW_DEFINITIONS.map((item) => [item.id, item]),
|
FOCUS_WORKSHEET_DEFINITIONS.map((item) => [item.id, item]),
|
||||||
) as Record<string, FocusDrawDefinitionItem>;
|
) as Record<string, FocusWorksheetDefinitionItem>;
|
||||||
|
|
||||||
function resolveFocusPublishId(
|
function resolveFocusPublishId(
|
||||||
routeId: string,
|
routeId: string,
|
||||||
@@ -150,7 +151,7 @@ function resolveFocusPublishId(
|
|||||||
return `grid-drawing-${mode || '3x3'}`;
|
return `grid-drawing-${mode || '3x3'}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (routeId in FOCUS_DRAW_BY_ID) {
|
if (routeId in FOCUS_WORKSHEET_BY_ID) {
|
||||||
return routeId;
|
return routeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +164,7 @@ export function getPublishMetaByFocusState(
|
|||||||
mode?: string,
|
mode?: string,
|
||||||
): DebugPublishMeta | null {
|
): DebugPublishMeta | null {
|
||||||
const id = resolveFocusPublishId(routeId, selectedTypeId, mode);
|
const id = resolveFocusPublishId(routeId, selectedTypeId, mode);
|
||||||
const item = FOCUS_DRAW_BY_ID[id];
|
const item = FOCUS_WORKSHEET_BY_ID[id];
|
||||||
if (!item) return null;
|
if (!item) return null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
} from './registry';
|
} from './registry';
|
||||||
import { getPublishMetaByFocusState } from './focusDraw.config';
|
import { getPublishMetaByFocusState } from './focusDraw.config';
|
||||||
import type { DebugPublishMeta } from '../../utils/debugPublish';
|
import type { DebugPublishMeta } from '../../utils/debugPublish';
|
||||||
|
import { incrementWorksheetLikes } from '../../utils/worksheetStats';
|
||||||
|
|
||||||
const TYPE_LIST = FOCUS_TYPE_CONFIGS.map((t) => ({
|
const TYPE_LIST = FOCUS_TYPE_CONFIGS.map((t) => ({
|
||||||
id: t.id,
|
id: t.id,
|
||||||
@@ -120,6 +121,7 @@ createFocusPage({
|
|||||||
selectedTypeId: id,
|
selectedTypeId: id,
|
||||||
functionId: id,
|
functionId: id,
|
||||||
pageTitle: title,
|
pageTitle: title,
|
||||||
|
isPreviewFavorite: false,
|
||||||
showActions: !!typeConfig.actions,
|
showActions: !!typeConfig.actions,
|
||||||
currentActions: typeConfig.actions || [],
|
currentActions: typeConfig.actions || [],
|
||||||
actionsTitle: typeConfig.actionsTitle || '选择模式',
|
actionsTitle: typeConfig.actionsTitle || '选择模式',
|
||||||
@@ -156,6 +158,7 @@ createFocusPage({
|
|||||||
this.setData({
|
this.setData({
|
||||||
currentMode: value,
|
currentMode: value,
|
||||||
pageTitle: title,
|
pageTitle: title,
|
||||||
|
isPreviewFavorite: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.initPageInfo(this.data.functionId, title);
|
this.initPageInfo(this.data.functionId, title);
|
||||||
@@ -196,15 +199,28 @@ createFocusPage({
|
|||||||
this.onRandom();
|
this.onRandom();
|
||||||
},
|
},
|
||||||
|
|
||||||
onPreviewFavorite() {
|
async onPreviewFavorite() {
|
||||||
const next = !this.data.isPreviewFavorite;
|
const next = !this.data.isPreviewFavorite;
|
||||||
this.setData({ isPreviewFavorite: next });
|
this.setData({ isPreviewFavorite: next });
|
||||||
|
if (next) {
|
||||||
|
const id = this.getWorksheetStatsId();
|
||||||
|
if (id) incrementWorksheetLikes(id);
|
||||||
|
}
|
||||||
wx.showToast({
|
wx.showToast({
|
||||||
title: next ? '收藏成功' : '已取消收藏',
|
title: next ? '收藏成功' : '已取消收藏',
|
||||||
icon: 'none',
|
icon: 'none',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getWorksheetStatsId(): string {
|
||||||
|
const meta = getPublishMetaByFocusState(
|
||||||
|
this.data.functionId,
|
||||||
|
this.data.selectedTypeId,
|
||||||
|
this.data.currentMode,
|
||||||
|
);
|
||||||
|
return meta?.id || this.data.functionId;
|
||||||
|
},
|
||||||
|
|
||||||
getPublishMeta(): DebugPublishMeta {
|
getPublishMeta(): DebugPublishMeta {
|
||||||
const meta = getPublishMetaByFocusState(
|
const meta = getPublishMetaByFocusState(
|
||||||
this.data.functionId,
|
this.data.functionId,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import type { DebugPublishMeta } from '../../utils/debugPublish';
|
import type { DebugPublishMeta } from '../../utils/debugPublish';
|
||||||
import { inferGradeFromAge } from '../../utils/debugPublish';
|
import { inferGradeFromAge } from '../../utils/debugPublish';
|
||||||
|
|
||||||
interface MathDrawDefinition {
|
/** 本地 worksheet 元数据(与云库 `worksheets` 文档 `_id` 对齐,用于发布与批量同步脚本) */
|
||||||
|
interface MathWorksheetDefinition {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
subtitle: string;
|
subtitle: string;
|
||||||
@@ -12,7 +13,7 @@ interface MathDrawDefinition {
|
|||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MATH_DRAW_DEFINITIONS = [
|
export const MATH_WORKSHEET_DEFINITIONS = [
|
||||||
{
|
{
|
||||||
id: 'number-find',
|
id: 'number-find',
|
||||||
title: '找数字,涂一涂',
|
title: '找数字,涂一涂',
|
||||||
@@ -28,8 +29,8 @@ const MATH_DRAW_DEFINITIONS = [
|
|||||||
id: 'number-write',
|
id: 'number-write',
|
||||||
title: '看数字,写一写',
|
title: '看数字,写一写',
|
||||||
subtitle: '按笔画顺序书写数字',
|
subtitle: '按笔画顺序书写数字',
|
||||||
ageMin: 3,
|
ageMin: 4,
|
||||||
ageMax: 5,
|
ageMax: 6,
|
||||||
difficulty: 1,
|
difficulty: 1,
|
||||||
tags: ['数学', '数字书写', '描红'],
|
tags: ['数学', '数字书写', '描红'],
|
||||||
sortOrder: 102,
|
sortOrder: 102,
|
||||||
@@ -68,8 +69,8 @@ const MATH_DRAW_DEFINITIONS = [
|
|||||||
id: 'number-object-fill',
|
id: 'number-object-fill',
|
||||||
title: '数物填写',
|
title: '数物填写',
|
||||||
subtitle: '数出数量,填写对应数字',
|
subtitle: '数出数量,填写对应数字',
|
||||||
ageMin: 3,
|
ageMin: 4,
|
||||||
ageMax: 5,
|
ageMax: 6,
|
||||||
difficulty: 1,
|
difficulty: 1,
|
||||||
tags: ['数学', '数物对应', '填写'],
|
tags: ['数学', '数物对应', '填写'],
|
||||||
sortOrder: 106,
|
sortOrder: 106,
|
||||||
@@ -88,8 +89,8 @@ const MATH_DRAW_DEFINITIONS = [
|
|||||||
id: 'counting-fill',
|
id: 'counting-fill',
|
||||||
title: '数一数,填一填',
|
title: '数一数,填一填',
|
||||||
subtitle: '数出物品数量,填写数字',
|
subtitle: '数出物品数量,填写数字',
|
||||||
ageMin: 3,
|
ageMin: 4,
|
||||||
ageMax: 5,
|
ageMax: 6,
|
||||||
difficulty: 1,
|
difficulty: 1,
|
||||||
tags: ['数学', '计数', '填写'],
|
tags: ['数学', '计数', '填写'],
|
||||||
sortOrder: 108,
|
sortOrder: 108,
|
||||||
@@ -128,8 +129,8 @@ const MATH_DRAW_DEFINITIONS = [
|
|||||||
id: 'number-decompose',
|
id: 'number-decompose',
|
||||||
title: '10以内数的分与合',
|
title: '10以内数的分与合',
|
||||||
subtitle: '把数字分一分,合一合',
|
subtitle: '把数字分一分,合一合',
|
||||||
ageMin: 4,
|
ageMin: 5,
|
||||||
ageMax: 6,
|
ageMax: 7,
|
||||||
difficulty: 1,
|
difficulty: 1,
|
||||||
tags: ['数学', '分与合', '10以内'],
|
tags: ['数学', '分与合', '10以内'],
|
||||||
sortOrder: 112,
|
sortOrder: 112,
|
||||||
@@ -158,8 +159,8 @@ const MATH_DRAW_DEFINITIONS = [
|
|||||||
id: 'addition-5',
|
id: 'addition-5',
|
||||||
title: '5以内加法',
|
title: '5以内加法',
|
||||||
subtitle: '图形化展示5以内加法',
|
subtitle: '图形化展示5以内加法',
|
||||||
ageMin: 3,
|
ageMin: 4,
|
||||||
ageMax: 5,
|
ageMax: 6,
|
||||||
difficulty: 1,
|
difficulty: 1,
|
||||||
tags: ['数学', '加法', '5以内', '计算题'],
|
tags: ['数学', '加法', '5以内', '计算题'],
|
||||||
sortOrder: 115,
|
sortOrder: 115,
|
||||||
@@ -178,8 +179,8 @@ const MATH_DRAW_DEFINITIONS = [
|
|||||||
id: 'subtraction-10',
|
id: 'subtraction-10',
|
||||||
title: '10以内减法',
|
title: '10以内减法',
|
||||||
subtitle: '图形化展示10以内减法',
|
subtitle: '图形化展示10以内减法',
|
||||||
ageMin: 4,
|
ageMin: 5,
|
||||||
ageMax: 6,
|
ageMax: 7,
|
||||||
difficulty: 2,
|
difficulty: 2,
|
||||||
tags: ['数学', '减法', '10以内', '计算题'],
|
tags: ['数学', '减法', '10以内', '计算题'],
|
||||||
sortOrder: 117,
|
sortOrder: 117,
|
||||||
@@ -238,7 +239,7 @@ const MATH_DRAW_DEFINITIONS = [
|
|||||||
id: 'practice-addition',
|
id: 'practice-addition',
|
||||||
title: '加法运算',
|
title: '加法运算',
|
||||||
subtitle: '10/20/50/100 以内加法',
|
subtitle: '10/20/50/100 以内加法',
|
||||||
ageMin: 5,
|
ageMin: 6,
|
||||||
ageMax: 8,
|
ageMax: 8,
|
||||||
difficulty: 3,
|
difficulty: 3,
|
||||||
tags: ['数学', '口算', '加法', '计算题'],
|
tags: ['数学', '口算', '加法', '计算题'],
|
||||||
@@ -248,7 +249,7 @@ const MATH_DRAW_DEFINITIONS = [
|
|||||||
id: 'practice-subtraction',
|
id: 'practice-subtraction',
|
||||||
title: '减法运算',
|
title: '减法运算',
|
||||||
subtitle: '10/20/50/100以内减法',
|
subtitle: '10/20/50/100以内减法',
|
||||||
ageMin: 5,
|
ageMin: 6,
|
||||||
ageMax: 8,
|
ageMax: 8,
|
||||||
difficulty: 3,
|
difficulty: 3,
|
||||||
tags: ['数学', '口算', '减法', '计算题'],
|
tags: ['数学', '口算', '减法', '计算题'],
|
||||||
@@ -258,7 +259,7 @@ const MATH_DRAW_DEFINITIONS = [
|
|||||||
id: 'practice-mixed',
|
id: 'practice-mixed',
|
||||||
title: '混合运算',
|
title: '混合运算',
|
||||||
subtitle: '10/20/50/100以内加减法混合',
|
subtitle: '10/20/50/100以内加减法混合',
|
||||||
ageMin: 5,
|
ageMin: 6,
|
||||||
ageMax: 8,
|
ageMax: 8,
|
||||||
difficulty: 3,
|
difficulty: 3,
|
||||||
tags: ['数学', '口算', '加减法', '计算题'],
|
tags: ['数学', '口算', '加减法', '计算题'],
|
||||||
@@ -268,19 +269,19 @@ const MATH_DRAW_DEFINITIONS = [
|
|||||||
id: 'multiplication-table',
|
id: 'multiplication-table',
|
||||||
title: '九九乘法表',
|
title: '九九乘法表',
|
||||||
subtitle: '学习九九乘法口诀',
|
subtitle: '学习九九乘法口诀',
|
||||||
ageMin: 6,
|
ageMin: 7,
|
||||||
ageMax: 8,
|
ageMax: 8,
|
||||||
difficulty: 3,
|
difficulty: 3,
|
||||||
tags: ['数学', '乘法', '九九乘法表'],
|
tags: ['数学', '乘法', '九九乘法表'],
|
||||||
sortOrder: 126,
|
sortOrder: 126,
|
||||||
},
|
},
|
||||||
] as const satisfies ReadonlyArray<MathDrawDefinition>;
|
] as const satisfies ReadonlyArray<MathWorksheetDefinition>;
|
||||||
|
|
||||||
type MathDrawDefinitionItem = (typeof MATH_DRAW_DEFINITIONS)[number];
|
type MathWorksheetDefinitionItem = (typeof MATH_WORKSHEET_DEFINITIONS)[number];
|
||||||
|
|
||||||
const MATH_DRAW_BY_ID = Object.fromEntries(
|
const MATH_WORKSHEET_BY_ID = Object.fromEntries(
|
||||||
MATH_DRAW_DEFINITIONS.map((item) => [item.id, item]),
|
MATH_WORKSHEET_DEFINITIONS.map((item) => [item.id, item]),
|
||||||
) as Record<string, MathDrawDefinitionItem>;
|
) as Record<string, MathWorksheetDefinitionItem>;
|
||||||
|
|
||||||
function resolveMathPublishId(
|
function resolveMathPublishId(
|
||||||
routeId: string,
|
routeId: string,
|
||||||
@@ -295,7 +296,7 @@ function resolveMathPublishId(
|
|||||||
return 'number-object-fill';
|
return 'number-object-fill';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (routeId in MATH_DRAW_BY_ID) {
|
if (routeId in MATH_WORKSHEET_BY_ID) {
|
||||||
return routeId;
|
return routeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,7 +309,7 @@ export function getPublishMetaByMathState(
|
|||||||
mode?: string,
|
mode?: string,
|
||||||
): DebugPublishMeta | null {
|
): DebugPublishMeta | null {
|
||||||
const id = resolveMathPublishId(routeId, selectedTypeId, mode);
|
const id = resolveMathPublishId(routeId, selectedTypeId, mode);
|
||||||
const item = MATH_DRAW_BY_ID[id];
|
const item = MATH_WORKSHEET_BY_ID[id];
|
||||||
if (!item) return null;
|
if (!item) return null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
} from './registry';
|
} from './registry';
|
||||||
import { getPublishMetaByMathState } from './mathDraw.config';
|
import { getPublishMetaByMathState } from './mathDraw.config';
|
||||||
import type { DebugPublishMeta } from '../../utils/debugPublish';
|
import type { DebugPublishMeta } from '../../utils/debugPublish';
|
||||||
|
import { incrementWorksheetLikes } from '../../utils/worksheetStats';
|
||||||
|
|
||||||
const TYPE_LIST = MATH_TYPE_CONFIGS.map((t) => ({
|
const TYPE_LIST = MATH_TYPE_CONFIGS.map((t) => ({
|
||||||
id: t.id,
|
id: t.id,
|
||||||
@@ -130,6 +131,7 @@ createMathPage({
|
|||||||
selectedTypeId: id,
|
selectedTypeId: id,
|
||||||
functionId: id,
|
functionId: id,
|
||||||
pageTitle: title,
|
pageTitle: title,
|
||||||
|
isPreviewFavorite: false,
|
||||||
showActions: !!typeConfig.actions,
|
showActions: !!typeConfig.actions,
|
||||||
currentActions: typeConfig.actions || [],
|
currentActions: typeConfig.actions || [],
|
||||||
actionsTitle: typeConfig.actionsTitle || '选择模式',
|
actionsTitle: typeConfig.actionsTitle || '选择模式',
|
||||||
@@ -170,6 +172,7 @@ createMathPage({
|
|||||||
this.setData({
|
this.setData({
|
||||||
currentMode: value,
|
currentMode: value,
|
||||||
pageTitle: title,
|
pageTitle: title,
|
||||||
|
isPreviewFavorite: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.initPageInfo(this.data.functionId, title);
|
this.initPageInfo(this.data.functionId, title);
|
||||||
@@ -260,15 +263,28 @@ createMathPage({
|
|||||||
this.onRandom();
|
this.onRandom();
|
||||||
},
|
},
|
||||||
|
|
||||||
onPreviewFavorite() {
|
async onPreviewFavorite() {
|
||||||
const next = !this.data.isPreviewFavorite;
|
const next = !this.data.isPreviewFavorite;
|
||||||
this.setData({ isPreviewFavorite: next });
|
this.setData({ isPreviewFavorite: next });
|
||||||
|
if (next) {
|
||||||
|
const id = this.getWorksheetStatsId();
|
||||||
|
if (id) incrementWorksheetLikes(id);
|
||||||
|
}
|
||||||
wx.showToast({
|
wx.showToast({
|
||||||
title: next ? '收藏成功' : '已取消收藏',
|
title: next ? '收藏成功' : '已取消收藏',
|
||||||
icon: 'none',
|
icon: 'none',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getWorksheetStatsId(): string {
|
||||||
|
const meta = getPublishMetaByMathState(
|
||||||
|
this.data.functionId,
|
||||||
|
this.data.selectedTypeId,
|
||||||
|
this.data.currentMode,
|
||||||
|
);
|
||||||
|
return meta?.id || this.data.functionId;
|
||||||
|
},
|
||||||
|
|
||||||
getPublishMeta(): DebugPublishMeta {
|
getPublishMeta(): DebugPublishMeta {
|
||||||
const meta = getPublishMetaByMathState(
|
const meta = getPublishMetaByMathState(
|
||||||
this.data.functionId,
|
this.data.functionId,
|
||||||
|
|||||||
@@ -0,0 +1,557 @@
|
|||||||
|
import type { AgeBandKey } from '../../core/data/difficulty';
|
||||||
|
|
||||||
|
export type AbilityItem = {
|
||||||
|
icon: string;
|
||||||
|
title: string;
|
||||||
|
desc: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 每周练习项:与云 / page-config 中 worksheet 字段对齐,便于跳转 */
|
||||||
|
export type WeekExercise = {
|
||||||
|
/** 与云数据库 worksheets._id、page-config category.items[].id 一致 */
|
||||||
|
_id: string;
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
/** 小程序内路径,可含 query,如 /mathPages/mathDraw/mathDraw?id=xxx */
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WeekPlan = {
|
||||||
|
week: number;
|
||||||
|
theme: string;
|
||||||
|
exercises: WeekExercise[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AGE_TAB_SUB: Record<AgeBandKey, string> = {
|
||||||
|
'3-4': '启蒙认知',
|
||||||
|
'4-5': '基础练习',
|
||||||
|
'5-6': '能力提升',
|
||||||
|
'6-7': '幼小衔接',
|
||||||
|
'7-8': '知识拓展',
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 各年龄段能力目标(初版,可由 AI Skill 更新) */
|
||||||
|
export const AGE_ABILITIES: Record<AgeBandKey, AbilityItem[]> = {
|
||||||
|
'3-4': [
|
||||||
|
{ icon: '🔢', title: '数感', desc: '认读 1-5、点数对应' },
|
||||||
|
{ icon: '✏️', title: '书写', desc: '涂鸦线条、简单描红' },
|
||||||
|
{ icon: '🧩', title: '思维', desc: '找相同、简单配对' },
|
||||||
|
],
|
||||||
|
'4-5': [
|
||||||
|
{ icon: '🔢', title: '数感', desc: '10 以内数数、比大小' },
|
||||||
|
{ icon: '✏️', title: '书写', desc: '笔画模仿、图形描边' },
|
||||||
|
{ icon: '🧩', title: '思维', desc: '规律排序、图形分类' },
|
||||||
|
],
|
||||||
|
'5-6': [
|
||||||
|
{ icon: '🔢', title: '数感', desc: '10以内加减法、凑十法' },
|
||||||
|
{ icon: '✏️', title: '书写', desc: '练字帖、拼音入门' },
|
||||||
|
{ icon: '🧩', title: '思维', desc: '方格推理、格子仿画 5×5' },
|
||||||
|
],
|
||||||
|
'6-7': [
|
||||||
|
{ icon: '🔢', title: '数感', desc: '20 以内运算、应用题启蒙' },
|
||||||
|
{ icon: '✏️', title: '书写', desc: '常用字书写、词语积累' },
|
||||||
|
{ icon: '🧩', title: '思维', desc: '逻辑填空、空间想象' },
|
||||||
|
],
|
||||||
|
'7-8': [
|
||||||
|
{ icon: '🔢', title: '数感', desc: '乘除启蒙、巧算练习' },
|
||||||
|
{ icon: '✏️', title: '书写', desc: '段落抄写、古诗诵读' },
|
||||||
|
{ icon: '🧩', title: '思维', desc: '数独入门、综合推理' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 4 周路线(初版):每项含 _id / subtitle / path,与线上 worksheet 或本地分类兜底一致。
|
||||||
|
* AI Skill 生成时应从 .cache/page-config.json 的 category.items[] 复制 id→_id、title、subtitle、path。
|
||||||
|
*/
|
||||||
|
export const AGE_WEEK_PLANS: Record<AgeBandKey, WeekPlan[]> = {
|
||||||
|
'3-4': [
|
||||||
|
{
|
||||||
|
week: 1,
|
||||||
|
theme: '数感启蒙',
|
||||||
|
exercises: [
|
||||||
|
{
|
||||||
|
_id: 'number-find',
|
||||||
|
title: '找数字,涂一涂',
|
||||||
|
subtitle: '在数字方阵中找出目标数字并涂色',
|
||||||
|
path: '/mathPages/mathDraw/mathDraw?id=number-find',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'addition-5',
|
||||||
|
title: '5以内加法',
|
||||||
|
subtitle: '图形化展示 5 以内加法',
|
||||||
|
path: '/mathPages/mathDraw/mathDraw?id=addition-5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'counting-matching',
|
||||||
|
title: '数一数,连一连',
|
||||||
|
subtitle: '连线配对数字和对应数量图形',
|
||||||
|
path: '/mathPages/mathDraw/mathDraw?id=counting-matching',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
week: 2,
|
||||||
|
theme: '形状与连线',
|
||||||
|
exercises: [
|
||||||
|
{
|
||||||
|
_id: 'shape-recognition',
|
||||||
|
title: '识别形状',
|
||||||
|
subtitle: '识别形状,涂一涂',
|
||||||
|
path: '/focusPages/focusDraw/focusDraw?id=shape-recognition',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'line-recognition',
|
||||||
|
title: '线条识别',
|
||||||
|
subtitle: '认识不同线条,画出颜色对应的线条',
|
||||||
|
path: '/focusPages/focusDraw/focusDraw?id=line-recognition',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'dot-connect',
|
||||||
|
title: '数字点连线',
|
||||||
|
subtitle: '按数字顺序连点成图',
|
||||||
|
path: '/focusPages/focusDraw/focusDraw?id=dot-connect',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
week: 3,
|
||||||
|
theme: '趣味专注',
|
||||||
|
exercises: [
|
||||||
|
{
|
||||||
|
_id: 'color-pattern',
|
||||||
|
title: '颜色找规律',
|
||||||
|
subtitle: '观察颜色规律,在空白图形中涂色',
|
||||||
|
path: '/focusPages/focusDraw/focusDraw?id=color-pattern',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'grid-drawing-3x3',
|
||||||
|
title: '格子仿画 3×3',
|
||||||
|
subtitle: '简单有趣,培养专注力',
|
||||||
|
path: '/focusPages/focusDraw/focusDraw?id=grid-drawing-3x3',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'match-connect',
|
||||||
|
title: '连连看',
|
||||||
|
subtitle: '根据物品连一连',
|
||||||
|
path: '/focusPages/focusDraw/focusDraw?id=match-connect',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
week: 4,
|
||||||
|
theme: '综合练习',
|
||||||
|
exercises: [
|
||||||
|
{
|
||||||
|
_id: 'addition-10',
|
||||||
|
title: '10以内加法',
|
||||||
|
subtitle: '图形化展示 10 以内加法',
|
||||||
|
path: '/mathPages/mathDraw/mathDraw?id=addition-10',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'word-recognition',
|
||||||
|
title: '识字卡',
|
||||||
|
subtitle: '输入或选字生成涂色识字卡',
|
||||||
|
path: '/pages/index/index',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'number-coloring',
|
||||||
|
title: '按数字,涂颜色',
|
||||||
|
subtitle: '按指定数字给对应圆圈涂色',
|
||||||
|
path: '/mathPages/mathDraw/mathDraw?id=number-coloring',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'4-5': [
|
||||||
|
{
|
||||||
|
week: 1,
|
||||||
|
theme: '数数与比较',
|
||||||
|
exercises: [
|
||||||
|
{
|
||||||
|
_id: 'counting-fill',
|
||||||
|
title: '数一数,填一填',
|
||||||
|
subtitle: '数出物品数量,填写数字',
|
||||||
|
path: '/mathPages/mathDraw/mathDraw?id=counting-fill',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'compare',
|
||||||
|
title: '数一数,比大小',
|
||||||
|
subtitle: '比较数量,填入 ><=',
|
||||||
|
path: '/mathPages/mathDraw/mathDraw?id=compare',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'number-object-match',
|
||||||
|
title: '数物连线',
|
||||||
|
subtitle: '连线相同数量的物品和数字',
|
||||||
|
path: '/mathPages/mathDraw/mathDraw?id=number-object-match',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
week: 2,
|
||||||
|
theme: '线条与形状',
|
||||||
|
exercises: [
|
||||||
|
{
|
||||||
|
_id: 'number-write',
|
||||||
|
title: '看数字,写一写',
|
||||||
|
subtitle: '按笔画顺序练习书写数字',
|
||||||
|
path: '/mathPages/mathDraw/mathDraw?id=number-write',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'shape-symbol',
|
||||||
|
title: '图形符号配对',
|
||||||
|
subtitle: '根据图形画对应符号',
|
||||||
|
path: '/focusPages/focusDraw/focusDraw?id=shape-symbol',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'counting-matching',
|
||||||
|
title: '数一数,连一连',
|
||||||
|
subtitle: '连线配对数字和对应数量图形',
|
||||||
|
path: '/mathPages/mathDraw/mathDraw?id=counting-matching',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
week: 3,
|
||||||
|
theme: '规律与分类',
|
||||||
|
exercises: [
|
||||||
|
{
|
||||||
|
_id: 'color-pattern',
|
||||||
|
title: '颜色找规律',
|
||||||
|
subtitle: '观察颜色规律,在空白图形中涂色',
|
||||||
|
path: '/focusPages/focusDraw/focusDraw?id=color-pattern',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'number-sort',
|
||||||
|
title: '数字排序',
|
||||||
|
subtitle: '写出正确的数字顺序',
|
||||||
|
path: '/mathPages/mathDraw/mathDraw?id=number-sort',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'color-shape-match',
|
||||||
|
title: '根据颜色画图形',
|
||||||
|
subtitle: '根据颜色画出对应图形',
|
||||||
|
path: '/focusPages/focusDraw/focusDraw?id=color-shape-match',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
week: 4,
|
||||||
|
theme: '综合提升',
|
||||||
|
exercises: [
|
||||||
|
{
|
||||||
|
_id: 'code-connect',
|
||||||
|
title: '译码连线',
|
||||||
|
subtitle: '按数字顺序将数字对应颜色连线',
|
||||||
|
path: '/focusPages/focusDraw/focusDraw?id=code-connect',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'match-connect',
|
||||||
|
title: '连连看',
|
||||||
|
subtitle: '根据物品连一连',
|
||||||
|
path: '/focusPages/focusDraw/focusDraw?id=match-connect',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'addition-10',
|
||||||
|
title: '10以内加法',
|
||||||
|
subtitle: '图形化展示 10 以内加法',
|
||||||
|
path: '/mathPages/mathDraw/mathDraw?id=addition-10',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'5-6': [
|
||||||
|
{
|
||||||
|
week: 1,
|
||||||
|
theme: '加减入门',
|
||||||
|
exercises: [
|
||||||
|
{
|
||||||
|
_id: 'addition-5',
|
||||||
|
title: '5以内加法',
|
||||||
|
subtitle: '图形化展示 5 以内加法',
|
||||||
|
path: '/mathPages/mathDraw/mathDraw?id=addition-5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'addition-10',
|
||||||
|
title: '10以内加法',
|
||||||
|
subtitle: '图形化展示 10 以内加法',
|
||||||
|
path: '/mathPages/mathDraw/mathDraw?id=addition-10',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'subtraction-10',
|
||||||
|
title: '10以内减法',
|
||||||
|
subtitle: '图形化展示 10 以内减法',
|
||||||
|
path: '/mathPages/mathDraw/mathDraw?id=subtraction-10',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
week: 2,
|
||||||
|
theme: '凑十与分解',
|
||||||
|
exercises: [
|
||||||
|
{
|
||||||
|
_id: 'make-ten',
|
||||||
|
title: '凑十法练习',
|
||||||
|
subtitle: '20 以内进位加法',
|
||||||
|
path: '/mathPages/mathDraw/mathDraw?id=make-ten',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'number-decompose',
|
||||||
|
title: '10以内数的分与合',
|
||||||
|
subtitle: '把数字分一分,合一合',
|
||||||
|
path: '/mathPages/mathDraw/mathDraw?id=number-decompose',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'one-digit-addition',
|
||||||
|
title: '一位数加法',
|
||||||
|
subtitle: '通过圆点学习一位数加法运算',
|
||||||
|
path: '/mathPages/mathDraw/mathDraw?id=one-digit-addition',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
week: 3,
|
||||||
|
theme: '逻辑与推理',
|
||||||
|
exercises: [
|
||||||
|
{
|
||||||
|
_id: 'grid-reasoning',
|
||||||
|
title: '方格推理',
|
||||||
|
subtitle: '推理出合并方格并连线',
|
||||||
|
path: '/focusPages/focusDraw/focusDraw?id=grid-reasoning',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'grid-drawing-5x5',
|
||||||
|
title: '格子仿画 5×5',
|
||||||
|
subtitle: '创意挑战,提升观察力',
|
||||||
|
path: '/focusPages/focusDraw/focusDraw?id=grid-drawing-5x5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'missing-number',
|
||||||
|
title: '填上缺少的数字',
|
||||||
|
subtitle: '在数列中找出并填写缺失数字',
|
||||||
|
path: '/mathPages/mathDraw/mathDraw?id=missing-number',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
week: 4,
|
||||||
|
theme: '综合应用',
|
||||||
|
exercises: [
|
||||||
|
{
|
||||||
|
_id: 'addition-subtraction-10',
|
||||||
|
title: '10以内加减法',
|
||||||
|
subtitle: '加减法混合运算',
|
||||||
|
path: '/mathPages/mathDraw/mathDraw?id=addition-subtraction-10',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'practice-addition',
|
||||||
|
title: '加法运算',
|
||||||
|
subtitle: '10/20/50/100 以内加法',
|
||||||
|
path: '/mathPages/mathDraw/mathDraw?id=practice-addition',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'copybook',
|
||||||
|
title: '练字帖',
|
||||||
|
subtitle: '选字生成田字格笔顺练字帖',
|
||||||
|
path: '/pages/copyBook/copyBook',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'6-7': [
|
||||||
|
{
|
||||||
|
week: 1,
|
||||||
|
theme: '20 以内运算',
|
||||||
|
exercises: [
|
||||||
|
{
|
||||||
|
_id: 'practice-addition',
|
||||||
|
title: '加法运算',
|
||||||
|
subtitle: '10/20/50/100 以内加法',
|
||||||
|
path: '/mathPages/mathDraw/mathDraw?id=practice-addition',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'practice-subtraction',
|
||||||
|
title: '减法运算',
|
||||||
|
subtitle: '10/20/50/100 以内减法',
|
||||||
|
path: '/mathPages/mathDraw/mathDraw?id=practice-subtraction',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'practice-mixed',
|
||||||
|
title: '混合运算',
|
||||||
|
subtitle: '10/20/50/100 以内加减法混合',
|
||||||
|
path: '/mathPages/mathDraw/mathDraw?id=practice-mixed',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
week: 2,
|
||||||
|
theme: '识字与词语',
|
||||||
|
exercises: [
|
||||||
|
{
|
||||||
|
_id: 'copybook',
|
||||||
|
title: '练字帖',
|
||||||
|
subtitle: '选字生成田字格笔顺练字帖',
|
||||||
|
path: '/pages/copyBook/copyBook',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'match-connect',
|
||||||
|
title: '连连看',
|
||||||
|
subtitle: '根据物品连一连',
|
||||||
|
path: '/focusPages/focusDraw/focusDraw?id=match-connect',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'word-recognition',
|
||||||
|
title: '识字卡',
|
||||||
|
subtitle: '输入或选字生成涂色识字卡',
|
||||||
|
path: '/pages/index/index',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
week: 3,
|
||||||
|
theme: '逻辑与空间',
|
||||||
|
exercises: [
|
||||||
|
{
|
||||||
|
_id: 'grid-reasoning',
|
||||||
|
title: '方格推理',
|
||||||
|
subtitle: '推理出合并方格并连线',
|
||||||
|
path: '/focusPages/focusDraw/focusDraw?id=grid-reasoning',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'position-coloring',
|
||||||
|
title: '方位涂涂乐',
|
||||||
|
subtitle: '观察位置,在方格中涂色',
|
||||||
|
path: '/focusPages/focusDraw/focusDraw?id=position-coloring',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'code-connect',
|
||||||
|
title: '译码连线',
|
||||||
|
subtitle: '按数字顺序将数字对应颜色连线',
|
||||||
|
path: '/focusPages/focusDraw/focusDraw?id=code-connect',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
week: 4,
|
||||||
|
theme: '综合提升',
|
||||||
|
exercises: [
|
||||||
|
{
|
||||||
|
_id: 'break-ten',
|
||||||
|
title: '破十法练习',
|
||||||
|
subtitle: '20 以内退位减法',
|
||||||
|
path: '/mathPages/mathDraw/mathDraw?id=break-ten',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'flat-ten',
|
||||||
|
title: '平十法练习',
|
||||||
|
subtitle: '20 以内退位减法',
|
||||||
|
path: '/mathPages/mathDraw/mathDraw?id=flat-ten',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'number-decompose-20',
|
||||||
|
title: '20以内数的分与合',
|
||||||
|
subtitle: '把数字分一分,合一合',
|
||||||
|
path: '/mathPages/mathDraw/mathDraw?id=number-decompose-20',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'7-8': [
|
||||||
|
{
|
||||||
|
week: 1,
|
||||||
|
theme: '乘除启蒙',
|
||||||
|
exercises: [
|
||||||
|
{
|
||||||
|
_id: 'multiplication-table',
|
||||||
|
title: '九九乘法表',
|
||||||
|
subtitle: '学习九九乘法口诀',
|
||||||
|
path: '/mathPages/mathDraw/mathDraw?id=multiplication-table',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'practice-addition',
|
||||||
|
title: '加法运算',
|
||||||
|
subtitle: '10/20/50/100 以内加法',
|
||||||
|
path: '/mathPages/mathDraw/mathDraw?id=practice-addition',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'practice-subtraction',
|
||||||
|
title: '减法运算',
|
||||||
|
subtitle: '10/20/50/100 以内减法',
|
||||||
|
path: '/mathPages/mathDraw/mathDraw?id=practice-subtraction',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
week: 2,
|
||||||
|
theme: '巧算与策略',
|
||||||
|
exercises: [
|
||||||
|
{
|
||||||
|
_id: 'make-ten',
|
||||||
|
title: '凑十法练习',
|
||||||
|
subtitle: '20 以内进位加法',
|
||||||
|
path: '/mathPages/mathDraw/mathDraw?id=make-ten',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'borrow-ten',
|
||||||
|
title: '借十法练习',
|
||||||
|
subtitle: '20 以上退位减法',
|
||||||
|
path: '/mathPages/mathDraw/mathDraw?id=borrow-ten',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'practice-mixed',
|
||||||
|
title: '混合运算',
|
||||||
|
subtitle: '10/20/50/100 以内加减法混合',
|
||||||
|
path: '/mathPages/mathDraw/mathDraw?id=practice-mixed',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
week: 3,
|
||||||
|
theme: '综合推理',
|
||||||
|
exercises: [
|
||||||
|
{
|
||||||
|
_id: 'grid-reasoning',
|
||||||
|
title: '方格推理',
|
||||||
|
subtitle: '推理出合并方格并连线',
|
||||||
|
path: '/focusPages/focusDraw/focusDraw?id=grid-reasoning',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'code-connect',
|
||||||
|
title: '译码连线',
|
||||||
|
subtitle: '按数字顺序将数字对应颜色连线',
|
||||||
|
path: '/focusPages/focusDraw/focusDraw?id=code-connect',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'dot-connect',
|
||||||
|
title: '数字点连线',
|
||||||
|
subtitle: '按数字顺序连点成图',
|
||||||
|
path: '/focusPages/focusDraw/focusDraw?id=dot-connect',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
week: 4,
|
||||||
|
theme: '能力拓展',
|
||||||
|
exercises: [
|
||||||
|
{
|
||||||
|
_id: 'letter-tracing-two-column',
|
||||||
|
title: '两列练习',
|
||||||
|
subtitle: '左 A-M、右 N-Z,大小写配对描红',
|
||||||
|
path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-two-column',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'word-recognition',
|
||||||
|
title: '识字卡',
|
||||||
|
subtitle: '输入或选字生成涂色识字卡',
|
||||||
|
path: '/pages/index/index',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: 'practice-mixed',
|
||||||
|
title: '混合运算',
|
||||||
|
subtitle: '10/20/50/100 以内加减法混合',
|
||||||
|
path: '/mathPages/mathDraw/mathDraw?id=practice-mixed',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
"backgroundColor": "#FEF6E7",
|
"backgroundColor": "#FEF6E7",
|
||||||
"enablePullDownRefresh": false,
|
"enablePullDownRefresh": false,
|
||||||
"usingComponents": {
|
"usingComponents": {
|
||||||
"nav-bar": "../../components3.0/nav-bar/nav-bar"
|
"nav-bar": "../../components3.0/nav-bar/nav-bar",
|
||||||
|
"worksheet-card": "../../components3.0/worksheet-card/worksheet-card"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
.age-page {
|
.age-page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: @bg-page;
|
background: @bg-page;
|
||||||
padding-bottom: calc(48rpx + env(safe-area-inset-bottom));
|
padding-bottom: calc(10rpx + env(safe-area-inset-bottom));
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,14 +289,27 @@
|
|||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.age-week-row__title {
|
.age-week-row__meta {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.age-week-row__title {
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: @text-title;
|
color: @text-title;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.age-week-row__subtitle {
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: @text-gray;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
.age-week-row__btn {
|
.age-week-row__btn {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -324,6 +337,39 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.age-rec-loading {
|
||||||
|
margin-top: @section-gap-lg;
|
||||||
|
padding: 48rpx @page-padding-x @section-gap-xs;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.age-rec-loading__icon {
|
||||||
|
margin-right: 12rpx;
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: @text-secondary;
|
||||||
|
animation: age-rec-loading-spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.age-rec-loading__text {
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: @text-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes age-rec-loading-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.age-rec__head {
|
.age-rec__head {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -356,28 +402,5 @@
|
|||||||
|
|
||||||
.age-rec__card {
|
.age-rec__card {
|
||||||
width: calc(50% - 16rpx);
|
width: calc(50% - 16rpx);
|
||||||
background: @bg-white;
|
|
||||||
border-radius: @radius-lg;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 1rpx solid rgba(50, 46, 37, 0.08);
|
|
||||||
box-shadow: @shadow;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.age-rec__img {
|
|
||||||
width: 100%;
|
|
||||||
height: 200rpx;
|
|
||||||
display: block;
|
|
||||||
background: fade(@brand, 15%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.age-rec__body {
|
|
||||||
padding: 20rpx 20rpx 24rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.age-rec__card-title {
|
|
||||||
font-size: 26rpx;
|
|
||||||
font-weight: 700;
|
|
||||||
color: @text-title;
|
|
||||||
line-height: 1.35;
|
|
||||||
}
|
|
||||||
+118
-189
@@ -1,187 +1,101 @@
|
|||||||
import { AGE_BANDS, AgeBandKey } from '../../core/data/difficulty';
|
import { AGE_BANDS, AgeBandKey } from '../../core/data/difficulty';
|
||||||
|
import { parseMiniProgramUrl } from '../../utils/index';
|
||||||
|
import {
|
||||||
|
AGE_ABILITIES,
|
||||||
|
AGE_TAB_SUB,
|
||||||
|
AGE_WEEK_PLANS,
|
||||||
|
type AbilityItem,
|
||||||
|
type WeekPlan,
|
||||||
|
} from './age.config';
|
||||||
|
|
||||||
type AbilityItem = {
|
const DEFAULT_AGE: AgeBandKey = '3-4';
|
||||||
icon: string;
|
|
||||||
|
const TAB_BAR_PATHS = new Set([
|
||||||
|
'/pages/home/home',
|
||||||
|
'/pages/category/category',
|
||||||
|
'/pages/age/age',
|
||||||
|
'/pages/favorites/favorites',
|
||||||
|
'/pages/profile/profile',
|
||||||
|
]);
|
||||||
|
|
||||||
|
function navigateByPath(path?: string, fallbackTitle?: string) {
|
||||||
|
const p = String(path || '').trim();
|
||||||
|
if (!p) {
|
||||||
|
wx.showToast({
|
||||||
|
title: fallbackTitle ? `${fallbackTitle} 即将上线` : '即将上线',
|
||||||
|
icon: 'none',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { path: basePath, query } = parseMiniProgramUrl(p);
|
||||||
|
if (TAB_BAR_PATHS.has(basePath)) {
|
||||||
|
if (Object.keys(query).length > 0) {
|
||||||
|
console.warn(
|
||||||
|
'[age navigateByPath] tabBar 不支持携带参数,已忽略 query:',
|
||||||
|
basePath,
|
||||||
|
query,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
wx.switchTab({ url: basePath });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
wx.navigateTo({ url: p }).catch((err) => {
|
||||||
|
console.error('navigateByPath error:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecommendedItemView = {
|
||||||
title: string;
|
title: string;
|
||||||
desc: string;
|
image: string;
|
||||||
|
subtitle: string;
|
||||||
|
ageBand: string;
|
||||||
|
difficulty: string;
|
||||||
|
path: string;
|
||||||
|
worksheetId?: string;
|
||||||
|
};
|
||||||
|
const RECOMMENDED_FALLBACK: RecommendedItemView[] = [];
|
||||||
|
|
||||||
|
const DIFFICULTY_LABELS: Record<string, string> = {
|
||||||
|
1: '入门',
|
||||||
|
2: '基础',
|
||||||
|
3: '进阶',
|
||||||
|
4: '挑战',
|
||||||
};
|
};
|
||||||
|
|
||||||
type WeekExercise = {
|
function toDifficultyLabel(value: unknown) {
|
||||||
title: string;
|
const text = String(value || '').trim();
|
||||||
};
|
return DIFFICULTY_LABELS[text] || text;
|
||||||
|
}
|
||||||
|
|
||||||
type WeekPlan = {
|
async function fetchAgeRecommended(
|
||||||
week: number;
|
ageKey: AgeBandKey,
|
||||||
theme: string;
|
): Promise<RecommendedItemView[]> {
|
||||||
exercises: WeekExercise[];
|
try {
|
||||||
};
|
const res = await wx.cloud.callFunction({
|
||||||
|
name: 'ageRecommendedQuery',
|
||||||
const AGE_TAB_SUB: Record<AgeBandKey, string> = {
|
data: { ageKey },
|
||||||
'3-4': '启蒙认知',
|
});
|
||||||
'4-5': '基础练习',
|
const payload = (res && (res.result as any)) || null;
|
||||||
'5-6': '能力提升',
|
if (!payload?.success) return [];
|
||||||
'6-7': '幼小衔接',
|
const items = (payload.data?.items || []) as any[];
|
||||||
'7-8': '知识拓展',
|
return items
|
||||||
};
|
.map((it) => ({
|
||||||
|
title: String(it.title || ''),
|
||||||
/** Stitch「为你推荐」占位图(需在小程序后台配置 download 合法域名含 lh3.googleusercontent.com) */
|
image: String(it.previewImg || ''),
|
||||||
const RECOMMENDED_FROM_STITCH: { title: string; image: string }[] = [
|
subtitle: String(it.subtitle || ''),
|
||||||
{
|
ageBand: String(it.ageBand || it.ageRange || ''),
|
||||||
title: '10以内加减法',
|
difficulty: toDifficultyLabel(
|
||||||
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuD-3lADhclTGODN9WtNh5v8bQPLysbllbKNkH40bAmVyE5d54PwfTBOiX9nER1hpE4rzHAfsmM_JhVnsE4EbdA914rYIb3S3e4r9QKFjUqYmj2XpE4MlYr_vLkG4oeMnvnHOqGC96nLGrPSOq-fpIddV7RSlYLDeNccBs0G2oUOy-6jIMujzTuEGk4nnxme5rpxs7rYjwcvVsqM5dlkpJthY42e4B3lSPH7ezx187WRSk--m4ZiWmAI52mMOiygzTSWoNormlvfvdM',
|
it.difficultyLabel || it.difficulty || '',
|
||||||
},
|
),
|
||||||
{
|
path: String(it.path || ''),
|
||||||
title: '拼音描红',
|
worksheetId: String(it.worksheetId || ''),
|
||||||
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuBYS4O7PyflR5q8M6oXKwj_8QHzex3OL3YvdHBncutm7a_bjlZj83-8UBpGUQvJO20V0BnAdU3iKEoLlUqwpS60CiPhVyxYaHEAqv52P5YUhJdjf0hpLir1Li4rdI_-eqGkPTasYl-5kD2r8-RjUokKg_HiW5_TF72UbxFXHQh4BefYJZfl0Oc9-aTQvkbFL0C3tagi9Qv1jRsgQldMFU4M7cHt8KkxhO1QDF7-R85ZFORJM0RiqV2l-jn0c-UscKtuWtroiX6UUKk',
|
}))
|
||||||
},
|
.filter((it) => it.title);
|
||||||
{
|
} catch (e) {
|
||||||
title: '趣味找不同',
|
console.warn('[age] fetchAgeRecommended failed:', e);
|
||||||
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuBSafbTyucs8JLpTgibRWwAJsHukrklTAuEnaAzax_Kr1RNCwHjqpQjta0Q-h47ClufzEmaHPgCIOlm_a2lL4gNlgBDyraCwzRU7aWCf_Fj_dM1cLGzROJmtqrdHKjQzTbCp0_Da8BvXi4a4H2X4RAYVPnLbZVmbgPXrTvpKgEI605kIMmr3n0--WojJQRsF4B_xCho80gABbIaMTHJHrs9xdEY6fZKg8ArQx7lRbJ6_EsLLB_C5a5crZ_begwruXKr3pmdU6KneC8',
|
return [];
|
||||||
},
|
}
|
||||||
{
|
}
|
||||||
title: '走迷宫大挑战',
|
|
||||||
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuAOZHI1Dz6ax4iFUb3QYL-EWp2euA_3kyUeCcu6V57BBCzarMwKnFzcpJxJn4X4woklWFB08Ii1w6zsk0H5eDaPdgUYmsvY0NFqvAhKN_IbZGZjZnclZlBHkLRcX64Nl767L2pg01FQu9Tp6nq2p3lAuYeEQ2fiIdXh4j-QHyikbmx4_aDlx5X5fC0fqMMxWykhwLNkDzl2Rs3ucXj7zYnUP-dNNY-aj_JvqkY4pqoXQDt9ZJcfSeL0dWes7nvD5U2C_JFhrk1bVkw',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '数字点连线',
|
|
||||||
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuD-3lADhclTGODN9WtNh5v8bQPLysbllbKNkH40bAmVyE5d54PwfTBOiX9nER1hpE4rzHAfsmM_JhVnsE4EbdA914rYIb3S3e4r9QKFjUqYmj2XpE4MlYr_vLkG4oeMnvnHOqGC96nLGrPSOq-fpIddV7RSlYLDeNccBs0G2oUOy-6jIMujzTuEGk4nnxme5rpxs7rYjwcvVsqM5dlkpJthY42e4B3lSPH7ezx187WRSk--m4ZiWmAI52mMOiygzTSWoNormlvfvdM',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '方格推理高手',
|
|
||||||
image: 'https://lh3.googleusercontent.com/aida-public/AB6AXuBYS4O7PyflR5q8M6oXKwj_8QHzex3OL3YvdHBncutm7a_bjlZj83-8UBpGUQvJO20V0BnAdU3iKEoLlUqwpS60CiPhVyxYaHEAqv52P5YUhJdjf0hpLir1Li4rdI_-eqGkPTasYl-5kD2r8-RjUokKg_HiW5_TF72UbxFXHQh4BefYJZfl0Oc9-aTQvkbFL0C3tagi9Qv1jRsgQldMFU4M7cHt8KkxhO1QDF7-R85ZFORJM0RiqV2l-jn0c-UscKtuWtroiX6UUKk',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
/** 各年龄段能力目标(Mock) */
|
|
||||||
const MOCK_ABILITY: Record<AgeBandKey, AbilityItem[]> = {
|
|
||||||
'3-4': [
|
|
||||||
{
|
|
||||||
icon: '🔢',
|
|
||||||
title: '数感',
|
|
||||||
desc: '认读 1-5、点数对应',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: '✏️',
|
|
||||||
title: '书写',
|
|
||||||
desc: '涂鸦线条、简单描红',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: '🧩',
|
|
||||||
title: '思维',
|
|
||||||
desc: '找相同、简单配对',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'4-5': [
|
|
||||||
{
|
|
||||||
icon: '🔢',
|
|
||||||
title: '数感',
|
|
||||||
desc: '10 以内数数、比大小',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: '✏️',
|
|
||||||
title: '书写',
|
|
||||||
desc: '笔画模仿、图形描边',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: '🧩',
|
|
||||||
title: '思维',
|
|
||||||
desc: '规律排序、图形分类',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'5-6': [
|
|
||||||
{
|
|
||||||
icon: '🔢',
|
|
||||||
title: '数感',
|
|
||||||
desc: '10以内加减法、凑十法',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: '✏️',
|
|
||||||
title: '书写',
|
|
||||||
desc: '练字帖、拼音入门',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: '🧩',
|
|
||||||
title: '思维',
|
|
||||||
desc: '方格推理、格子仿画 5×5',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'6-7': [
|
|
||||||
{
|
|
||||||
icon: '🔢',
|
|
||||||
title: '数感',
|
|
||||||
desc: '20 以内运算、应用题启蒙',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: '✏️',
|
|
||||||
title: '书写',
|
|
||||||
desc: '常用字书写、词语积累',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: '🧩',
|
|
||||||
title: '思维',
|
|
||||||
desc: '逻辑填空、空间想象',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'7-8': [
|
|
||||||
{
|
|
||||||
icon: '🔢',
|
|
||||||
title: '数感',
|
|
||||||
desc: '乘除启蒙、巧算练习',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: '✏️',
|
|
||||||
title: '书写',
|
|
||||||
desc: '段落抄写、古诗诵读',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: '🧩',
|
|
||||||
title: '思维',
|
|
||||||
desc: '数独入门、综合推理',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 4 周路线(各年龄段共用结构,内容 Mock) */
|
|
||||||
const MOCK_WEEKS: WeekPlan[] = [
|
|
||||||
{
|
|
||||||
week: 1,
|
|
||||||
theme: '数感启蒙',
|
|
||||||
exercises: [
|
|
||||||
{ title: '找数字涂一涂' },
|
|
||||||
{ title: '5 以内加法' },
|
|
||||||
{ title: '数数连一连' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
week: 2,
|
|
||||||
theme: '形状与连线',
|
|
||||||
exercises: [
|
|
||||||
{ title: '识别形状' },
|
|
||||||
{ title: '线条识别' },
|
|
||||||
{ title: '数字点连线' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
week: 3,
|
|
||||||
theme: '趣味专注',
|
|
||||||
exercises: [
|
|
||||||
{ title: '颜色找规律' },
|
|
||||||
{ title: '方格推理' },
|
|
||||||
{ title: '格子仿画' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
week: 4,
|
|
||||||
theme: '综合练习',
|
|
||||||
exercises: [
|
|
||||||
{ title: '10以内加减法' },
|
|
||||||
{ title: '识字卡' },
|
|
||||||
{ title: '连连看' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const DEFAULT_AGE: AgeBandKey = '5-6';
|
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
data: {
|
data: {
|
||||||
@@ -190,24 +104,34 @@ Page({
|
|||||||
rangeText: b.label.replace(/\s*岁\s*$/, ''),
|
rangeText: b.label.replace(/\s*岁\s*$/, ''),
|
||||||
subLabel: AGE_TAB_SUB[b.key],
|
subLabel: AGE_TAB_SUB[b.key],
|
||||||
})),
|
})),
|
||||||
activeAgeKey: DEFAULT_AGE,
|
activeAgeKey: DEFAULT_AGE as AgeBandKey,
|
||||||
goalTitle: '',
|
goalTitle: '',
|
||||||
abilityItems: [] as AbilityItem[],
|
abilityItems: [] as AbilityItem[],
|
||||||
weekPlans: MOCK_WEEKS,
|
weekPlans: [] as WeekPlan[],
|
||||||
recommendedItems: RECOMMENDED_FROM_STITCH,
|
recommendedItems: RECOMMENDED_FALLBACK,
|
||||||
|
recommendedLoading: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
onLoad() {
|
onLoad() {
|
||||||
this.applyAge(DEFAULT_AGE);
|
this.applyAge(DEFAULT_AGE);
|
||||||
},
|
},
|
||||||
|
|
||||||
applyAge(key: AgeBandKey) {
|
async applyAge(key: AgeBandKey) {
|
||||||
const band = AGE_BANDS.find((b) => b.key === key);
|
const band = AGE_BANDS.find((b) => b.key === key);
|
||||||
const goalTitle = band ? `${band.label}能力目标` : '能力目标';
|
const goalTitle = band ? `${band.label}能力目标` : '能力目标';
|
||||||
this.setData({
|
this.setData({
|
||||||
activeAgeKey: key,
|
activeAgeKey: key,
|
||||||
goalTitle,
|
goalTitle,
|
||||||
abilityItems: MOCK_ABILITY[key],
|
abilityItems: AGE_ABILITIES[key] || [],
|
||||||
|
weekPlans: AGE_WEEK_PLANS[key] || [],
|
||||||
|
recommendedLoading: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const recommended = await fetchAgeRecommended(key);
|
||||||
|
this.setData({
|
||||||
|
recommendedItems:
|
||||||
|
recommended.length > 0 ? recommended : RECOMMENDED_FALLBACK,
|
||||||
|
recommendedLoading: false,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -218,18 +142,23 @@ Page({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onPracticeTap(e: WechatMiniprogram.TouchEvent) {
|
onPracticeTap(e: WechatMiniprogram.TouchEvent) {
|
||||||
|
const path = e.currentTarget.dataset.path as string | undefined;
|
||||||
const title = e.currentTarget.dataset.title as string | undefined;
|
const title = e.currentTarget.dataset.title as string | undefined;
|
||||||
if (!title) return;
|
navigateByPath(path, title);
|
||||||
wx.showToast({ title: `即将前往:${title}`, icon: 'none' });
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onRecommendMore() {
|
onRecommendMore() {
|
||||||
wx.switchTab({ url: '/pages/home/home' });
|
wx.switchTab({ url: '/pages/home/home' });
|
||||||
},
|
},
|
||||||
|
|
||||||
onRecommendTap(e: WechatMiniprogram.TouchEvent) {
|
onTapCard(e: WechatMiniprogram.CustomEvent) {
|
||||||
const title = e.currentTarget.dataset.title as string | undefined;
|
const detail = (e.detail || {}) as { title?: string; path?: string };
|
||||||
if (!title) return;
|
const path =
|
||||||
wx.showToast({ title: `去看看:${title}`, icon: 'none' });
|
detail.path ||
|
||||||
|
(e.currentTarget.dataset.path as string | undefined);
|
||||||
|
const title =
|
||||||
|
detail.title ||
|
||||||
|
(e.currentTarget.dataset.title as string | undefined);
|
||||||
|
navigateByPath(path, title);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -59,12 +59,22 @@
|
|||||||
<view
|
<view
|
||||||
wx:for="{{item.exercises}}"
|
wx:for="{{item.exercises}}"
|
||||||
wx:for-item="ex"
|
wx:for-item="ex"
|
||||||
wx:key="title"
|
wx:key="_id"
|
||||||
class="age-week-row">
|
class="age-week-row">
|
||||||
<text class="age-week-row__title">{{ex.title}}</text>
|
<view class="age-week-row__meta">
|
||||||
|
<text class="age-week-row__title"
|
||||||
|
>{{ex.title}}</text
|
||||||
|
>
|
||||||
|
<text
|
||||||
|
wx:if="{{ex.subtitle}}"
|
||||||
|
class="age-week-row__subtitle"
|
||||||
|
>{{ex.subtitle}}</text
|
||||||
|
>
|
||||||
|
</view>
|
||||||
<view
|
<view
|
||||||
class="age-week-row__btn"
|
class="age-week-row__btn"
|
||||||
data-title="{{ex.title}}"
|
data-title="{{ex.title}}"
|
||||||
|
data-path="{{ex.path}}"
|
||||||
catchtap="onPracticeTap">
|
catchtap="onPracticeTap">
|
||||||
<text>去练习</text>
|
<text>去练习</text>
|
||||||
<text
|
<text
|
||||||
@@ -78,7 +88,14 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="age-rec">
|
<view wx:if="{{recommendedLoading}}" class="age-rec-loading">
|
||||||
|
<text
|
||||||
|
class="toy-icon toy-icon-loading age-rec-loading__icon"
|
||||||
|
aria-hidden="true"></text>
|
||||||
|
<text class="age-rec-loading__text">推荐加载中...</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view wx:else class="age-rec">
|
||||||
<view class="age-rec__head">
|
<view class="age-rec__head">
|
||||||
<text class="age-rec__title">为你推荐</text>
|
<text class="age-rec__title">为你推荐</text>
|
||||||
<text class="age-rec__more" bindtap="onRecommendMore"
|
<text class="age-rec__more" bindtap="onRecommendMore"
|
||||||
@@ -86,20 +103,21 @@
|
|||||||
>
|
>
|
||||||
</view>
|
</view>
|
||||||
<view class="age-rec__grid">
|
<view class="age-rec__grid">
|
||||||
<view
|
<worksheet-card
|
||||||
wx:for="{{recommendedItems}}"
|
wx:for="{{recommendedItems}}"
|
||||||
wx:key="title"
|
wx:key="title"
|
||||||
class="age-rec__card"
|
class="age-rec__card"
|
||||||
|
variant="track"
|
||||||
|
title="{{item.title}}"
|
||||||
|
preview-img="{{item.image}}"
|
||||||
|
subtitle="{{item.subtitle}}"
|
||||||
|
age-band="{{item.ageBand}}"
|
||||||
|
difficulty="{{item.difficulty}}"
|
||||||
|
path="{{item.path}}"
|
||||||
data-title="{{item.title}}"
|
data-title="{{item.title}}"
|
||||||
bindtap="onRecommendTap">
|
data-path="{{item.path}}"
|
||||||
<image
|
bindtap="onTapCard"
|
||||||
class="age-rec__img"
|
/>
|
||||||
src="{{item.image}}"
|
|
||||||
mode="aspectFill" />
|
|
||||||
<view class="age-rec__body">
|
|
||||||
<text class="age-rec__card-title">{{item.title}}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"usingComponents": {
|
"usingComponents": {
|
||||||
"nav-bar": "/components3.0/nav-bar/nav-bar",
|
"nav-bar": "/components3.0/nav-bar/nav-bar",
|
||||||
"category-tabs": "/components3.0/category-tabs/category-tabs",
|
"category-tabs": "/components3.0/category-tabs/category-tabs",
|
||||||
|
"worksheet-card": "/components3.0/worksheet-card/worksheet-card",
|
||||||
"van-icon": "@vant/weapp/icon/index",
|
"van-icon": "@vant/weapp/icon/index",
|
||||||
"toy-icon": "../../toy/icon/icon"
|
"toy-icon": "../../toy/icon/icon"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -225,79 +225,6 @@
|
|||||||
.home-track-card {
|
.home-track-card {
|
||||||
width: calc((750rpx - 2 * @page-padding-x - 2 * @home-track-gap) / 2.2);
|
width: calc((750rpx - 2 * @page-padding-x - 2 * @home-track-gap) / 2.2);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
border-radius: 24rpx;
|
|
||||||
overflow: hidden;
|
|
||||||
background: #ffffff;
|
|
||||||
box-shadow: 0 6rpx 20rpx rgba(50, 46, 37, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-track-card__top {
|
|
||||||
/* 与 supportPages/mathIndex .function-item-img-wrapper 一致 */
|
|
||||||
height: 270rpx;
|
|
||||||
flex-shrink: 0;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: center;
|
|
||||||
border-bottom: 1rpx solid rgba(50, 46, 37, 0.08);
|
|
||||||
background: #f5edd8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-track-card__cover {
|
|
||||||
width: 100%;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-track-card__icon {
|
|
||||||
font-size: 72rpx;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-track-card__tags {
|
|
||||||
position: absolute;
|
|
||||||
right: 12rpx;
|
|
||||||
bottom: 12rpx;
|
|
||||||
display: flex;
|
|
||||||
gap: 10rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-track-card__body {
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 100%;
|
|
||||||
padding: 20rpx @page-padding-inner-x 22rpx;
|
|
||||||
background: #ffffff;
|
|
||||||
white-space: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-track-card__title {
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 100%;
|
|
||||||
font-size: 28rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
color: @text-title;
|
|
||||||
line-height: 1.35;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
line-clamp: 2;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
overflow: hidden;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.home-track-card__sub {
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 100%;
|
|
||||||
margin-top: 8rpx;
|
|
||||||
font-size: 20rpx;
|
|
||||||
color: @text-gray;
|
|
||||||
line-height: 1.45;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
line-clamp: 2;
|
|
||||||
overflow: hidden;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-age-scroll {
|
.home-age-scroll {
|
||||||
|
|||||||
@@ -254,9 +254,14 @@ Page({
|
|||||||
navigateByPath(path, title);
|
navigateByPath(path, title);
|
||||||
},
|
},
|
||||||
|
|
||||||
onTapTrackCard(e: WechatMiniprogram.TouchEvent) {
|
onTapCard(e: WechatMiniprogram.CustomEvent) {
|
||||||
const path = e.currentTarget.dataset.path as string | undefined;
|
const detail = (e.detail || {}) as { title?: string; path?: string };
|
||||||
const title = e.currentTarget.dataset.title as string | undefined;
|
const path =
|
||||||
|
detail.path ||
|
||||||
|
(e.currentTarget.dataset.path as string | undefined);
|
||||||
|
const title =
|
||||||
|
detail.title ||
|
||||||
|
(e.currentTarget.dataset.title as string | undefined);
|
||||||
navigateByPath(path, title);
|
navigateByPath(path, title);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -186,40 +186,22 @@
|
|||||||
<!-- <text class="home-section-panel__sub">{{item.subtitle}}</text> -->
|
<!-- <text class="home-section-panel__sub">{{item.subtitle}}</text> -->
|
||||||
<scroll-view class="home-track-scroll" scroll-x enable-flex>
|
<scroll-view class="home-track-scroll" scroll-x enable-flex>
|
||||||
<view class="home-track-row">
|
<view class="home-track-row">
|
||||||
<view
|
<worksheet-card
|
||||||
wx:for="{{item.items}}"
|
wx:for="{{item.items}}"
|
||||||
wx:key="id"
|
wx:key="id"
|
||||||
class="home-track-card"
|
class="home-track-card"
|
||||||
|
variant="track"
|
||||||
|
title="{{item.title}}"
|
||||||
|
subtitle="{{item.subtitle}}"
|
||||||
|
preview-img="{{item.previewImg}}"
|
||||||
|
icon="{{item.icon}}"
|
||||||
|
age-band="{{item.ageBand}}"
|
||||||
|
difficulty="{{item.difficulty}}"
|
||||||
|
path="{{item.path}}"
|
||||||
data-title="{{item.title}}"
|
data-title="{{item.title}}"
|
||||||
data-path="{{item.path}}"
|
data-path="{{item.path}}"
|
||||||
bindtap="onTapTrackCard">
|
bindtap="onTapCard"
|
||||||
<view class="home-track-card__top">
|
/>
|
||||||
<image
|
|
||||||
wx:if="{{item.previewImg}}"
|
|
||||||
class="home-track-card__cover"
|
|
||||||
src="{{item.previewImg}}"
|
|
||||||
mode="widthFix" />
|
|
||||||
<text wx:else class="home-track-card__icon"
|
|
||||||
>{{item.icon}}</text
|
|
||||||
>
|
|
||||||
<view class="home-track-card__tags">
|
|
||||||
<text class="tag tag--age"
|
|
||||||
>{{item.ageBand}}</text
|
|
||||||
>
|
|
||||||
<text class="tag tag--diff"
|
|
||||||
>{{item.difficulty}}</text
|
|
||||||
>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view class="home-track-card__body">
|
|
||||||
<text class="home-track-card__title"
|
|
||||||
>{{item.title}}</text
|
|
||||||
>
|
|
||||||
<text class="home-track-card__sub"
|
|
||||||
>{{item.subtitle}}</text
|
|
||||||
>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -14,6 +14,13 @@ const DEBUG_ENTRIES: DebugEntry[] = [
|
|||||||
icon: '🗂️',
|
icon: '🗂️',
|
||||||
path: '/supportPages/categoryManage/categoryManage',
|
path: '/supportPages/categoryManage/categoryManage',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'worksheet-sync',
|
||||||
|
title: 'Worksheet 同步',
|
||||||
|
subtitle: '按本地配置批量同步年龄/标签,并重建 home+category 配置。',
|
||||||
|
icon: '🔄',
|
||||||
|
path: '/supportPages/worksheetSync/worksheetSync',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'category-content',
|
id: 'category-content',
|
||||||
title: '分类页内容管理',
|
title: '分类页内容管理',
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"navigationBarTitleText": "Worksheet 同步",
|
||||||
|
"navigationBarTextStyle": "black",
|
||||||
|
"navigationBarBackgroundColor": "#F8F0E0",
|
||||||
|
"backgroundColor": "#F8F0E0",
|
||||||
|
"enablePullDownRefresh": true,
|
||||||
|
"usingComponents": {
|
||||||
|
"toy-button": "/toy/button-v2/button"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
@import '../../style/theme.less';
|
||||||
|
|
||||||
|
page {
|
||||||
|
min-height: 100%;
|
||||||
|
background: @bg-header;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-sync-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 24rpx;
|
||||||
|
padding-bottom: 320rpx;
|
||||||
|
background: @bg-header;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-sync-summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 28rpx;
|
||||||
|
background: @bg-white;
|
||||||
|
border-radius: @radius-xl;
|
||||||
|
box-shadow: @shadow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-sync-summary__info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-sync-summary__title {
|
||||||
|
display: block;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: @text-title;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-sync-summary__desc {
|
||||||
|
display: block;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: @text-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-sync-summary__badge {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 20rpx;
|
||||||
|
padding: 6rpx 16rpx;
|
||||||
|
border-radius: 999rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #605b50;
|
||||||
|
background: fade(#8a8478, 15%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-sync-summary__badge--ready {
|
||||||
|
color: #4a7a12;
|
||||||
|
background: fade(#93d333, 18%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-sync-page__stats {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-sync-stat {
|
||||||
|
flex: 1;
|
||||||
|
padding: 24rpx 16rpx;
|
||||||
|
background: @bg-white;
|
||||||
|
border-radius: @radius-xl;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: @shadow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-sync-stat + .ws-sync-stat {
|
||||||
|
margin-left: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-sync-stat__num {
|
||||||
|
display: block;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
color: @text-title;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-sync-stat__label {
|
||||||
|
display: block;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: @text-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-sync-page__notice {
|
||||||
|
margin-top: 24rpx;
|
||||||
|
padding: 24rpx 28rpx;
|
||||||
|
background: @bg-white;
|
||||||
|
border-radius: @radius;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: @text-secondary;
|
||||||
|
box-shadow: @shadow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-sync-page__notice--error {
|
||||||
|
color: #8a6d00;
|
||||||
|
background: fade(#ffb703, 18%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-sync-result {
|
||||||
|
margin-top: 24rpx;
|
||||||
|
padding: 28rpx;
|
||||||
|
background: @bg-white;
|
||||||
|
border-radius: @radius-xl;
|
||||||
|
box-shadow: @shadow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-sync-result__title {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: @text-title;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-sync-result__body {
|
||||||
|
display: block;
|
||||||
|
margin-top: 12rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: @text-secondary;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-sync-page__build {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16rpx;
|
||||||
|
padding: 24rpx 40rpx;
|
||||||
|
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
|
||||||
|
background: @bg-header;
|
||||||
|
box-shadow: 0 -4rpx 16rpx rgba(50, 46, 37, 0.06);
|
||||||
|
}
|
||||||
@@ -0,0 +1,315 @@
|
|||||||
|
type CloudFunctionResult<T> = {
|
||||||
|
success?: boolean;
|
||||||
|
message?: string;
|
||||||
|
data?: T;
|
||||||
|
dryRun?: boolean;
|
||||||
|
total?: number;
|
||||||
|
failed?: number;
|
||||||
|
results?: Array<{ id: string | null; ok: boolean; error?: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function callCloudFunction<T>(
|
||||||
|
name: string,
|
||||||
|
data?: Record<string, unknown>,
|
||||||
|
): Promise<CloudFunctionResult<T>> {
|
||||||
|
const response = (await wx.cloud.callFunction({
|
||||||
|
name,
|
||||||
|
data: data || {},
|
||||||
|
})) as { result?: CloudFunctionResult<T> };
|
||||||
|
return response.result || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
type LocalDef = {
|
||||||
|
id: string;
|
||||||
|
ageMin: number;
|
||||||
|
ageMax: number;
|
||||||
|
tags: readonly string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfigModule = Record<string, readonly LocalDef[] | unknown>;
|
||||||
|
|
||||||
|
type ConfigSource = {
|
||||||
|
label: string;
|
||||||
|
modulePath: string;
|
||||||
|
exportName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CONFIG_SOURCES: ConfigSource[] = [
|
||||||
|
{
|
||||||
|
label: 'math',
|
||||||
|
modulePath: '../../mathPages/mathDraw/mathDraw.config',
|
||||||
|
exportName: 'MATH_WORKSHEET_DEFINITIONS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'focus',
|
||||||
|
modulePath: '../../focusPages/focusDraw/focusDraw.config',
|
||||||
|
exportName: 'FOCUS_WORKSHEET_DEFINITIONS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'letterTracing',
|
||||||
|
modulePath: '../../englishPages/letterTracing/letterTracing.config',
|
||||||
|
exportName: 'LETTER_TRACING_WORKSHEET_DEFINITIONS',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function isLocalDef(value: unknown): value is LocalDef {
|
||||||
|
const row = value as Partial<LocalDef>;
|
||||||
|
return (
|
||||||
|
!!row &&
|
||||||
|
typeof row.id === 'string' &&
|
||||||
|
typeof row.ageMin === 'number' &&
|
||||||
|
typeof row.ageMax === 'number' &&
|
||||||
|
Array.isArray(row.tags)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadConfigSource(source: ConfigSource): Promise<LocalDef[]> {
|
||||||
|
const mod = (await require.async(source.modulePath)) as ConfigModule;
|
||||||
|
const rows = mod[source.exportName];
|
||||||
|
|
||||||
|
if (!Array.isArray(rows) || !rows.every(isLocalDef)) {
|
||||||
|
throw new Error(`${source.label} 配置格式不正确`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...rows];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function collectAll(): Promise<LocalDef[]> {
|
||||||
|
const groups = await Promise.all(CONFIG_SOURCES.map(loadConfigSource));
|
||||||
|
return groups.flat();
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertUniqueIds(rows: LocalDef[]) {
|
||||||
|
const set = new Set<string>();
|
||||||
|
for (const r of rows) {
|
||||||
|
if (set.has(r.id)) {
|
||||||
|
throw new Error(`本地配置存在重复 id:${r.id}`);
|
||||||
|
}
|
||||||
|
set.add(r.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarizeResult(
|
||||||
|
title: string,
|
||||||
|
result: CloudFunctionResult<unknown>,
|
||||||
|
): string {
|
||||||
|
if (!result.success) {
|
||||||
|
return `${title}失败:${result.message || '未知错误'}`;
|
||||||
|
}
|
||||||
|
const total = result.total ?? 0;
|
||||||
|
const failed = result.failed ?? 0;
|
||||||
|
const ok = total - failed;
|
||||||
|
const firstError = result.results?.find((r) => !r.ok)?.error;
|
||||||
|
return `${title}完成:成功 ${ok}/${total}${
|
||||||
|
failed ? `,失败 ${failed}(如:${firstError || '未知原因'})` : ''
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
Page({
|
||||||
|
data: {
|
||||||
|
configLoading: true,
|
||||||
|
configReady: false,
|
||||||
|
configError: '',
|
||||||
|
configTotal: 0,
|
||||||
|
configSourceCount: CONFIG_SOURCES.length,
|
||||||
|
loadingAges: false,
|
||||||
|
loadingTags: false,
|
||||||
|
loadingRebuild: false,
|
||||||
|
statusText: '配置加载中,请稍候',
|
||||||
|
},
|
||||||
|
|
||||||
|
onLoad() {
|
||||||
|
void this.loadConfigs();
|
||||||
|
},
|
||||||
|
|
||||||
|
async onPullDownRefresh() {
|
||||||
|
await this.loadConfigs();
|
||||||
|
wx.stopPullDownRefresh();
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadConfigs() {
|
||||||
|
this.setData({
|
||||||
|
configLoading: true,
|
||||||
|
configReady: false,
|
||||||
|
configError: '',
|
||||||
|
statusText: '配置加载中,请稍候',
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rows = await collectAll();
|
||||||
|
assertUniqueIds(rows);
|
||||||
|
this.setData({
|
||||||
|
configLoading: false,
|
||||||
|
configReady: true,
|
||||||
|
configTotal: rows.length,
|
||||||
|
statusText: `配置已加载:${rows.length} 个 worksheet,可执行同步`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const msg = error instanceof Error ? error.message : '配置加载失败';
|
||||||
|
this.setData({
|
||||||
|
configLoading: false,
|
||||||
|
configReady: false,
|
||||||
|
configError: msg,
|
||||||
|
statusText: msg,
|
||||||
|
});
|
||||||
|
wx.showToast({ title: msg, icon: 'none' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async sync(mode: 'ages' | 'tags') {
|
||||||
|
if (!this.data.configReady) {
|
||||||
|
wx.showToast({ title: '配置未加载完成', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = await collectAll();
|
||||||
|
assertUniqueIds(rows);
|
||||||
|
|
||||||
|
const patches =
|
||||||
|
mode === 'ages'
|
||||||
|
? rows.map((r) => ({
|
||||||
|
id: r.id,
|
||||||
|
ageMin: r.ageMin,
|
||||||
|
ageMax: r.ageMax,
|
||||||
|
grade: inferGradeFromAge(r.ageMin, r.ageMax),
|
||||||
|
}))
|
||||||
|
: rows.map((r) => ({
|
||||||
|
id: r.id,
|
||||||
|
tags: [...r.tags],
|
||||||
|
}));
|
||||||
|
|
||||||
|
wx.showLoading({ title: '同步中...' });
|
||||||
|
const startedAt = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const patchRes = await callCloudFunction('worksheetsBatchPatch', {
|
||||||
|
patches,
|
||||||
|
dryRun: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!patchRes.success) {
|
||||||
|
throw new Error(patchRes.message || '同步失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsed = Math.round((Date.now() - startedAt) / 1000);
|
||||||
|
|
||||||
|
const text = [
|
||||||
|
summarizeResult(
|
||||||
|
mode === 'ages'
|
||||||
|
? '同步年龄(worksheetsBatchPatch)'
|
||||||
|
: '同步标签(worksheetsBatchPatch)',
|
||||||
|
patchRes,
|
||||||
|
),
|
||||||
|
'page-config 未自动重建,如需刷新前台展示请点击“重建配置”。',
|
||||||
|
`耗时:${elapsed}s`,
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
this.setData({ statusText: text });
|
||||||
|
wx.showToast({ title: '已完成', icon: 'success' });
|
||||||
|
} catch (error) {
|
||||||
|
const msg = error instanceof Error ? error.message : '同步失败';
|
||||||
|
this.setData({ statusText: msg });
|
||||||
|
wx.showToast({ title: msg, icon: 'none' });
|
||||||
|
} finally {
|
||||||
|
wx.hideLoading();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async rebuildPageConfig() {
|
||||||
|
wx.showLoading({ title: '重建中...' });
|
||||||
|
const startedAt = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rebuildRes = await callCloudFunction('pageContentBuild', {
|
||||||
|
page: 'all',
|
||||||
|
});
|
||||||
|
const elapsed = Math.round((Date.now() - startedAt) / 1000);
|
||||||
|
|
||||||
|
const text = [
|
||||||
|
summarizeResult(
|
||||||
|
'重建 page-config(pageContentBuild: all)',
|
||||||
|
rebuildRes,
|
||||||
|
),
|
||||||
|
`耗时:${elapsed}s`,
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
this.setData({ statusText: text });
|
||||||
|
if (!rebuildRes.success) {
|
||||||
|
throw new Error(rebuildRes.message || '重建失败');
|
||||||
|
}
|
||||||
|
wx.showToast({ title: '已重建', icon: 'success' });
|
||||||
|
} catch (error) {
|
||||||
|
const msg = error instanceof Error ? error.message : '重建失败';
|
||||||
|
this.setData({ statusText: msg });
|
||||||
|
wx.showToast({ title: msg, icon: 'none' });
|
||||||
|
} finally {
|
||||||
|
wx.hideLoading();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async onSyncAges() {
|
||||||
|
if (
|
||||||
|
!this.data.configReady ||
|
||||||
|
this.data.loadingAges ||
|
||||||
|
this.data.loadingTags ||
|
||||||
|
this.data.loadingRebuild
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
this.setData({ loadingAges: true });
|
||||||
|
try {
|
||||||
|
await this.sync('ages');
|
||||||
|
} finally {
|
||||||
|
this.setData({ loadingAges: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async onSyncTags() {
|
||||||
|
if (
|
||||||
|
!this.data.configReady ||
|
||||||
|
this.data.loadingAges ||
|
||||||
|
this.data.loadingTags ||
|
||||||
|
this.data.loadingRebuild
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
this.setData({ loadingTags: true });
|
||||||
|
try {
|
||||||
|
await this.sync('tags');
|
||||||
|
} finally {
|
||||||
|
this.setData({ loadingTags: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async onRebuildConfig() {
|
||||||
|
if (
|
||||||
|
this.data.loadingAges ||
|
||||||
|
this.data.loadingTags ||
|
||||||
|
this.data.loadingRebuild
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
this.setData({ loadingRebuild: true });
|
||||||
|
try {
|
||||||
|
await this.rebuildPageConfig();
|
||||||
|
} finally {
|
||||||
|
this.setData({ loadingRebuild: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<view class="ws-sync-page">
|
||||||
|
<view class="ws-sync-summary">
|
||||||
|
<view class="ws-sync-summary__info">
|
||||||
|
<text class="ws-sync-summary__title">Worksheet 批量同步</text>
|
||||||
|
<text class="ws-sync-summary__desc">
|
||||||
|
同步本地配置到 worksheets,必要时单独重建 page-config 数据。
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="ws-sync-summary__badge {{configReady ? 'ws-sync-summary__badge--ready' : ''}}">
|
||||||
|
<text
|
||||||
|
>{{configReady ? '可同步' : configLoading ? '加载中' :
|
||||||
|
'需处理'}}</text
|
||||||
|
>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="ws-sync-page__stats">
|
||||||
|
<view class="ws-sync-stat">
|
||||||
|
<text class="ws-sync-stat__num">{{configTotal}}</text>
|
||||||
|
<text class="ws-sync-stat__label">配置项</text>
|
||||||
|
</view>
|
||||||
|
<view class="ws-sync-stat">
|
||||||
|
<text class="ws-sync-stat__num">{{configSourceCount}}</text>
|
||||||
|
<text class="ws-sync-stat__label">分包</text>
|
||||||
|
</view>
|
||||||
|
<view class="ws-sync-stat">
|
||||||
|
<text class="ws-sync-stat__num"
|
||||||
|
>{{configReady ? 'Ready' : '--'}}</text
|
||||||
|
>
|
||||||
|
<text class="ws-sync-stat__label">状态</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view wx:if="{{configLoading}}" class="ws-sync-page__notice">
|
||||||
|
正在异步加载 math / focus / letterTracing 配置...
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view
|
||||||
|
wx:elif="{{configError}}"
|
||||||
|
class="ws-sync-page__notice ws-sync-page__notice--error">
|
||||||
|
{{configError}}
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="ws-sync-result">
|
||||||
|
<text class="ws-sync-result__title">最近一次结果</text>
|
||||||
|
<text class="ws-sync-result__body">{{statusText}}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="ws-sync-page__build">
|
||||||
|
<toy-button
|
||||||
|
type="primary"
|
||||||
|
width="100%"
|
||||||
|
loading="{{loadingAges}}"
|
||||||
|
disabled="{{!configReady || loadingAges || loadingTags || loadingRebuild}}"
|
||||||
|
bindtap="onSyncAges">
|
||||||
|
{{loadingAges ? '同步中...' : '同步年龄'}}
|
||||||
|
</toy-button>
|
||||||
|
<toy-button
|
||||||
|
width="100%"
|
||||||
|
loading="{{loadingTags}}"
|
||||||
|
disabled="{{!configReady || loadingAges || loadingTags || loadingRebuild}}"
|
||||||
|
bindtap="onSyncTags">
|
||||||
|
{{loadingTags ? '同步中...' : '同步标签'}}
|
||||||
|
</toy-button>
|
||||||
|
<toy-button
|
||||||
|
width="100%"
|
||||||
|
loading="{{loadingRebuild}}"
|
||||||
|
disabled="{{loadingAges || loadingTags || loadingRebuild}}"
|
||||||
|
bindtap="onRebuildConfig">
|
||||||
|
{{loadingRebuild ? '重建中...' : '重建配置'}}
|
||||||
|
</toy-button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
import { checkAndSaveImage } from './saveImage';
|
import { checkAndSaveImage } from './saveImage';
|
||||||
import tracker from './tracker';
|
import tracker from './tracker';
|
||||||
|
import { incrementWorksheetDownloads } from './worksheetStats';
|
||||||
|
|
||||||
// 存储键名
|
// 存储键名
|
||||||
const STORAGE_KEY_DOWNLOAD_COUNT = 'downloadCount';
|
const STORAGE_KEY_DOWNLOAD_COUNT = 'downloadCount';
|
||||||
@@ -195,8 +196,9 @@ function doDownload(
|
|||||||
errorToast?: string;
|
errorToast?: string;
|
||||||
trackerName?: string;
|
trackerName?: string;
|
||||||
trackerMode?: string;
|
trackerMode?: string;
|
||||||
|
worksheetId?: string;
|
||||||
},
|
},
|
||||||
) {
|
): Promise<boolean> {
|
||||||
// 上报下载埋点
|
// 上报下载埋点
|
||||||
if (options.trackerName) {
|
if (options.trackerName) {
|
||||||
if (options.trackerMode) {
|
if (options.trackerMode) {
|
||||||
@@ -206,11 +208,15 @@ function doDownload(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 增加下载次数
|
return checkAndSaveImage(canvas).then((saved) => {
|
||||||
|
if (saved) {
|
||||||
incrementDownloadCount();
|
incrementDownloadCount();
|
||||||
|
if (options.worksheetId) {
|
||||||
// 执行下载(checkAndSaveImage 内部已有成功提示,这里不需要额外处理)
|
incrementWorksheetDownloads(options.worksheetId);
|
||||||
checkAndSaveImage(canvas);
|
}
|
||||||
|
}
|
||||||
|
return saved;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -221,6 +227,7 @@ function doDownload(
|
|||||||
* @param options.errorToast 错误提示(可选,默认:'请先生成内容')
|
* @param options.errorToast 错误提示(可选,默认:'请先生成内容')
|
||||||
* @param options.trackerName 埋点名称(可选)
|
* @param options.trackerName 埋点名称(可选)
|
||||||
* @param options.trackerMode 埋点模式(可选)
|
* @param options.trackerMode 埋点模式(可选)
|
||||||
|
* @param options.worksheetId worksheet ID(可选,保存成功后更新 downloads)
|
||||||
*/
|
*/
|
||||||
export async function downloadPrint(
|
export async function downloadPrint(
|
||||||
canvas: Canvas | null,
|
canvas: Canvas | null,
|
||||||
@@ -229,20 +236,20 @@ export async function downloadPrint(
|
|||||||
errorToast?: string;
|
errorToast?: string;
|
||||||
trackerName?: string;
|
trackerName?: string;
|
||||||
trackerMode?: string;
|
trackerMode?: string;
|
||||||
|
worksheetId?: string;
|
||||||
} = {},
|
} = {},
|
||||||
): Promise<void> {
|
): Promise<boolean> {
|
||||||
// 检查 canvas 是否存在
|
// 检查 canvas 是否存在
|
||||||
if (!canvas) {
|
if (!canvas) {
|
||||||
wx.showToast({
|
wx.showToast({
|
||||||
title: options.errorToast || '请先生成内容',
|
title: options.errorToast || '请先生成内容',
|
||||||
icon: 'none',
|
icon: 'none',
|
||||||
});
|
});
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDevBypassLimits()) {
|
if (isDevBypassLimits()) {
|
||||||
doDownload(canvas, options);
|
return doDownload(canvas, options);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取今日下载次数
|
// 获取今日下载次数
|
||||||
@@ -251,21 +258,19 @@ export async function downloadPrint(
|
|||||||
|
|
||||||
// 如果已观看广告,直接下载
|
// 如果已观看广告,直接下载
|
||||||
if (hasWatchedAd) {
|
if (hasWatchedAd) {
|
||||||
doDownload(canvas, options);
|
return doDownload(canvas, options);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果未超过免费次数,直接下载
|
// 如果未超过免费次数,直接下载
|
||||||
if (downloadCount < FREE_DOWNLOAD_LIMIT) {
|
if (downloadCount < FREE_DOWNLOAD_LIMIT) {
|
||||||
doDownload(canvas, options);
|
return doDownload(canvas, options);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 超过免费次数,需要观看广告
|
// 超过免费次数,需要观看广告
|
||||||
const watched = await showVideoAd();
|
const watched = await showVideoAd();
|
||||||
if (watched) {
|
if (watched) {
|
||||||
// 观看完成,可以下载
|
// 观看完成,可以下载
|
||||||
doDownload(canvas, options);
|
return doDownload(canvas, options);
|
||||||
}
|
}
|
||||||
/* else {
|
/* else {
|
||||||
// 未观看完成,提示用户
|
// 未观看完成,提示用户
|
||||||
@@ -275,4 +280,5 @@ export async function downloadPrint(
|
|||||||
duration: 2000,
|
duration: 2000,
|
||||||
});
|
});
|
||||||
} */
|
} */
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { A4_EXPORT_SIZE_150DPI } from '../constants/colors';
|
|||||||
/**
|
/**
|
||||||
* 导出为固定打印分辨率(A4 150 DPI),避免高 DPR 设备导出过大图导致打印慢、文件大
|
* 导出为固定打印分辨率(A4 150 DPI),避免高 DPR 设备导出过大图导致打印慢、文件大
|
||||||
*/
|
*/
|
||||||
const doSaveImage = (canvas: Canvas) => {
|
const doSaveImage = (canvas: Canvas): Promise<boolean> =>
|
||||||
|
new Promise((resolve) => {
|
||||||
const { width: destWidth, height: destHeight } = A4_EXPORT_SIZE_150DPI;
|
const { width: destWidth, height: destHeight } = A4_EXPORT_SIZE_150DPI;
|
||||||
wx.canvasToTempFilePath({
|
wx.canvasToTempFilePath({
|
||||||
canvas,
|
canvas,
|
||||||
@@ -15,18 +16,21 @@ const doSaveImage = (canvas: Canvas) => {
|
|||||||
wx.saveImageToPhotosAlbum({
|
wx.saveImageToPhotosAlbum({
|
||||||
filePath: res.tempFilePath,
|
filePath: res.tempFilePath,
|
||||||
success: () => {
|
success: () => {
|
||||||
// 下载成功提示(下载次数记录在 downloadPrint.ts 中处理)
|
|
||||||
wx.showToast({
|
wx.showToast({
|
||||||
title: '保存成功,请在相册中查看',
|
title: '保存成功,请在相册中查看',
|
||||||
icon: 'success',
|
icon: 'success',
|
||||||
duration: 2000,
|
duration: 2000,
|
||||||
});
|
});
|
||||||
|
resolve(true);
|
||||||
|
},
|
||||||
|
fail: () => {
|
||||||
|
wx.showToast({
|
||||||
|
title: '保存涂色卡失败,请重试!',
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
resolve(false);
|
||||||
},
|
},
|
||||||
// fail: () => wx.showToast({
|
|
||||||
// title: '保存涂色卡失败,请重试!',
|
|
||||||
// icon: 'none',
|
|
||||||
// duration: 2000,
|
|
||||||
// })
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
fail: () => {
|
fail: () => {
|
||||||
@@ -35,31 +39,39 @@ const doSaveImage = (canvas: Canvas) => {
|
|||||||
icon: 'none',
|
icon: 'none',
|
||||||
duration: 2000,
|
duration: 2000,
|
||||||
});
|
});
|
||||||
|
resolve(false);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
});
|
||||||
|
|
||||||
const requestPhotoPermission = (canvas: Canvas) => {
|
const requestPhotoPermission = (canvas: Canvas): Promise<boolean> =>
|
||||||
|
new Promise((resolve) => {
|
||||||
wx.authorize({
|
wx.authorize({
|
||||||
scope: 'scope.writePhotosAlbum',
|
scope: 'scope.writePhotosAlbum',
|
||||||
success: () => doSaveImage(canvas),
|
success: () => {
|
||||||
fail: () =>
|
doSaveImage(canvas).then(resolve);
|
||||||
|
},
|
||||||
|
fail: () => {
|
||||||
wx.showToast({
|
wx.showToast({
|
||||||
title: '请在“设置-添加到相册”中开启相册权限以保存涂色卡',
|
title: '请在“设置-添加到相册”中开启相册权限以保存涂色卡',
|
||||||
icon: 'none',
|
icon: 'none',
|
||||||
duration: 3000,
|
duration: 3000,
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
};
|
resolve(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
export const checkAndSaveImage = (canvas: Canvas) => {
|
export const checkAndSaveImage = (canvas: Canvas): Promise<boolean> =>
|
||||||
|
new Promise((resolve) => {
|
||||||
wx.getSetting({
|
wx.getSetting({
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
if (!res.authSetting['scope.writePhotosAlbum']) {
|
if (!res.authSetting['scope.writePhotosAlbum']) {
|
||||||
requestPhotoPermission(canvas);
|
requestPhotoPermission(canvas).then(resolve);
|
||||||
} else {
|
} else {
|
||||||
doSaveImage(canvas);
|
doSaveImage(canvas).then(resolve);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
fail: () => resolve(false),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
type WorksheetStatsField = 'likes' | 'downloads';
|
||||||
|
|
||||||
|
export async function updateWorksheetStats(
|
||||||
|
id: string,
|
||||||
|
field: WorksheetStatsField,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const worksheetId = String(id || '').trim();
|
||||||
|
if (!worksheetId) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { result } = await wx.cloud.callFunction({
|
||||||
|
name: 'worksheetsStatsUpdate',
|
||||||
|
data: {
|
||||||
|
id: worksheetId,
|
||||||
|
field,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return !!(result as { success?: boolean } | undefined)?.success;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('update worksheet stats failed', field, worksheetId, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function incrementWorksheetLikes(id: string): Promise<boolean> {
|
||||||
|
return updateWorksheetStats(id, 'likes');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function incrementWorksheetDownloads(id: string): Promise<boolean> {
|
||||||
|
return updateWorksheetStats(id, 'downloads');
|
||||||
|
}
|
||||||
+8
-1
@@ -3,11 +3,18 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"scripts": {},
|
"scripts": {
|
||||||
|
"worksheet:patch:ages": "tsx scripts/worksheet-sync-cloud-payload.ts --ages",
|
||||||
|
"worksheet:patch:tags": "tsx scripts/worksheet-sync-cloud-payload.ts --tags",
|
||||||
|
"worksheet:patch:all": "tsx scripts/worksheet-sync-cloud-payload.ts --all",
|
||||||
|
"worksheet:patch:dry": "tsx scripts/worksheet-sync-cloud-payload.ts --all --dry-run"
|
||||||
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "",
|
"license": "",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"tsx": "^4.19.2",
|
||||||
|
"@types/node": "^22.15.0",
|
||||||
"@eslint/js": "^9.17.0",
|
"@eslint/js": "^9.17.0",
|
||||||
"eslint": "^9.17.0",
|
"eslint": "^9.17.0",
|
||||||
"eslint-config-prettier": "9.1.0",
|
"eslint-config-prettier": "9.1.0",
|
||||||
|
|||||||
@@ -24,12 +24,19 @@
|
|||||||
"miniprogram": {
|
"miniprogram": {
|
||||||
"list": [
|
"list": [
|
||||||
{
|
{
|
||||||
"name": "supportPages/debug/debug",
|
"name": "pages/age/age",
|
||||||
"pathName": "supportPages/debug/debug",
|
"pathName": "pages/age/age",
|
||||||
"query": "",
|
"query": "",
|
||||||
"scene": null,
|
"scene": null,
|
||||||
"launchMode": "default"
|
"launchMode": "default"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "supportPages/debug/debug",
|
||||||
|
"pathName": "supportPages/debug/debug",
|
||||||
|
"query": "",
|
||||||
|
"launchMode": "default",
|
||||||
|
"scene": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "englishPages/letterTracing/letterTracing",
|
"name": "englishPages/letterTracing/letterTracing",
|
||||||
"pathName": "englishPages/letterTracing/letterTracing",
|
"pathName": "englishPages/letterTracing/letterTracing",
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
# Skill: generate-age-abilities
|
||||||
|
|
||||||
|
> 生成分龄页各年龄段的「能力目标」数据,更新 `age.config.ts` 中的 `AGE_ABILITIES`。
|
||||||
|
|
||||||
|
## 触发条件
|
||||||
|
|
||||||
|
当用户提到以下内容时触发:
|
||||||
|
- "生成能力目标"、"更新能力目标"、"generate age abilities"
|
||||||
|
- "更新分龄页第二部分"、"更新分龄能力"
|
||||||
|
|
||||||
|
## 任务描述
|
||||||
|
|
||||||
|
你是一位拥有 15 年经验的儿童早教专家,专注于 3-8 岁儿童的认知发展和学习能力培养。你需要基于当前系统中已上线的所有 worksheet(打印练习册)内容,为 5 个年龄段(3-4 岁、4-5 岁、5-6 岁、6-7 岁、7-8 岁)生成精准的能力目标描述。
|
||||||
|
|
||||||
|
## 执行步骤
|
||||||
|
|
||||||
|
### Step 1:获取 worksheet 数据
|
||||||
|
|
||||||
|
`page-config.json` 存储在微信云存储中(路径:`/content/page-config.json`),本地项目中不存在此文件。按以下优先级获取数据:
|
||||||
|
|
||||||
|
**方式一:读取本地缓存(优先)**
|
||||||
|
|
||||||
|
1. 使用 Glob 搜索 `.cache/page-config.json` 是否存在
|
||||||
|
2. 如果存在,读取 `category.categories` 数组,提取每个分类下的 `items[]`
|
||||||
|
3. 从每个 item 中提取:`id`、`title`、`description`、`category`、`ageBand`、`difficulty`
|
||||||
|
|
||||||
|
**方式二:通过微信开发者工具下载**
|
||||||
|
|
||||||
|
如果本地缓存不存在,提示用户按以下步骤获取:
|
||||||
|
|
||||||
|
1. 打开微信开发者工具
|
||||||
|
2. 进入「云开发控制台」→「存储」
|
||||||
|
3. 找到 `/content/page-config.json` 文件,点击下载
|
||||||
|
4. 将文件保存到项目根目录的 `.cache/page-config.json`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 确保 .cache 目录存在
|
||||||
|
mkdir -p .cache
|
||||||
|
# 用户手动将下载的文件放入:.cache/page-config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
**方式三:通过云函数调用获取**
|
||||||
|
|
||||||
|
如果用户已安装 `tcb` CLI(腾讯云开发 CLI),可以直接下载:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p .cache
|
||||||
|
tcb storage:download /content/page-config.json .cache/page-config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
> **重要**:请在获取数据后告知用户文件中有多少个 worksheet,以及覆盖了哪些分类和年龄段,让用户确认数据是最新的。
|
||||||
|
|
||||||
|
### Step 2:分析 worksheet 内容
|
||||||
|
|
||||||
|
1. 将所有 worksheet 按年龄段(`ageBand`)分组
|
||||||
|
2. 在每个年龄段内,按分类(`category`)统计覆盖的知识领域
|
||||||
|
3. 分析每个年龄段的 worksheet 主要涉及哪些能力维度
|
||||||
|
|
||||||
|
当前系统的分类体系:
|
||||||
|
- `math` - 数感启蒙(数字认知、计算、数学思维)
|
||||||
|
- `puzzle` - 益智游戏(逻辑推理、空间想象、专注力)
|
||||||
|
- `pinyin` - 汉语拼音(拼音认读、拼写)
|
||||||
|
- `chinese` - 趣味识字(汉字书写、识字、词语)
|
||||||
|
- `english` - 英语启蒙(字母、单词、简单句型)
|
||||||
|
- `craft` - 创意手工(动手能力、创造力)
|
||||||
|
|
||||||
|
年龄段定义(来自 `core/data/difficulty.ts`):
|
||||||
|
- `3-4`:3-4 岁
|
||||||
|
- `4-5`:4-5 岁
|
||||||
|
- `5-6`:5-6 岁
|
||||||
|
- `6-7`:6-7 岁
|
||||||
|
- `7-8`:7-8 岁
|
||||||
|
|
||||||
|
### Step 3:生成能力目标
|
||||||
|
|
||||||
|
以早教专家视角,为每个年龄段生成 **3 个核心能力维度**:
|
||||||
|
|
||||||
|
**生成原则:**
|
||||||
|
|
||||||
|
1. **基于实际内容**:能力目标必须反映系统中实际存在的 worksheet 内容,不能脱离实际可练习的内容空谈
|
||||||
|
2. **发展阶梯性**:5 个年龄段的能力描述应体现明显的递进关系
|
||||||
|
3. **具体可衡量**:描述要具体到可练习的技能点,避免笼统表述
|
||||||
|
4. **简洁有力**:每个 `desc` 控制在 10 个汉字以内,用顿号分隔 2-3 个技能点
|
||||||
|
|
||||||
|
**能力维度选择参考(可根据实际 worksheet 内容调整):**
|
||||||
|
- 🔢 数感(数字认知、计算能力)
|
||||||
|
- ✏️ 书写(笔画、汉字、拼音书写)
|
||||||
|
- 🧩 思维(逻辑推理、空间想象)
|
||||||
|
- 🔤 语言(拼音、英语启蒙)
|
||||||
|
- 🎨 创造(手工、绘画、创意表达)
|
||||||
|
|
||||||
|
如果某个年龄段的 worksheet 内容集中在特定领域,可以调整能力维度以更准确地反映实际内容。
|
||||||
|
|
||||||
|
### Step 4:更新 age.config.ts
|
||||||
|
|
||||||
|
1. 读取 `miniprogram/pages/age/age.config.ts` 文件
|
||||||
|
2. 找到 `AGE_ABILITIES` 常量(如果不存在则在现有代码中 `MOCK_ABILITY` 的位置创建)
|
||||||
|
3. 替换为新生成的数据
|
||||||
|
|
||||||
|
**输出数据结构:**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const AGE_ABILITIES: Record<AgeBandKey, AbilityItem[]> = {
|
||||||
|
'3-4': [
|
||||||
|
{ icon: '🔢', title: '数感', desc: '认读 1-5、点数对应' },
|
||||||
|
{ icon: '✏️', title: '书写', desc: '涂鸦线条、简单描红' },
|
||||||
|
{ icon: '🧩', title: '思维', desc: '找相同、简单配对' },
|
||||||
|
],
|
||||||
|
'4-5': [
|
||||||
|
{ icon: '🔢', title: '数感', desc: '10 以内数数、比大小' },
|
||||||
|
{ icon: '✏️', title: '书写', desc: '笔画模仿、图形描边' },
|
||||||
|
{ icon: '🧩', title: '思维', desc: '规律排序、图形分类' },
|
||||||
|
],
|
||||||
|
'5-6': [
|
||||||
|
{ icon: '🔢', title: '数感', desc: '10以内加减法、凑十法' },
|
||||||
|
{ icon: '✏️', title: '书写', desc: '练字帖、拼音入门' },
|
||||||
|
{ icon: '🧩', title: '思维', desc: '方格推理、格子仿画 5×5' },
|
||||||
|
],
|
||||||
|
'6-7': [
|
||||||
|
{ icon: '🔢', title: '数感', desc: '20 以内运算、应用题启蒙' },
|
||||||
|
{ icon: '✏️', title: '书写', desc: '常用字书写、词语积累' },
|
||||||
|
{ icon: '🧩', title: '思维', desc: '逻辑填空、空间想象' },
|
||||||
|
],
|
||||||
|
'7-8': [
|
||||||
|
{ icon: '🔢', title: '数感', desc: '乘除启蒙、巧算练习' },
|
||||||
|
{ icon: '✏️', title: '书写', desc: '段落抄写、古诗诵读' },
|
||||||
|
{ icon: '🧩', title: '思维', desc: '数独入门、综合推理' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 5:验证
|
||||||
|
|
||||||
|
1. 确认每个年龄段都有 3 个能力维度
|
||||||
|
2. 确认 `desc` 简洁(≤10 个汉字)
|
||||||
|
3. 确认 5 个年龄段之间有明显的能力递进
|
||||||
|
4. 确认 `icon` 使用了合适的 emoji
|
||||||
|
5. 确认数据结构与 `AbilityItem` 类型匹配
|
||||||
|
|
||||||
|
## 相关文件
|
||||||
|
|
||||||
|
- `miniprogram/pages/age/age.config.ts` — 输出目标文件
|
||||||
|
- `miniprogram/pages/age/age.ts` — 分龄页逻辑(引用 config)
|
||||||
|
- `miniprogram/core/data/difficulty.ts` — AGE_BANDS 定义
|
||||||
|
- `miniprogram/core/data/categories.ts` — CATEGORY_LIST 分类定义
|
||||||
|
- `.cache/page-config.json` — worksheet 数据本地缓存(从云存储下载)
|
||||||
|
- 云存储 `/content/page-config.json` — worksheet 数据源(Source of Truth)
|
||||||
|
- `cloudfunctions/worksheetsQuery/` — 查询 worksheet 的云函数(备选数据源)
|
||||||
|
- `docs/分龄页内容管理方案.md` — 方案文档
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
# Skill: generate-age-weekly-plans
|
||||||
|
|
||||||
|
> 生成分龄页各年龄段的「4 周推荐学习路线」,更新 `age.config.ts` 中的 `AGE_WEEK_PLANS`。
|
||||||
|
|
||||||
|
## 触发条件
|
||||||
|
|
||||||
|
当用户提到以下内容时触发:
|
||||||
|
- "生成学习路线"、"更新学习路线"、"更新周计划"、"generate weekly plans"
|
||||||
|
- "更新分龄页第三部分"、"更新分龄路线"
|
||||||
|
|
||||||
|
## 任务描述
|
||||||
|
|
||||||
|
你是一位拥有 15 年经验的儿童早教专家,专注于 3-8 岁儿童的系统化学习规划。你需要基于当前系统中已上线的所有 worksheet(打印练习册),为 5 个年龄段设计科学的 4 周学习路线,确保每周推荐的练习都是系统中实际存在的 worksheet。
|
||||||
|
|
||||||
|
## 执行步骤
|
||||||
|
|
||||||
|
### Step 1:获取 worksheet 数据
|
||||||
|
|
||||||
|
`page-config.json` 存储在微信云存储中(路径:`/content/page-config.json`),本地项目中不存在此文件。按以下优先级获取数据:
|
||||||
|
|
||||||
|
**方式一:读取本地缓存(优先)**
|
||||||
|
|
||||||
|
1. 使用 Glob 搜索 `.cache/page-config.json` 是否存在
|
||||||
|
2. 如果存在,读取 `category.categories` 数组,提取每个分类下的 `items[]`
|
||||||
|
3. 从每个 item 中提取关键信息(与 `pageContentBuild` 下发的 `category.items[]` 一致):
|
||||||
|
- `id`(云 worksheet `_id`,写入配置时作为 `_id` 字段)
|
||||||
|
- `title`、`subtitle`、`path`
|
||||||
|
- 可选辅助:`ageMin` / `ageMax`、`category`、`difficulty`、`downloads`、`likes`
|
||||||
|
|
||||||
|
**方式二:通过微信开发者工具下载**
|
||||||
|
|
||||||
|
如果本地缓存不存在,提示用户按以下步骤获取:
|
||||||
|
|
||||||
|
1. 打开微信开发者工具
|
||||||
|
2. 进入「云开发控制台」→「存储」
|
||||||
|
3. 找到 `/content/page-config.json` 文件,点击下载
|
||||||
|
4. 将文件保存到项目根目录的 `.cache/page-config.json`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 确保 .cache 目录存在
|
||||||
|
mkdir -p .cache
|
||||||
|
# 用户手动将下载的文件放入:.cache/page-config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
**方式三:通过云函数调用获取**
|
||||||
|
|
||||||
|
如果用户已安装 `tcb` CLI(腾讯云开发 CLI),可以直接下载:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p .cache
|
||||||
|
tcb storage:download /content/page-config.json .cache/page-config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
> **重要**:请在获取数据后告知用户文件中有多少个 worksheet,以及覆盖了哪些分类和年龄段,让用户确认数据是最新的。
|
||||||
|
|
||||||
|
### Step 2:按年龄段分组和分析
|
||||||
|
|
||||||
|
1. 将所有 worksheet 按年龄段(`ageBand`)分组
|
||||||
|
2. 在每个年龄段内,按分类统计 worksheet 数量
|
||||||
|
3. 记录每个 worksheet 的难度等级(`difficulty`)
|
||||||
|
4. 按 `downloads + likes` 排序,优先选择受欢迎的 worksheet
|
||||||
|
|
||||||
|
当前系统的分类体系:
|
||||||
|
- `math` - 数感启蒙
|
||||||
|
- `puzzle` - 益智游戏
|
||||||
|
- `pinyin` - 汉语拼音
|
||||||
|
- `chinese` - 趣味识字
|
||||||
|
- `english` - 英语启蒙
|
||||||
|
- `craft` - 创意手工
|
||||||
|
|
||||||
|
年龄段定义(来自 `core/data/difficulty.ts`):
|
||||||
|
- `3-4`:3-4 岁
|
||||||
|
- `4-5`:4-5 岁
|
||||||
|
- `5-6`:5-6 岁
|
||||||
|
- `6-7`:6-7 岁
|
||||||
|
- `7-8`:7-8 岁
|
||||||
|
|
||||||
|
### Step 3:设计 4 周学习路线
|
||||||
|
|
||||||
|
以早教专家视角,为每个年龄段设计 4 周学习计划。
|
||||||
|
|
||||||
|
**编排原则:**
|
||||||
|
|
||||||
|
1. **由易到难**:第 1 周以基础入门为主,逐周提升难度
|
||||||
|
2. **跨领域覆盖**:4 周合计应覆盖该年龄段可用的多个学科分类,避免单一
|
||||||
|
3. **主题连贯**:每周设定一个明确的学习主题(如"数感启蒙""形状与空间""趣味文字"等)
|
||||||
|
4. **基于实际内容**:每个练习必须对应系统中实际存在的 worksheet,**必须**带上真实的 `_id`、`subtitle`、`path`(与数据源一致,禁止手写虚构 path)
|
||||||
|
5. **合理搭配**:每周 3 个练习应尽量来自不同分类,提供多元学习体验
|
||||||
|
6. **第 4 周综合**:最后一周安排综合练习,涵盖前 3 周的核心技能
|
||||||
|
|
||||||
|
**主题命名规范:**
|
||||||
|
- 简洁明了,4-6 个汉字
|
||||||
|
- 体现该周的学习重点
|
||||||
|
- 示例:数感启蒙、形状与连线、趣味专注、综合练习、拼音入门、创意手工
|
||||||
|
|
||||||
|
**当 worksheet 不足时的处理:**
|
||||||
|
- 如果某个年龄段的 worksheet 不足 12 个(4 周 × 3 个),允许同一 worksheet 在不同周出现
|
||||||
|
- 如果某个年龄段完全没有 worksheet,使用相邻年龄段的内容并标注
|
||||||
|
- 若某年龄段可用 worksheet 不足,可放宽年龄筛选,但每条仍须来自数据源中的真实 `id/title/subtitle/path`,**不要**编造 `_id` 或 path
|
||||||
|
|
||||||
|
### Step 4:更新 age.config.ts
|
||||||
|
|
||||||
|
1. 读取 `miniprogram/pages/age/age.config.ts` 文件
|
||||||
|
2. 找到 `AGE_WEEK_PLANS` 常量(如果不存在则在现有代码中 `MOCK_WEEKS` 的位置创建)
|
||||||
|
3. 替换为新生成的数据
|
||||||
|
|
||||||
|
**`WeekExercise` 输出结构(写入 `age.config.ts`,与小程序分龄页跳转一致):**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export type WeekExercise = {
|
||||||
|
_id: string; // 等于数据源 item.id(即云 worksheets._id)
|
||||||
|
title: string; // item.title
|
||||||
|
subtitle: string; // item.subtitle
|
||||||
|
path: string; // item.path,可含 query;点击「去练习」将按 path 跳转
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**`AGE_WEEK_PLANS` 示例片段:**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const AGE_WEEK_PLANS: Record<AgeBandKey, WeekPlan[]> = {
|
||||||
|
'3-4': [
|
||||||
|
{
|
||||||
|
week: 1,
|
||||||
|
theme: '数感启蒙',
|
||||||
|
exercises: [
|
||||||
|
{
|
||||||
|
_id: 'xxxxxxxx',
|
||||||
|
title: '找数字,涂一涂',
|
||||||
|
subtitle: '在数字方阵中找出目标数字并涂色',
|
||||||
|
path: '/mathPages/mathDraw/mathDraw?id=number-find',
|
||||||
|
},
|
||||||
|
// ...
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// week 2–4
|
||||||
|
],
|
||||||
|
// '4-5' … '7-8'
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**数据规范:**
|
||||||
|
- 每个年龄段必须有 4 个 `WeekPlan`
|
||||||
|
- 每个 `WeekPlan` 必须有 3 个 `exercises`
|
||||||
|
- 每个 `exercises[]` 元素必须四字段齐全:`_id`、`title`、`subtitle`、`path`
|
||||||
|
- `_id` 与 `path` 必须与 `.cache/page-config.json`(或云函数拉取结果)中对应 worksheet **逐字一致**
|
||||||
|
- `theme` 简洁描述本周学习主题
|
||||||
|
- 列表 `wx:key` 使用 `_id`,避免同 title 重复导致渲染异常
|
||||||
|
|
||||||
|
### Step 5:验证
|
||||||
|
|
||||||
|
1. **结构完整性**:5 个年龄段 × 4 周 × 3 练习 = 60 个练习项
|
||||||
|
2. **内容真实性**:抽查每条 `_id`、`path`、`subtitle` 是否与数据源一致(可复制粘贴比对)
|
||||||
|
3. **递进合理性**:确认每个年龄段的内容难度符合该年龄段认知水平
|
||||||
|
4. **跨周不重复**:尽量避免同一 worksheet 在同一年龄段的不同周重复出现
|
||||||
|
5. **主题覆盖度**:检查 4 周主题是否覆盖了该年龄段的主要学科
|
||||||
|
6. **类型匹配**:确认数据结构与 `WeekPlan` 和 `WeekExercise` 类型定义一致
|
||||||
|
|
||||||
|
## 相关文件
|
||||||
|
|
||||||
|
- `miniprogram/pages/age/age.config.ts` — 输出目标文件
|
||||||
|
- `miniprogram/pages/age/age.ts` — 分龄页逻辑(引用 config)
|
||||||
|
- `miniprogram/core/data/difficulty.ts` — AGE_BANDS 定义
|
||||||
|
- `miniprogram/core/data/categories.ts` — CATEGORY_LIST 分类定义
|
||||||
|
- `.cache/page-config.json` — worksheet 数据本地缓存(从云存储下载)
|
||||||
|
- 云存储 `/content/page-config.json` — worksheet 数据源(Source of Truth)
|
||||||
|
- `cloudfunctions/worksheetsQuery/` — 查询 worksheet 的云函数(备选数据源)
|
||||||
|
- `docs/分龄页内容管理方案.md` — 方案文档
|
||||||
Reference in New Issue
Block a user