feat:更新core 和首页代码
This commit is contained in:
@@ -86,8 +86,8 @@
|
||||
},
|
||||
"tabBar": {
|
||||
"color": "#999999",
|
||||
"selectedColor": "#333333",
|
||||
"backgroundColor": "#ffffff",
|
||||
"selectedColor": "#FFD709",
|
||||
"backgroundColor": "#F8F0E0",
|
||||
"borderStyle": "white",
|
||||
"list": [
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"component": true,
|
||||
"styleIsolation": "apply-shared",
|
||||
"usingComponents": {}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
padding: 16rpx 24rpx;
|
||||
padding: 30rpx 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -33,4 +33,4 @@
|
||||
background: #f7ee47;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.88);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"component": true,
|
||||
"styleIsolation": "apply-shared",
|
||||
"usingComponents": {}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { CategoryType } from '../models/worksheet';
|
||||
import type { CategoryType } from '../models/category';
|
||||
|
||||
export const CATEGORIES: CategoryType[] = [
|
||||
{
|
||||
id: 'math',
|
||||
name: '数学启蒙',
|
||||
name: '数感启蒙',
|
||||
icon: '📐',
|
||||
color: '#e3f2fd',
|
||||
gradient: ['#fff9c4', '#fff176'],
|
||||
},
|
||||
{
|
||||
id: 'chinese',
|
||||
name: '语文启蒙',
|
||||
name: '趣味识字',
|
||||
icon: '✏️',
|
||||
color: '#e8f5e9',
|
||||
gradient: ['#c8e6c9', '#a5d6a7'],
|
||||
@@ -22,13 +22,13 @@ export const CATEGORIES: CategoryType[] = [
|
||||
color: '#e0f7fa',
|
||||
gradient: ['#b2ebf2', '#80deea'],
|
||||
},
|
||||
{
|
||||
id: 'focus',
|
||||
name: '专注力',
|
||||
icon: '🧩',
|
||||
color: '#f3e5f5',
|
||||
gradient: ['#e1bee7', '#ce93d8'],
|
||||
},
|
||||
// {
|
||||
// id: 'focus',
|
||||
// name: '专注力',
|
||||
// icon: '🧩',
|
||||
// color: '#f3e5f5',
|
||||
// gradient: ['#e1bee7', '#ce93d8'],
|
||||
// },
|
||||
{
|
||||
id: 'puzzle',
|
||||
name: '益智游戏',
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { DifficultyLevel } from '../models/worksheet';
|
||||
|
||||
/** 产品文档:难度 入门 / 基础 / 进阶 / 挑战 → 云库 difficulty 1–4 */
|
||||
export const DIFFICULTY_LEVEL_TO_STARS: Record<
|
||||
DifficultyLevel,
|
||||
1 | 2 | 3 | 4
|
||||
> = {
|
||||
beginner: 1,
|
||||
basic: 2,
|
||||
intermediate: 3,
|
||||
advanced: 4,
|
||||
};
|
||||
|
||||
export const STARS_TO_DIFFICULTY_LEVEL: Record<
|
||||
1 | 2 | 3 | 4,
|
||||
DifficultyLevel
|
||||
> = {
|
||||
1: 'beginner',
|
||||
2: 'basic',
|
||||
3: 'intermediate',
|
||||
4: 'advanced',
|
||||
};
|
||||
|
||||
export function difficultyToStars(
|
||||
level?: DifficultyLevel,
|
||||
): 1 | 2 | 3 | 4 {
|
||||
return level ? DIFFICULTY_LEVEL_TO_STARS[level] : 2;
|
||||
}
|
||||
|
||||
/** 分龄 Tab / 筛选(现有功能清单 + 产品设计 3-8 岁) */
|
||||
export const AGE_BANDS = [
|
||||
{ key: '3-4', minAge: 3, maxAge: 4, label: '3-4 岁' },
|
||||
{ key: '4-5', minAge: 4, maxAge: 5, label: '4-5 岁' },
|
||||
{ key: '5-6', minAge: 5, maxAge: 6, label: '5-6 岁' },
|
||||
{ key: '6-7', minAge: 6, maxAge: 7, label: '6-7 岁' },
|
||||
{ key: '7-8', minAge: 7, maxAge: 8, label: '7-8 岁' },
|
||||
] as const;
|
||||
|
||||
export type AgeBandKey = (typeof AGE_BANDS)[number]['key'];
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* 识字 Tab 词库分类(现有功能清单 §四):供生成识字卡、与 future worksheet-service 对齐
|
||||
*/
|
||||
|
||||
export interface WordCategory {
|
||||
id: string;
|
||||
name: string;
|
||||
/** 示例与常用字,可渐进扩充 */
|
||||
words: string[];
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
export const WORD_BANKS: WordCategory[] = [
|
||||
{
|
||||
id: 'basic-stroke',
|
||||
name: '基础独体字',
|
||||
words: ['人', '口', '手', '上', '下', '日', '月', '水', '火', '山', '大', '小', '木', '门', '心', '子', '女', '马', '牛', '羊'],
|
||||
},
|
||||
{
|
||||
id: 'daily-highfreq',
|
||||
name: '生活高频字',
|
||||
words: ['爸', '妈', '书', '包', '吃', '喝', '走', '跑', '睡', '醒', '玩', '学', '校', '衣', '裤', '鞋', '床', '灯', '车', '门'],
|
||||
},
|
||||
{
|
||||
id: 'nature-action',
|
||||
name: '自然与动作',
|
||||
words: ['风', '雪', '星', '光', '春', '夏', '秋', '冬', '雨', '云', '花', '草', '树', '叶', '看', '听', '说', '想', '开', '关'],
|
||||
},
|
||||
{
|
||||
id: 'measure',
|
||||
name: '常用量词',
|
||||
words: ['个', '只', '条', '本', '张', '块', '头', '匹', '位', '双', '串', '瓶', '盒', '杯', '碗', '把', '朵', '颗', '辆', '件'],
|
||||
},
|
||||
{
|
||||
id: 'color',
|
||||
name: '颜色',
|
||||
words: ['红', '黄', '蓝', '绿', '白', '黑', '紫', '粉', '灰', '橙', '金', '银', '青', '棕', '米', '肉', '雪', '墨', '桃', '奶'],
|
||||
},
|
||||
{
|
||||
id: 'animal',
|
||||
name: '动物',
|
||||
words: ['猫', '狗', '鸟', '鱼', '兔', '鸡', '鸭', '猪', '猴', '象', '虎', '熊', '狮', '鹿', '鼠', '蛇', '龟', '虾', '贝', '虫'],
|
||||
},
|
||||
{
|
||||
id: 'food',
|
||||
name: '食物',
|
||||
words: ['米', '饭', '面', '汤', '菜', '肉', '蛋', '奶', '果', '瓜', '糖', '饼', '饺', '粥', '茶', '豆', '茄', '笋', '枣', '梨'],
|
||||
},
|
||||
{
|
||||
id: 'body-health',
|
||||
name: '身体与健康',
|
||||
words: ['头', '眼', '耳', '鼻', '嘴', '牙', '臂', '腿', '脚', '汗', '痛', '病', '药', '医', '笑', '哭', '渴', '饿', '饱', '累'],
|
||||
},
|
||||
];
|
||||
|
||||
export function getWordCategoryById(id: string): WordCategory | undefined {
|
||||
return WORD_BANKS.find((c) => c.id === id);
|
||||
}
|
||||
|
||||
export function getAllWordCategories(): WordCategory[] {
|
||||
return WORD_BANKS;
|
||||
}
|
||||
+263
-110
@@ -1,7 +1,14 @@
|
||||
import { WorksheetType } from '../models/worksheet';
|
||||
import type {
|
||||
GeneratorType,
|
||||
LayoutConfig,
|
||||
TemplateType,
|
||||
WorksheetDefinition,
|
||||
WorksheetType,
|
||||
} from '../models/worksheet';
|
||||
import { difficultyToStars } from './difficulty';
|
||||
|
||||
/**
|
||||
* MathFunctionType - 保持向后兼容
|
||||
* MathFunctionType - 保持向后兼容(mathPageMixin / mathIndex)
|
||||
*/
|
||||
export interface MathFunctionType {
|
||||
id: string;
|
||||
@@ -13,7 +20,7 @@ export interface MathFunctionType {
|
||||
}
|
||||
|
||||
/**
|
||||
* FocusFunctionType - 保持向后兼容
|
||||
* FocusFunctionType - 保持向后兼容(focusPageMixin / focusIndex)
|
||||
*/
|
||||
export interface FocusFunctionType {
|
||||
id: string;
|
||||
@@ -25,128 +32,270 @@ export interface FocusFunctionType {
|
||||
img?: string;
|
||||
}
|
||||
|
||||
interface SheetEngine {
|
||||
subcategory: string;
|
||||
template: TemplateType;
|
||||
generator: GeneratorType;
|
||||
generatorConfig?: Record<string, unknown>;
|
||||
layoutConfig?: LayoutConfig;
|
||||
tags?: string[];
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
function legacyPath(ui: WorksheetType): string | null {
|
||||
if (!ui.page) return null;
|
||||
if (ui.subpackage) {
|
||||
return `/${ui.subpackage}/${ui.page}/${ui.page}`;
|
||||
}
|
||||
return `/pages/${ui.page}/${ui.page}`;
|
||||
}
|
||||
|
||||
function sheet(ui: WorksheetType, engine: SheetEngine): WorksheetDefinition {
|
||||
const ageMin = ui.ageRange?.[0] ?? 3;
|
||||
const ageMax = ui.ageRange?.[1] ?? 8;
|
||||
return {
|
||||
...ui,
|
||||
subcategory: engine.subcategory,
|
||||
ageMin,
|
||||
ageMax,
|
||||
difficultyLevel: difficultyToStars(ui.difficulty),
|
||||
previewImage: ui.img ?? '',
|
||||
tags: engine.tags?.length ? engine.tags : [engine.subcategory],
|
||||
isNew: false,
|
||||
isHot: false,
|
||||
sortOrder: engine.sortOrder,
|
||||
downloadCount: 0,
|
||||
status: 'active',
|
||||
template: engine.template,
|
||||
generator: engine.generator,
|
||||
generatorConfig: engine.generatorConfig ?? {},
|
||||
layoutConfig: engine.layoutConfig ?? { showBorder: true },
|
||||
userConfigurable: null,
|
||||
legacyPage: legacyPath(ui),
|
||||
};
|
||||
}
|
||||
|
||||
const L = {
|
||||
gridEx: { showBorder: true, fontSize: 18 } satisfies LayoutConfig,
|
||||
card: { showBorder: true, columns: 2, fontSize: 16 } satisfies LayoutConfig,
|
||||
trace: {
|
||||
showBorder: true,
|
||||
showInstruction: true,
|
||||
fontSize: 20,
|
||||
} satisfies LayoutConfig,
|
||||
};
|
||||
|
||||
// 现有功能清单 §二:数感启蒙 26 种 + §三 13 种 + §四 §五 语文 2
|
||||
// prettier-ignore
|
||||
export const MATH_FUNCTION_TYPES: MathFunctionType[] = [
|
||||
{ id: 'number-sort', page: 'numberSort', title: '数字排序', desc: '数字排序,写出正确顺序', icon: '🔢', img: '/assets/mathEntrance/number-sort.png' },
|
||||
{ id: 'one-digit-addition', page: 'oneDigitAddition', title: '一位数加法', desc: '通过圆点学习一位数加法运算', icon: '➕', img: '/assets/mathEntrance/one-digit-addition.png' },
|
||||
{ id: 'make-ten', page: 'makeTen', title: '凑十法练习', desc: '通过凑十法学习20以内进位加法', icon: '➕', img: '/assets/mathEntrance/make-ten.png' },
|
||||
{ id: 'break-ten', page: 'breakTen', title: '破十法练习', desc: '通过破十法学习20以内退位减法', icon: '➖', img: '/assets/mathEntrance/break-ten.png' },
|
||||
{ id: 'flat-ten', page: 'flatTen', title: '平十法练习', desc: '通过平十法学习20以内退位减法', icon: '➖', img: '/assets/mathEntrance/flat-ten.png' },
|
||||
{ id: 'borrow-ten', page: 'borrowTen', title: '借十法练习', desc: '通过借十法学习20以上退位减法', icon: '➖', img: '/assets/mathEntrance/borrow-ten.png' },
|
||||
{ id: 'number-decompose-20', page: 'numberDecompose', title: '20以内数的分与合', desc: '把数字分一分,合一合', icon: '🔢', img: '/assets/mathEntrance/number-decompose-20.png' },
|
||||
{ id: 'number-decompose', page: 'numberDecompose', title: '10以内数的分与合', desc: '把数字分一分,合一合', icon: '🔢', img: '/assets/mathEntrance/number-decompose.png' },
|
||||
{ id: 'number-find', page: 'numberFind', title: '找数字,涂一涂', desc: '找一找下面相同的数字,涂上颜色', img: '/assets/mathEntrance/number-find.png' },
|
||||
{ id: 'number-write', page: 'numberFind', title: '看数字,写一写', desc: '看数字,按笔画顺序写一写', img: '/assets/mathEntrance/number-write.png' },
|
||||
{ id: 'number-coloring', page: 'countMatch', title: '按数字,涂颜色', desc: '按数字给相应的圆圈涂上颜色', icon: '🎨', img: '/assets/mathEntrance/number-coloring.png' },
|
||||
{ id: 'counting-matching', page: 'countMatch', title: '数一数,连一连', desc: '通过连线配对数字和对应的数量图形', icon: '🔗', img: '/assets/mathEntrance/count-match.png' },
|
||||
{ id: 'number-object-match', page: 'numberObjectMatch', title: '数物连线', desc: '连线相同数量的物品和数字', icon: '🔗', img: '/assets/mathEntrance/number-object-match.png' },
|
||||
{ id: 'number-object-fill', page: 'numberObjectMatch', title: '数物填写', desc: '数一数物品数量,填写对应的数字', icon: '✏️', img: '/assets/mathEntrance/number-object-fill.png' },
|
||||
{ id: 'counting-select', page: 'countingSelect', title: '数一数,选一选', desc: '数出物品数量,圈出正确答案', icon: '✓', img: '/assets/mathEntrance/counting-select.png' },
|
||||
{ id: 'counting-fill', page: 'countingSelect', title: '数一数,填一填', desc: '数出物品数量,填写对应的数字', icon: '✏️', img: '/assets/mathEntrance/counting-fill.png' },
|
||||
{ id: 'compare', page: 'compare', title: '数一数,比大小', desc: '比较数量大小,在 ⭕️ 中填入>、<、=', icon: '⚖️', img: '/assets/mathEntrance/compare.png' },
|
||||
{ id: 'missing-number', page: 'missingNumber', title: '填上缺少的数字', desc: '在数字序列中找出并填写缺失的数字', icon: '❓', img: '/assets/mathEntrance/missing-number.png' },
|
||||
{ id: 'addition-5', page: 'addition', title: '5以内加法', desc: '练习5以内的加法运算,图形化展示', icon: '➕', img: '/assets/mathEntrance/addition-5.png' },
|
||||
{ id: 'addition-10', page: 'addition', title: '10以内加法', desc: '练习10以内的加法运算,图形化展示', icon: '➕', img: '/assets/mathEntrance/addition-10.png' },
|
||||
{ id: 'subtraction-10', page: 'addition', title: '10以内减法', desc: '练习10以内的减法运算,图形化展示', icon: '➖', img: '/assets/mathEntrance/subtraction-10.png' },
|
||||
{ id: 'addition-subtraction-10', page: 'addition', title: '10以内加减法', desc: '练习10以内的加法和减法混合运算', icon: '±', img: '/assets/mathEntrance/addition-subtraction-10.png' },
|
||||
{ id: 'practice-addition', page: 'calculationPractice', title: '加法运算', desc: '练习10/20/50/100以内加法运算', icon: '➕', img: '/assets/mathEntrance/practice-addition.png' },
|
||||
{ id: 'practice-subtraction', page: 'calculationPractice', title: '减法运算', desc: '练习10/20/50/100以内减法运算', icon: '➖', img: '/assets/mathEntrance/practice-subtraction.png' },
|
||||
{ id: 'practice-mixed', page: 'calculationPractice', title: '混合运算', desc: '练习10/20/50/100以内加减法混合运算', icon: '±', img: '/assets/mathEntrance/practice-mixed.png' },
|
||||
{ id: 'multiplication-table', page: 'multiplicationTable', title: '九九乘法表', desc: '学习九九乘法口诀', icon: '✖️', img: '/assets/mathEntrance/multiplication-table.png' },
|
||||
export const ALL_WORKSHEETS: WorksheetDefinition[] = [
|
||||
sheet(
|
||||
{ id: 'number-find', title: '找数字,涂一涂', desc: '在数字方阵中找出目标数字并涂色', category: 'math', page: 'numberFind', subpackage: 'mathPages', img: '/assets/mathEntrance/number-find.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#fff3e0', previewText: '🔍 1 2 3' },
|
||||
{ subcategory: 'number-sense', template: 'grid-coloring', generator: 'counting', sortOrder: 1, layoutConfig: { ...L.gridEx }, generatorConfig: { functionId: 'number-find' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'number-write', title: '看数字,写一写', desc: '按笔画顺序练习书写数字', category: 'math', page: 'numberFind', subpackage: 'mathPages', img: '/assets/mathEntrance/number-write.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#fff3e0', previewText: '1 2 3' },
|
||||
{ subcategory: 'number-sense', template: 'tracing-writing', generator: 'counting', sortOrder: 2, layoutConfig: { ...L.trace }, generatorConfig: { functionId: 'number-write' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'number-coloring', title: '按数字,涂颜色', desc: '按指定数字给对应圆圈涂色', category: 'math', page: 'countMatch', subpackage: 'mathPages', icon: '🎨', img: '/assets/mathEntrance/number-coloring.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#fce4ec', previewText: '🎨' },
|
||||
{ subcategory: 'number-sense', template: 'grid-coloring', generator: 'counting', sortOrder: 3, layoutConfig: { ...L.gridEx }, generatorConfig: { functionId: 'number-coloring' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'counting-matching', title: '数一数,连一连', desc: '连线配对数字和对应数量图形', category: 'math', page: 'countMatch', subpackage: 'mathPages', icon: '🔗', img: '/assets/mathEntrance/count-match.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e8f5e9', previewText: '3↔🍎🍎🍎' },
|
||||
{ subcategory: 'counting', template: 'match-connect', generator: 'counting', sortOrder: 4, layoutConfig: { ...L.gridEx }, generatorConfig: { functionId: 'counting-matching' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'number-object-match', title: '数物连线', desc: '连线相同数量的物品和数字', category: 'math', page: 'numberObjectMatch', subpackage: 'mathPages', icon: '🔗', img: '/assets/mathEntrance/number-object-match.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e8f5e9', previewText: '🔗' },
|
||||
{ subcategory: 'counting', template: 'match-connect', generator: 'counting', sortOrder: 5, generatorConfig: { functionId: 'number-object-match' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'number-object-fill', title: '数物填写', desc: '数物品数量,填写对应数字', category: 'math', page: 'numberObjectMatch', subpackage: 'mathPages', icon: '✏️', img: '/assets/mathEntrance/number-object-fill.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e8f5e9', previewText: '✏️' },
|
||||
{ subcategory: 'counting', template: 'grid-exercise', generator: 'counting', sortOrder: 6, generatorConfig: { functionId: 'number-object-fill' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'counting-select', title: '数一数,选一选', desc: '数出物品数量,圈出正确答案', category: 'math', page: 'countingSelect', subpackage: 'mathPages', icon: '✓', img: '/assets/mathEntrance/counting-select.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e8f5e9', previewText: '✓' },
|
||||
{ subcategory: 'counting', template: 'grid-exercise', generator: 'counting', sortOrder: 7, generatorConfig: { functionId: 'counting-select' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'counting-fill', title: '数一数,填一填', desc: '数出物品数量,填写数字', category: 'math', page: 'countingSelect', subpackage: 'mathPages', icon: '✏️', img: '/assets/mathEntrance/counting-fill.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e8f5e9', previewText: '✏️' },
|
||||
{ subcategory: 'counting', template: 'grid-exercise', generator: 'counting', sortOrder: 8, generatorConfig: { functionId: 'counting-fill' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'compare', title: '数一数,比大小', desc: '比较数量,填入 ><=', category: 'math', page: 'compare', subpackage: 'mathPages', icon: '⚖️', img: '/assets/mathEntrance/compare.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e3f2fd', previewText: '3 ○ 5' },
|
||||
{ subcategory: 'counting', template: 'grid-exercise', generator: 'comparison', sortOrder: 9, generatorConfig: { functionId: 'compare' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'number-sort', title: '数字排序', desc: '写出正确的数字顺序', category: 'math', page: 'numberSort', subpackage: 'mathPages', icon: '🔢', img: '/assets/mathEntrance/number-sort.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#fff3e0', previewText: '1→2→3' },
|
||||
{ subcategory: 'sequence', template: 'sequence-pattern', generator: 'number-sequence', sortOrder: 10, generatorConfig: { functionId: 'number-sort' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'missing-number', title: '填上缺少的数字', desc: '在数列中找出并填写缺失数字', category: 'math', page: 'missingNumber', subpackage: 'mathPages', icon: '❓', img: '/assets/mathEntrance/missing-number.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#fff3e0', previewText: '1_3_5' },
|
||||
{ subcategory: 'sequence', template: 'sequence-pattern', generator: 'number-sequence', sortOrder: 11, generatorConfig: { functionId: 'missing-number' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'number-decompose', title: '10以内数的分与合', desc: '把数字分一分,合一合', category: 'math', page: 'numberDecompose', subpackage: 'mathPages', icon: '🔢', img: '/assets/mathEntrance/number-decompose.png', ageRange: [4, 6], difficulty: 'beginner', previewBg: '#fff3e0', previewText: '8=?+?' },
|
||||
{ subcategory: 'decompose', template: 'grid-exercise', generator: 'number-decompose', sortOrder: 12, generatorConfig: { max: 10 } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'number-decompose-20', title: '20以内数的分与合', desc: '把数字分一分,合一合', category: 'math', page: 'numberDecompose', subpackage: 'mathPages', icon: '🔢', img: '/assets/mathEntrance/number-decompose-20.png', ageRange: [5, 7], difficulty: 'basic', previewBg: '#fff3e0', previewText: '15=?+?' },
|
||||
{ subcategory: 'decompose', template: 'grid-exercise', generator: 'number-decompose', sortOrder: 13, generatorConfig: { max: 20 } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'one-digit-addition', title: '一位数加法', desc: '通过圆点学习一位数加法运算', category: 'math', page: 'oneDigitAddition', subpackage: 'mathPages', icon: '➕', img: '/assets/mathEntrance/one-digit-addition.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e3f2fd', previewText: '2+3=?' },
|
||||
{ subcategory: 'arithmetic-visual', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 14, generatorConfig: { operators: ['+'], maxNumber: 9, showDots: true } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'addition-5', title: '5以内加法', desc: '图形化展示 5 以内加法', category: 'math', page: 'addition', subpackage: 'mathPages', icon: '➕', img: '/assets/mathEntrance/addition-5.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e3f2fd', previewText: '2+1=?' },
|
||||
{ subcategory: 'arithmetic-visual', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 15, generatorConfig: { operators: ['+'], maxNumber: 5, showDots: true } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'addition-10', title: '10以内加法', desc: '图形化展示 10 以内加法', category: 'math', page: 'addition', subpackage: 'mathPages', icon: '➕', img: '/assets/mathEntrance/addition-10.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e3f2fd', previewText: '3+5=?' },
|
||||
{ subcategory: 'arithmetic-visual', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 16, generatorConfig: { operators: ['+'], maxNumber: 10, showDots: true } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'subtraction-10', title: '10以内减法', desc: '图形化展示 10 以内减法', category: 'math', page: 'addition', subpackage: 'mathPages', icon: '➖', img: '/assets/mathEntrance/subtraction-10.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e3f2fd', previewText: '8-3=?' },
|
||||
{ subcategory: 'arithmetic-visual', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 17, generatorConfig: { operators: ['-'], maxNumber: 10, showDots: true } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'addition-subtraction-10', title: '10以内加减法', desc: '加减法混合运算', category: 'math', page: 'addition', subpackage: 'mathPages', icon: '±', img: '/assets/mathEntrance/addition-subtraction-10.png', ageRange: [5, 7], difficulty: 'basic', previewBg: '#e3f2fd', previewText: '±' },
|
||||
{ subcategory: 'arithmetic-visual', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 18, generatorConfig: { operators: ['+', '-'], maxNumber: 10, showDots: true } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'make-ten', title: '凑十法练习', desc: '20 以内进位加法', category: 'math', page: 'makeTen', subpackage: 'mathPages', icon: '➕', img: '/assets/mathEntrance/make-ten.png', ageRange: [5, 7], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '7+?=10' },
|
||||
{ subcategory: 'methods-ten', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 19, generatorConfig: { method: 'make-ten' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'break-ten', title: '破十法练习', desc: '20 以内退位减法', category: 'math', page: 'breakTen', subpackage: 'mathPages', icon: '➖', img: '/assets/mathEntrance/break-ten.png', ageRange: [5, 7], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '15-8=?' },
|
||||
{ subcategory: 'methods-ten', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 20, generatorConfig: { method: 'break-ten' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'flat-ten', title: '平十法练习', desc: '20 以内退位减法', category: 'math', page: 'flatTen', subpackage: 'mathPages', icon: '➖', img: '/assets/mathEntrance/flat-ten.png', ageRange: [5, 7], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '14-6=?' },
|
||||
{ subcategory: 'methods-ten', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 21, generatorConfig: { method: 'flat-ten' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'borrow-ten', title: '借十法练习', desc: '20 以上退位减法', category: 'math', page: 'borrowTen', subpackage: 'mathPages', icon: '➖', img: '/assets/mathEntrance/borrow-ten.png', ageRange: [6, 8], difficulty: 'advanced', previewBg: '#e3f2fd', previewText: '32-8=?' },
|
||||
{ subcategory: 'methods-ten', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 22, generatorConfig: { method: 'borrow-ten' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'practice-addition', title: '加法运算', desc: '10/20/50/100 以内加法', category: 'math', page: 'calculationPractice', subpackage: 'mathPages', icon: '➕', img: '/assets/mathEntrance/practice-addition.png', ageRange: [5, 8], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '25+18=?' },
|
||||
{ subcategory: 'practice', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 23, generatorConfig: { operators: ['+'], preset: 'vertical' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'practice-subtraction', title: '减法运算', desc: '10/20/50/100 以内减法', category: 'math', page: 'calculationPractice', subpackage: 'mathPages', icon: '➖', img: '/assets/mathEntrance/practice-subtraction.png', ageRange: [5, 8], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '43-17=?' },
|
||||
{ subcategory: 'practice', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 24, generatorConfig: { operators: ['-'], preset: 'vertical' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'practice-mixed', title: '混合运算', desc: '10/20/50/100 以内加减法混合', category: 'math', page: 'calculationPractice', subpackage: 'mathPages', icon: '±', img: '/assets/mathEntrance/practice-mixed.png', ageRange: [5, 8], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '±' },
|
||||
{ subcategory: 'practice', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 25, generatorConfig: { operators: ['+', '-'], preset: 'vertical' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'multiplication-table', title: '九九乘法表', desc: '学习九九乘法口诀', category: 'math', page: 'multiplicationTable', subpackage: 'mathPages', icon: '✖️', img: '/assets/mathEntrance/multiplication-table.png', ageRange: [6, 8], difficulty: 'intermediate', previewBg: '#f3e5f5', previewText: '3×4=12' },
|
||||
{ subcategory: 'practice', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 26, generatorConfig: { operators: ['×'], mode: 'multiplication-table' } },
|
||||
),
|
||||
|
||||
sheet(
|
||||
{ id: 'color-shape-match', title: '根据颜色画图形', desc: '根据颜色画出对应图形', category: 'focus', page: 'colorShapeMatch', subpackage: 'focusPages', icon: '🎯', img: '/assets/focusEntrance/color-shape-match.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#f3e5f5', previewText: '🎯' },
|
||||
{ subcategory: 'focus-visual', template: 'grid-exercise', generator: 'shape-grid', sortOrder: 27, generatorConfig: { functionId: 'color-shape-match' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'shape-symbol', title: '图形符号配对', desc: '根据图形画对应符号', category: 'focus', page: 'shapeSymbol', subpackage: 'focusPages', icon: '🔗', img: '/assets/focusEntrance/shape-symbol.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e8eaf6', previewText: '△→♠' },
|
||||
{ subcategory: 'focus-visual', template: 'match-connect', generator: 'shape-grid', sortOrder: 28, generatorConfig: { functionId: 'shape-symbol' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'shape-recognition', title: '识别形状', desc: '识别形状,涂一涂', category: 'focus', page: 'shape', subpackage: 'focusPages', icon: '🔍', img: '/assets/focusEntrance/shape-recognition.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#fce4ec', previewText: '△ ○ □' },
|
||||
{ subcategory: 'focus-visual', template: 'grid-coloring', generator: 'shape-grid', sortOrder: 29, generatorConfig: { functionId: 'shape-recognition' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'position-coloring', title: '方位涂涂乐', desc: '观察位置,在方格中涂色', category: 'focus', page: 'positionColoring', subpackage: 'focusPages', icon: '📍', img: '/assets/focusEntrance/position-coloring.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e8f5e9', previewText: '📍' },
|
||||
{ subcategory: 'focus-grid', template: 'grid-coloring', generator: 'shape-grid', sortOrder: 30, generatorConfig: { functionId: 'position-coloring' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'color-pattern', title: '颜色找规律', desc: '观察颜色规律,在空白图形中涂色', category: 'focus', page: 'colorPattern', subpackage: 'focusPages', icon: '🎨', img: '/assets/focusEntrance/color-pattern.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#fff8e1', previewText: '🎨' },
|
||||
{ subcategory: 'focus-grid', template: 'grid-coloring', generator: 'color-pattern', sortOrder: 31, generatorConfig: { functionId: 'color-pattern' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'match-connect', title: '连连看', desc: '根据物品连一连', category: 'focus', page: 'matchConnect', subpackage: 'focusPages', icon: '🔗', img: '/assets/focusEntrance/match-connect.png', ageRange: [3, 6], difficulty: 'beginner', previewBg: '#e0f7fa', previewText: '🔗' },
|
||||
{ subcategory: 'focus-connect', template: 'match-connect', generator: 'shape-grid', sortOrder: 32, generatorConfig: { functionId: 'match-connect' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'line-recognition', title: '线条识别', desc: '认识不同线条,画出颜色对应的线条', category: 'focus', page: 'lineRecognition', subpackage: 'focusPages', icon: '📏', img: '/assets/focusEntrance/line-recognition.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#f1f8e9', previewText: '〰️' },
|
||||
{ subcategory: 'focus-visual', template: 'tracing-writing', generator: 'shape-grid', sortOrder: 33, generatorConfig: { functionId: 'line-recognition' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'grid-reasoning', title: '方格推理', desc: '推理出合并方格并连线', category: 'focus', page: 'gridReasoning', subpackage: 'focusPages', icon: '🧩', img: '/assets/focusEntrance/grid-reasoning.png', ageRange: [5, 7], difficulty: 'intermediate', previewBg: '#e8eaf6', previewText: '🧩' },
|
||||
{ subcategory: 'focus-logic', template: 'special-graphic', generator: 'shape-grid', sortOrder: 34, generatorConfig: { functionId: 'grid-reasoning' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'code-connect', title: '译码连线', desc: '按数字顺序将数字对应颜色连线', category: 'focus', page: 'codeConnect', subpackage: 'focusPages', icon: '🔢', img: '/assets/focusEntrance/code-connect.png', ageRange: [5, 7], difficulty: 'intermediate', previewBg: '#fff3e0', previewText: '🔢' },
|
||||
{ subcategory: 'focus-connect', template: 'match-connect', generator: 'dot-connect', sortOrder: 35, generatorConfig: { functionId: 'code-connect' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'dot-connect', title: '数字点连线', desc: '按数字顺序连点成图', category: 'focus', page: 'dotConnect', subpackage: 'focusPages', icon: '🔗', img: '/assets/focusEntrance/dot-connect.png', ageRange: [3, 6], difficulty: 'beginner', previewBg: '#e3f2fd', previewText: '1→2→3' },
|
||||
{ subcategory: 'focus-connect', template: 'match-connect', generator: 'dot-connect', sortOrder: 36, generatorConfig: { functionId: 'dot-connect' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'grid-drawing-3x3', title: '格子仿画 3×3', desc: '简单有趣,培养专注力', category: 'focus', page: 'gridDrawing', subpackage: 'focusPages', icon: '🎨', mode: '3x3', img: '/assets/focusEntrance/grid-drawing-3x3.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#fce4ec', previewText: '▦' },
|
||||
{ subcategory: 'grid-copy', template: 'grid-exercise', generator: 'shape-grid', sortOrder: 37, generatorConfig: { grid: '3x3' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'grid-drawing-5x5', title: '格子仿画 5×5', desc: '创意挑战,提升观察力', category: 'focus', page: 'gridDrawing', subpackage: 'focusPages', icon: '🎨', mode: '5x5', img: '/assets/focusEntrance/grid-drawing-5x5.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#fce4ec', previewText: '▦' },
|
||||
{ subcategory: 'grid-copy', template: 'grid-exercise', generator: 'shape-grid', sortOrder: 38, generatorConfig: { grid: '5x5' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'grid-drawing-7x7', title: '格子仿画 7×7', desc: '大师挑战,锻炼耐心', category: 'focus', page: 'gridDrawing', subpackage: 'focusPages', icon: '🎨', mode: '7x7', img: '/assets/focusEntrance/grid-drawing-7x7.png', ageRange: [5, 8], difficulty: 'intermediate', previewBg: '#fce4ec', previewText: '▦' },
|
||||
{ subcategory: 'grid-copy', template: 'grid-exercise', generator: 'shape-grid', sortOrder: 39, generatorConfig: { grid: '7x7' } },
|
||||
),
|
||||
|
||||
sheet(
|
||||
{ id: 'word-recognition', title: '识字卡', desc: '输入或选字生成涂色识字卡(grid/find 模板)', category: 'chinese', page: 'index', subpackage: '', icon: '📖', img: '', ageRange: [3, 6], difficulty: 'beginner', previewBg: '#fce4ec', previewText: '大 小' },
|
||||
{ subcategory: 'literacy', template: 'card-layout', generator: 'custom', sortOrder: 40, generatorConfig: { functionId: 'word-recognition' }, layoutConfig: { ...L.card } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'copybook', title: '练字帖', desc: '选字生成田字格笔顺练字帖', category: 'chinese', page: 'copyBook', subpackage: '', icon: '✏️', img: '', ageRange: [4, 7], difficulty: 'basic', previewBg: '#fff8e1', previewText: '横竖撇' },
|
||||
{ subcategory: 'copybook', template: 'tracing-writing', generator: 'character-tracing', sortOrder: 41, generatorConfig: { functionId: 'copybook' }, layoutConfig: { ...L.trace } },
|
||||
),
|
||||
];
|
||||
|
||||
// prettier-ignore
|
||||
export const FOCUS_FUNCTION_TYPES: FocusFunctionType[] = [
|
||||
{ id: 'color-shape-match', page: 'colorShapeMatch', title: '根据颜色画图形', desc: '根据颜色画出对应的图形', icon: '🎯', img: '/assets/focusEntrance/color-shape-match.png' },
|
||||
{ id: 'shape-symbol', page: 'shapeSymbol', title: '图形符号配对', desc: '根据图形画对应的符号', icon: '🔗', img: '/assets/focusEntrance/shape-symbol.png' },
|
||||
{ id: 'shape-recognition', page: 'shape', title: '识别形状', desc: '识别形状,涂一涂', icon: '🔍', img: '/assets/focusEntrance/shape-recognition.png' },
|
||||
{ id: 'position-coloring', page: 'positionColoring', title: '方位涂涂乐', desc: '观察卡片位置,在对应的方格中涂上颜色', icon: '📍', img: '/assets/focusEntrance/position-coloring.png' },
|
||||
{ id: 'color-pattern', page: 'colorPattern', title: '颜色找规律', desc: '观察颜色规律,在空白图形中涂上颜色', icon: '🎨', img: '/assets/focusEntrance/color-pattern.png' },
|
||||
{ id: 'match-connect', page: 'matchConnect', title: '连连看', desc: '快来根据物品连一连吧!', icon: '🔗', img: '/assets/focusEntrance/match-connect.png' },
|
||||
{ id: 'line-recognition', page: 'lineRecognition', title: '线条识别', desc: '认识不同线条,画出颜色对应的线条', icon: '📏', img: '/assets/focusEntrance/line-recognition.png' },
|
||||
{ id: 'grid-reasoning', page: 'gridReasoning', title: '方格推理', desc: '仔细观察,推理出合并方格并连线', icon: '🧩', img: '/assets/focusEntrance/grid-reasoning.png' },
|
||||
{ id: 'code-connect', page: 'codeConnect', title: '译码连线', desc: '按照数字顺序,将数字对应的颜色连线', icon: '🔢', img: '/assets/focusEntrance/code-connect.png' },
|
||||
{ id: 'dot-connect', page: 'dotConnect', title: '数字点连线', desc: '按数字顺序连点', icon: '🔗', img: '/assets/focusEntrance/dot-connect.png' },
|
||||
{ id: 'grid-drawing-3x3', page: 'gridDrawing', title: '格子仿画 3x3', desc: '🎯 简单有趣,培养专注力', icon: '🎨', mode: '3x3', img: '/assets/focusEntrance/grid-drawing-3x3.png' },
|
||||
{ id: 'grid-drawing-5x5', page: 'gridDrawing', title: '格子仿画 5x5', desc: '✨ 创意挑战,提升观察力', icon: '🎨', mode: '5x5', img: '/assets/focusEntrance/grid-drawing-5x5.png' },
|
||||
{ id: 'grid-drawing-7x7', page: 'gridDrawing', title: '格子仿画 7x7', desc: '🌟 大师挑战,锻炼耐心', icon: '🎨', mode: '7x7', img: '/assets/focusEntrance/grid-drawing-7x7.png' },
|
||||
];
|
||||
export const MATH_FUNCTION_TYPES: MathFunctionType[] = ALL_WORKSHEETS
|
||||
.filter((w) => w.category === 'math')
|
||||
.sort((a, b) => a.sortOrder - b.sortOrder)
|
||||
.map((w) => ({
|
||||
id: w.id,
|
||||
page: w.page,
|
||||
title: w.title,
|
||||
desc: w.desc,
|
||||
icon: w.icon,
|
||||
img: w.img,
|
||||
}));
|
||||
|
||||
/**
|
||||
* 统一的 WorksheetType 列表 - 合并所有分类
|
||||
* 用于新版首页和分类页展示
|
||||
*/
|
||||
// prettier-ignore
|
||||
export const ALL_WORKSHEETS: WorksheetType[] = [
|
||||
// === 数学 ===
|
||||
{ id: 'number-sort', title: '数字排序', desc: '数字排序,写出正确顺序', category: 'math', page: 'numberSort', subpackage: 'mathPages', icon: '🔢', img: '/assets/mathEntrance/number-sort.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#fff3e0', previewText: '1→2→3' },
|
||||
{ id: 'one-digit-addition', title: '一位数加法', desc: '通过圆点学习一位数加法运算', category: 'math', page: 'oneDigitAddition', subpackage: 'mathPages', icon: '➕', img: '/assets/mathEntrance/one-digit-addition.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e3f2fd', previewText: '2+3=?' },
|
||||
{ id: 'make-ten', title: '凑十法练习', desc: '通过凑十法学习20以内进位加法', category: 'math', page: 'makeTen', subpackage: 'mathPages', icon: '➕', img: '/assets/mathEntrance/make-ten.png', ageRange: [5, 7], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '7+?=10' },
|
||||
{ id: 'break-ten', title: '破十法练习', desc: '通过破十法学习20以内退位减法', category: 'math', page: 'breakTen', subpackage: 'mathPages', icon: '➖', img: '/assets/mathEntrance/break-ten.png', ageRange: [5, 7], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '15-8=?' },
|
||||
{ id: 'flat-ten', title: '平十法练习', desc: '通过平十法学习20以内退位减法', category: 'math', page: 'flatTen', subpackage: 'mathPages', icon: '➖', img: '/assets/mathEntrance/flat-ten.png', ageRange: [5, 7], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '14-6=?' },
|
||||
{ id: 'borrow-ten', title: '借十法练习', desc: '通过借十法学习20以上退位减法', category: 'math', page: 'borrowTen', subpackage: 'mathPages', icon: '➖', img: '/assets/mathEntrance/borrow-ten.png', ageRange: [6, 8], difficulty: 'advanced', previewBg: '#e3f2fd', previewText: '32-8=?' },
|
||||
{ id: 'number-decompose-20', title: '20以内分与合', desc: '把数字分一分,合一合', category: 'math', page: 'numberDecompose', subpackage: 'mathPages', icon: '🔢', img: '/assets/mathEntrance/number-decompose-20.png', ageRange: [5, 7], difficulty: 'basic', previewBg: '#fff3e0', previewText: '15=?+?' },
|
||||
{ id: 'number-decompose', title: '10以内分与合', desc: '把数字分一分,合一合', category: 'math', page: 'numberDecompose', subpackage: 'mathPages', icon: '🔢', img: '/assets/mathEntrance/number-decompose.png', ageRange: [4, 6], difficulty: 'beginner', previewBg: '#fff3e0', previewText: '8=?+?' },
|
||||
{ id: 'number-find', title: '找数字涂一涂', desc: '找一找下面相同的数字,涂上颜色', category: 'math', page: 'numberFind', subpackage: 'mathPages', img: '/assets/mathEntrance/number-find.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#fff3e0', previewText: '🔍 1 2 3' },
|
||||
{ id: 'number-write', title: '看数字写一写', desc: '看数字,按笔画顺序写一写', category: 'math', page: 'numberFind', subpackage: 'mathPages', img: '/assets/mathEntrance/number-write.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#fff3e0', previewText: '1 2 3' },
|
||||
{ id: 'number-coloring', title: '按数字涂颜色', desc: '按数字给相应的圆圈涂上颜色', category: 'math', page: 'countMatch', subpackage: 'mathPages', icon: '🎨', img: '/assets/mathEntrance/number-coloring.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#fce4ec', previewText: '🎨' },
|
||||
{ id: 'counting-matching', title: '数一数连一连', desc: '通过连线配对数字和对应的数量图形', category: 'math', page: 'countMatch', subpackage: 'mathPages', icon: '🔗', img: '/assets/mathEntrance/count-match.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e8f5e9', previewText: '3↔🍎🍎🍎' },
|
||||
{ id: 'number-object-match', title: '数物连线', desc: '连线相同数量的物品和数字', category: 'math', page: 'numberObjectMatch', subpackage: 'mathPages', icon: '🔗', img: '/assets/mathEntrance/number-object-match.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e8f5e9', previewText: '🔗' },
|
||||
{ id: 'number-object-fill', title: '数物填写', desc: '数一数物品数量,填写对应的数字', category: 'math', page: 'numberObjectMatch', subpackage: 'mathPages', icon: '✏️', img: '/assets/mathEntrance/number-object-fill.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e8f5e9', previewText: '✏️' },
|
||||
{ id: 'counting-select', title: '数一数选一选', desc: '数出物品数量,圈出正确答案', category: 'math', page: 'countingSelect', subpackage: 'mathPages', icon: '✓', img: '/assets/mathEntrance/counting-select.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e8f5e9', previewText: '✓' },
|
||||
{ id: 'counting-fill', title: '数一数填一填', desc: '数出物品数量,填写对应的数字', category: 'math', page: 'countingSelect', subpackage: 'mathPages', icon: '✏️', img: '/assets/mathEntrance/counting-fill.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e8f5e9', previewText: '✏️' },
|
||||
{ id: 'compare', title: '比大小', desc: '比较数量大小,在 ⭕️ 中填入>、<、=', category: 'math', page: 'compare', subpackage: 'mathPages', icon: '⚖️', img: '/assets/mathEntrance/compare.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e3f2fd', previewText: '3 ○ 5' },
|
||||
{ id: 'missing-number', title: '填缺少的数字', desc: '在数字序列中找出并填写缺失的数字', category: 'math', page: 'missingNumber', subpackage: 'mathPages', icon: '❓', img: '/assets/mathEntrance/missing-number.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#fff3e0', previewText: '1_3_5' },
|
||||
{ id: 'addition-5', title: '5以内加法', desc: '练习5以内的加法运算', category: 'math', page: 'addition', subpackage: 'mathPages', icon: '➕', img: '/assets/mathEntrance/addition-5.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e3f2fd', previewText: '2+1=?' },
|
||||
{ id: 'addition-10', title: '10以内加法', desc: '练习10以内的加法运算', category: 'math', page: 'addition', subpackage: 'mathPages', icon: '➕', img: '/assets/mathEntrance/addition-10.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e3f2fd', previewText: '3+5=?' },
|
||||
{ id: 'subtraction-10', title: '10以内减法', desc: '练习10以内的减法运算', category: 'math', page: 'addition', subpackage: 'mathPages', icon: '➖', img: '/assets/mathEntrance/subtraction-10.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e3f2fd', previewText: '8-3=?' },
|
||||
{ id: 'addition-subtraction-10', title: '10以内加减法', desc: '练习加法和减法混合运算', category: 'math', page: 'addition', subpackage: 'mathPages', icon: '±', img: '/assets/mathEntrance/addition-subtraction-10.png', ageRange: [5, 7], difficulty: 'basic', previewBg: '#e3f2fd', previewText: '±' },
|
||||
{ id: 'practice-addition', title: '加法运算', desc: '练习10/20/50/100以内加法运算', category: 'math', page: 'calculationPractice', subpackage: 'mathPages', icon: '➕', img: '/assets/mathEntrance/practice-addition.png', ageRange: [5, 8], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '25+18=?' },
|
||||
{ id: 'practice-subtraction', title: '减法运算', desc: '练习10/20/50/100以内减法运算', category: 'math', page: 'calculationPractice', subpackage: 'mathPages', icon: '➖', img: '/assets/mathEntrance/practice-subtraction.png', ageRange: [5, 8], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '43-17=?' },
|
||||
{ id: 'practice-mixed', title: '混合运算', desc: '练习10/20/50/100以内加减法混合运算', category: 'math', page: 'calculationPractice', subpackage: 'mathPages', icon: '±', img: '/assets/mathEntrance/practice-mixed.png', ageRange: [5, 8], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '±' },
|
||||
{ id: 'multiplication-table', title: '九九乘法表', desc: '学习九九乘法口诀', category: 'math', page: 'multiplicationTable', subpackage: 'mathPages', icon: '✖️', img: '/assets/mathEntrance/multiplication-table.png', ageRange: [6, 8], difficulty: 'intermediate', previewBg: '#f3e5f5', previewText: '3×4=12' },
|
||||
export const FOCUS_FUNCTION_TYPES: FocusFunctionType[] = ALL_WORKSHEETS
|
||||
.filter((w) => w.category === 'focus')
|
||||
.sort((a, b) => a.sortOrder - b.sortOrder)
|
||||
.map((w) => ({
|
||||
id: w.id,
|
||||
page: w.page,
|
||||
title: w.title,
|
||||
desc: w.desc,
|
||||
icon: w.icon ?? '・',
|
||||
mode: w.mode,
|
||||
img: w.img,
|
||||
}));
|
||||
|
||||
// === 专注力 ===
|
||||
{ id: 'color-shape-match', title: '颜色画图形', desc: '根据颜色画出对应的图形', category: 'focus', page: 'colorShapeMatch', subpackage: 'focusPages', icon: '🎯', img: '/assets/focusEntrance/color-shape-match.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#f3e5f5', previewText: '🎯' },
|
||||
{ id: 'shape-symbol', title: '图形符号配对', desc: '根据图形画对应的符号', category: 'focus', page: 'shapeSymbol', subpackage: 'focusPages', icon: '🔗', img: '/assets/focusEntrance/shape-symbol.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e8eaf6', previewText: '△→♠' },
|
||||
{ id: 'shape-recognition', title: '识别形状', desc: '识别形状,涂一涂', category: 'focus', page: 'shape', subpackage: 'focusPages', icon: '🔍', img: '/assets/focusEntrance/shape-recognition.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#fce4ec', previewText: '△ ○ □' },
|
||||
{ id: 'position-coloring', title: '方位涂涂乐', desc: '观察卡片位置,在对应的方格中涂上颜色', category: 'focus', page: 'positionColoring', subpackage: 'focusPages', icon: '📍', img: '/assets/focusEntrance/position-coloring.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e8f5e9', previewText: '📍' },
|
||||
{ id: 'color-pattern', title: '颜色找规律', desc: '观察颜色规律,在空白图形中涂上颜色', category: 'focus', page: 'colorPattern', subpackage: 'focusPages', icon: '🎨', img: '/assets/focusEntrance/color-pattern.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#fff8e1', previewText: '🎨' },
|
||||
{ id: 'match-connect', title: '连连看', desc: '快来根据物品连一连吧!', category: 'focus', page: 'matchConnect', subpackage: 'focusPages', icon: '🔗', img: '/assets/focusEntrance/match-connect.png', ageRange: [3, 6], difficulty: 'beginner', previewBg: '#e0f7fa', previewText: '🔗' },
|
||||
{ id: 'line-recognition', title: '线条识别', desc: '认识不同线条,画出颜色对应的线条', category: 'focus', page: 'lineRecognition', subpackage: 'focusPages', icon: '📏', img: '/assets/focusEntrance/line-recognition.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#f1f8e9', previewText: '〰️' },
|
||||
{ id: 'grid-reasoning', title: '方格推理', desc: '仔细观察,推理出合并方格并连线', category: 'focus', page: 'gridReasoning', subpackage: 'focusPages', icon: '🧩', img: '/assets/focusEntrance/grid-reasoning.png', ageRange: [5, 7], difficulty: 'intermediate', previewBg: '#e8eaf6', previewText: '🧩' },
|
||||
{ id: 'code-connect', title: '译码连线', desc: '按照数字顺序,将数字对应的颜色连线', category: 'focus', page: 'codeConnect', subpackage: 'focusPages', icon: '🔢', img: '/assets/focusEntrance/code-connect.png', ageRange: [5, 7], difficulty: 'intermediate', previewBg: '#fff3e0', previewText: '🔢' },
|
||||
{ id: 'dot-connect', title: '数字点连线', desc: '按数字顺序连点', category: 'focus', page: 'dotConnect', subpackage: 'focusPages', icon: '🔗', img: '/assets/focusEntrance/dot-connect.png', ageRange: [3, 6], difficulty: 'beginner', previewBg: '#e3f2fd', previewText: '1→2→3' },
|
||||
{ id: 'grid-drawing-3x3', title: '格子仿画 3x3', desc: '简单有趣,培养专注力', category: 'focus', page: 'gridDrawing', subpackage: 'focusPages', icon: '🎨', mode: '3x3', img: '/assets/focusEntrance/grid-drawing-3x3.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#fce4ec', previewText: '▦' },
|
||||
{ id: 'grid-drawing-5x5', title: '格子仿画 5x5', desc: '创意挑战,提升观察力', category: 'focus', page: 'gridDrawing', subpackage: 'focusPages', icon: '🎨', mode: '5x5', img: '/assets/focusEntrance/grid-drawing-5x5.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#fce4ec', previewText: '▦' },
|
||||
{ id: 'grid-drawing-7x7', title: '格子仿画 7x7', desc: '大师挑战,锻炼耐心', category: 'focus', page: 'gridDrawing', subpackage: 'focusPages', icon: '🎨', mode: '7x7', img: '/assets/focusEntrance/grid-drawing-7x7.png', ageRange: [5, 8], difficulty: 'intermediate', previewBg: '#fce4ec', previewText: '▦' },
|
||||
|
||||
// === 语文 ===
|
||||
{ id: 'word-recognition', title: '识字卡', desc: '认识汉字,选字生成识字卡', category: 'chinese', page: 'index', subpackage: '', icon: '📖', ageRange: [3, 6], difficulty: 'beginner', previewBg: '#fce4ec', previewText: '大 小' },
|
||||
{ id: 'copybook', title: '练字帖', desc: '选字生成田字格练字帖', category: 'chinese', page: 'copyBook', subpackage: '', icon: '✏️', ageRange: [4, 7], difficulty: 'basic', previewBg: '#fff8e1', previewText: '横竖撇' },
|
||||
];
|
||||
|
||||
/**
|
||||
* 按分类获取题型列表
|
||||
*/
|
||||
export function getWorksheetsByCategory(category: string): WorksheetType[] {
|
||||
export function getWorksheetsByCategory(category: string): WorksheetDefinition[] {
|
||||
if (category === 'all') return ALL_WORKSHEETS;
|
||||
return ALL_WORKSHEETS.filter((w) => w.category === category);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按年龄范围筛选
|
||||
*/
|
||||
export function getWorksheetsByAge(worksheets: WorksheetType[], age: number): WorksheetType[] {
|
||||
export function getWorksheetsByAge<T extends WorksheetType>(
|
||||
worksheets: readonly T[],
|
||||
age: number,
|
||||
): T[] {
|
||||
return worksheets.filter((w) => {
|
||||
if (!w.ageRange) return true;
|
||||
return age >= w.ageRange[0] && age <= w.ageRange[1];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取题型的导航路径
|
||||
*/
|
||||
export function getWorksheetPath(worksheet: WorksheetType): string {
|
||||
if (!worksheet.page) return '';
|
||||
if (worksheet.subpackage) {
|
||||
@@ -154,3 +303,7 @@ export function getWorksheetPath(worksheet: WorksheetType): string {
|
||||
}
|
||||
return `/pages/${worksheet.page}/${worksheet.page}`;
|
||||
}
|
||||
|
||||
export function getWorksheetById(id: string): WorksheetDefinition | undefined {
|
||||
return ALL_WORKSHEETS.find((w) => w.id === id);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { WorksheetCategory } from './worksheet';
|
||||
|
||||
/** Tab / 首页用的分类展示模型 */
|
||||
export interface CategoryType {
|
||||
id: WorksheetCategory;
|
||||
name: string;
|
||||
icon: string;
|
||||
color: string;
|
||||
/** 分类用于背景或装饰的渐变色,数组中为渐变起止的两个颜色值 */
|
||||
gradient: [string, string];
|
||||
}
|
||||
|
||||
/** 云集合 `categories` 文档(与小程序云开发方案 §3.4 对齐) */
|
||||
export interface CategoryRecord {
|
||||
_id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
color: string;
|
||||
sortOrder: number;
|
||||
parentId?: string | null;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/** 云集合 `download_logs` 文档(§3.7) */
|
||||
export interface DownloadLogRecord {
|
||||
_id: string;
|
||||
userId: string;
|
||||
worksheetId: string;
|
||||
params?: Record<string, unknown> | null;
|
||||
createdAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/** 云集合 `favorites` 文档(§3.6) */
|
||||
export interface FavoriteRecord {
|
||||
_id: string;
|
||||
userId: string;
|
||||
worksheetId: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/** 云集合 `feedback` 文档(§3.8) */
|
||||
export interface FeedbackRecord {
|
||||
_id: string;
|
||||
userId: string;
|
||||
content: string;
|
||||
contact?: string | null;
|
||||
createdAt: Date;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/** 云集合 `learning_plans` 文档(§3.9);weeks 结构与后端 WeekPlan[] 一致,此处用 unknown[] 保持 core 零依赖 */
|
||||
export interface LearningPlanRecord {
|
||||
_id: string;
|
||||
ageMin: number;
|
||||
ageMax: number;
|
||||
ageLabel: string;
|
||||
milestones: string[];
|
||||
weeks: unknown[];
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/** 云集合 `users` 文档(与小程序云开发方案 §3.5 对齐) */
|
||||
export interface UserRecord {
|
||||
_id: string;
|
||||
openid: string;
|
||||
unionid?: string | null;
|
||||
nickName?: string | null;
|
||||
avatarUrl?: string | null;
|
||||
totalDownloads: number;
|
||||
createdAt: Date;
|
||||
lastActiveAt: Date;
|
||||
}
|
||||
@@ -1,6 +1,116 @@
|
||||
export type WorksheetCategory = 'math' | 'focus' | 'chinese' | 'english' | 'puzzle' | 'craft';
|
||||
export type DifficultyLevel = 'beginner' | 'basic' | 'intermediate' | 'advanced';
|
||||
/**
|
||||
* 题型与模板引擎相关模型:对齐技术架构 §5.4 与云开发 worksheets 集合 §3.3
|
||||
*/
|
||||
|
||||
/** 小程序现状:专注力单独成类;云端 category 枚举无 focus 时可映射为 puzzle + subcategory */
|
||||
export type WorksheetCategory =
|
||||
| 'math'
|
||||
| 'focus'
|
||||
| 'chinese'
|
||||
| 'english'
|
||||
| 'puzzle'
|
||||
| 'craft';
|
||||
|
||||
/** 云 worksheets.category 严格枚举(§3.3) */
|
||||
export type WorksheetCloudCategory =
|
||||
| 'math'
|
||||
| 'chinese'
|
||||
| 'english'
|
||||
| 'puzzle'
|
||||
| 'craft';
|
||||
|
||||
export type DifficultyLevel =
|
||||
| 'beginner'
|
||||
| 'basic'
|
||||
| 'intermediate'
|
||||
| 'advanced';
|
||||
|
||||
export type WorksheetStatus = 'active' | 'draft' | 'hidden';
|
||||
|
||||
/** 排版模板(技术架构 §5.4) */
|
||||
export type TemplateType =
|
||||
| 'grid-exercise'
|
||||
| 'match-connect'
|
||||
| 'grid-coloring'
|
||||
| 'card-layout'
|
||||
| 'tracing-writing'
|
||||
| 'full-page-asset'
|
||||
| 'sequence-pattern'
|
||||
| 'special-graphic';
|
||||
|
||||
/** 数据生成器(技术架构 §5.4) */
|
||||
export type GeneratorType =
|
||||
| 'arithmetic'
|
||||
| 'number-sequence'
|
||||
| 'number-decompose'
|
||||
| 'counting'
|
||||
| 'comparison'
|
||||
| 'shape-grid'
|
||||
| 'color-pattern'
|
||||
| 'character-tracing'
|
||||
| 'letter-tracing'
|
||||
| 'pinyin-tracing'
|
||||
| 'static-asset'
|
||||
| 'maze'
|
||||
| 'dot-connect'
|
||||
| 'clock'
|
||||
| 'custom';
|
||||
|
||||
export interface LayoutConfig {
|
||||
columns?: number;
|
||||
rows?: number;
|
||||
fontSize?: number;
|
||||
showBorder?: boolean;
|
||||
showTitle?: boolean;
|
||||
padding?: number;
|
||||
itemSpacing?: number;
|
||||
showInstruction?: boolean;
|
||||
instructionText?: string;
|
||||
}
|
||||
|
||||
export interface UserConfigField {
|
||||
key: string;
|
||||
label: string;
|
||||
type: 'select' | 'slider' | 'switch';
|
||||
options?: { label: string; value: unknown }[];
|
||||
min?: number;
|
||||
max?: number;
|
||||
defaultValue: unknown;
|
||||
}
|
||||
|
||||
/** 与架构文档 WorksheetConfig 一致,供云端下发 / 本地内置 */
|
||||
export interface WorksheetConfig {
|
||||
id: string;
|
||||
title: string;
|
||||
desc: string;
|
||||
category: WorksheetCloudCategory;
|
||||
subcategory: string;
|
||||
ageMin: number;
|
||||
ageMax: number;
|
||||
difficulty: 1 | 2 | 3 | 4;
|
||||
previewImage: string;
|
||||
tags: string[];
|
||||
isNew: boolean;
|
||||
isHot: boolean;
|
||||
sortOrder: number;
|
||||
status: WorksheetStatus;
|
||||
template: TemplateType;
|
||||
generator: GeneratorType;
|
||||
generatorConfig: Record<string, unknown>;
|
||||
layoutConfig: LayoutConfig;
|
||||
userConfigurable?: UserConfigField[] | null;
|
||||
legacyPage?: string | null;
|
||||
downloadCount: number;
|
||||
}
|
||||
|
||||
/** 云集合 `worksheets` 文档(§3.3,含数据库字段) */
|
||||
export interface WorksheetRecord extends WorksheetConfig {
|
||||
_id: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
/** 列表卡片 / 导航用(兼容存量页面) */
|
||||
export interface WorksheetType {
|
||||
id: string;
|
||||
title: string;
|
||||
@@ -17,10 +127,61 @@ export interface WorksheetType {
|
||||
previewText?: string;
|
||||
}
|
||||
|
||||
export interface CategoryType {
|
||||
id: WorksheetCategory;
|
||||
name: string;
|
||||
icon: string;
|
||||
color: string;
|
||||
gradient: [string, string];
|
||||
/** 内置兜底 + 模板引擎字段(本地 core/data/worksheets) */
|
||||
export interface WorksheetDefinition extends WorksheetType {
|
||||
subcategory: string;
|
||||
ageMin: number;
|
||||
ageMax: number;
|
||||
/** 1–4,对应云 worksheets.difficulty */
|
||||
difficultyLevel: 1 | 2 | 3 | 4;
|
||||
previewImage: string;
|
||||
tags: string[];
|
||||
isNew: boolean;
|
||||
isHot: boolean;
|
||||
sortOrder: number;
|
||||
downloadCount: number;
|
||||
status: WorksheetStatus;
|
||||
template: TemplateType;
|
||||
generator: GeneratorType;
|
||||
generatorConfig: Record<string, unknown>;
|
||||
layoutConfig: LayoutConfig;
|
||||
userConfigurable?: UserConfigField[] | null;
|
||||
legacyPage?: string | null;
|
||||
}
|
||||
|
||||
/** 写入云库时的 category:focus 归为 puzzle,子类区分 */
|
||||
export function toCloudCategory(
|
||||
category: WorksheetCategory,
|
||||
): WorksheetCloudCategory {
|
||||
if (category === 'focus') return 'puzzle';
|
||||
return category;
|
||||
}
|
||||
|
||||
/** 由内置定义生成云侧 worksheets 文档形状(不含 _id/时间,供同步层补全) */
|
||||
export function worksheetDefinitionToConfig(
|
||||
def: WorksheetDefinition,
|
||||
): Omit<WorksheetRecord, '_id' | 'createdAt' | 'updatedAt'> {
|
||||
return {
|
||||
id: def.id,
|
||||
title: def.title,
|
||||
desc: def.desc,
|
||||
category: toCloudCategory(def.category),
|
||||
subcategory: def.subcategory,
|
||||
ageMin: def.ageMin,
|
||||
ageMax: def.ageMax,
|
||||
difficulty: def.difficultyLevel,
|
||||
previewImage: def.previewImage,
|
||||
tags: def.tags,
|
||||
isNew: def.isNew,
|
||||
isHot: def.isHot,
|
||||
sortOrder: def.sortOrder,
|
||||
downloadCount: def.downloadCount,
|
||||
status: def.status,
|
||||
template: def.template,
|
||||
generator: def.generator,
|
||||
generatorConfig: def.generatorConfig,
|
||||
layoutConfig: def.layoutConfig,
|
||||
userConfigurable: def.userConfigurable ?? null,
|
||||
legacyPage: def.legacyPage ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* 展示层格式化(无 i18n 框架依赖,供 UI / Canvas 文案使用)
|
||||
*/
|
||||
|
||||
/** 适龄文案,如 3-5岁 */
|
||||
export function formatAgeRangeLabel(minAge: number, maxAge: number): string {
|
||||
return `${minAge}-${maxAge}岁`;
|
||||
}
|
||||
|
||||
/** 比较符号(全角,与现有比大小题型一致) */
|
||||
export function formatCompareSymbol(
|
||||
relation: 'gt' | 'lt' | 'eq',
|
||||
): '>' | '<' | '=' {
|
||||
switch (relation) {
|
||||
case 'gt':
|
||||
return '>';
|
||||
case 'lt':
|
||||
return '<';
|
||||
default:
|
||||
return '=';
|
||||
}
|
||||
}
|
||||
|
||||
/** 数字左侧补零,如 padLeadingZero(3, 2) => "03" */
|
||||
export function padLeadingZero(num: number, width: number): string {
|
||||
const s = String(Math.trunc(Math.abs(num)));
|
||||
if (s.length >= width) return num < 0 ? `-${s}` : s;
|
||||
const pad = '0'.repeat(width - s.length);
|
||||
return num < 0 ? `-${pad}${s}` : `${pad}${s}`;
|
||||
}
|
||||
|
||||
/** 截断过长字符串 */
|
||||
export function truncateText(
|
||||
text: string,
|
||||
maxChars: number,
|
||||
ellipsis = '…',
|
||||
): string {
|
||||
if (text.length <= maxChars) return text;
|
||||
const cut = Math.max(0, maxChars - ellipsis.length);
|
||||
return text.slice(0, cut) + ellipsis;
|
||||
}
|
||||
|
||||
/** 非负整数千分位(中文场景常用逗号或空格,此处用半角逗号) */
|
||||
export function formatThousands(n: number): string {
|
||||
const i = Math.trunc(n);
|
||||
if (i < 0) return `-${formatThousands(-i)}`;
|
||||
return String(i).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* 纯数学工具(无随机、无平台依赖),供 generators / 模板引擎使用
|
||||
*/
|
||||
|
||||
/** 将数值限制在 [min, max] */
|
||||
export function clamp(value: number, min: number, max: number): number {
|
||||
return Math.min(max, Math.max(min, value));
|
||||
}
|
||||
|
||||
/** 最大公约数(非负整数) */
|
||||
export function gcd(a: number, b: number): number {
|
||||
let x = Math.abs(Math.trunc(a));
|
||||
let y = Math.abs(Math.trunc(b));
|
||||
while (y !== 0) {
|
||||
const t = y;
|
||||
y = x % y;
|
||||
x = t;
|
||||
}
|
||||
return x || 1;
|
||||
}
|
||||
|
||||
/** 最小公倍数 */
|
||||
export function lcm(a: number, b: number): number {
|
||||
const g = gcd(a, b);
|
||||
return Math.abs(Math.trunc(a) * Math.trunc(b)) / g;
|
||||
}
|
||||
|
||||
/** 闭区间 [min, max] 上的整数列表 */
|
||||
export function integerRangeInclusive(min: number, max: number): number[] {
|
||||
const lo = Math.min(min, max);
|
||||
const hi = Math.max(min, max);
|
||||
const out: number[] = [];
|
||||
for (let i = lo; i <= hi; i += 1) {
|
||||
out.push(i);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 数组求和 */
|
||||
export function sum(numbers: readonly number[]): number {
|
||||
let s = 0;
|
||||
for (let i = 0; i < numbers.length; i += 1) {
|
||||
s += numbers[i];
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
/** 算术平均(空数组为 0) */
|
||||
export function mean(numbers: readonly number[]): number {
|
||||
if (numbers.length === 0) return 0;
|
||||
return sum(numbers) / numbers.length;
|
||||
}
|
||||
|
||||
/** 各位数字之和(十进制,忽略负号) */
|
||||
export function digitSum(n: number): number {
|
||||
let v = Math.abs(Math.trunc(n));
|
||||
let s = 0;
|
||||
while (v > 0) {
|
||||
s += v % 10;
|
||||
v = Math.floor(v / 10);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
export function isEven(n: number): boolean {
|
||||
return n % 2 === 0;
|
||||
}
|
||||
|
||||
export function isOdd(n: number): boolean {
|
||||
return !isEven(n);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将浮点数舍入到指定小数位(避免 0.1+0.2 展示问题)
|
||||
*/
|
||||
export function roundToDecimals(value: number, decimals: number): number {
|
||||
const p = 10 ** decimals;
|
||||
return Math.round(value * p) / p;
|
||||
}
|
||||
@@ -13,11 +13,20 @@ export function randomPick<T>(arr: T[]): T {
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数组中随机选取 n 个不重复元素
|
||||
* 从数组中随机选取 n 个不重复元素(Fisher-Yates,无 sort 偏置)
|
||||
*/
|
||||
export function randomPickN<T>(arr: T[], n: number): T[] {
|
||||
const shuffled = [...arr].sort(() => Math.random() - 0.5);
|
||||
return shuffled.slice(0, Math.min(n, arr.length));
|
||||
export function randomPickN<T>(arr: readonly T[], n: number): T[] {
|
||||
return sampleWithoutReplacement(arr, n);
|
||||
}
|
||||
|
||||
/**
|
||||
* 无放回抽样,不改变入参数组
|
||||
*/
|
||||
export function sampleWithoutReplacement<T>(
|
||||
arr: readonly T[],
|
||||
n: number,
|
||||
): T[] {
|
||||
return shuffle([...arr]).slice(0, Math.min(n, arr.length));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
# SVG 路径分析 - 汉字笔画解析
|
||||
|
||||
## 📝 概述
|
||||
|
||||
本文档详细解析汉字练字贴中使用的 SVG 路径数据,帮助理解汉字笔画的数字化表示方法。
|
||||
|
||||
## 🎯 示例路径分析
|
||||
|
||||
### 路径片段
|
||||
|
||||
```
|
||||
"M15.66 8.1 L15.66 7.56 L17.28 4.86 L13.5 1.62 L14.04 7.56 L14.04 8.1 L9.18 32.4 L16.2 36.18 L23.76 42.12 L23.76 38.88 L10.8 31.86 Z"
|
||||
```
|
||||
|
||||
## 🔧 SVG 路径命令
|
||||
|
||||
### 基本命令
|
||||
|
||||
- **M** = MoveTo(移动到)- 开始一个新的子路径,不绘制线条
|
||||
- **L** = LineTo(画线到)- 从当前位置画直线到指定点
|
||||
- **Z** = ClosePath(闭合路径)- 闭合当前子路径,连接起点和终点
|
||||
|
||||
### 坐标系统
|
||||
|
||||
- 使用 54x54 的视窗坐标系
|
||||
- 坐标原点 (0,0) 在左上角
|
||||
- X 轴向右递增,Y 轴向下递增
|
||||
- 坐标范围:0-54
|
||||
|
||||
## 📊 逐点分析
|
||||
|
||||
| 命令 | 坐标 | 描述 | 移动方向 |
|
||||
| ---- | -------------- | -------------- | ---------- |
|
||||
| M | (15.66, 8.1) | 起始点 | - |
|
||||
| L | (15.66, 7.56) | 向上移动 | ↑ |
|
||||
| L | (17.28, 4.86) | 向右上移动 | ↗ |
|
||||
| L | (13.5, 1.62) | 向左上移动 | ↖ |
|
||||
| L | (14.04, 7.56) | 向右下移动 | ↘ |
|
||||
| L | (14.04, 8.1) | 向下移动 | ↓ |
|
||||
| L | (9.18, 32.4) | 向左下大幅移动 | ↙ |
|
||||
| L | (16.2, 36.18) | 向右下移动 | ↘ |
|
||||
| L | (23.76, 42.12) | 向右下移动 | ↘ |
|
||||
| L | (23.76, 38.88) | 向上移动 | ↑ |
|
||||
| L | (10.8, 31.86) | 向左上移动 | ↖ |
|
||||
| Z | - | 闭合路径 | 回到起始点 |
|
||||
|
||||
## 🖌️ 笔画特征分析
|
||||
|
||||
### 形状特征
|
||||
|
||||
1. **起始区域**:在坐标 (15.66, 8.1) 附近,位于画布上方
|
||||
2. **转折点**:包含多个转折,形成复杂的曲线
|
||||
3. **主要移动**:从上方区域移动到下方区域
|
||||
4. **闭合性**:使用 Z 命令闭合路径
|
||||
|
||||
### 书法特征
|
||||
|
||||
- **复杂笔画**:包含多个方向的线条变化
|
||||
- **曲线特征**:有明显的弯曲和转折
|
||||
- **可能类型**:撇捺组合、弯钩笔画或复合笔画
|
||||
|
||||
## 🎨 在汉字中的应用
|
||||
|
||||
### 汉字"好"的笔画结构
|
||||
|
||||
这个路径很可能是汉字"好"字中的某个重要笔画:
|
||||
|
||||
#### 可能的组成部分
|
||||
|
||||
1. **女字旁**:左边的"女"字的一部分
|
||||
2. **子字旁**:右边的"子"字的一部分
|
||||
3. **复合笔画**:多个基本笔画的组合
|
||||
|
||||
### 笔画顺序
|
||||
|
||||
在汉字书写中,这个笔画可能:
|
||||
|
||||
- 不是第一笔(起始位置不在左上角)
|
||||
- 包含多个书写方向的变化
|
||||
- 需要一定的书写技巧
|
||||
|
||||
## 🔍 技术实现
|
||||
|
||||
### 在代码中的使用
|
||||
|
||||
```typescript
|
||||
// SVG 路径数据
|
||||
const strokes = [
|
||||
'M15.66 8.1 L15.66 7.56 L17.28 4.86 L13.5 1.62 L14.04 7.56 L14.04 8.1 L9.18 32.4 L16.2 36.18 L23.76 42.12 L23.76 38.88 L10.8 31.86 Z',
|
||||
// ... 更多笔画
|
||||
];
|
||||
|
||||
// 绘制函数
|
||||
function drawSvgPathCommands(ctx, pathD, offsetX, offsetY, scale) {
|
||||
// 解析并绘制 SVG 路径
|
||||
}
|
||||
```
|
||||
|
||||
### 坐标变换
|
||||
|
||||
- **缩放**:根据田字格大小进行缩放
|
||||
- **偏移**:根据田字格位置进行偏移
|
||||
- **适配**:适配不同的显示尺寸
|
||||
|
||||
## 📚 相关文档
|
||||
|
||||
- [SVG 路径规范](https://www.w3.org/TR/SVG/paths.html)
|
||||
- [汉字笔画分类](https://zh.wikipedia.org/wiki/汉字笔画)
|
||||
- [田字格练字原理](./tianzige-principles.md)
|
||||
|
||||
## 🎯 总结
|
||||
|
||||
这个 SVG 路径片段展示了汉字数字化的精确表示方法:
|
||||
|
||||
1. **精确性**:每个转折点都有精确的坐标
|
||||
2. **完整性**:完整描述了笔画的形状和走向
|
||||
3. **可缩放性**:通过坐标系统支持不同尺寸的显示
|
||||
4. **书法性**:保持了汉字书法的艺术特征
|
||||
|
||||
通过这种数字化表示,我们可以在计算机上精确重现汉字的书写过程,为练字教学提供技术支持。
|
||||
|
||||
---
|
||||
|
||||
_文档创建时间:2024年_
|
||||
_最后更新:2024年_
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { WorksheetType } from '../../core/models/worksheet';
|
||||
import type {
|
||||
WorksheetDefinition,
|
||||
WorksheetType,
|
||||
} from '../../core/models/worksheet';
|
||||
import {
|
||||
getWorksheetPath,
|
||||
getWorksheetsByAge,
|
||||
@@ -63,7 +66,7 @@ function toDisplayList(list: WorksheetType[]): DisplayItem[] {
|
||||
Page({
|
||||
data: {
|
||||
title: '分类',
|
||||
sourceItems: [] as WorksheetType[],
|
||||
sourceItems: [] as WorksheetDefinition[],
|
||||
items: [] as DisplayItem[],
|
||||
ageFilters: [
|
||||
{ key: 'all', label: '全部' },
|
||||
@@ -95,13 +98,11 @@ Page({
|
||||
const minAge = options.minAge != null ? Number(options.minAge) : NaN;
|
||||
const maxAge = options.maxAge != null ? Number(options.maxAge) : NaN;
|
||||
if (!Number.isNaN(minAge) && !Number.isNaN(maxAge)) {
|
||||
const map = new Map<string, WorksheetType>();
|
||||
const map = new Map<string, WorksheetDefinition>();
|
||||
const lo = Math.min(minAge, maxAge);
|
||||
const hi = Math.max(minAge, maxAge);
|
||||
for (let age = lo; age <= hi; age += 1) {
|
||||
getWorksheetsByAge(base, age).forEach((w) =>
|
||||
map.set(w.id, w),
|
||||
);
|
||||
getWorksheetsByAge(base, age).forEach((w) => map.set(w.id, w));
|
||||
}
|
||||
base = Array.from(map.values());
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "涂鸦丫",
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarBackgroundColor": "#FEF6E7",
|
||||
"backgroundColor": "#FEF6E7",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"worksheet-card": "/components/shared/worksheet-card/worksheet-card",
|
||||
"category-tabs": "/components/shared/category-tabs/category-tabs"
|
||||
"category-tabs": "/components/shared/category-tabs/category-tabs",
|
||||
"van-icon": "@vant/weapp/icon/index"
|
||||
}
|
||||
}
|
||||
|
||||
+196
-111
@@ -1,172 +1,257 @@
|
||||
/* 首页体验配色:主题 #FFD709 / 正文 #605B50 / 底 #FEF6E7 / 按钮字 #322E25 / 标签 #91F78E */
|
||||
@import '../../style/theme3.less';
|
||||
|
||||
@home-padding: 28rpx;
|
||||
|
||||
.home-page {
|
||||
min-height: 100vh;
|
||||
background: #fef6e7;
|
||||
background: @bg-page;
|
||||
padding-bottom: 48rpx;
|
||||
box-sizing: border-box;
|
||||
color: #605b50;
|
||||
color: @text-secondary;
|
||||
// padding: 0 24rpx 48rpx;
|
||||
}
|
||||
|
||||
.home-header {
|
||||
background: #fef6e7;
|
||||
padding: 24rpx 24rpx 20rpx;
|
||||
.home-nav {
|
||||
background: @bg-header;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.home-brand {
|
||||
.home-nav__inner {
|
||||
height: 44px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.home-nav__logo {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.home-nav__name {
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
color: @text-title;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.home-search-wrap {
|
||||
margin-top: 28rpx;
|
||||
padding: 0 @home-padding;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.home-search {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.home-brand__duck {
|
||||
font-size: 48rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.home-brand__name {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #322e25;
|
||||
}
|
||||
|
||||
.home-search {
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border-radius: 40rpx;
|
||||
background: #e5dcc9;
|
||||
border-radius: 999rpx;
|
||||
padding: 22rpx 32rpx;
|
||||
border: 1rpx solid rgba(255, 215, 9, 0.35);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.home-search__icon {
|
||||
flex-shrink: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.home-search__placeholder {
|
||||
font-size: 28rpx;
|
||||
color: rgba(96, 91, 80, 0.55);
|
||||
color: rgba(50, 46, 37, 0.45);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.home-body {
|
||||
padding: 24rpx 24rpx 0;
|
||||
.home-tabs-wrap {
|
||||
// margin-top: 28rpx;
|
||||
}
|
||||
|
||||
.home-section {
|
||||
margin-bottom: 40rpx;
|
||||
.home-hero-placeholder {
|
||||
// margin: 8rpx auto 0;
|
||||
margin: 0 24rpx;
|
||||
// width: 684rpx;
|
||||
height: 480rpx;
|
||||
border-radius: 40rpx;
|
||||
background: rgba(50, 46, 37, 0.04);
|
||||
}
|
||||
|
||||
.home-section--hot {
|
||||
margin-bottom: 32rpx;
|
||||
.home-section-panel {
|
||||
margin-top: 48rpx;
|
||||
padding: 0 @home-padding;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.home-section__title-row {
|
||||
margin-bottom: 20rpx;
|
||||
.home-section-panel--hot {
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
.home-section__title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #605b50;
|
||||
}
|
||||
|
||||
.home-section__head {
|
||||
.home-section-head {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.home-section__head-left {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.home-section__icon {
|
||||
font-size: 36rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.home-section__name {
|
||||
.home-section-head__title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #605b50;
|
||||
color: @text-title;
|
||||
}
|
||||
|
||||
.home-section__more {
|
||||
font-size: 26rpx;
|
||||
color: #322e25;
|
||||
font-weight: 500;
|
||||
.home-section-head__more {
|
||||
font-size: 24rpx;
|
||||
color: @text-selected-btn;
|
||||
}
|
||||
|
||||
.home-hot__scroll {
|
||||
.home-age-scroll {
|
||||
margin-top: 32rpx;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.home-hot__row {
|
||||
.home-age-row {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
gap: 24rpx;
|
||||
padding-bottom: 8rpx;
|
||||
gap: 20rpx;
|
||||
padding-right: @home-padding;
|
||||
}
|
||||
|
||||
.home-hot__item {
|
||||
display: inline-block;
|
||||
width: 240rpx;
|
||||
vertical-align: top;
|
||||
.home-age-item {
|
||||
width: calc((750rpx - 48rpx - 40rpx) / 3.2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.home-hot__card-wrap {
|
||||
width: 240rpx;
|
||||
padding: 4rpx;
|
||||
border-radius: 28rpx;
|
||||
box-sizing: border-box;
|
||||
background: linear-gradient(145deg, rgba(255, 215, 9, 0.85), rgba(254, 246, 231, 0.95));
|
||||
.home-age-item__icon-wrap {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(248, 240, 224, 0.85);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.home-age-item__icon-wrap--active {
|
||||
background: rgba(255, 215, 9, 0.28);
|
||||
}
|
||||
|
||||
.home-age-item__icon {
|
||||
font-size: 44rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.home-age-item__label {
|
||||
margin-top: 12rpx;
|
||||
font-size: 26rpx;
|
||||
color: @text-title;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.home-hot-list {
|
||||
margin-top: 36rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 28rpx;
|
||||
}
|
||||
|
||||
.home-hot-card {
|
||||
background: #ffffff;
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.home-hot-card__thumb {
|
||||
width: 144rpx;
|
||||
height: 144rpx;
|
||||
border-radius: 24rpx;
|
||||
background: linear-gradient(140deg, #ad060f 0%, #d93b0d 40%, #a20606 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.home-hot-card__thumb-emoji {
|
||||
font-size: 56rpx;
|
||||
}
|
||||
|
||||
.home-hot-card__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.home-hot-card__title {
|
||||
font-size: 30rpx;
|
||||
color: #2f2f2f;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.home-hot-card__desc {
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #616161;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.home-hot-card__meta {
|
||||
margin-top: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.home-hot-card__tag {
|
||||
font-size: 22rpx;
|
||||
color: @text-selected-btn;
|
||||
background: #ffd709;
|
||||
border-radius: 999rpx;
|
||||
padding: 4rpx 16rpx;
|
||||
}
|
||||
|
||||
.home-hot-card__score {
|
||||
font-size: 24rpx;
|
||||
color: #6b6b6b;
|
||||
}
|
||||
|
||||
.home-hot-card__action {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 50%;
|
||||
background: #ffd709;
|
||||
color: #2b2b2b;
|
||||
font-size: 36rpx;
|
||||
line-height: 56rpx;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* category-tabs:仅首页通过 apply-shared + .home-page 生效 */
|
||||
.home-page .category-tabs {
|
||||
background: #fef6e7;
|
||||
background: @bg-page;
|
||||
}
|
||||
|
||||
.home-page .category-tabs__pill {
|
||||
background: rgba(96, 91, 80, 0.08);
|
||||
color: #605b50;
|
||||
color: @text-secondary;
|
||||
}
|
||||
|
||||
.home-page .category-tabs__pill--active {
|
||||
background: #ffd709;
|
||||
color: #322e25;
|
||||
color: @text-selected-btn;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* worksheet-card:仅首页 */
|
||||
.home-page .worksheet-card {
|
||||
box-shadow: 0 8rpx 28rpx rgba(50, 46, 37, 0.08);
|
||||
}
|
||||
|
||||
.home-page .worksheet-card__title {
|
||||
color: #605b50;
|
||||
}
|
||||
|
||||
.home-page .worksheet-card__preview-text {
|
||||
color: #605b50;
|
||||
}
|
||||
|
||||
.home-page .worksheet-card__tag--age,
|
||||
.home-page .worksheet-card__tag--diff {
|
||||
background: #91f78e;
|
||||
color: #322e25;
|
||||
}
|
||||
|
||||
.home-grid {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.home-grid__cell {
|
||||
width: calc((100% - 40rpx) / 3);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
@@ -1,81 +1,77 @@
|
||||
import { ALL_WORKSHEETS, getWorksheetsByCategory, getWorksheetPath } from '../../core/data/worksheets';
|
||||
import { CATEGORIES } from '../../core/data/categories';
|
||||
import { WorksheetType } from '../../core/models/worksheet';
|
||||
import { AGE_BANDS, AgeBandKey } from '../../core/data/difficulty';
|
||||
|
||||
const CATEGORY_TABS = [
|
||||
{ id: 'all', name: '全部' },
|
||||
...CATEGORIES.map((c) => ({ id: c.id, name: c.name })),
|
||||
];
|
||||
|
||||
const AGE_BAND_ICONS = ['🌱', '🚀', '🎓', '🧠', '🏆'] as const;
|
||||
|
||||
const MOCK_HOT_RECOMMENDS = [
|
||||
{
|
||||
id: 'hot-1',
|
||||
title: '100以内的加减法',
|
||||
desc: '让算术变得像游戏一样简单',
|
||||
tag: '数学',
|
||||
score: '4.8',
|
||||
emoji: '🍊',
|
||||
},
|
||||
{
|
||||
id: 'hot-2',
|
||||
title: '创意涂鸦大作战',
|
||||
desc: '释放孩子的艺术想象力',
|
||||
tag: '艺术',
|
||||
score: '4.9',
|
||||
emoji: '🎨',
|
||||
},
|
||||
] as const;
|
||||
|
||||
const { statusBarHeight } = wx.getWindowInfo();
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight,
|
||||
tabs: CATEGORY_TABS,
|
||||
activeTab: 'all',
|
||||
sections: [] as Array<{ title: string; icon: string; category: string; items: WorksheetType[] }>,
|
||||
hotItems: [] as WorksheetType[],
|
||||
filteredItems: null as WorksheetType[] | null,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
const sections = CATEGORIES.filter((cat) => {
|
||||
const items = getWorksheetsByCategory(cat.id);
|
||||
return items.length > 0;
|
||||
}).map((cat) => ({
|
||||
title: cat.name,
|
||||
icon: cat.icon,
|
||||
category: cat.id,
|
||||
items: getWorksheetsByCategory(cat.id).slice(0, 3),
|
||||
}));
|
||||
|
||||
const hotItems = ALL_WORKSHEETS.filter((w) => w.page).slice(0, 4);
|
||||
|
||||
this.setData({ sections, hotItems });
|
||||
ageBands: AGE_BANDS.map((item, idx) => ({
|
||||
...item,
|
||||
icon: AGE_BAND_ICONS[idx] || '🌟',
|
||||
})),
|
||||
activeAgeBand: AGE_BANDS[0].key as AgeBandKey,
|
||||
hotRecommends: [...MOCK_HOT_RECOMMENDS],
|
||||
},
|
||||
|
||||
onTabChange(e: WechatMiniprogram.CustomEvent) {
|
||||
const activeTab = e.detail.id;
|
||||
if (activeTab === 'all') {
|
||||
this.setData({ activeTab, filteredItems: null });
|
||||
} else {
|
||||
const filteredItems = getWorksheetsByCategory(activeTab);
|
||||
this.setData({ activeTab, filteredItems });
|
||||
}
|
||||
this.setData({ activeTab: e.detail.id });
|
||||
},
|
||||
|
||||
onCardTap(e: WechatMiniprogram.TouchEvent) {
|
||||
const id = e.currentTarget.dataset.id as string | undefined;
|
||||
if (!id) return;
|
||||
const item = ALL_WORKSHEETS.find((w) => w.id === id);
|
||||
if (!item) return;
|
||||
const path = getWorksheetPath(item);
|
||||
if (path) {
|
||||
wx.navigateTo({ url: path });
|
||||
}
|
||||
onTapAgeBand(e: WechatMiniprogram.TouchEvent) {
|
||||
const key = e.currentTarget.dataset.key as AgeBandKey | undefined;
|
||||
if (!key) return;
|
||||
this.setData({ activeAgeBand: key });
|
||||
},
|
||||
|
||||
onHotCardTap(e: WechatMiniprogram.TouchEvent) {
|
||||
const id = e.currentTarget.dataset.id as string | undefined;
|
||||
if (!id) return;
|
||||
const item = ALL_WORKSHEETS.find((w) => w.id === id);
|
||||
if (!item) return;
|
||||
const path = getWorksheetPath(item);
|
||||
if (path) {
|
||||
wx.navigateTo({ url: path });
|
||||
}
|
||||
onTapHotRecommend(e: WechatMiniprogram.TouchEvent) {
|
||||
const title = e.currentTarget.dataset.title as string | undefined;
|
||||
if (!title) return;
|
||||
wx.showToast({ title: `已选择:${title}`, icon: 'none' });
|
||||
},
|
||||
|
||||
onSeeMore(e: WechatMiniprogram.TouchEvent) {
|
||||
const { category } = e.currentTarget.dataset;
|
||||
wx.navigateTo({
|
||||
url: `/pages/category/category?category=${category}`,
|
||||
});
|
||||
onTapAgeMore() {
|
||||
wx.showToast({ title: '分龄全部 即将上线', icon: 'none' });
|
||||
},
|
||||
|
||||
onTapHotMore() {
|
||||
wx.showToast({ title: '热门推荐 即将上线', icon: 'none' });
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
return {
|
||||
title: '涂鸦丫 - 快来生成宝宝的专属学习卡',
|
||||
path: '/pages/home/home',
|
||||
imageUrl: 'https://cdn.joeyone.cn/doodle/share-img/index-share-v2.png',
|
||||
imageUrl:
|
||||
'https://cdn.joeyone.cn/doodle/share-img/index-share-v2.png',
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -1,105 +1,93 @@
|
||||
<wxs module="fmt">
|
||||
function ageLabel(arr) {
|
||||
if (!arr || arr.length < 2) {
|
||||
return '';
|
||||
}
|
||||
return arr[0] + '-' + arr[1] + '岁';
|
||||
}
|
||||
module.exports.ageLabel = ageLabel;
|
||||
</wxs>
|
||||
|
||||
<view class="home-page">
|
||||
<view class="home-header">
|
||||
<view class="home-brand">
|
||||
<text class="home-brand__duck">🦆</text>
|
||||
<text class="home-brand__name">涂鸦丫</text>
|
||||
</view>
|
||||
<view class="home-search">
|
||||
<text class="home-search__placeholder">搜索练习单(即将上线)</text>
|
||||
<view class="home-nav" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="home-nav__inner">
|
||||
<image
|
||||
src="/assets/imgs/doodle-logo.png"
|
||||
mode="aspectFill"
|
||||
class="home-nav__logo" />
|
||||
<text class="home-nav__name">涂鸦丫</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<category-tabs tabs="{{tabs}}" active-tab="{{activeTab}}" bind:change="onTabChange" />
|
||||
<view class="home-search-wrap">
|
||||
<view class="home-search">
|
||||
<van-icon
|
||||
name="search"
|
||||
size="36rpx"
|
||||
color="rgba(50, 46, 37, 0.45)"
|
||||
class="home-search__icon" />
|
||||
<text class="home-search__placeholder">搜索你喜欢的练习册...</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="home-body">
|
||||
<block wx:if="{{activeTab != 'all'}}">
|
||||
<view class="home-grid">
|
||||
<view class="home-tabs-wrap">
|
||||
<category-tabs
|
||||
tabs="{{tabs}}"
|
||||
active-tab="{{activeTab}}"
|
||||
bind:change="onTabChange" />
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="home-hero-placeholder"
|
||||
aria-role="img"
|
||||
aria-label="推荐位占位" />
|
||||
|
||||
<view class="home-section-panel">
|
||||
<view class="home-section-head">
|
||||
<text class="home-section-head__title">按年龄挑选</text>
|
||||
<text class="home-section-head__more" bindtap="onTapAgeMore"
|
||||
>查看全部</text
|
||||
>
|
||||
</view>
|
||||
<scroll-view class="home-age-scroll" scroll-x enable-flex>
|
||||
<view class="home-age-row">
|
||||
<view
|
||||
wx:for="{{filteredItems}}"
|
||||
wx:key="id"
|
||||
class="home-grid__cell"
|
||||
bindtap="onCardTap"
|
||||
data-id="{{item.id}}"
|
||||
>
|
||||
<worksheet-card
|
||||
title="{{item.title}}"
|
||||
preview-bg="{{item.previewBg}}"
|
||||
preview-text="{{item.previewText}}"
|
||||
img="{{item.img}}"
|
||||
age-range="{{fmt.ageLabel(item.ageRange)}}"
|
||||
difficulty="{{item.difficulty}}"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<block wx:else>
|
||||
<view class="home-section home-section--hot">
|
||||
<view class="home-section__title-row">
|
||||
<text class="home-section__title">🔥 热门下载</text>
|
||||
</view>
|
||||
<scroll-view class="home-hot__scroll" scroll-x enable-flex>
|
||||
<view class="home-hot__row">
|
||||
<view
|
||||
wx:for="{{hotItems}}"
|
||||
wx:key="id"
|
||||
class="home-hot__item"
|
||||
bindtap="onHotCardTap"
|
||||
data-id="{{item.id}}"
|
||||
>
|
||||
<view class="home-hot__card-wrap">
|
||||
<worksheet-card
|
||||
title="{{item.title}}"
|
||||
preview-bg="{{item.previewBg}}"
|
||||
preview-text="{{item.previewText}}"
|
||||
img="{{item.img}}"
|
||||
age-range="{{fmt.ageLabel(item.ageRange)}}"
|
||||
difficulty="{{item.difficulty}}"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<view wx:for="{{sections}}" wx:key="category" class="home-section">
|
||||
<view class="home-section__head">
|
||||
<view class="home-section__head-left">
|
||||
<text class="home-section__icon">{{item.icon}}</text>
|
||||
<text class="home-section__name">{{item.title}}</text>
|
||||
</view>
|
||||
<text class="home-section__more" bindtap="onSeeMore" data-category="{{item.category}}">更多</text>
|
||||
</view>
|
||||
<view class="home-grid">
|
||||
wx:for="{{ageBands}}"
|
||||
wx:key="key"
|
||||
class="home-age-item"
|
||||
data-key="{{item.key}}"
|
||||
bindtap="onTapAgeBand">
|
||||
<view
|
||||
wx:for="{{item.items}}"
|
||||
wx:for-item="w"
|
||||
wx:key="id"
|
||||
class="home-grid__cell"
|
||||
bindtap="onCardTap"
|
||||
data-id="{{w.id}}"
|
||||
>
|
||||
<worksheet-card
|
||||
title="{{w.title}}"
|
||||
preview-bg="{{w.previewBg}}"
|
||||
preview-text="{{w.previewText}}"
|
||||
img="{{w.img}}"
|
||||
age-range="{{fmt.ageLabel(w.ageRange)}}"
|
||||
difficulty="{{w.difficulty}}"
|
||||
/>
|
||||
class="home-age-item__icon-wrap {{activeAgeBand === item.key ? 'home-age-item__icon-wrap--active' : ''}}">
|
||||
<text class="home-age-item__icon">{{item.icon}}</text>
|
||||
</view>
|
||||
<text class="home-age-item__label">{{item.label}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<view class="home-section-panel home-section-panel--hot">
|
||||
<view class="home-section-head">
|
||||
<text class="home-section-head__title">热门推荐</text>
|
||||
<text class="home-section-head__more" bindtap="onTapHotMore"
|
||||
>查看全部</text
|
||||
>
|
||||
</view>
|
||||
<view class="home-hot-list">
|
||||
<view
|
||||
wx:for="{{hotRecommends}}"
|
||||
wx:key="id"
|
||||
class="home-hot-card"
|
||||
data-title="{{item.title}}"
|
||||
bindtap="onTapHotRecommend">
|
||||
<view class="home-hot-card__thumb">
|
||||
<text class="home-hot-card__thumb-emoji"
|
||||
>{{item.emoji}}</text
|
||||
>
|
||||
</view>
|
||||
<view class="home-hot-card__content">
|
||||
<text class="home-hot-card__title">{{item.title}}</text>
|
||||
<text class="home-hot-card__desc">{{item.desc}}</text>
|
||||
<view class="home-hot-card__meta">
|
||||
<text class="home-hot-card__tag">{{item.tag}}</text>
|
||||
<text class="home-hot-card__score"
|
||||
>{{item.score}} ★</text
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
<view class="home-hot-card__action">+</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -3,11 +3,57 @@
|
||||
* 封装微信小程序 Canvas API,为未来 Web 平台迁移做准备
|
||||
*/
|
||||
|
||||
type QueryHost =
|
||||
| WechatMiniprogram.Page.TrivialInstance
|
||||
| WechatMiniprogram.Component.TrivialInstance;
|
||||
|
||||
type Canvas2DContextLike = WechatMiniprogram.RenderingContext & {
|
||||
scale: (x: number, y: number) => void;
|
||||
};
|
||||
|
||||
export interface CanvasInitResult {
|
||||
canvas: WechatMiniprogram.Canvas;
|
||||
ctx: CanvasRenderingContext2D;
|
||||
ctx: Canvas2DContextLike;
|
||||
width: number;
|
||||
height: number;
|
||||
dpr: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 与架构文档 ICanvasAdapter 对齐的小程序实现
|
||||
*/
|
||||
export interface ICanvasAdapter {
|
||||
getCanvas(): WechatMiniprogram.Canvas;
|
||||
getContext(): Canvas2DContextLike;
|
||||
getSize(): { width: number; height: number; dpr: number };
|
||||
setSize(width: number, height: number): void;
|
||||
loadImage(src: string): Promise<WechatMiniprogram.Image>;
|
||||
toDataURL(type?: 'png' | 'jpg', quality?: number): Promise<string>;
|
||||
toBlob(): Promise<unknown>;
|
||||
toTempFilePath(options?: {
|
||||
quality?: number;
|
||||
fileType?: 'png' | 'jpg';
|
||||
}): Promise<string>;
|
||||
}
|
||||
|
||||
function createQuery(host?: QueryHost): WechatMiniprogram.SelectorQuery {
|
||||
if (host && typeof host.createSelectorQuery === 'function') {
|
||||
return host.createSelectorQuery();
|
||||
}
|
||||
return wx.createSelectorQuery();
|
||||
}
|
||||
|
||||
function getDpr(): number {
|
||||
try {
|
||||
return Math.max(1, wx.getSystemInfoSync().pixelRatio || 1);
|
||||
} catch {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
function ensureAspectRatio(aspectRatio: number): number {
|
||||
if (!Number.isFinite(aspectRatio) || aspectRatio <= 0) return 1;
|
||||
return aspectRatio;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -15,43 +61,53 @@ export interface CanvasInitResult {
|
||||
* @param selector Canvas 选择器 (如 '#canvasContent')
|
||||
* @param wrapperSelector 容器选择器 (如 '#canvasWrapper')
|
||||
* @param aspectRatio 宽高比 (width / height)
|
||||
* @param host 可选:页面/组件实例,组件内调用建议传入 this
|
||||
*/
|
||||
export function initCanvas(
|
||||
selector: string,
|
||||
wrapperSelector: string,
|
||||
aspectRatio: number,
|
||||
host?: QueryHost,
|
||||
): Promise<CanvasInitResult> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const query = wx.createSelectorQuery();
|
||||
query
|
||||
const ratio = ensureAspectRatio(aspectRatio);
|
||||
createQuery(host)
|
||||
.select(wrapperSelector)
|
||||
.boundingClientRect((rect) => {
|
||||
if (!rect) {
|
||||
if (!rect || !rect.width) {
|
||||
reject(new Error('Canvas wrapper not found'));
|
||||
return;
|
||||
}
|
||||
|
||||
const boxWidth = rect.width;
|
||||
const boxHeight = boxWidth / aspectRatio;
|
||||
const boxHeight = boxWidth / ratio;
|
||||
|
||||
wx.createSelectorQuery()
|
||||
createQuery(host)
|
||||
.select(selector)
|
||||
.fields({ node: true, size: true })
|
||||
.exec((res) => {
|
||||
if (!res[0]) {
|
||||
if (!res || !res[0] || !res[0].node) {
|
||||
reject(new Error('Canvas node not found'));
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = res[0].node;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const dpr = wx.getSystemInfoSync().pixelRatio;
|
||||
const canvas = res[0].node as WechatMiniprogram.Canvas;
|
||||
const ctx = canvas.getContext(
|
||||
'2d',
|
||||
) as Canvas2DContextLike;
|
||||
const dpr = getDpr();
|
||||
|
||||
canvas.width = boxWidth * dpr;
|
||||
canvas.height = boxHeight * dpr;
|
||||
ctx.scale(dpr, dpr);
|
||||
|
||||
resolve({ canvas, ctx, width: boxWidth, height: boxHeight });
|
||||
resolve({
|
||||
canvas,
|
||||
ctx,
|
||||
width: boxWidth,
|
||||
height: boxHeight,
|
||||
dpr,
|
||||
});
|
||||
});
|
||||
})
|
||||
.exec();
|
||||
@@ -69,9 +125,9 @@ export function canvasToTempFilePath(
|
||||
wx.canvasToTempFilePath({
|
||||
canvas,
|
||||
fileType: options?.fileType || 'png',
|
||||
quality: options?.quality || 1,
|
||||
quality: options?.quality ?? 1,
|
||||
success: (res) => resolve(res.tempFilePath),
|
||||
fail: reject,
|
||||
fail: (err) => reject(err),
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -86,7 +142,64 @@ export function loadImage(
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = canvas.createImage();
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = reject;
|
||||
img.onerror = (err) => reject(err);
|
||||
img.src = src;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过初始化结果创建微信 Canvas 适配器实例
|
||||
*/
|
||||
export function createWxCanvasAdapter(init: CanvasInitResult): ICanvasAdapter {
|
||||
let width = init.width;
|
||||
let height = init.height;
|
||||
let dpr = init.dpr;
|
||||
let scaled = true;
|
||||
|
||||
const applyScale = () => {
|
||||
if (!scaled) {
|
||||
init.ctx.scale(dpr, dpr);
|
||||
scaled = true;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
getCanvas() {
|
||||
return init.canvas;
|
||||
},
|
||||
getContext() {
|
||||
return init.ctx;
|
||||
},
|
||||
getSize() {
|
||||
return { width, height, dpr };
|
||||
},
|
||||
setSize(nextWidth: number, nextHeight: number) {
|
||||
width = Math.max(1, nextWidth);
|
||||
height = Math.max(1, nextHeight);
|
||||
dpr = getDpr();
|
||||
init.canvas.width = width * dpr;
|
||||
init.canvas.height = height * dpr;
|
||||
scaled = false;
|
||||
applyScale();
|
||||
},
|
||||
loadImage(src: string) {
|
||||
return loadImage(init.canvas, src);
|
||||
},
|
||||
async toDataURL(type = 'png', quality = 1) {
|
||||
const maybeCanvas = init.canvas as unknown as {
|
||||
toDataURL?: (mimeType?: string, q?: number) => string;
|
||||
};
|
||||
if (typeof maybeCanvas.toDataURL === 'function') {
|
||||
const mimeType = type === 'jpg' ? 'image/jpeg' : 'image/png';
|
||||
return maybeCanvas.toDataURL(mimeType, quality);
|
||||
}
|
||||
throw new Error('toDataURL is not supported in current runtime');
|
||||
},
|
||||
async toBlob() {
|
||||
throw new Error('toBlob is not supported in WeChat Mini Program');
|
||||
},
|
||||
toTempFilePath(options) {
|
||||
return canvasToTempFilePath(init.canvas, options);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// v3.0 主题色体系(theme3)
|
||||
// 主题色 #FFD709 · 页背景 #FEF6E7 · Header #F8F0E0 · 主标题字 #322E25 · 选中按钮字 #6C5A00
|
||||
|
||||
// 品牌 / 主题色
|
||||
@brand: #ffd709;
|
||||
@brand-dark: #e5c200;
|
||||
@brand-light: #fff4c4;
|
||||
|
||||
// 与 theme.less 对齐的别名
|
||||
@primary-color: @brand;
|
||||
@select-color: @brand;
|
||||
|
||||
// 页面结构背景
|
||||
@bg-page: #fef6e7;
|
||||
@bg-header: #f8f0e0;
|
||||
@bg-default: @bg-page;
|
||||
@bg-white: #ffffff;
|
||||
@bg-gray: #f5edd8;
|
||||
|
||||
// 文本:标题 / title
|
||||
@text-title: #322e25;
|
||||
@text-primary: @text-title;
|
||||
@text-default: @text-title;
|
||||
|
||||
// 选中态按钮上的文字
|
||||
@text-selected-btn: #6c5a00;
|
||||
|
||||
// 辅助文本(正文可读色,可与标题区分)
|
||||
@text-secondary: #605b50;
|
||||
@text-light: rgba(50, 46, 37, 0.45);
|
||||
@text-gray: #8a8478;
|
||||
@text-white: #ffffff;
|
||||
@text-disable: rgba(50, 46, 37, 0.35);
|
||||
|
||||
// 链接与外链(沿用可读蓝绿,可按需覆盖)
|
||||
@link-color: #2794f4;
|
||||
@wechat-color: #1fe000;
|
||||
|
||||
// 边框(适配暖色底)
|
||||
@border: 1rpx solid rgba(50, 46, 37, 0.08);
|
||||
@border-color: rgba(255, 215, 9, 0.35);
|
||||
|
||||
// 圆角
|
||||
@radius-sm: 16rpx;
|
||||
@radius: 24rpx;
|
||||
@radius-lg: 32rpx;
|
||||
|
||||
// 阴影
|
||||
@shadow: 0 4rpx 16rpx rgba(50, 46, 37, 0.06);
|
||||
@shadow-lg: 0 8rpx 32rpx rgba(50, 46, 37, 0.1);
|
||||
|
||||
// icon
|
||||
@font-size-icon: 40rpx;
|
||||
Reference in New Issue
Block a user