Files
doodle-mini/miniprogram/mathPages/mathDraw/registry.ts
T

738 lines
29 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { BaseDrawService } from '../../core/draw/baseDraw';
import NumberFindDraw from '../shared/service/numberFindDraw';
import CountMatchDraw from '../shared/service/countMatchDraw';
import NumberColorDraw from '../shared/service/numberColorDraw';
import AdditionDraw from '../shared/service/additionDraw';
import MissingNumberDraw from '../shared/service/missingNumberDraw';
import CompareDraw from '../shared/service/compareDraw';
import CountingSelectDraw from '../shared/service/countingSelectDraw';
import NumberDecomposeDraw from '../shared/service/numberDecomposeDraw';
import NumberSortDraw from '../shared/service/numberSortDraw';
import NumberObjectMatchDraw from '../shared/service/numberObjectMatchDraw';
import MakeTenDraw from '../shared/service/makeTenDraw';
import BreakTenDraw from '../shared/service/breakTenDraw';
import FlatTenDraw from '../shared/service/flatTenDraw';
import OneDigitAdditionDraw from '../shared/service/oneDigitAdditionDraw';
import CalculationPracticeDraw from '../shared/service/calculationPracticeDraw';
import MultiplicationTableDraw from '../shared/service/multiplicationTableDraw';
import { getRandomNumberColor, getNumberColors, NUMBER_COLORS } from '../../constants/colors';
// ─── Types ───
export interface MathTypeAction {
value: string;
label: string;
}
export interface MathTypeConfig {
id: string;
title: string;
subTitle: string;
icon: string;
actionsTitle?: string;
actions?: MathTypeAction[];
defaultMode?: string;
hasNumberGrid?: boolean;
getTitle?: (mode: string) => string;
getSubTitle?: (mode: string) => string;
createDrawService: (
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, any>,
) => BaseDrawService;
generateData: (mode?: string, extra?: any) => any;
drawArgs?: (data: any, mode: string) => any[];
/** 需要根据 functionId 决定不同的 actions/DrawService 的复合类型 */
variants?: Record<string, {
title: string;
subTitle: string;
actions?: MathTypeAction[];
defaultMode?: string;
createDrawService?: (canvas: Canvas, ctx: RenderingContext, options?: Record<string, any>) => BaseDrawService;
}>;
}
// ─── Data Generators ───
function generateNumberFindData(_mode?: string, extra?: any) {
const selectedNumber = extra?.selectedNumber || Math.floor(Math.random() * 10) + 1;
return { selectedNumber };
}
function generateCountMatchData(mode?: string) {
const availableNumbers = Array.from({ length: 10 }, (_, i) => i + 1);
const leftNumbers: number[] = [];
const pool = [...availableNumbers];
for (let i = 0; i < 5; i++) {
const idx = Math.floor(Math.random() * pool.length);
leftNumbers.push(pool.splice(idx, 1)[0]);
}
const rightNumbers = [...leftNumbers];
for (let i = rightNumbers.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[rightNumbers[i], rightNumbers[j]] = [rightNumbers[j], rightNumbers[i]];
}
return { leftNumbers, rightNumbers, _drawMode: mode || 'twelve-animals' };
}
function generateNumberColorData(mode?: string) {
const availableNumbers = Array.from({ length: 10 }, (_, i) => i + 1);
const numbers: number[] = [];
const pool = [...availableNumbers];
for (let i = 0; i < 6; i++) {
const idx = Math.floor(Math.random() * pool.length);
numbers.push(pool.splice(idx, 1)[0]);
}
return { numbers, _drawMode: mode || 'caterpillar' };
}
function generateAdditionData(mode?: string) {
const type = mode || 'addition-5';
const problems: Array<{ type: 'addition' | 'subtraction'; left: number; right: number; result: number }> = [];
for (let i = 0; i < 5; i++) {
if (type === 'addition-5') {
const left = Math.floor(Math.random() * 4) + 1;
const right = Math.floor(Math.random() * (5 - left)) + 1;
problems.push({ type: 'addition', left, right, result: left + right });
} else if (type === 'addition-10') {
const left = Math.floor(Math.random() * 9) + 1;
const right = Math.floor(Math.random() * (10 - left)) + 1;
problems.push({ type: 'addition', left, right, result: left + right });
} else if (type === 'subtraction-10') {
const left = Math.floor(Math.random() * 10) + 1;
const right = Math.floor(Math.random() * (left - 1)) + 1;
problems.push({ type: 'subtraction', left, right, result: left - right });
} else {
if (Math.random() < 0.5) {
const left = Math.floor(Math.random() * 9) + 1;
const right = Math.floor(Math.random() * (10 - left)) + 1;
problems.push({ type: 'addition', left, right, result: left + right });
} else {
const left = Math.floor(Math.random() * 10) + 1;
const right = Math.floor(Math.random() * (left - 1)) + 1;
problems.push({ type: 'subtraction', left, right, result: left - right });
}
}
}
return { problems };
}
function generateMissingNumberData(mode?: string) {
const maxNumber = parseInt(mode || '10') || 10;
const gridMap: Record<number, number> = { 10: 3, 20: 2, 40: 2, 50: 2, 80: 1, 100: 1, 120: 1 };
const gridCount = gridMap[maxNumber] || 1;
const grids: Array<{ numbers: (number | null)[]; colors: (string | null)[] }> = [];
for (let g = 0; g < gridCount; g++) {
const total = maxNumber;
const numbers = Array.from({ length: total }, (_, i) => i + 1);
const hideCount = Math.floor(total * (0.4 + Math.random() * 0.2));
const hidden = new Set<number>();
while (hidden.size < hideCount) hidden.add(Math.floor(Math.random() * total));
const gridNumbers: (number | null)[] = numbers.map((n, i) => hidden.has(i) ? null : n);
const gridColors: (string | null)[] = gridNumbers.map((n) => n ? getRandomNumberColor() : null);
grids.push({ numbers: gridNumbers, colors: gridColors });
}
return { grids, maxNumber };
}
function generateCompareData() {
const problems: Array<{
leftCount: number; rightCount: number;
leftImageIndex: number; rightImageIndex: number;
imageType: 'fruits' | 'twelve-animals';
}> = [];
for (let i = 0; i < 12; i++) {
const imageType: 'fruits' | 'twelve-animals' = Math.random() < 0.5 ? 'fruits' : 'twelve-animals';
const maxIdx = imageType === 'fruits' ? 22 : 12;
problems.push({
leftCount: Math.floor(Math.random() * 10) + 1,
rightCount: Math.floor(Math.random() * 10) + 1,
leftImageIndex: Math.floor(Math.random() * maxIdx) + 1,
rightImageIndex: Math.floor(Math.random() * maxIdx) + 1,
imageType,
});
}
return { problems };
}
function generateCountingSelectData(_mode?: string, extra?: any) {
const isFillMode = extra?.functionId === 'counting-fill';
const problems: Array<{
count: number; imageIndex: number; imageType: 'fruits' | 'twelve-animals';
options?: number[]; correctIndex?: number;
}> = [];
for (let i = 0; i < 9; i++) {
const imageType: 'fruits' | 'twelve-animals' = Math.random() < 0.5 ? 'fruits' : 'twelve-animals';
const maxIdx = imageType === 'fruits' ? 22 : 12;
const count = Math.floor(Math.random() * 10) + 1;
const problem: any = { count, imageIndex: Math.floor(Math.random() * maxIdx) + 1, imageType };
if (!isFillMode) {
const options: number[] = [];
const correctIndex = Math.floor(Math.random() * 3);
for (let j = 0; j < 3; j++) {
if (j === correctIndex) { options.push(count); }
else { let w; do { w = Math.floor(Math.random() * 10) + 1; } while (w === count); options.push(w); }
}
problem.options = options;
problem.correctIndex = correctIndex;
}
problems.push(problem);
}
return { problems };
}
function generateNumberDecomposeData(mode?: string, extra?: any) {
const decomposeMode = mode || 'with-image';
const maxNumber = extra?.maxNumber || 10;
const is20 = maxNumber === 20;
const minWhole = is20 ? 11 : 2;
const maxWhole = maxNumber;
const problems: Array<{
whole: number | null; part1: number | null; part2: number | null;
imageIndex?: number; imageType?: 'fruits' | 'twelve-animals';
}> = [];
const usedKeys = new Set<string>();
const problemCount = decomposeMode === 'with-image' ? 9 : 15;
let attempts = 0;
while (problems.length < problemCount && attempts < problemCount * 50) {
attempts++;
const whole = Math.floor(Math.random() * (maxWhole - minWhole + 1)) + minWhole;
const part1 = Math.floor(Math.random() * (whole - 1)) + 1;
const part2 = whole - part1;
if (decomposeMode === 'compose') {
const key = `${Math.min(part1, part2)}:${Math.max(part1, part2)}`;
if (usedKeys.has(key)) continue;
usedKeys.add(key);
problems.push({ whole: null, part1, part2 });
} else {
const showPart1 = Math.random() < 0.5;
const key = `${whole}:${Math.min(part1, part2)}:${Math.max(part1, part2)}:${showPart1}`;
if (usedKeys.has(key)) continue;
usedKeys.add(key);
const problem: any = {
whole,
part1: showPart1 ? part1 : null,
part2: showPart1 ? null : part2,
};
if (decomposeMode === 'with-image') {
const imgType: 'fruits' | 'twelve-animals' = Math.random() < 0.5 ? 'fruits' : 'twelve-animals';
problem.imageIndex = Math.floor(Math.random() * (imgType === 'fruits' ? 22 : 12)) + 1;
problem.imageType = imgType;
}
problems.push(problem);
}
}
return { problems, mode: decomposeMode };
}
function generateNumberSortData() {
const availableColors = getNumberColors();
const groups: Array<{ numbers: Array<{ number: number; color: string; angle: number }> }> = [];
for (let i = 0; i < 6; i++) {
const pool = Array.from({ length: 9 }, (_, i) => i + 1);
const selected: number[] = [];
for (let j = 0; j < 4; j++) {
const idx = Math.floor(Math.random() * pool.length);
selected.push(pool.splice(idx, 1)[0]);
}
const shuffled = [...selected];
for (let j = shuffled.length - 1; j > 0; j--) {
const k = Math.floor(Math.random() * (j + 1));
[shuffled[j], shuffled[k]] = [shuffled[k], shuffled[j]];
}
groups.push({
numbers: shuffled.map((num) => ({
number: num,
color: NUMBER_COLORS[num as keyof typeof NUMBER_COLORS] || availableColors[Math.floor(Math.random() * availableColors.length)],
angle: ((Math.random() - 0.5) * 60 * Math.PI) / 180,
})),
});
}
return { groups };
}
function generateNumberObjectMatchData() {
const fruitsIndices = Array.from({ length: 22 }, (_, i) => i + 1);
const animalsIndices = Array.from({ length: 12 }, (_, i) => i + 1);
const allImages = [
...fruitsIndices.map((idx) => ({ imageIndex: idx, folder: 'fruits' as const })),
...animalsIndices.map((idx) => ({ imageIndex: idx, folder: 'twelve-animals' as const })),
];
const selectedImages = [...allImages].sort(() => Math.random() - 0.5).slice(0, 4);
const gridImages = Array.from({ length: 20 }, () => {
const img = selectedImages[Math.floor(Math.random() * selectedImages.length)];
return { ...img, offsetX: (Math.random() - 0.5) * 30, offsetY: (Math.random() - 0.5) * 30 };
});
const bottomImages = [...selectedImages];
const countMap = new Map<string, number>();
gridImages.forEach((it) => {
const key = `${it.folder}-${it.imageIndex}`;
countMap.set(key, (countMap.get(key) || 0) + 1);
});
const imageCounts = selectedImages.map((img) => countMap.get(`${img.folder}-${img.imageIndex}`) || 0);
const bottomNumbers = [...imageCounts].sort(() => Math.random() - 0.5);
return { gridImages, bottomImages, bottomNumbers };
}
function generateMakeTenData() {
const problems: Array<{ left: number; right: number; result: number }> = [];
const used = new Set<string>();
let attempts = 0;
while (problems.length < 9 && attempts < 1000) {
attempts++;
const bigFirst = Math.random() < 0.5;
let left: number, right: number;
if (bigFirst) {
left = Math.floor(Math.random() * 4) + 6;
const minR = Math.max(1, 11 - left);
const maxR = Math.min(left - 1, 20 - left);
if (maxR < minR) continue;
right = Math.floor(Math.random() * (maxR - minR + 1)) + minR;
} else {
right = Math.floor(Math.random() * 4) + 6;
const minL = Math.max(1, 11 - right);
const maxL = Math.min(right - 1, 20 - right);
if (maxL < minL) continue;
left = Math.floor(Math.random() * (maxL - minL + 1)) + minL;
}
const key = `${left},${right}`;
if (used.has(key)) continue;
used.add(key);
problems.push({ left, right, result: left + right });
}
return { problems };
}
function generateBreakTenData() {
const problems: Array<{ minuend: number; subtrahend: number; result: number }> = [];
const used = new Set<string>();
let attempts = 0;
while (problems.length < 9 && attempts < 1000) {
attempts++;
const minuend = Math.floor(Math.random() * 10) + 11;
const subtrahend = Math.floor(Math.random() * 9) + 1;
if (minuend <= subtrahend) continue;
const key = `${minuend},${subtrahend}`;
if (used.has(key)) continue;
used.add(key);
problems.push({ minuend, subtrahend, result: minuend - subtrahend });
}
return { problems };
}
function generateFlatTenData() {
const allValid: Array<{ minuend: number; subtrahend: number; result: number }> = [];
for (let m = 11; m < 20; m++) {
const ones = m % 10;
for (let s = 1; s <= 10; s++) {
if (s > ones && m > s) allValid.push({ minuend: m, subtrahend: s, result: m - s });
}
}
const shuffled = [...allValid];
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return { problems: shuffled.slice(0, Math.min(9, shuffled.length)) };
}
function generateBorrowTenData(mode?: string) {
let maxMinuend = 50;
if (mode === 'within-30') maxMinuend = 30;
else if (mode === 'within-100') maxMinuend = 100;
const byMinuend = new Map<number, Array<{ minuend: number; subtrahend: number; result: number }>>();
for (let m = 20; m <= maxMinuend; m++) {
const ones = m % 10;
for (let s = 1; s <= 10; s++) {
if (ones < s && m > s) {
if (!byMinuend.has(m)) byMinuend.set(m, []);
byMinuend.get(m)!.push({ minuend: m, subtrahend: s, result: m - s });
}
}
}
const minuends = Array.from(byMinuend.keys());
for (let i = minuends.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[minuends[i], minuends[j]] = [minuends[j], minuends[i]];
}
const problems: Array<{ minuend: number; subtrahend: number; result: number }> = [];
const target = 9;
if (minuends.length >= target) {
for (let i = 0; i < target; i++) {
const arr = byMinuend.get(minuends[i])!;
problems.push(arr[Math.floor(Math.random() * arr.length)]);
}
} else {
const per = Math.floor(target / minuends.length);
const rem = target % minuends.length;
for (let i = 0; i < minuends.length; i++) {
const arr = [...byMinuend.get(minuends[i])!];
for (let k = arr.length - 1; k > 0; k--) { const j = Math.floor(Math.random() * (k + 1)); [arr[k], arr[j]] = [arr[j], arr[k]]; }
const count = per + (i < rem ? 1 : 0);
for (let j = 0; j < Math.min(count, arr.length); j++) problems.push(arr[j]);
}
}
for (let i = problems.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[problems[i], problems[j]] = [problems[j], problems[i]];
}
return { problems };
}
function generateOneDigitAdditionData() {
const problems: Array<{ left: number; right: number; result: number }> = [];
const used = new Set<string>();
let attempts = 0;
while (problems.length < 6 && attempts < 1000) {
attempts++;
const left = Math.floor(Math.random() * 9) + 1;
const right = Math.floor(Math.random() * (10 - left)) + 1;
const key = `${left},${right}`;
if (used.has(key)) continue;
used.add(key);
problems.push({ left, right, result: left + right });
}
return { problems };
}
function generateCalculationPracticeData(mode?: string, extra?: any) {
const opType = extra?.operationType || 'addition';
let maxValue = 10;
if (mode === 'within-20') maxValue = 20;
else if (mode === 'within-50') maxValue = 50;
else if (mode === 'within-100') maxValue = 100;
const problems: Array<{ left: number; operator: '+' | '-'; right: number; result: number }> = [];
const used = new Set<string>();
let attempts = 0;
while (problems.length < 33 && attempts < 2000) {
attempts++;
let left: number, right: number, operator: '+' | '-', result: number;
const doAdd = opType === 'addition' ? true : opType === 'subtraction' ? false : Math.random() < 0.5;
if (doAdd) {
left = Math.floor(Math.random() * (maxValue - 1)) + 1;
right = Math.floor(Math.random() * (maxValue - left)) + 1;
operator = '+'; result = left + right;
} else {
left = Math.floor(Math.random() * maxValue) + 1;
right = Math.floor(Math.random() * left) + 1;
operator = '-'; result = left - right;
}
const key = `${left},${operator},${right}`;
if (used.has(key)) continue;
used.add(key);
problems.push({ left, operator, right, result });
}
return { problems };
}
function generateMultiplicationTableData() {
return {};
}
// ─── Registry ───
export const MATH_TYPE_CONFIGS: MathTypeConfig[] = [
{
id: 'number-find',
title: '看数字,涂一涂',
subTitle: '找一找下面相同的数字,涂上颜色',
icon: '🔍',
hasNumberGrid: true,
createDrawService: (canvas, ctx, opts) => new NumberFindDraw(canvas, ctx, opts),
generateData: generateNumberFindData,
},
{
id: 'counting-matching',
title: '数一数,连一连',
subTitle: '通过连线配对数字和对应的数量图形',
icon: '🔗',
actionsTitle: '选择图片',
actions: [
{ value: 'twelve-animals', label: '十二生肖' },
{ value: 'fruits', label: '水果' },
],
defaultMode: 'twelve-animals',
createDrawService: (canvas, ctx, opts) => new CountMatchDraw(canvas, ctx, opts),
generateData: generateCountMatchData,
},
{
id: 'number-coloring',
title: '按数字涂颜色',
subTitle: '按数字给相应的圆圈涂上颜色',
icon: '🎨',
actionsTitle: '选择样式',
actions: [
{ value: 'caterpillar', label: '毛毛虫' },
{ value: 'circle', label: '圆圈' },
],
defaultMode: 'caterpillar',
createDrawService: (canvas, ctx, opts) => new NumberColorDraw(canvas, ctx, opts),
generateData: generateNumberColorData,
},
{
id: 'addition',
title: '图形加减法',
subTitle: '通过图形化方式学习加减法运算',
icon: '',
actionsTitle: '选择类型',
actions: [
{ value: 'addition-5', label: '5以内加法' },
{ value: 'addition-10', label: '10以内加法' },
{ value: 'subtraction-10', label: '10以内减法' },
{ value: 'addition-subtraction-10', label: '10以内加减法' },
],
defaultMode: 'addition-5',
createDrawService: (canvas, ctx, opts) => new AdditionDraw(canvas, ctx, opts),
generateData: generateAdditionData,
},
{
id: 'missing-number',
title: '填缺少的数字',
subTitle: '在数字序列中找出并填写缺失的数字',
icon: '❓',
actionsTitle: '选择范围',
actions: [
{ value: '10', label: '10以内' },
{ value: '20', label: '20以内' },
{ value: '40', label: '40以内' },
{ value: '50', label: '50以内' },
{ value: '80', label: '80以内' },
{ value: '100', label: '100以内' },
{ value: '120', label: '120以内' },
],
defaultMode: '10',
createDrawService: (canvas, ctx, opts) => new MissingNumberDraw(canvas, ctx, opts),
generateData: generateMissingNumberData,
},
{
id: 'compare',
title: '数一数,比大小',
subTitle: '数一数,比较数量,在⭕️中填入>、<、=',
icon: '⚖️',
createDrawService: (canvas, ctx, opts) => new CompareDraw(canvas, ctx, opts),
generateData: generateCompareData,
},
{
id: 'counting-select',
title: '数一数,选一选',
subTitle: '数出物品数量,从多个选项中选择正确答案',
icon: '🎯',
createDrawService: (canvas, ctx, opts) => new CountingSelectDraw(canvas, ctx, opts),
generateData: generateCountingSelectData,
},
{
id: 'number-decompose',
title: '10以内分与合',
subTitle: '学习数的分解与组合',
icon: '🌳',
actionsTitle: '选择模式',
actions: [
{ value: 'with-image', label: '有图片模式' },
{ value: 'decompose', label: '分模式' },
{ value: 'compose', label: '组合模式' },
],
defaultMode: 'with-image',
createDrawService: (canvas, ctx, opts) => new NumberDecomposeDraw(canvas, ctx, opts),
generateData: (mode) => generateNumberDecomposeData(mode, { maxNumber: 10 }),
},
{
id: 'number-decompose-20',
title: '20以内分与合',
subTitle: '学习20以内数的分解与组合',
icon: '🌲',
actionsTitle: '选择模式',
actions: [
{ value: 'decompose', label: '20以内的分解' },
{ value: 'compose', label: '20以内的组合' },
],
defaultMode: 'decompose',
createDrawService: (canvas, ctx, opts) => new NumberDecomposeDraw(canvas, ctx, opts),
generateData: (mode) => generateNumberDecomposeData(mode, { maxNumber: 20 }),
},
{
id: 'number-sort',
title: '数字排序',
subTitle: '数字排序,写出正确顺序',
icon: '🔢',
createDrawService: (canvas, ctx, opts) => new NumberSortDraw(canvas, ctx, opts),
generateData: generateNumberSortData,
},
{
id: 'number-object-match',
title: '数物对应',
subTitle: '数一数图片数量并对应数字',
icon: '🧮',
actionsTitle: '选择模式',
actions: [
{ value: 'match', label: '连线' },
{ value: 'fill', label: '填写' },
],
defaultMode: 'match',
createDrawService: (canvas, ctx, opts) => new NumberObjectMatchDraw(canvas, ctx, opts),
generateData: generateNumberObjectMatchData,
},
{
id: 'make-ten',
title: '凑十法练习',
subTitle: '通过凑十法学习20以内进位加法',
icon: '🔟',
createDrawService: (canvas, ctx, opts) => new MakeTenDraw(canvas, ctx, opts),
generateData: generateMakeTenData,
},
{
id: 'break-ten',
title: '破十法练习',
subTitle: '通过破十法学习20以内退位减法',
icon: '💥',
createDrawService: (canvas, ctx, opts) => new BreakTenDraw(canvas, ctx, opts),
generateData: generateBreakTenData,
},
{
id: 'flat-ten',
title: '平十法练习',
subTitle: '通过平十法学习退位减法',
icon: '📐',
createDrawService: (canvas, ctx, opts) => new FlatTenDraw(canvas, ctx, opts),
generateData: generateFlatTenData,
},
{
id: 'borrow-ten',
title: '借十法练习',
subTitle: '拆大数,借出10,减小数,加剩数',
icon: '🏦',
actionsTitle: '选择范围',
actions: [
{ value: 'within-30', label: '30以内' },
{ value: 'within-50', label: '50以内' },
{ value: 'within-100', label: '100以内' },
],
defaultMode: 'within-50',
createDrawService: (canvas, ctx, opts) => new BreakTenDraw(canvas, ctx, { ...opts, mnemonic: '拆大数,借出10,减小数,加剩数' }),
generateData: generateBorrowTenData,
},
{
id: 'one-digit-addition',
title: '一位数加法',
subTitle: '通过圆点学习一位数加法运算',
icon: '⚫',
createDrawService: (canvas, ctx, opts) => new OneDigitAdditionDraw(canvas, ctx, opts),
generateData: generateOneDigitAdditionData,
},
{
id: 'practice-addition',
title: '加法口算',
subTitle: '加法计算练习题',
icon: '📝',
actionsTitle: '选择范围',
actions: [
{ value: 'within-10', label: '10以内' },
{ value: 'within-20', label: '20以内' },
{ value: 'within-50', label: '50以内' },
{ value: 'within-100', label: '100以内' },
],
defaultMode: 'within-10',
createDrawService: (canvas, ctx, opts) => new CalculationPracticeDraw(canvas, ctx, opts),
generateData: (mode) => generateCalculationPracticeData(mode, { operationType: 'addition' }),
},
{
id: 'practice-subtraction',
title: '减法口算',
subTitle: '减法计算练习题',
icon: '📝',
actionsTitle: '选择范围',
actions: [
{ value: 'within-10', label: '10以内' },
{ value: 'within-20', label: '20以内' },
{ value: 'within-50', label: '50以内' },
{ value: 'within-100', label: '100以内' },
],
defaultMode: 'within-10',
createDrawService: (canvas, ctx, opts) => new CalculationPracticeDraw(canvas, ctx, opts),
generateData: (mode) => generateCalculationPracticeData(mode, { operationType: 'subtraction' }),
},
{
id: 'practice-mixed',
title: '混合口算',
subTitle: '加减法混合计算练习题',
icon: '📝',
actionsTitle: '选择范围',
actions: [
{ value: 'within-10', label: '10以内' },
{ value: 'within-20', label: '20以内' },
{ value: 'within-50', label: '50以内' },
{ value: 'within-100', label: '100以内' },
],
defaultMode: 'within-10',
createDrawService: (canvas, ctx, opts) => new CalculationPracticeDraw(canvas, ctx, opts),
generateData: (mode) => generateCalculationPracticeData(mode, { operationType: 'mixed' }),
},
{
id: 'multiplication-table',
title: '九九乘法表',
subTitle: '九九乘法表',
icon: '✖️',
createDrawService: (canvas, ctx, opts) => new MultiplicationTableDraw(canvas, ctx, opts),
generateData: generateMultiplicationTableData,
},
];
/**
* 通过路由 ID 查找类型配置
* 处理别名映射:counting-fill → counting-select, number-object-fill → number-object-match 等
*/
export function findTypeByRouteId(
routeId: string,
): { typeConfig: MathTypeConfig; mode?: string; extra?: Record<string, any> } | null {
const direct = MATH_TYPE_CONFIGS.find((t) => t.id === routeId);
if (direct) return { typeConfig: direct };
// 别名映射
const aliases: Record<string, { typeId: string; extra?: Record<string, any> }> = {
'counting-fill': { typeId: 'counting-select', extra: { functionId: 'counting-fill' } },
'number-object-fill': { typeId: 'number-object-match', extra: { defaultMode: 'fill' } },
'number-write': { typeId: 'number-find', extra: { functionId: 'number-write' } },
'number-find': { typeId: 'number-find' },
'addition-5': { typeId: 'addition' },
'addition-10': { typeId: 'addition', extra: { defaultMode: 'addition-10' } },
'subtraction-10': { typeId: 'addition', extra: { defaultMode: 'subtraction-10' } },
'addition-subtraction-10': { typeId: 'addition', extra: { defaultMode: 'addition-subtraction-10' } },
};
const alias = aliases[routeId];
if (alias) {
const cfg = MATH_TYPE_CONFIGS.find((t) => t.id === alias.typeId);
if (cfg) return { typeConfig: cfg, mode: alias.extra?.defaultMode, extra: alias.extra };
}
return { typeConfig: MATH_TYPE_CONFIGS[0] };
}