feat: 数感启蒙、专注力页面重构、首页入口开发
This commit is contained in:
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"navigationBarTitleText": "加减法计算",
|
||||
"navigationBarBackgroundColor": "#FFD719",
|
||||
"homeButton": true,
|
||||
"backgroundColor": "#F6F6F6",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
||||
"math-type-selector": "../../components/math-type-selector/math-type-selector",
|
||||
"math-bottom-buttons": "../../components/math-bottom-buttons/math-bottom-buttons",
|
||||
"draw-ad": "../../components/draw-ad/draw-ad"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
@import '../../base/baseDrawPage.wxss';
|
||||
@@ -1,219 +0,0 @@
|
||||
import AdditionDraw from '../shared/service/additionDraw';
|
||||
import {
|
||||
createMathPage,
|
||||
CanvasDataState,
|
||||
} from '../shared/common/mathPageMixin';
|
||||
|
||||
createMathPage({
|
||||
canvas: null as Canvas | null,
|
||||
ctx: null as RenderingContext | null,
|
||||
boxHeight: 0,
|
||||
boxWidth: 0,
|
||||
drawService: null as AdditionDraw | null,
|
||||
calculationData: null as {
|
||||
problems: Array<{
|
||||
type: 'addition' | 'subtraction';
|
||||
left: number;
|
||||
right: number;
|
||||
result: number;
|
||||
}>;
|
||||
} | null,
|
||||
|
||||
data: {
|
||||
pageTitle: '加减法计算',
|
||||
subTitle: '通过图形化方式学习加减法运算',
|
||||
functionId: '',
|
||||
hasContent: false,
|
||||
showShareDialog: false,
|
||||
currentMode: 'addition-5', // 'addition-5', 'addition-10', 'subtraction-10', 'addition-subtraction-10'
|
||||
currentModeName: '5以内加法',
|
||||
typeActions: [
|
||||
{ name: '5以内加法', value: 'addition-5' },
|
||||
{ name: '10以内加法', value: 'addition-10' },
|
||||
{ name: '10以内减法', value: 'subtraction-10' },
|
||||
{ name: '10以内加减法', value: 'addition-subtraction-10' },
|
||||
],
|
||||
imageType: 'twelve-animals', // 图片类型:twelve-animals 或 fruits
|
||||
} as CanvasDataState & {
|
||||
currentMode: string;
|
||||
currentModeName: string;
|
||||
typeActions: Array<{ name: string; value: string }>;
|
||||
imageType: string;
|
||||
},
|
||||
|
||||
onLoad(options: { id?: string }) {
|
||||
const functionId = options.id || 'addition-5';
|
||||
this.initPageInfo(functionId, '加减法计算');
|
||||
|
||||
// 根据 functionId 设置默认类型
|
||||
if (functionId === 'addition-5') {
|
||||
this.setData({
|
||||
currentMode: 'addition-5',
|
||||
currentModeName: '5以内加法',
|
||||
});
|
||||
} else if (functionId === 'addition-10') {
|
||||
this.setData({
|
||||
currentMode: 'addition-10',
|
||||
currentModeName: '10以内加法',
|
||||
});
|
||||
} else if (functionId === 'subtraction-10') {
|
||||
this.setData({
|
||||
currentMode: 'subtraction-10',
|
||||
currentModeName: '10以内减法',
|
||||
});
|
||||
} else if (functionId === 'addition-subtraction-10') {
|
||||
this.setData({
|
||||
currentMode: 'addition-subtraction-10',
|
||||
currentModeName: '10以内加减法',
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onReady() {
|
||||
this.initCanvas({
|
||||
createDrawService: (
|
||||
canvas: Canvas,
|
||||
ctx: RenderingContext,
|
||||
options?: Record<string, any>,
|
||||
) => {
|
||||
return new AdditionDraw(canvas, ctx, options);
|
||||
},
|
||||
drawServiceOptions: {
|
||||
subTitle: this.data.subTitle,
|
||||
},
|
||||
onCanvasReady: () => {
|
||||
// 初始随机生成
|
||||
this.onRandom();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 绘制Canvas内容
|
||||
*/
|
||||
async drawCanvas() {
|
||||
if (!this.ctx || !this.drawService || !this.calculationData) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 更新 Header 的 Title
|
||||
if (this.drawService) {
|
||||
this.drawService.options.title = this.data.currentModeName;
|
||||
}
|
||||
|
||||
await this.drawService.draw(
|
||||
this.calculationData,
|
||||
this.data.currentMode,
|
||||
);
|
||||
this.setData({ hasContent: true });
|
||||
} catch (error) {
|
||||
console.error('绘制失败:', error);
|
||||
this.setData({ hasContent: false });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 随机生成
|
||||
*/
|
||||
onRandom() {
|
||||
const problems: Array<{
|
||||
type: 'addition' | 'subtraction';
|
||||
left: number;
|
||||
right: number;
|
||||
result: number;
|
||||
}> = [];
|
||||
|
||||
const type = this.data.currentMode;
|
||||
|
||||
// 生成5道题目
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (type === 'addition-5') {
|
||||
// 5以内加法:和 ≤ 5
|
||||
// left >= 1, right >= 1, left + right <= 5
|
||||
const maxSum = 5;
|
||||
const left = Math.floor(Math.random() * (maxSum - 1)) + 1; // 1 到 4
|
||||
const maxRight = maxSum - left; // 确保 left + right <= 5
|
||||
const right = Math.floor(Math.random() * maxRight) + 1; // 1 到 maxRight
|
||||
const result = left + right;
|
||||
problems.push({
|
||||
type: 'addition',
|
||||
left,
|
||||
right,
|
||||
result,
|
||||
});
|
||||
} else if (type === 'addition-10') {
|
||||
// 10以内加法:和 ≤ 10
|
||||
// left >= 1, right >= 1, left + right <= 10
|
||||
const maxSum = 10;
|
||||
const left = Math.floor(Math.random() * (maxSum - 1)) + 1; // 1 到 9
|
||||
const maxRight = maxSum - left; // 确保 left + right <= 10
|
||||
const right = Math.floor(Math.random() * maxRight) + 1; // 1 到 maxRight
|
||||
const result = left + right;
|
||||
problems.push({
|
||||
type: 'addition',
|
||||
left,
|
||||
right,
|
||||
result,
|
||||
});
|
||||
} else if (type === 'subtraction-10') {
|
||||
// 10以内减法:被减数 ≤ 10
|
||||
// left <= 10, left - right = result, result >= 1
|
||||
const maxLeft = 10;
|
||||
const left = Math.floor(Math.random() * maxLeft) + 1; // 1 到 10
|
||||
const maxRight = left - 1; // 确保 result >= 1
|
||||
const right = Math.floor(Math.random() * maxRight) + 1; // 1 到 maxRight
|
||||
const result = left - right;
|
||||
problems.push({
|
||||
type: 'subtraction',
|
||||
left,
|
||||
right,
|
||||
result,
|
||||
});
|
||||
} else if (type === 'addition-subtraction-10') {
|
||||
// 加减法混合
|
||||
if (Math.random() < 0.5) {
|
||||
// 加法:和 ≤ 10
|
||||
const maxSum = 10;
|
||||
const left = Math.floor(Math.random() * (maxSum - 1)) + 1; // 1 到 9
|
||||
const maxRight = maxSum - left; // 确保 left + right <= 10
|
||||
const right = Math.floor(Math.random() * maxRight) + 1; // 1 到 maxRight
|
||||
const result = left + right;
|
||||
problems.push({
|
||||
type: 'addition',
|
||||
left,
|
||||
right,
|
||||
result,
|
||||
});
|
||||
} else {
|
||||
// 减法:被减数 ≤ 10
|
||||
const maxLeft = 10;
|
||||
const left = Math.floor(Math.random() * maxLeft) + 1; // 1 到 10
|
||||
const maxRight = left - 1; // 确保 result >= 1
|
||||
const right = Math.floor(Math.random() * maxRight) + 1; // 1 到 maxRight
|
||||
const result = left - right;
|
||||
problems.push({
|
||||
type: 'subtraction',
|
||||
left,
|
||||
right,
|
||||
result,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.calculationData = { problems };
|
||||
this.drawCanvas();
|
||||
},
|
||||
|
||||
/** 选择类型 */
|
||||
onSelectType(event: any) {
|
||||
const { name, value } = event.detail;
|
||||
this.setData({
|
||||
currentMode: value,
|
||||
currentModeName: name,
|
||||
});
|
||||
// 重新生成数据
|
||||
this.onRandom();
|
||||
},
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
<!-- 模版不能跨包引用 -->
|
||||
<import src="../shared/templates/canvas-page-template.wxml" />
|
||||
|
||||
<template
|
||||
is="canvasPageTemplate"
|
||||
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: true, currentModeName: currentModeName, typeActions: typeActions, adType: 'mathDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"navigationBarTitleText": "借十法练习",
|
||||
"navigationBarBackgroundColor": "#FFD719",
|
||||
"homeButton": true,
|
||||
"backgroundColor": "#F6F6F6",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
||||
"math-type-selector": "../../components/math-type-selector/math-type-selector",
|
||||
"math-bottom-buttons": "../../components/math-bottom-buttons/math-bottom-buttons",
|
||||
"draw-ad": "../../components/draw-ad/draw-ad"
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
// 借十法练习页面样式
|
||||
@import '../../base/baseDrawPage.wxss';
|
||||
@@ -1,195 +0,0 @@
|
||||
import BreakTenDraw from '../shared/service/breakTenDraw';
|
||||
import {
|
||||
createMathPage,
|
||||
CanvasDataState,
|
||||
} from '../shared/common/mathPageMixin';
|
||||
|
||||
createMathPage({
|
||||
canvas: null as Canvas | null,
|
||||
ctx: null as RenderingContext | null,
|
||||
boxHeight: 0,
|
||||
boxWidth: 0,
|
||||
drawService: null as BreakTenDraw | null,
|
||||
borrowTenData: null as {
|
||||
problems: Array<{
|
||||
minuend: number; // 被减数
|
||||
subtrahend: number; // 减数
|
||||
result: number;
|
||||
}>;
|
||||
} | null,
|
||||
|
||||
data: {
|
||||
pageTitle: '借十法练习',
|
||||
functionId: '',
|
||||
hasContent: false,
|
||||
showShareDialog: false,
|
||||
currentMode: 'within-50', // 'within-30', 'within-50', 'within-100'
|
||||
currentModeName: '50以内',
|
||||
typeActions: [
|
||||
{ name: '30以内', value: 'within-30' },
|
||||
{ name: '50以内', value: 'within-50' },
|
||||
{ name: '100以内', value: 'within-100' },
|
||||
],
|
||||
} as CanvasDataState & {
|
||||
currentMode: string;
|
||||
currentModeName: string;
|
||||
typeActions: Array<{ name: string; value: string }>;
|
||||
},
|
||||
|
||||
onLoad(options: { id?: string }) {
|
||||
const functionId = options.id || 'borrow-ten';
|
||||
this.initPageInfo(functionId, '借十法练习');
|
||||
},
|
||||
|
||||
onReady() {
|
||||
this.initCanvas({
|
||||
createDrawService: (
|
||||
canvas: Canvas,
|
||||
ctx: RenderingContext,
|
||||
options?: Record<string, any>,
|
||||
) => {
|
||||
return new BreakTenDraw(canvas, ctx, options);
|
||||
},
|
||||
drawServiceOptions: {
|
||||
mnemonic: '拆大数,借出10,减小数,加剩数', // 自定义口诀
|
||||
},
|
||||
onCanvasReady: () => {
|
||||
// 初始随机生成
|
||||
this.onRandom();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 绘制Canvas内容
|
||||
*/
|
||||
async drawCanvas() {
|
||||
if (!this.ctx || !this.drawService || !this.borrowTenData) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.drawService.draw(this.borrowTenData);
|
||||
this.setData({ hasContent: true });
|
||||
} catch (error) {
|
||||
console.error('绘制失败:', error);
|
||||
this.setData({ hasContent: false });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 随机生成
|
||||
*/
|
||||
onRandom() {
|
||||
// 根据当前模式确定被减数的最大值
|
||||
let maxMinuend = 30; // 默认30以内
|
||||
if (this.data.currentMode === 'within-50') {
|
||||
maxMinuend = 50;
|
||||
} else if (this.data.currentMode === 'within-100') {
|
||||
maxMinuend = 100;
|
||||
}
|
||||
|
||||
// 按被减数分组,列出每个被减数对应的所有符合条件的减数组合
|
||||
const problemsByMinuend: Map<
|
||||
number,
|
||||
Array<{
|
||||
minuend: number;
|
||||
subtrahend: number;
|
||||
result: number;
|
||||
}>
|
||||
> = new Map();
|
||||
|
||||
// 遍历所有可能的被减数(20到maxMinuend)和减数(1-10)组合
|
||||
for (let minuend = 20; minuend <= maxMinuend; minuend++) {
|
||||
const minuendOnes = minuend % 10; // 被减数的个位数
|
||||
|
||||
for (let subtrahend = 1; subtrahend <= 10; subtrahend++) {
|
||||
// 检查条件:
|
||||
// 1. 被减数的个位 < 减数(借十法的关键要求:减数要大于被减数的个位数)
|
||||
// 2. 被减数 > 减数(确保结果大于0)
|
||||
if (minuendOnes < subtrahend && minuend > subtrahend) {
|
||||
if (!problemsByMinuend.has(minuend)) {
|
||||
problemsByMinuend.set(minuend, []);
|
||||
}
|
||||
problemsByMinuend.get(minuend)!.push({
|
||||
minuend,
|
||||
subtrahend,
|
||||
result: minuend - subtrahend,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 从每个被减数组中选择题目,确保每个被减数都有代表
|
||||
const problems: Array<{
|
||||
minuend: number;
|
||||
subtrahend: number;
|
||||
result: number;
|
||||
}> = [];
|
||||
|
||||
// 获取所有被减数列表并打乱
|
||||
const minuends = Array.from(problemsByMinuend.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]];
|
||||
}
|
||||
|
||||
// 从每个被减数组中随机选择,确保分布均匀
|
||||
// 如果被减数数量 >= 9,每个被减数至少选1个;如果 < 9,尽量均匀分配
|
||||
const targetCount = 9;
|
||||
const minuendCount = minuends.length;
|
||||
|
||||
if (minuendCount >= targetCount) {
|
||||
// 被减数数量足够,从每个被减数组中随机选择1个
|
||||
for (let i = 0; i < targetCount; i++) {
|
||||
const minuend = minuends[i];
|
||||
const problemsForMinuend = problemsByMinuend.get(minuend)!;
|
||||
const randomIndex = Math.floor(
|
||||
Math.random() * problemsForMinuend.length,
|
||||
);
|
||||
problems.push(problemsForMinuend[randomIndex]);
|
||||
}
|
||||
} else {
|
||||
// 被减数数量不足,尽量均匀分配
|
||||
const perMinuend = Math.floor(targetCount / minuendCount);
|
||||
const remainder = targetCount % minuendCount;
|
||||
|
||||
for (let i = 0; i < minuendCount; i++) {
|
||||
const minuend = minuends[i];
|
||||
const problemsForMinuend = problemsByMinuend.get(minuend)!;
|
||||
const count = perMinuend + (i < remainder ? 1 : 0); // 前remainder个多选1个
|
||||
|
||||
// 从该被减数组中随机选择count个(不重复)
|
||||
const shuffled = [...problemsForMinuend];
|
||||
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]];
|
||||
}
|
||||
|
||||
for (let j = 0; j < Math.min(count, shuffled.length); j++) {
|
||||
problems.push(shuffled[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]];
|
||||
}
|
||||
|
||||
this.borrowTenData = { problems };
|
||||
this.drawCanvas();
|
||||
},
|
||||
|
||||
/** 选择类型 */
|
||||
onSelectType(event: any) {
|
||||
const { name, value } = event.detail;
|
||||
this.setData({
|
||||
currentMode: value,
|
||||
currentModeName: name,
|
||||
});
|
||||
// 重新生成数据
|
||||
this.onRandom();
|
||||
},
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
<!-- 模版不能跨包引用 -->
|
||||
<import src="../shared/templates/canvas-page-template.wxml" />
|
||||
|
||||
<template
|
||||
is="canvasPageTemplate"
|
||||
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: true, currentModeName: currentModeName, typeActions: typeActions, adType: 'mathDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"navigationBarTitleText": "破十法练习",
|
||||
"navigationBarBackgroundColor": "#FFD719",
|
||||
"homeButton": true,
|
||||
"backgroundColor": "#F6F6F6",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"toy-button": "../../toy/button-v2/button",
|
||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
||||
"math-type-selector": "../../components/math-type-selector/math-type-selector",
|
||||
"math-bottom-buttons": "../../components/math-bottom-buttons/math-bottom-buttons",
|
||||
"draw-ad": "../../components/draw-ad/draw-ad"
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
// 破十法练习页面样式
|
||||
@import '../../base/baseDrawPage.wxss';
|
||||
@@ -1,128 +0,0 @@
|
||||
import BreakTenDraw from '../shared/service/breakTenDraw';
|
||||
import {
|
||||
createMathPage,
|
||||
CanvasDataState,
|
||||
} from '../shared/common/mathPageMixin';
|
||||
|
||||
createMathPage({
|
||||
canvas: null as Canvas | null,
|
||||
ctx: null as RenderingContext | null,
|
||||
boxHeight: 0,
|
||||
boxWidth: 0,
|
||||
drawService: null as BreakTenDraw | null,
|
||||
breakTenData: null as {
|
||||
problems: Array<{
|
||||
minuend: number; // 被减数
|
||||
subtrahend: number; // 减数
|
||||
result: number;
|
||||
}>;
|
||||
} | null,
|
||||
|
||||
data: {
|
||||
pageTitle: '破十法练习',
|
||||
subTitle: '通过破十法学习20以内退位减法',
|
||||
functionId: '',
|
||||
hasContent: false,
|
||||
showShareDialog: false,
|
||||
} as CanvasDataState,
|
||||
|
||||
onLoad(options: { id?: string }) {
|
||||
const functionId = options.id || 'break-ten';
|
||||
this.initPageInfo(functionId, '破十法练习');
|
||||
},
|
||||
|
||||
onReady() {
|
||||
this.initCanvas({
|
||||
createDrawService: (
|
||||
canvas: Canvas,
|
||||
ctx: RenderingContext,
|
||||
options?: Record<string, any>,
|
||||
) => {
|
||||
return new BreakTenDraw(canvas, ctx, options);
|
||||
},
|
||||
drawServiceOptions: {
|
||||
subTitle: this.data.subTitle,
|
||||
},
|
||||
onCanvasReady: () => {
|
||||
// 初始随机生成
|
||||
this.onRandom();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 绘制Canvas内容
|
||||
*/
|
||||
async drawCanvas() {
|
||||
if (!this.ctx || !this.drawService || !this.breakTenData) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.drawService.draw(this.breakTenData);
|
||||
this.setData({ hasContent: true });
|
||||
} catch (error) {
|
||||
console.error('绘制失败:', error);
|
||||
this.setData({ hasContent: false });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 随机生成
|
||||
*/
|
||||
onRandom() {
|
||||
const problems: Array<{
|
||||
minuend: number;
|
||||
subtrahend: number;
|
||||
result: number;
|
||||
}> = [];
|
||||
// 用于记录已生成的题目,避免重复
|
||||
const usedProblems = new Set<string>();
|
||||
|
||||
// 生成9道题目(3行3列)
|
||||
let attempts = 0;
|
||||
const maxAttempts = 1000; // 最大尝试次数,避免无限循环
|
||||
|
||||
while (problems.length < 9 && attempts < maxAttempts) {
|
||||
attempts++;
|
||||
// 生成两个数,确保:
|
||||
// 1. 被减数大于10且小于等于20
|
||||
// 2. 减数小于10
|
||||
// 3. 结果大于0(被减数 > 减数)
|
||||
const minuend = Math.floor(Math.random() * 10) + 11; // 11-20
|
||||
const subtrahend = Math.floor(Math.random() * 9) + 1; // 1-9
|
||||
|
||||
// 确保被减数 > 减数
|
||||
if (minuend <= subtrahend) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const result = minuend - subtrahend;
|
||||
|
||||
// 检查是否重复
|
||||
const problemKey = `${minuend},${subtrahend}`;
|
||||
if (usedProblems.has(problemKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 添加到已使用集合
|
||||
usedProblems.add(problemKey);
|
||||
|
||||
problems.push({
|
||||
minuend,
|
||||
subtrahend,
|
||||
result,
|
||||
});
|
||||
}
|
||||
|
||||
// 如果尝试次数过多仍未生成足够的题目,输出警告
|
||||
if (problems.length < 9) {
|
||||
console.warn(
|
||||
`只生成了 ${problems.length} 道题目,可能符合条件的题目组合不足`,
|
||||
);
|
||||
}
|
||||
|
||||
this.breakTenData = { problems };
|
||||
this.drawCanvas();
|
||||
},
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
<!-- 模版不能跨包引用 -->
|
||||
<import src="../shared/templates/canvas-page-template.wxml" />
|
||||
|
||||
<template
|
||||
is="canvasPageTemplate"
|
||||
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: false, adType: 'mathDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"navigationBarTitleText": "计算练习题",
|
||||
"navigationBarBackgroundColor": "#FFD719",
|
||||
"homeButton": true,
|
||||
"backgroundColor": "#F6F6F6",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"toy-button": "../../toy/button-v2/button",
|
||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
||||
"math-bottom-buttons": "../../components/math-bottom-buttons/math-bottom-buttons",
|
||||
"math-type-selector": "../../components/math-type-selector/math-type-selector",
|
||||
"draw-ad": "../../components/draw-ad/draw-ad"
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
// 计算练习题页面样式
|
||||
@import '../../base/baseDrawPage.wxss';
|
||||
@@ -1,208 +0,0 @@
|
||||
import CalculationPracticeDraw from '../shared/service/calculationPracticeDraw';
|
||||
import {
|
||||
createMathPage,
|
||||
CanvasDataState,
|
||||
} from '../shared/common/mathPageMixin';
|
||||
|
||||
type OperationType = 'addition' | 'subtraction' | 'mixed';
|
||||
|
||||
createMathPage({
|
||||
canvas: null as Canvas | null,
|
||||
ctx: null as RenderingContext | null,
|
||||
boxHeight: 0,
|
||||
boxWidth: 0,
|
||||
drawService: null as CalculationPracticeDraw | null,
|
||||
calculationPracticeData: null as {
|
||||
problems: Array<{
|
||||
left: number;
|
||||
operator: '+' | '-';
|
||||
right: number;
|
||||
result: number;
|
||||
}>;
|
||||
} | null,
|
||||
|
||||
data: {
|
||||
pageTitle: '计算练习题',
|
||||
functionId: '',
|
||||
hasContent: false,
|
||||
showShareDialog: false,
|
||||
operationType: 'addition' as OperationType, // 运算类型:加法、减法、混合
|
||||
currentMode: 'within-10', // 默认10以内
|
||||
currentModeName: '10以内',
|
||||
typeActions: [
|
||||
{ name: '10以内', value: 'within-10' },
|
||||
{ name: '20以内', value: 'within-20' },
|
||||
{ name: '50以内', value: 'within-50' },
|
||||
{ name: '100以内', value: 'within-100' },
|
||||
],
|
||||
} as CanvasDataState & {
|
||||
operationType: OperationType;
|
||||
currentMode: string;
|
||||
currentModeName: string;
|
||||
typeActions: Array<{ name: string; value: string }>;
|
||||
},
|
||||
|
||||
onLoad(options: { id?: string; type?: string }) {
|
||||
const functionId = options.id || 'practice-addition';
|
||||
|
||||
// 根据 functionId 确定运算类型
|
||||
let operationType: OperationType = 'addition';
|
||||
if (functionId === 'practice-subtraction') {
|
||||
operationType = 'subtraction';
|
||||
} else if (functionId === 'practice-mixed') {
|
||||
operationType = 'mixed';
|
||||
} else if (options.type) {
|
||||
operationType = options.type as OperationType;
|
||||
}
|
||||
|
||||
let pageTitle = '计算练习题';
|
||||
if (operationType === 'addition') {
|
||||
pageTitle = '加法运算';
|
||||
} else if (operationType === 'subtraction') {
|
||||
pageTitle = '减法运算';
|
||||
} else if (operationType === 'mixed') {
|
||||
pageTitle = '混合运算';
|
||||
}
|
||||
|
||||
this.setData({
|
||||
operationType,
|
||||
pageTitle,
|
||||
});
|
||||
this.initPageInfo(functionId, pageTitle);
|
||||
},
|
||||
|
||||
onReady() {
|
||||
this.initCanvas({
|
||||
createDrawService: (
|
||||
canvas: Canvas,
|
||||
ctx: RenderingContext,
|
||||
options?: Record<string, any>,
|
||||
) => {
|
||||
return new CalculationPracticeDraw(canvas, ctx, options);
|
||||
},
|
||||
drawServiceOptions: {},
|
||||
onCanvasReady: () => {
|
||||
// 初始随机生成
|
||||
this.onRandom();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 绘制Canvas内容
|
||||
*/
|
||||
async drawCanvas() {
|
||||
if (!this.ctx || !this.drawService || !this.calculationPracticeData) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.drawService.draw(this.calculationPracticeData);
|
||||
this.setData({ hasContent: true });
|
||||
} catch (error) {
|
||||
console.error('绘制失败:', error);
|
||||
this.setData({ hasContent: false });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 随机生成
|
||||
*/
|
||||
onRandom() {
|
||||
// 根据当前模式确定最大值
|
||||
let maxValue = 10;
|
||||
if (this.data.currentMode === 'within-20') {
|
||||
maxValue = 20;
|
||||
} else if (this.data.currentMode === 'within-50') {
|
||||
maxValue = 50;
|
||||
} else if (this.data.currentMode === 'within-100') {
|
||||
maxValue = 100;
|
||||
}
|
||||
|
||||
const problems: Array<{
|
||||
left: number;
|
||||
operator: '+' | '-';
|
||||
right: number;
|
||||
result: number;
|
||||
}> = [];
|
||||
const usedProblems = new Set<string>();
|
||||
let attempts = 0;
|
||||
const maxAttempts = 2000;
|
||||
|
||||
const targetCount = 11 * 3; // 12行3列 = 36道题
|
||||
|
||||
while (problems.length < targetCount && attempts < maxAttempts) {
|
||||
attempts++;
|
||||
|
||||
let left: number;
|
||||
let right: number;
|
||||
let operator: '+' | '-';
|
||||
let result: number;
|
||||
|
||||
// 根据运算类型生成题目
|
||||
if (this.data.operationType === 'addition') {
|
||||
// 加法:left + right <= maxValue
|
||||
left = Math.floor(Math.random() * (maxValue - 1)) + 1; // 1 到 maxValue-1
|
||||
const maxRight = maxValue - left;
|
||||
right = Math.floor(Math.random() * maxRight) + 1; // 1 到 maxRight
|
||||
operator = '+';
|
||||
result = left + right;
|
||||
} else if (this.data.operationType === 'subtraction') {
|
||||
// 减法:left - right >= 0, left <= maxValue
|
||||
left = Math.floor(Math.random() * maxValue) + 1; // 1 到 maxValue
|
||||
right = Math.floor(Math.random() * left) + 1; // 1 到 left
|
||||
operator = '-';
|
||||
result = left - right;
|
||||
} else {
|
||||
// 混合运算:随机选择加法或减法
|
||||
const isAddition = Math.random() < 0.5;
|
||||
if (isAddition) {
|
||||
left = Math.floor(Math.random() * (maxValue - 1)) + 1;
|
||||
const maxRight = maxValue - left;
|
||||
right = Math.floor(Math.random() * maxRight) + 1;
|
||||
operator = '+';
|
||||
result = left + right;
|
||||
} else {
|
||||
left = Math.floor(Math.random() * maxValue) + 1;
|
||||
right = Math.floor(Math.random() * left) + 1;
|
||||
operator = '-';
|
||||
result = left - right;
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 "left,operator,right" 作为唯一标识,避免重复
|
||||
const problemKey = `${left},${operator},${right}`;
|
||||
if (usedProblems.has(problemKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
usedProblems.add(problemKey);
|
||||
problems.push({
|
||||
left,
|
||||
operator,
|
||||
right,
|
||||
result,
|
||||
});
|
||||
}
|
||||
|
||||
if (problems.length < targetCount) {
|
||||
console.warn(
|
||||
`只生成了 ${problems.length} 道题目,可能符合条件的题目组合不足`,
|
||||
);
|
||||
}
|
||||
|
||||
this.calculationPracticeData = { problems };
|
||||
this.drawCanvas();
|
||||
},
|
||||
|
||||
/** 选择类型 */
|
||||
onSelectType(event: any) {
|
||||
const { name, value } = event.detail;
|
||||
this.setData({
|
||||
currentMode: value,
|
||||
currentModeName: name,
|
||||
});
|
||||
// 重新生成数据
|
||||
this.onRandom();
|
||||
},
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
<!-- 模版不能跨包引用 -->
|
||||
<import src="../shared/templates/canvas-page-template.wxml" />
|
||||
|
||||
<template
|
||||
is="canvasPageTemplate"
|
||||
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: true, currentModeName: currentModeName, typeActions: typeActions, adType: 'mathDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"navigationBarTitleText": "数一数,比大小",
|
||||
"navigationBarBackgroundColor": "#FFD719",
|
||||
"homeButton": true,
|
||||
"backgroundColor": "#F6F6F6",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"toy-button": "../../toy/button-v2/button",
|
||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
||||
"math-bottom-buttons": "../../components/math-bottom-buttons/math-bottom-buttons",
|
||||
"draw-ad": "../../components/draw-ad/draw-ad"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
@import '../../base/baseDrawPage.wxss';
|
||||
@@ -1,124 +0,0 @@
|
||||
import CompareDraw from '../shared/service/compareDraw';
|
||||
import {
|
||||
createMathPage,
|
||||
CanvasDataState,
|
||||
} from '../shared/common/mathPageMixin';
|
||||
|
||||
// 获取公共方
|
||||
|
||||
createMathPage({
|
||||
canvas: null as Canvas | null,
|
||||
ctx: null as RenderingContext | null,
|
||||
boxHeight: 0,
|
||||
boxWidth: 0,
|
||||
drawService: null as CompareDraw | null,
|
||||
compareData: null as {
|
||||
problems: Array<{
|
||||
leftCount: number;
|
||||
rightCount: number;
|
||||
leftImageIndex: number;
|
||||
rightImageIndex: number;
|
||||
imageType: 'fruits' | 'twelve-animals';
|
||||
}>;
|
||||
} | null,
|
||||
|
||||
data: {
|
||||
pageTitle: '数一数,比大小',
|
||||
subTitle: '数一数,比较数量,在⭕️中填入>、<、=',
|
||||
functionId: '',
|
||||
hasContent: false,
|
||||
showShareDialog: false,
|
||||
} as CanvasDataState,
|
||||
|
||||
onLoad(options: { id?: string }) {
|
||||
const functionId = options.id || 'compare';
|
||||
this.initPageInfo(functionId, '数一数,比大小');
|
||||
},
|
||||
|
||||
onReady() {
|
||||
this.initCanvas({
|
||||
createDrawService: (
|
||||
canvas: Canvas,
|
||||
ctx: RenderingContext,
|
||||
options?: Record<string, any>,
|
||||
) => {
|
||||
return new CompareDraw(canvas, ctx, options);
|
||||
},
|
||||
drawServiceOptions: {
|
||||
subTitle: this.data.subTitle,
|
||||
},
|
||||
onCanvasReady: () => {
|
||||
// Canvas 初始化完成后,生成初始数据
|
||||
this.onRandom();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 绘制Canvas内容
|
||||
*/
|
||||
async drawCanvas() {
|
||||
if (!this.ctx || !this.drawService || !this.compareData) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.drawService.draw(this.compareData);
|
||||
this.setData({ hasContent: true });
|
||||
} catch (error) {
|
||||
console.error('绘制失败:', error);
|
||||
this.setData({ hasContent: false });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 随机生成
|
||||
*/
|
||||
onRandom() {
|
||||
this.generateCompareData();
|
||||
this.drawCanvas();
|
||||
},
|
||||
|
||||
/**
|
||||
* 生成比较数据
|
||||
*/
|
||||
generateCompareData() {
|
||||
const problems: Array<{
|
||||
leftCount: number;
|
||||
rightCount: number;
|
||||
leftImageIndex: number;
|
||||
rightImageIndex: number;
|
||||
imageType: 'fruits' | 'twelve-animals';
|
||||
}> = [];
|
||||
|
||||
// 生成6道题目
|
||||
for (let i = 0; i < 12; i++) {
|
||||
// 随机选择图片类型
|
||||
const imageType: 'fruits' | 'twelve-animals' =
|
||||
Math.random() < 0.5 ? 'fruits' : 'twelve-animals';
|
||||
|
||||
// 根据图片类型确定最大索引
|
||||
const maxImageIndex = imageType === 'fruits' ? 22 : 12;
|
||||
|
||||
// 生成左右两边的数量(1-10)
|
||||
const leftCount = Math.floor(Math.random() * 10) + 1;
|
||||
const rightCount = Math.floor(Math.random() * 10) + 1;
|
||||
|
||||
// 随机选择图片索引
|
||||
const leftImageIndex =
|
||||
Math.floor(Math.random() * maxImageIndex) + 1;
|
||||
const rightImageIndex =
|
||||
Math.floor(Math.random() * maxImageIndex) + 1;
|
||||
|
||||
problems.push({
|
||||
leftCount,
|
||||
rightCount,
|
||||
leftImageIndex,
|
||||
rightImageIndex,
|
||||
imageType,
|
||||
});
|
||||
}
|
||||
|
||||
this.compareData = { problems };
|
||||
},
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
<!-- 模版不能跨包引用 -->
|
||||
<import src="../shared/templates/canvas-page-template.wxml" />
|
||||
|
||||
<template
|
||||
is="canvasPageTemplate"
|
||||
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: false, adType: 'mathDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"navigationBarTitleText": "数一数,连一连",
|
||||
"navigationBarBackgroundColor": "#FFD719",
|
||||
"homeButton": true,
|
||||
"backgroundColor": "#F6F6F6",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
||||
"math-type-selector": "../../components/math-type-selector/math-type-selector",
|
||||
"math-bottom-buttons": "../../components/math-bottom-buttons/math-bottom-buttons",
|
||||
"draw-ad": "../../components/draw-ad/draw-ad"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
@import '../../base/baseDrawPage.wxss';
|
||||
@@ -1,194 +0,0 @@
|
||||
import CountMatchDraw from '../shared/service/countMatchDraw';
|
||||
import NumberColorDraw from '../shared/service/numberColorDraw';
|
||||
import {
|
||||
createMathPage,
|
||||
CanvasDataState,
|
||||
} from '../shared/common/mathPageMixin';
|
||||
|
||||
createMathPage({
|
||||
canvas: null as Canvas | null,
|
||||
ctx: null as RenderingContext | null,
|
||||
boxHeight: 0,
|
||||
boxWidth: 0,
|
||||
drawService: null as CountMatchDraw | NumberColorDraw | null,
|
||||
matchData: null as {
|
||||
leftNumbers: number[];
|
||||
rightNumbers: number[];
|
||||
} | null,
|
||||
colorData: null as {
|
||||
numbers: number[];
|
||||
} | null,
|
||||
|
||||
data: {
|
||||
pageTitle: '数一数,连一连',
|
||||
functionId: '',
|
||||
hasContent: false,
|
||||
showShareDialog: false,
|
||||
showTypeSelector: true, // 控制是否显示类型选择器
|
||||
currentMode: 'twelve-animals', // 'twelve-animals' 或 'fruits' 或 'circle' 或 'caterpillar'
|
||||
currentModeName: '十二生肖',
|
||||
typeActions: [
|
||||
{ name: '十二生肖', value: 'twelve-animals' },
|
||||
{ name: '水果', value: 'fruits' },
|
||||
],
|
||||
} as CanvasDataState & {
|
||||
showTypeSelector: boolean;
|
||||
currentMode: string;
|
||||
currentModeName: string;
|
||||
typeActions: Array<{ name: string; value: string }>;
|
||||
},
|
||||
|
||||
onLoad(options: { id?: string; mode?: string }) {
|
||||
const functionId = options.id || 'counting-matching';
|
||||
this.initPageInfo(functionId, '数一数,连一连');
|
||||
|
||||
// 根据 functionId 设置不同的类型选择器
|
||||
if (functionId === 'number-coloring') {
|
||||
const currentMode = options.mode || 'caterpillar';
|
||||
this.setData({
|
||||
currentMode: currentMode,
|
||||
currentModeName:
|
||||
currentMode === 'caterpillar' ? '毛毛虫' : '圆圈',
|
||||
typeActions: [
|
||||
{ name: '毛毛虫', value: 'caterpillar' },
|
||||
{ name: '圆圈', value: 'circle' },
|
||||
],
|
||||
});
|
||||
} else {
|
||||
const currentMode = options.mode || 'twelve-animals';
|
||||
this.setData({
|
||||
currentMode,
|
||||
currentModeName:
|
||||
currentMode === 'twelve-animals' ? '十二生肖' : '水果',
|
||||
typeActions: [
|
||||
{ name: '十二生肖', value: 'twelve-animals' },
|
||||
{ name: '水果', value: 'fruits' },
|
||||
],
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onReady() {
|
||||
this.initCanvas({
|
||||
createDrawService: (
|
||||
canvas: Canvas,
|
||||
ctx: RenderingContext,
|
||||
options?: Record<string, any>,
|
||||
) => {
|
||||
// 根据 functionId 创建不同的绘制服务
|
||||
if (this.data.functionId === 'number-coloring') {
|
||||
return new NumberColorDraw(canvas, ctx, options);
|
||||
} else {
|
||||
return new CountMatchDraw(canvas, ctx, options);
|
||||
}
|
||||
},
|
||||
drawServiceOptions: {
|
||||
subTitle:
|
||||
this.data.functionId === 'number-coloring'
|
||||
? '按数字给相应的圆圈涂上颜色'
|
||||
: '通过连线配对数字和对应的数量图形',
|
||||
},
|
||||
onCanvasReady: () => {
|
||||
// 初始随机生成
|
||||
this.onRandom();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 绘制Canvas内容
|
||||
*/
|
||||
async drawCanvas() {
|
||||
if (!this.ctx || !this.drawService) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.data.functionId === 'number-coloring') {
|
||||
if (!this.colorData) {
|
||||
return;
|
||||
}
|
||||
await (this.drawService as NumberColorDraw).draw(
|
||||
this.colorData,
|
||||
this.data.currentMode,
|
||||
);
|
||||
} else {
|
||||
if (!this.matchData) {
|
||||
return;
|
||||
}
|
||||
await (this.drawService as CountMatchDraw).draw(
|
||||
this.matchData,
|
||||
this.data.currentMode,
|
||||
);
|
||||
}
|
||||
this.setData({ hasContent: true });
|
||||
} catch (error) {
|
||||
console.error('绘制失败:', error);
|
||||
this.setData({ hasContent: false });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 随机生成
|
||||
*/
|
||||
onRandom() {
|
||||
if (this.data.functionId === 'number-coloring') {
|
||||
// 按数字涂颜色模式:生成6个随机数字(1-10)
|
||||
const availableNumbers = Array.from(
|
||||
{ length: 10 },
|
||||
(_, i) => i + 1,
|
||||
);
|
||||
const numbers: number[] = [];
|
||||
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const randomIndex = Math.floor(
|
||||
Math.random() * availableNumbers.length,
|
||||
);
|
||||
const number = availableNumbers.splice(randomIndex, 1)[0];
|
||||
numbers.push(number);
|
||||
}
|
||||
|
||||
this.colorData = { numbers };
|
||||
} else {
|
||||
// 数一数连一连模式:生成5个不同的数字(1-10)
|
||||
const availableNumbers = Array.from(
|
||||
{ length: 10 },
|
||||
(_, i) => i + 1,
|
||||
);
|
||||
const leftNumbers: number[] = [];
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const randomIndex = Math.floor(
|
||||
Math.random() * availableNumbers.length,
|
||||
);
|
||||
const number = availableNumbers.splice(randomIndex, 1)[0];
|
||||
leftNumbers.push(number);
|
||||
}
|
||||
|
||||
// 复制数字数组并打乱顺序,作为右侧显示的数字
|
||||
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],
|
||||
];
|
||||
}
|
||||
|
||||
this.matchData = { leftNumbers, rightNumbers };
|
||||
}
|
||||
|
||||
this.drawCanvas();
|
||||
},
|
||||
|
||||
/** 选择类型 */
|
||||
onSelectType(event: any) {
|
||||
const { name, value } = event.detail;
|
||||
this.setData({
|
||||
currentMode: value,
|
||||
currentModeName: name,
|
||||
});
|
||||
// 重新生成数据
|
||||
this.onRandom();
|
||||
},
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
<!-- 模版不能跨包引用 -->
|
||||
<import src="../shared/templates/canvas-page-template.wxml" />
|
||||
|
||||
<template
|
||||
is="canvasPageTemplate"
|
||||
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: true, currentModeName: currentModeName, typeActions: typeActions, adType: 'mathDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"navigationBarTitleText": "数一数,选一选",
|
||||
"navigationBarBackgroundColor": "#FFD719",
|
||||
"homeButton": true,
|
||||
"backgroundColor": "#F6F6F6",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"toy-button": "../../toy/button-v2/button",
|
||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
||||
"math-bottom-buttons": "../../components/math-bottom-buttons/math-bottom-buttons",
|
||||
"draw-ad": "../../components/draw-ad/draw-ad"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
@import '../../base/baseDrawPage.wxss';
|
||||
@@ -1,151 +0,0 @@
|
||||
import CountingSelectDraw from '../shared/service/countingSelectDraw';
|
||||
import {
|
||||
createMathPage,
|
||||
CanvasDataState,
|
||||
} from '../shared/common/mathPageMixin';
|
||||
|
||||
createMathPage({
|
||||
canvas: null as Canvas | null,
|
||||
ctx: null as RenderingContext | null,
|
||||
boxHeight: 0,
|
||||
boxWidth: 0,
|
||||
drawService: null as CountingSelectDraw | null,
|
||||
countingSelectData: null as {
|
||||
problems: Array<{
|
||||
count: number; // 图片数量(正确答案)
|
||||
imageIndex: number; // 图片索引
|
||||
imageType: 'fruits' | 'twelve-animals'; // 图片类型
|
||||
options?: number[]; // 三个数字选项(选一选模式需要)
|
||||
correctIndex?: number; // 正确答案在options中的索引(选一选模式需要)
|
||||
}>;
|
||||
} | null,
|
||||
|
||||
data: {
|
||||
pageTitle: '数一数,选一选',
|
||||
subTitle: '数出物品数量,从多个选项中选择正确答案',
|
||||
functionId: '',
|
||||
hasContent: false,
|
||||
showShareDialog: false,
|
||||
} as CanvasDataState,
|
||||
|
||||
onLoad(options: { id?: string }) {
|
||||
const functionId = options.id || 'counting-select';
|
||||
// 初始化页面信息(会从 mathFunctions.ts 读取 title 和 desc)
|
||||
this.initPageInfo(functionId, '数一数,选一选');
|
||||
},
|
||||
|
||||
onReady() {
|
||||
this.initCanvas({
|
||||
createDrawService: (
|
||||
canvas: Canvas,
|
||||
ctx: RenderingContext,
|
||||
options?: Record<string, any>,
|
||||
) => {
|
||||
return new CountingSelectDraw(canvas, ctx, options);
|
||||
},
|
||||
drawServiceOptions: {
|
||||
subTitle: this.data.subTitle,
|
||||
},
|
||||
onCanvasReady: () => {
|
||||
// Canvas 初始化完成后,生成初始数据
|
||||
this.onRandom();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 绘制Canvas内容
|
||||
*/
|
||||
async drawCanvas() {
|
||||
if (!this.ctx || !this.drawService || !this.countingSelectData) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 判断是选一选还是填一填模式
|
||||
const mode =
|
||||
this.data.functionId === 'counting-fill' ? 'fill' : 'select';
|
||||
await this.drawService.draw(this.countingSelectData, mode);
|
||||
this.setData({ hasContent: true });
|
||||
} catch (error) {
|
||||
console.error('绘制失败:', error);
|
||||
this.setData({ hasContent: false });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 随机生成
|
||||
*/
|
||||
onRandom() {
|
||||
this.generateCountingSelectData();
|
||||
this.drawCanvas();
|
||||
},
|
||||
|
||||
/**
|
||||
* 生成数一数选一选/填一填数据
|
||||
*/
|
||||
generateCountingSelectData() {
|
||||
const isFillMode = this.data.functionId === 'counting-fill';
|
||||
const problems: Array<{
|
||||
count: number;
|
||||
imageIndex: number;
|
||||
imageType: 'fruits' | 'twelve-animals';
|
||||
options?: number[];
|
||||
correctIndex?: number;
|
||||
}> = [];
|
||||
|
||||
// 生成9道题目
|
||||
for (let i = 0; i < 9; i++) {
|
||||
// 随机选择图片类型
|
||||
const imageType: 'fruits' | 'twelve-animals' =
|
||||
Math.random() < 0.5 ? 'fruits' : 'twelve-animals';
|
||||
|
||||
// 根据图片类型确定最大索引
|
||||
const maxImageIndex = imageType === 'fruits' ? 22 : 12;
|
||||
|
||||
// 生成图片数量(1-10)
|
||||
const count = Math.floor(Math.random() * 10) + 1;
|
||||
|
||||
// 随机选择图片索引
|
||||
const imageIndex = Math.floor(Math.random() * maxImageIndex) + 1;
|
||||
|
||||
const problem: {
|
||||
count: number;
|
||||
imageIndex: number;
|
||||
imageType: 'fruits' | 'twelve-animals';
|
||||
options?: number[];
|
||||
correctIndex?: number;
|
||||
} = {
|
||||
count,
|
||||
imageIndex,
|
||||
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 wrongAnswer: number;
|
||||
do {
|
||||
wrongAnswer = Math.floor(Math.random() * 10) + 1;
|
||||
} while (wrongAnswer === count);
|
||||
options.push(wrongAnswer);
|
||||
}
|
||||
}
|
||||
|
||||
problem.options = options;
|
||||
problem.correctIndex = correctIndex;
|
||||
}
|
||||
|
||||
problems.push(problem);
|
||||
}
|
||||
|
||||
this.countingSelectData = { problems };
|
||||
},
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
<!-- 模版不能跨包引用 -->
|
||||
<import src="../shared/templates/canvas-page-template.wxml" />
|
||||
|
||||
<template
|
||||
is="canvasPageTemplate"
|
||||
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: false, adType: 'mathDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"navigationBarTitleText": "平十法练习",
|
||||
"navigationBarBackgroundColor": "#FFD719",
|
||||
"homeButton": true,
|
||||
"backgroundColor": "#F6F6F6",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
||||
"toy-button": "../../toy/button-v2/button",
|
||||
"math-bottom-buttons": "../../components/math-bottom-buttons/math-bottom-buttons",
|
||||
"draw-ad": "../../components/draw-ad/draw-ad"
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
// 平十法练习页面样式
|
||||
@import '../../base/baseDrawPage.wxss';
|
||||
@@ -1,121 +0,0 @@
|
||||
import FlatTenDraw from '../shared/service/flatTenDraw';
|
||||
import {
|
||||
createMathPage,
|
||||
CanvasDataState,
|
||||
} from '../shared/common/mathPageMixin';
|
||||
|
||||
createMathPage({
|
||||
canvas: null as Canvas | null,
|
||||
ctx: null as RenderingContext | null,
|
||||
boxHeight: 0,
|
||||
boxWidth: 0,
|
||||
drawService: null as FlatTenDraw | null,
|
||||
flatTenData: null as {
|
||||
problems: Array<{
|
||||
minuend: number; // 被减数
|
||||
subtrahend: number; // 减数
|
||||
result: number;
|
||||
}>;
|
||||
} | null,
|
||||
|
||||
data: {
|
||||
pageTitle: '平十法练习',
|
||||
functionId: '',
|
||||
hasContent: false,
|
||||
showShareDialog: false,
|
||||
} as CanvasDataState,
|
||||
|
||||
onLoad(options: { id?: string }) {
|
||||
const functionId = options.id || 'flat-ten';
|
||||
this.initPageInfo(functionId, '平十法练习');
|
||||
},
|
||||
|
||||
onReady() {
|
||||
this.initCanvas({
|
||||
createDrawService: (
|
||||
canvas: Canvas,
|
||||
ctx: RenderingContext,
|
||||
options?: Record<string, any>,
|
||||
) => {
|
||||
return new FlatTenDraw(canvas, ctx, options);
|
||||
},
|
||||
drawServiceOptions: {},
|
||||
onCanvasReady: () => {
|
||||
// 初始随机生成
|
||||
this.onRandom();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 绘制Canvas内容
|
||||
*/
|
||||
async drawCanvas() {
|
||||
if (!this.ctx || !this.drawService || !this.flatTenData) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.drawService.draw(this.flatTenData);
|
||||
this.setData({ hasContent: true });
|
||||
} catch (error) {
|
||||
console.error('绘制失败:', error);
|
||||
this.setData({ hasContent: false });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 随机生成
|
||||
*/
|
||||
onRandom() {
|
||||
// 先列出所有符合条件的题目组合
|
||||
const allValidProblems: Array<{
|
||||
minuend: number;
|
||||
subtrahend: number;
|
||||
result: number;
|
||||
}> = [];
|
||||
|
||||
// 遍历所有可能的被减数(11-19)和减数(1-10)组合
|
||||
for (let minuend = 11; minuend < 20; minuend++) {
|
||||
const minuendOnes = minuend % 10; // 被减数的个位数
|
||||
|
||||
for (let subtrahend = 1; subtrahend <= 10; subtrahend++) {
|
||||
// 检查条件:
|
||||
// 1. 减数 > 被减数的个位数(平十法的关键要求)
|
||||
// 2. 被减数 > 减数(确保结果大于0)
|
||||
if (subtrahend > minuendOnes && minuend > subtrahend) {
|
||||
allValidProblems.push({
|
||||
minuend,
|
||||
subtrahend,
|
||||
result: minuend - subtrahend,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 从所有符合条件的组合中随机选择9个,确保分布均匀
|
||||
const problems: Array<{
|
||||
minuend: number;
|
||||
subtrahend: number;
|
||||
result: number;
|
||||
}> = [];
|
||||
|
||||
// 如果符合条件的组合少于9个,使用所有组合
|
||||
const targetCount = Math.min(9, allValidProblems.length);
|
||||
|
||||
// 使用 Fisher-Yates 洗牌算法随机选择
|
||||
const shuffled = [...allValidProblems];
|
||||
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]];
|
||||
}
|
||||
|
||||
// 取前9个
|
||||
for (let i = 0; i < targetCount; i++) {
|
||||
problems.push(shuffled[i]);
|
||||
}
|
||||
|
||||
this.flatTenData = { problems };
|
||||
this.drawCanvas();
|
||||
},
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
<!-- 模版不能跨包引用 -->
|
||||
<import src="../shared/templates/canvas-page-template.wxml" />
|
||||
|
||||
<template
|
||||
is="canvasPageTemplate"
|
||||
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: false, adType: 'mathDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"navigationBarTitleText": "凑十法练习",
|
||||
"navigationBarBackgroundColor": "#FFD719",
|
||||
"homeButton": true,
|
||||
"backgroundColor": "#F6F6F6",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"toy-button": "../../toy/button-v2/button",
|
||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
||||
"math-bottom-buttons": "../../components/math-bottom-buttons/math-bottom-buttons",
|
||||
"draw-ad": "../../components/draw-ad/draw-ad"
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
// 凑十法练习页面样式
|
||||
|
||||
@import '../../base/baseDrawPage.wxss';
|
||||
@@ -1,168 +0,0 @@
|
||||
import MakeTenDraw from '../shared/service/makeTenDraw';
|
||||
import {
|
||||
createMathPage,
|
||||
CanvasDataState,
|
||||
} from '../shared/common/mathPageMixin';
|
||||
|
||||
createMathPage({
|
||||
canvas: null as Canvas | null,
|
||||
ctx: null as RenderingContext | null,
|
||||
boxHeight: 0,
|
||||
boxWidth: 0,
|
||||
drawService: null as MakeTenDraw | null,
|
||||
makeTenData: null as {
|
||||
problems: Array<{
|
||||
left: number;
|
||||
right: number;
|
||||
result: number;
|
||||
}>;
|
||||
} | null,
|
||||
|
||||
data: {
|
||||
pageTitle: '凑十法练习',
|
||||
subTitle: '通过凑十法学习20以内进位加法',
|
||||
functionId: '',
|
||||
hasContent: false,
|
||||
showShareDialog: false,
|
||||
} as CanvasDataState,
|
||||
|
||||
onLoad(options: { id?: string }) {
|
||||
const functionId = options.id || 'make-ten';
|
||||
this.initPageInfo(functionId, '凑十法练习');
|
||||
},
|
||||
|
||||
onReady() {
|
||||
this.initCanvas({
|
||||
createDrawService: (
|
||||
canvas: Canvas,
|
||||
ctx: RenderingContext,
|
||||
options?: Record<string, any>,
|
||||
) => {
|
||||
return new MakeTenDraw(canvas, ctx, options);
|
||||
},
|
||||
drawServiceOptions: {
|
||||
subTitle: this.data.subTitle,
|
||||
},
|
||||
onCanvasReady: () => {
|
||||
// 初始随机生成
|
||||
this.onRandom();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 绘制Canvas内容
|
||||
*/
|
||||
async drawCanvas() {
|
||||
if (!this.ctx || !this.drawService || !this.makeTenData) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.drawService.draw(this.makeTenData);
|
||||
this.setData({ hasContent: true });
|
||||
} catch (error) {
|
||||
console.error('绘制失败:', error);
|
||||
this.setData({ hasContent: false });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 随机生成
|
||||
*/
|
||||
onRandom() {
|
||||
const problems: Array<{
|
||||
left: number;
|
||||
right: number;
|
||||
result: number;
|
||||
}> = [];
|
||||
// 用于记录已生成的题目,避免重复(9+2 和 2+9 视为不同)
|
||||
const usedProblems = new Set<string>();
|
||||
|
||||
// 生成9道题目(3行3列)
|
||||
let attempts = 0;
|
||||
const maxAttempts = 1000; // 最大尝试次数,避免无限循环
|
||||
|
||||
while (problems.length < 9 && attempts < maxAttempts) {
|
||||
attempts++;
|
||||
// 生成两个数,确保:
|
||||
// 1. 两个加数都在1-9之间(不能是10)
|
||||
// 2. 和大于10且小于等于20(需要凑十法)
|
||||
// 3. 大数至少是6(因为5无法满足条件:5 + 最大小数4 = 9 < 11)
|
||||
let left: number;
|
||||
let right: number;
|
||||
|
||||
// 随机决定大数在前还是在后
|
||||
const bigFirst = Math.random() < 0.5;
|
||||
|
||||
if (bigFirst) {
|
||||
// 大数在前:left 在 6-9 之间,right 在 1-9 之间
|
||||
// 确保 left + right >= 11 且 left + right <= 20
|
||||
left = Math.floor(Math.random() * 4) + 6; // 6-9
|
||||
// right 的最小值:确保 left + right >= 11,即 right >= 11 - left
|
||||
const minRight = Math.max(1, 11 - left);
|
||||
// right 的最大值:确保 left + right <= 20,即 right <= 20 - left,且 right <= 9
|
||||
const maxRight = Math.min(9, 20 - left);
|
||||
// 确保 right < left(因为 left 是大数)
|
||||
const actualMaxRight = Math.min(maxRight, left - 1);
|
||||
// 检查是否有有效的 right 值
|
||||
if (actualMaxRight >= minRight) {
|
||||
right =
|
||||
Math.floor(
|
||||
Math.random() * (actualMaxRight - minRight + 1),
|
||||
) + minRight;
|
||||
} else {
|
||||
// 如果无法生成有效值,跳过本次循环
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// 大数在后:left 在 1-9 之间,right 在 6-9 之间
|
||||
// 确保 left + right >= 11 且 left + right <= 20
|
||||
right = Math.floor(Math.random() * 4) + 6; // 6-9
|
||||
// left 的最小值:确保 left + right >= 11,即 left >= 11 - right
|
||||
const minLeft = Math.max(1, 11 - right);
|
||||
// left 的最大值:确保 left + right <= 20,即 left <= 20 - right,且 left <= 9
|
||||
const maxLeft = Math.min(9, 20 - right);
|
||||
// 确保 left < right(因为 right 是大数)
|
||||
const actualMaxLeft = Math.min(maxLeft, right - 1);
|
||||
// 检查是否有有效的 left 值
|
||||
if (actualMaxLeft >= minLeft) {
|
||||
left =
|
||||
Math.floor(
|
||||
Math.random() * (actualMaxLeft - minLeft + 1),
|
||||
) + minLeft;
|
||||
} else {
|
||||
// 如果无法生成有效值,跳过本次循环
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否重复(9+2 和 2+9 视为不同)
|
||||
const problemKey = `${left},${right}`;
|
||||
if (usedProblems.has(problemKey)) {
|
||||
// 如果已存在,跳过本次循环,重新生成
|
||||
continue;
|
||||
}
|
||||
|
||||
// 添加到已使用集合
|
||||
usedProblems.add(problemKey);
|
||||
|
||||
const result = left + right;
|
||||
problems.push({
|
||||
left,
|
||||
right,
|
||||
result,
|
||||
});
|
||||
}
|
||||
|
||||
// 如果尝试次数过多仍未生成足够的题目,输出警告
|
||||
if (problems.length < 9) {
|
||||
console.warn(
|
||||
`只生成了 ${problems.length} 道题目,可能符合条件的题目组合不足`,
|
||||
);
|
||||
}
|
||||
|
||||
this.makeTenData = { problems };
|
||||
this.drawCanvas();
|
||||
},
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
<!-- 模版不能跨包引用 -->
|
||||
<import src="../shared/templates/canvas-page-template.wxml" />
|
||||
|
||||
<template
|
||||
is="canvasPageTemplate"
|
||||
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: false, adType: 'mathDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "数学练习",
|
||||
"navigationBarBackgroundColor": "#F8F0E0",
|
||||
"navigationBarTextStyle": "black",
|
||||
"backgroundColor": "#FEF6E7",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"nav-bar": "../../components3.0/nav-bar/nav-bar",
|
||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
||||
"draw-ad": "../../components/draw-ad/draw-ad",
|
||||
"preview-footer-actions": "../../components3.0/preview-footer-actions/preview-footer-actions",
|
||||
"toy-icon": "../../toy/icon/icon"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
@import '../../style/theme.less';
|
||||
|
||||
page {
|
||||
background-color: @bg-page;
|
||||
}
|
||||
|
||||
.md-page {
|
||||
min-height: 100vh;
|
||||
padding: 0 @page-padding-x;
|
||||
padding-bottom: calc(200rpx + env(safe-area-inset-bottom));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.md-main {
|
||||
padding-top: 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 64rpx;
|
||||
}
|
||||
|
||||
/* ===== 预览卡 ===== */
|
||||
.md-preview-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.md-preview-card {
|
||||
background: @bg-white;
|
||||
border-radius: @radius-xl;
|
||||
box-shadow: @shadow;
|
||||
padding: 48rpx;
|
||||
border: @border;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.md-preview-kicker {
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: @fs-section-head-title;
|
||||
font-weight: bold;
|
||||
color: @text-secondary;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.md-canvas-wrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 400rpx;
|
||||
background: @bg-gray;
|
||||
border-radius: @radius-sm;
|
||||
border: 2rpx dashed rgba(50, 46, 37, 0.12);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.md-canvas {
|
||||
max-width: 100%;
|
||||
border-radius: @radius-sm;
|
||||
box-shadow: @shadow;
|
||||
}
|
||||
|
||||
/* ===== 区块 ===== */
|
||||
.md-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32rpx;
|
||||
}
|
||||
|
||||
.md-section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #6d3b00;
|
||||
padding-left: 8rpx;
|
||||
}
|
||||
|
||||
/* ===== 练习类型 2列卡片网格 ===== */
|
||||
.md-type-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 32rpx;
|
||||
}
|
||||
|
||||
.md-type-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
padding: 32rpx 24rpx;
|
||||
border-radius: 24rpx;
|
||||
background: #f8f0e0;
|
||||
box-sizing: border-box;
|
||||
transition:
|
||||
transform 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
background 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.md-type-card__icon {
|
||||
font-size: 42rpx;
|
||||
line-height: 1;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.md-type-card--active {
|
||||
background: linear-gradient(145deg, #ffd709 0%, #efc900 100%);
|
||||
box-shadow: 0 8rpx 32rpx rgba(50, 46, 37, 0.06);
|
||||
}
|
||||
|
||||
.md-type-card--pressed {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.md-type-card__label {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: @text-secondary;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.md-type-card--active .md-type-card__label {
|
||||
color: #453900;
|
||||
}
|
||||
|
||||
/* ===== 子选项 Chip ===== */
|
||||
.md-chip-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.md-chip-row--wrap {
|
||||
flex-wrap: wrap;
|
||||
|
||||
.md-chip {
|
||||
flex: none;
|
||||
min-width: calc(33.33% - 16rpx);
|
||||
}
|
||||
}
|
||||
|
||||
.md-chip {
|
||||
flex: 1;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
padding: 24rpx 16rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
line-height: 40rpx;
|
||||
color: @text-secondary;
|
||||
background: @bg-card;
|
||||
border-radius: 32rpx;
|
||||
transition:
|
||||
background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
transform 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
color 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.md-chip--pressed {
|
||||
transform: scale(0.96);
|
||||
background: @brand;
|
||||
color: @text-selected-btn;
|
||||
box-shadow: 0 2rpx 8rpx rgba(50, 46, 37, 0.08);
|
||||
}
|
||||
|
||||
.md-chip--active {
|
||||
background: @brand;
|
||||
color: @text-selected-btn;
|
||||
box-shadow: 0 2rpx 8rpx rgba(50, 46, 37, 0.08);
|
||||
}
|
||||
|
||||
/* ===== 数字选择网格(numberFind 专用) ===== */
|
||||
.md-number-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 30rpx;
|
||||
padding: 0 8rpx;
|
||||
}
|
||||
|
||||
.md-number-cell {
|
||||
aspect-ratio: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 24rpx;
|
||||
background: @bg-card;
|
||||
box-sizing: border-box;
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
transform 0.2s ease,
|
||||
box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.md-number-cell--pressed {
|
||||
transform: scale(0.95);
|
||||
background: @brand;
|
||||
box-shadow: 0 2rpx 8rpx rgba(50, 46, 37, 0.08);
|
||||
}
|
||||
|
||||
.md-number-cell--active {
|
||||
background: @brand;
|
||||
box-shadow: 0 2rpx 8rpx rgba(50, 46, 37, 0.08);
|
||||
}
|
||||
|
||||
.md-number-text {
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: @text-secondary;
|
||||
line-height: 1;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.md-number-cell--active .md-number-text,
|
||||
.md-number-cell--pressed .md-number-text {
|
||||
color: @text-selected-btn;
|
||||
}
|
||||
|
||||
/* ===== 随机生成按钮 ===== */
|
||||
.md-shuffle-btn {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 108rpx;
|
||||
padding: 0 32rpx;
|
||||
border-radius: 32rpx;
|
||||
border: 8rpx solid @bg-card;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
transition:
|
||||
transform 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.md-shuffle-btn--hover {
|
||||
transform: scale(0.98);
|
||||
opacity: 0.92;
|
||||
background-color: rgba(234, 226, 208, 0.55);
|
||||
}
|
||||
|
||||
.md-shuffle-btn toy-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.md-shuffle-btn__text {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
color: @text-secondary;
|
||||
transition: color 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
import { createMathPage } from '../shared/common/mathPageMixin';
|
||||
import { BaseDrawService } from '../../core/draw/baseDraw';
|
||||
import {
|
||||
MATH_TYPE_CONFIGS,
|
||||
findTypeByRouteId,
|
||||
type MathTypeConfig,
|
||||
type MathTypeAction,
|
||||
} from './registry';
|
||||
|
||||
const TYPE_LIST = MATH_TYPE_CONFIGS.map((t) => ({
|
||||
id: t.id,
|
||||
title: t.title,
|
||||
icon: t.icon,
|
||||
}));
|
||||
|
||||
createMathPage({
|
||||
canvas: null as Canvas | null,
|
||||
ctx: null as RenderingContext | null,
|
||||
boxHeight: 0,
|
||||
boxWidth: 0,
|
||||
drawService: null as BaseDrawService | null,
|
||||
currentTypeConfig: null as MathTypeConfig | null,
|
||||
currentData: null as any,
|
||||
routeExtra: null as Record<string, any> | null,
|
||||
|
||||
data: {
|
||||
pageTitle: '数学练习',
|
||||
functionId: '',
|
||||
hasContent: false,
|
||||
showShareDialog: false,
|
||||
boxWidth: 0,
|
||||
boxHeight: 0,
|
||||
selectedTypeId: '',
|
||||
typeList: TYPE_LIST,
|
||||
showActions: false,
|
||||
actionsTitle: '选择模式',
|
||||
currentActions: [] as MathTypeAction[],
|
||||
currentMode: '',
|
||||
showNumberGrid: false,
|
||||
numberList: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||
selectedNumber: 0,
|
||||
},
|
||||
|
||||
onLoad(options: { id?: string; mode?: string }) {
|
||||
const routeId = options.id || MATH_TYPE_CONFIGS[0].id;
|
||||
const result = findTypeByRouteId(routeId);
|
||||
if (!result) return;
|
||||
|
||||
const { typeConfig, mode, extra } = result;
|
||||
const initialMode =
|
||||
options.mode || mode || typeConfig.defaultMode || '';
|
||||
|
||||
this.currentTypeConfig = typeConfig;
|
||||
this.routeExtra = extra || null;
|
||||
|
||||
const title = typeConfig.getTitle
|
||||
? typeConfig.getTitle(initialMode)
|
||||
: typeConfig.title;
|
||||
|
||||
this.setData({
|
||||
selectedTypeId: typeConfig.id,
|
||||
functionId: routeId,
|
||||
pageTitle: title,
|
||||
showActions: !!typeConfig.actions,
|
||||
currentActions: typeConfig.actions || [],
|
||||
actionsTitle: typeConfig.actionsTitle || '选择模式',
|
||||
currentMode: initialMode,
|
||||
showNumberGrid: !!typeConfig.hasNumberGrid,
|
||||
selectedNumber: typeConfig.hasNumberGrid
|
||||
? Math.floor(Math.random() * 10) + 1
|
||||
: 0,
|
||||
});
|
||||
|
||||
this.initPageInfo(routeId, title);
|
||||
},
|
||||
|
||||
onReady() {
|
||||
if (!this.currentTypeConfig) return;
|
||||
|
||||
const typeConfig = this.currentTypeConfig;
|
||||
const mode = this.data.currentMode;
|
||||
|
||||
this.initCanvas({
|
||||
createDrawService: (
|
||||
canvas: Canvas,
|
||||
ctx: RenderingContext,
|
||||
options?: Record<string, any>,
|
||||
) => typeConfig.createDrawService(canvas, ctx, options),
|
||||
drawServiceOptions: {
|
||||
title: this.data.pageTitle,
|
||||
subTitle: typeConfig.getSubTitle
|
||||
? typeConfig.getSubTitle(mode)
|
||||
: typeConfig.subTitle,
|
||||
functionId: this.data.functionId,
|
||||
},
|
||||
onCanvasReady: () => {
|
||||
this.onRandom();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/** 切换练习类型 */
|
||||
onSelectType(e: WechatMiniprogram.TouchEvent) {
|
||||
const id = e.currentTarget.dataset.id as string;
|
||||
if (!id || id === this.data.selectedTypeId) return;
|
||||
|
||||
const typeConfig = MATH_TYPE_CONFIGS.find((t) => t.id === id);
|
||||
if (!typeConfig) return;
|
||||
|
||||
this.currentTypeConfig = typeConfig;
|
||||
this.routeExtra = null;
|
||||
const mode = typeConfig.defaultMode || '';
|
||||
const title = typeConfig.getTitle
|
||||
? typeConfig.getTitle(mode)
|
||||
: typeConfig.title;
|
||||
const subTitle = typeConfig.getSubTitle
|
||||
? typeConfig.getSubTitle(mode)
|
||||
: typeConfig.subTitle;
|
||||
|
||||
this.setData({
|
||||
selectedTypeId: id,
|
||||
functionId: id,
|
||||
pageTitle: title,
|
||||
showActions: !!typeConfig.actions,
|
||||
currentActions: typeConfig.actions || [],
|
||||
actionsTitle: typeConfig.actionsTitle || '选择模式',
|
||||
currentMode: mode,
|
||||
showNumberGrid: !!typeConfig.hasNumberGrid,
|
||||
selectedNumber: typeConfig.hasNumberGrid
|
||||
? Math.floor(Math.random() * 10) + 1
|
||||
: 0,
|
||||
});
|
||||
|
||||
this.initPageInfo(id, title);
|
||||
|
||||
if (this.canvas && this.ctx) {
|
||||
this.drawService = typeConfig.createDrawService(
|
||||
this.canvas,
|
||||
this.ctx,
|
||||
{ title, subTitle, functionId: id },
|
||||
);
|
||||
this.onRandom();
|
||||
}
|
||||
},
|
||||
|
||||
/** 切换子选项(模式) */
|
||||
onSelectMode(e: WechatMiniprogram.TouchEvent) {
|
||||
const value = e.currentTarget.dataset.value as string;
|
||||
if (!value || value === this.data.currentMode) return;
|
||||
|
||||
const typeConfig = this.currentTypeConfig;
|
||||
if (!typeConfig) return;
|
||||
|
||||
const title = typeConfig.getTitle
|
||||
? typeConfig.getTitle(value)
|
||||
: typeConfig.title;
|
||||
const subTitle = typeConfig.getSubTitle
|
||||
? typeConfig.getSubTitle(value)
|
||||
: typeConfig.subTitle;
|
||||
|
||||
this.setData({
|
||||
currentMode: value,
|
||||
pageTitle: title,
|
||||
});
|
||||
|
||||
this.initPageInfo(this.data.functionId, title);
|
||||
|
||||
if (this.drawService) {
|
||||
(this.drawService as any).options.title = title;
|
||||
(this.drawService as any).options.subTitle = subTitle;
|
||||
}
|
||||
|
||||
this.onRandom();
|
||||
},
|
||||
|
||||
/** 选择数字(numberFind 专用) */
|
||||
onSelectNumber(e: WechatMiniprogram.TouchEvent) {
|
||||
const number = parseInt(e.currentTarget.dataset.number);
|
||||
if (isNaN(number) || number === this.data.selectedNumber) return;
|
||||
this.setData({ selectedNumber: number });
|
||||
this.drawCanvas();
|
||||
},
|
||||
|
||||
/** 执行 Canvas 绘制 */
|
||||
async drawCanvas() {
|
||||
if (!this.ctx || !this.drawService || !this.currentData) return;
|
||||
|
||||
try {
|
||||
const typeId = this.data.selectedTypeId;
|
||||
const mode = this.data.currentMode;
|
||||
|
||||
if (typeId === 'number-find') {
|
||||
await this.drawService.draw(this.data.selectedNumber);
|
||||
} else if (
|
||||
typeId === 'counting-matching' ||
|
||||
typeId === 'number-coloring' ||
|
||||
typeId === 'number-object-match'
|
||||
) {
|
||||
await this.drawService.draw(this.currentData, mode);
|
||||
} else if (typeId === 'missing-number') {
|
||||
await this.drawService.draw(this.currentData, mode);
|
||||
} else if (typeId === 'addition') {
|
||||
if (this.drawService) {
|
||||
(this.drawService as any).options.title =
|
||||
this.data.pageTitle;
|
||||
}
|
||||
await this.drawService.draw(this.currentData, mode);
|
||||
} else if (typeId === 'counting-select') {
|
||||
const drawMode =
|
||||
this.routeExtra?.functionId === 'counting-fill'
|
||||
? 'fill'
|
||||
: 'select';
|
||||
await this.drawService.draw(this.currentData, drawMode);
|
||||
} else if (typeId === 'multiplication-table') {
|
||||
await this.drawService.draw({});
|
||||
} else {
|
||||
await this.drawService.draw(this.currentData);
|
||||
}
|
||||
|
||||
this.setData({ hasContent: true });
|
||||
} catch (error) {
|
||||
console.error('绘制失败:', error);
|
||||
this.setData({ hasContent: false });
|
||||
}
|
||||
},
|
||||
|
||||
/** 随机生成数据并绘制 */
|
||||
onRandom() {
|
||||
if (!this.currentTypeConfig) return;
|
||||
|
||||
const extra = {
|
||||
selectedNumber: this.data.selectedNumber,
|
||||
functionId: this.data.functionId,
|
||||
...this.routeExtra,
|
||||
};
|
||||
|
||||
if (this.currentTypeConfig.hasNumberGrid) {
|
||||
const num = Math.floor(Math.random() * 10) + 1;
|
||||
this.setData({ selectedNumber: num });
|
||||
extra.selectedNumber = num;
|
||||
}
|
||||
|
||||
this.currentData = this.currentTypeConfig.generateData(
|
||||
this.data.currentMode,
|
||||
extra,
|
||||
);
|
||||
this.drawCanvas();
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
<nav-bar title="{{pageTitle}}" />
|
||||
|
||||
<view class="md-page">
|
||||
<view class="md-main">
|
||||
<!-- 打印预览卡 -->
|
||||
<view class="md-preview-wrap">
|
||||
<view class="md-preview-card">
|
||||
<text class="md-preview-kicker">打印预览</text>
|
||||
<view id="canvasWrapper" class="md-canvas-wrap">
|
||||
<canvas
|
||||
type="2d"
|
||||
id="canvasContent"
|
||||
class="md-canvas"
|
||||
style="width:{{boxWidth}}px;height:{{boxHeight}}px" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 练习类型选择(2列卡片网格) -->
|
||||
<view class="md-section">
|
||||
<text class="md-section-title">练习类型</text>
|
||||
<view class="md-type-grid">
|
||||
<view
|
||||
wx:for="{{typeList}}"
|
||||
wx:key="id"
|
||||
class="md-type-card {{selectedTypeId === item.id ? 'md-type-card--active' : ''}}"
|
||||
data-id="{{item.id}}"
|
||||
hover-class="md-type-card--pressed"
|
||||
hover-start-time="0"
|
||||
hover-stay-time="70"
|
||||
bindtap="onSelectType">
|
||||
<text class="md-type-card__icon">{{item.icon}}</text>
|
||||
<text class="md-type-card__label">{{item.title}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 子选项(当前类型有模式切换时显示) -->
|
||||
<view wx:if="{{showActions}}" class="md-section">
|
||||
<text class="md-section-title">{{actionsTitle}}</text>
|
||||
<view class="md-chip-row {{currentActions.length > 3 ? 'md-chip-row--wrap' : ''}}">
|
||||
<view
|
||||
wx:for="{{currentActions}}"
|
||||
wx:key="value"
|
||||
class="md-chip {{currentMode === item.value ? 'md-chip--active' : ''}}"
|
||||
data-value="{{item.value}}"
|
||||
hover-class="md-chip--pressed"
|
||||
hover-start-time="0"
|
||||
hover-stay-time="70"
|
||||
bindtap="onSelectMode">
|
||||
{{item.label}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 数字选择网格(numberFind 专用) -->
|
||||
<view wx:if="{{showNumberGrid}}" class="md-section">
|
||||
<text class="md-section-title">选择数字</text>
|
||||
<view class="md-number-grid">
|
||||
<view
|
||||
wx:for="{{numberList}}"
|
||||
wx:key="*this"
|
||||
class="md-number-cell {{selectedNumber === item ? 'md-number-cell--active' : ''}}"
|
||||
data-number="{{item}}"
|
||||
hover-class="md-number-cell--pressed"
|
||||
hover-start-time="0"
|
||||
hover-stay-time="70"
|
||||
bindtap="onSelectNumber">
|
||||
<text class="md-number-text">{{item}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 随机生成按钮 -->
|
||||
<view
|
||||
class="md-shuffle-btn"
|
||||
hover-class="md-shuffle-btn--hover"
|
||||
hover-start-time="0"
|
||||
hover-stay-time="70"
|
||||
bindtap="onRandom">
|
||||
<toy-icon
|
||||
name="refresh"
|
||||
size="40rpx"
|
||||
color="#605b50"
|
||||
custom-class="md-shuffle-btn__icon" />
|
||||
<text class="md-shuffle-btn__text">随机生成</text>
|
||||
</view>
|
||||
|
||||
<!-- 广告位 -->
|
||||
<draw-ad type="mathDraw"></draw-ad>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<preview-footer-actions
|
||||
disabled="{{!hasContent}}"
|
||||
bind:primary="exportToPrint"
|
||||
bind:secondary="onShare" />
|
||||
|
||||
<!-- 分享引导弹窗 -->
|
||||
<share-guide-popup
|
||||
show="{{showShareDialog}}"
|
||||
bind:onClose="onCloseShareDialog"
|
||||
bind:onShareSuccess="onShareSuccess" />
|
||||
@@ -0,0 +1,737 @@
|
||||
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] };
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"navigationBarTitleText": "填上缺少的数字",
|
||||
"navigationBarBackgroundColor": "#FFD719",
|
||||
"homeButton": true,
|
||||
"backgroundColor": "#F6F6F6",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
||||
"math-type-selector": "../../components/math-type-selector/math-type-selector",
|
||||
"math-bottom-buttons": "../../components/math-bottom-buttons/math-bottom-buttons",
|
||||
"draw-ad": "../../components/draw-ad/draw-ad"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
@import '../../base/baseDrawPage.wxss';
|
||||
@@ -1,198 +0,0 @@
|
||||
import { getRandomNumberColor } from '../../constants/colors';
|
||||
import MissingNumberDraw from '../shared/service/missingNumberDraw';
|
||||
import {
|
||||
createMathPage,
|
||||
CanvasDataState,
|
||||
} from '../shared/common/mathPageMixin';
|
||||
|
||||
createMathPage({
|
||||
canvas: null as Canvas | null,
|
||||
ctx: null as RenderingContext | null,
|
||||
boxHeight: 0,
|
||||
boxWidth: 0,
|
||||
drawService: null as MissingNumberDraw | null,
|
||||
missingNumberData: null as {
|
||||
grids: Array<{
|
||||
numbers: (number | null)[];
|
||||
colors: (string | null)[];
|
||||
}>;
|
||||
maxNumber: number;
|
||||
} | null,
|
||||
|
||||
data: {
|
||||
pageTitle: '填上缺少的数字',
|
||||
subTitle: '在数字序列中找出并填写缺失的数字',
|
||||
functionId: '',
|
||||
hasContent: false,
|
||||
showShareDialog: false,
|
||||
showTypeSelector: true,
|
||||
currentMode: 10,
|
||||
currentModeName: '10以内',
|
||||
typeActions: [
|
||||
{ name: '10以内', value: 10 },
|
||||
{ name: '20以内', value: 20 },
|
||||
{ name: '40以内', value: 40 },
|
||||
{ name: '50以内', value: 50 },
|
||||
{ name: '80以内', value: 80 },
|
||||
{ name: '100以内', value: 100 },
|
||||
{ name: '120以内', value: 120 },
|
||||
],
|
||||
} as CanvasDataState & {
|
||||
showTypeSelector: boolean;
|
||||
currentMode: number;
|
||||
currentModeName: string;
|
||||
typeActions: Array<{ name: string; value: number }>;
|
||||
},
|
||||
|
||||
onLoad(options: { id?: string; mode?: number }) {
|
||||
const functionId = options.id || 'missing-number';
|
||||
this.initPageInfo(functionId, '填上缺少的数字');
|
||||
|
||||
this.setData({
|
||||
currentMode: options.mode || 10,
|
||||
currentModeName: options.mode ? `${options.mode}以内` : '10以内',
|
||||
});
|
||||
},
|
||||
|
||||
onReady() {
|
||||
this.initCanvas({
|
||||
createDrawService: (
|
||||
canvas: Canvas,
|
||||
ctx: RenderingContext,
|
||||
options?: Record<string, any>,
|
||||
) => {
|
||||
return new MissingNumberDraw(canvas, ctx, options);
|
||||
},
|
||||
drawServiceOptions: {
|
||||
subTitle: this.data.subTitle,
|
||||
},
|
||||
onCanvasReady: () => {
|
||||
// Canvas 初始化完成后,生成初始数据
|
||||
this.onRandom();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 绘制Canvas内容
|
||||
*/
|
||||
async drawCanvas() {
|
||||
if (!this.ctx || !this.drawService || !this.missingNumberData) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.drawService.draw(
|
||||
this.missingNumberData,
|
||||
String(this.data.currentMode),
|
||||
);
|
||||
this.setData({ hasContent: true });
|
||||
} catch (error) {
|
||||
console.error('绘制失败:', error);
|
||||
this.setData({ hasContent: false });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 随机生成
|
||||
*/
|
||||
onRandom() {
|
||||
const maxNumber = this.data.currentMode;
|
||||
this.generateMissingNumberData(maxNumber);
|
||||
this.drawCanvas();
|
||||
},
|
||||
|
||||
/**
|
||||
* 生成缺失数字数据
|
||||
*/
|
||||
generateMissingNumberData(maxNumber: number) {
|
||||
const grids: Array<{
|
||||
numbers: (number | null)[];
|
||||
colors: (string | null)[];
|
||||
}> = [];
|
||||
const gridMap = {
|
||||
10: {
|
||||
gridCount: 3,
|
||||
},
|
||||
20: {
|
||||
gridCount: 2,
|
||||
},
|
||||
40: {
|
||||
gridCount: 2,
|
||||
},
|
||||
50: {
|
||||
gridCount: 2,
|
||||
},
|
||||
80: {
|
||||
gridCount: 1,
|
||||
},
|
||||
100: {
|
||||
gridCount: 1,
|
||||
},
|
||||
120: {
|
||||
gridCount: 1,
|
||||
},
|
||||
};
|
||||
const { gridCount } = gridMap[maxNumber as keyof typeof gridMap];
|
||||
|
||||
// 生成多个网格
|
||||
for (let gridIndex = 0; gridIndex < gridCount; gridIndex++) {
|
||||
let startNumber = 1;
|
||||
let endNumber = maxNumber;
|
||||
|
||||
const actualNumbersPerGrid = endNumber - startNumber + 1;
|
||||
|
||||
const numbers: number[] = Array.from(
|
||||
{ length: actualNumbersPerGrid },
|
||||
(_, i) => startNumber + i,
|
||||
);
|
||||
|
||||
// 随机隐藏一部分数字(隐藏40-60%)
|
||||
const hideCount = Math.floor(
|
||||
actualNumbersPerGrid * (0.4 + Math.random() * 0.2),
|
||||
);
|
||||
const hiddenIndices = new Set<number>();
|
||||
|
||||
while (hiddenIndices.size < hideCount) {
|
||||
const randomIndex = Math.floor(
|
||||
Math.random() * actualNumbersPerGrid,
|
||||
);
|
||||
hiddenIndices.add(randomIndex);
|
||||
}
|
||||
|
||||
const gridNumbers: (number | null)[] = numbers.map((num, index) =>
|
||||
hiddenIndices.has(index) ? null : num,
|
||||
);
|
||||
|
||||
// 为每个数字分配颜色(包括null位置)
|
||||
const gridColors: (string | null)[] = gridNumbers.map((num) =>
|
||||
num ? this.getRandomNumberColor() : null,
|
||||
);
|
||||
|
||||
grids.push({ numbers: gridNumbers, colors: gridColors });
|
||||
}
|
||||
|
||||
this.missingNumberData = {
|
||||
grids,
|
||||
maxNumber,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取随机数字颜色
|
||||
*/
|
||||
getRandomNumberColor(): string {
|
||||
return getRandomNumberColor();
|
||||
},
|
||||
|
||||
/** 选择类型 */
|
||||
onSelectType(event: any) {
|
||||
const { name, value } = event.detail;
|
||||
this.setData({
|
||||
currentMode: value,
|
||||
currentModeName: name,
|
||||
});
|
||||
// 重新生成数据
|
||||
this.onRandom();
|
||||
},
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
<!-- 模版不能跨包引用 -->
|
||||
<import src="../shared/templates/canvas-page-template.wxml" />
|
||||
|
||||
<template
|
||||
is="canvasPageTemplate"
|
||||
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: true, currentModeName: currentModeName, typeActions: typeActions, adType: 'mathDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"navigationBarTitleText": "九九乘法表",
|
||||
"navigationBarBackgroundColor": "#FFD719",
|
||||
"homeButton": true,
|
||||
"backgroundColor": "#F6F6F6",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"toy-button": "../../toy/button-v2/button",
|
||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
||||
"math-bottom-buttons": "../../components/math-bottom-buttons/math-bottom-buttons",
|
||||
"draw-ad": "../../components/draw-ad/draw-ad"
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
// 九九乘法表页面样式
|
||||
@import '../../base/baseDrawPage.wxss';
|
||||
@@ -1,72 +0,0 @@
|
||||
import MultiplicationTableDraw from '../shared/service/multiplicationTableDraw';
|
||||
import {
|
||||
createMathPage,
|
||||
CanvasDataState,
|
||||
} from '../shared/common/mathPageMixin';
|
||||
|
||||
createMathPage({
|
||||
canvas: null as Canvas | null,
|
||||
ctx: null as RenderingContext | null,
|
||||
boxHeight: 0,
|
||||
boxWidth: 0,
|
||||
drawService: null as MultiplicationTableDraw | null,
|
||||
multiplicationTableData: null as {} | null,
|
||||
|
||||
data: {
|
||||
pageTitle: '九九乘法表',
|
||||
functionId: '',
|
||||
hasContent: false,
|
||||
showShareDialog: false,
|
||||
} as CanvasDataState,
|
||||
|
||||
onLoad(options: { id?: string }) {
|
||||
const functionId = options.id || 'multiplication-table';
|
||||
this.setData({
|
||||
pageTitle: '九九乘法表',
|
||||
});
|
||||
this.initPageInfo(functionId, '九九乘法表');
|
||||
},
|
||||
|
||||
onReady() {
|
||||
this.initCanvas({
|
||||
createDrawService: (
|
||||
canvas: Canvas,
|
||||
ctx: RenderingContext,
|
||||
options?: Record<string, any>,
|
||||
) => {
|
||||
return new MultiplicationTableDraw(canvas, ctx, options);
|
||||
},
|
||||
drawServiceOptions: {
|
||||
title: '九九乘法表',
|
||||
},
|
||||
onCanvasReady: () => {
|
||||
// 初始绘制
|
||||
this.drawCanvas();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 绘制Canvas内容
|
||||
*/
|
||||
async drawCanvas() {
|
||||
if (!this.ctx || !this.drawService) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.drawService.draw({});
|
||||
this.setData({ hasContent: true });
|
||||
} catch (error) {
|
||||
console.error('绘制失败:', error);
|
||||
this.setData({ hasContent: false });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 随机生成(重新绘制,颜色会随机变化)
|
||||
*/
|
||||
onRandom() {
|
||||
this.drawCanvas();
|
||||
},
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
<!-- 模版不能跨包引用 -->
|
||||
<import src="../shared/templates/canvas-page-template.wxml" />
|
||||
|
||||
<template
|
||||
is="canvasPageTemplate"
|
||||
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: false, adType: 'mathDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"navigationBarTitleText": "10以内数的分与合",
|
||||
"navigationBarBackgroundColor": "#FFD719",
|
||||
"homeButton": true,
|
||||
"backgroundColor": "#F6F6F6",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
||||
"math-bottom-buttons": "../../components/math-bottom-buttons/math-bottom-buttons",
|
||||
"math-type-selector": "../../components/math-type-selector/math-type-selector",
|
||||
"draw-ad": "../../components/draw-ad/draw-ad"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
@import '../../base/baseDrawPage.wxss';
|
||||
@@ -1,300 +0,0 @@
|
||||
import NumberDecomposeDraw from '../shared/service/numberDecomposeDraw';
|
||||
import {
|
||||
createMathPage,
|
||||
CanvasDataState,
|
||||
} from '../shared/common/mathPageMixin';
|
||||
|
||||
type DecomposeMode = 'with-image' | 'decompose' | 'compose';
|
||||
|
||||
createMathPage({
|
||||
canvas: null as Canvas | null,
|
||||
ctx: null as RenderingContext | null,
|
||||
boxHeight: 0,
|
||||
boxWidth: 0,
|
||||
drawService: null as NumberDecomposeDraw | null,
|
||||
decomposeData: null as {
|
||||
problems: Array<{
|
||||
whole: number | null; // 总数(null表示组合模式需要填写)
|
||||
part1: number | null; // 第一个部分(null表示需要填写)
|
||||
part2: number | null; // 第二个部分(null表示需要填写)
|
||||
imageIndex?: number; // 图片索引(有图片模式需要)
|
||||
imageType?: 'fruits' | 'twelve-animals'; // 图片类型
|
||||
}>;
|
||||
mode: DecomposeMode;
|
||||
} | null,
|
||||
|
||||
data: {
|
||||
pageTitle: '10以内数的分与合',
|
||||
subTitle: '学习数的分解与组合',
|
||||
functionId: '',
|
||||
hasContent: false,
|
||||
showShareDialog: false,
|
||||
showTypeSelector: true,
|
||||
currentMode: 'with-image',
|
||||
currentModeName: '有图片模式',
|
||||
typeActions: [
|
||||
{ name: '有图片模式', value: 'with-image' },
|
||||
{ name: '分模式', value: 'decompose' },
|
||||
{ name: '组合模式', value: 'compose' },
|
||||
],
|
||||
maxNumber: 10, // 最大数字:10 或 20
|
||||
} as CanvasDataState & {
|
||||
showTypeSelector: boolean;
|
||||
currentMode: DecomposeMode;
|
||||
currentModeName: string;
|
||||
typeActions: Array<{ name: string; value: DecomposeMode }>;
|
||||
maxNumber: number;
|
||||
},
|
||||
|
||||
onLoad(options: { id?: string; mode?: DecomposeMode }) {
|
||||
const functionId = options.id || 'number-decompose';
|
||||
const is20Within = functionId === 'number-decompose-20';
|
||||
const maxNumber = is20Within ? 20 : 10;
|
||||
|
||||
// 20以内只有两种模式,10以内有三种模式
|
||||
let defaultMode: DecomposeMode;
|
||||
let typeActions: Array<{ name: string; value: DecomposeMode }>;
|
||||
let defaultTypeName: string;
|
||||
|
||||
if (is20Within) {
|
||||
// 20以内:只有分模式和组合模式
|
||||
defaultMode = options.mode || 'decompose';
|
||||
typeActions = [
|
||||
{ name: '20以内的分解', value: 'decompose' },
|
||||
{ name: '20以内的组合', value: 'compose' },
|
||||
];
|
||||
defaultTypeName =
|
||||
defaultMode === 'decompose' ? '20以内的分解' : '20以内的组合';
|
||||
} else {
|
||||
// 10以内:有图片模式、分模式、组合模式
|
||||
defaultMode = options.mode || 'with-image';
|
||||
typeActions = [
|
||||
{ name: '有图片模式', value: 'with-image' },
|
||||
{ name: '分模式', value: 'decompose' },
|
||||
{ name: '组合模式', value: 'compose' },
|
||||
];
|
||||
defaultTypeName =
|
||||
defaultMode === 'with-image'
|
||||
? '有图片模式'
|
||||
: defaultMode === 'decompose'
|
||||
? '分模式'
|
||||
: '组合模式';
|
||||
}
|
||||
|
||||
this.setData({
|
||||
currentMode: defaultMode,
|
||||
currentModeName: defaultTypeName,
|
||||
typeActions,
|
||||
maxNumber,
|
||||
});
|
||||
this.initPageInfo(
|
||||
functionId,
|
||||
is20Within ? '20以内数的分与合' : '10以内数的分与合',
|
||||
);
|
||||
},
|
||||
|
||||
onReady() {
|
||||
this.initCanvas({
|
||||
createDrawService: (
|
||||
canvas: Canvas,
|
||||
ctx: RenderingContext,
|
||||
options?: Record<string, any>,
|
||||
) => {
|
||||
return new NumberDecomposeDraw(canvas, ctx, options);
|
||||
},
|
||||
drawServiceOptions: {
|
||||
subTitle: this.data.subTitle,
|
||||
},
|
||||
onCanvasReady: () => {
|
||||
// Canvas 初始化完成后,生成初始数据
|
||||
this.onRandom();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 绘制Canvas内容
|
||||
*/
|
||||
async drawCanvas() {
|
||||
if (!this.ctx || !this.drawService || !this.decomposeData) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.drawService.draw(this.decomposeData);
|
||||
this.setData({ hasContent: true });
|
||||
} catch (error) {
|
||||
console.error('绘制失败:', error);
|
||||
this.setData({ hasContent: false });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 随机生成
|
||||
*/
|
||||
onRandom() {
|
||||
this.generateDecomposeData();
|
||||
this.drawCanvas();
|
||||
},
|
||||
|
||||
/**
|
||||
* 生成分解数据(确保不重复)
|
||||
*/
|
||||
generateDecomposeData() {
|
||||
const mode = this.data.currentMode;
|
||||
const maxNumber = this.data.maxNumber;
|
||||
const is20Within = maxNumber === 20;
|
||||
|
||||
const problems: Array<{
|
||||
whole: number | null;
|
||||
part1: number | null;
|
||||
part2: number | null;
|
||||
imageIndex?: number;
|
||||
imageType?: 'fruits' | 'twelve-animals';
|
||||
}> = [];
|
||||
|
||||
// 用于去重的 Set,存储题目唯一标识
|
||||
// 格式:对于分解模式 "whole:part1:part2:showPart1",对于组合模式 "part1:part2"
|
||||
const usedKeys = new Set<string>();
|
||||
|
||||
// 确定根节点的数字范围
|
||||
const minWhole = is20Within ? 11 : 2; // 20以内从11开始,10以内从2开始
|
||||
const maxWhole = maxNumber;
|
||||
|
||||
if (mode === 'with-image') {
|
||||
// 有图片模式:9个题目,一行三列,总共三行(仅10以内)
|
||||
const problemCount = 9;
|
||||
let attempts = 0;
|
||||
const maxAttempts = problemCount * 50; // 最大尝试次数
|
||||
|
||||
while (problems.length < problemCount && attempts < maxAttempts) {
|
||||
attempts++;
|
||||
|
||||
// 生成总数(2-10)
|
||||
const whole =
|
||||
Math.floor(Math.random() * (maxWhole - minWhole + 1)) +
|
||||
minWhole;
|
||||
// 随机选择一个部分(1 到 whole-1)
|
||||
const part1 = Math.floor(Math.random() * (whole - 1)) + 1;
|
||||
const part2 = whole - part1;
|
||||
|
||||
// 随机决定显示 part1 还是 part2(另一个为 null)
|
||||
const showPart1 = Math.random() < 0.5;
|
||||
|
||||
// 生成唯一标识(统一格式:part1 <= part2)
|
||||
const minPart = Math.min(part1, part2);
|
||||
const maxPart = Math.max(part1, part2);
|
||||
const key = `${whole}:${minPart}:${maxPart}:${showPart1}`;
|
||||
|
||||
// 检查是否已存在
|
||||
if (usedKeys.has(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
usedKeys.add(key);
|
||||
|
||||
// 随机选择图片类型和索引
|
||||
const imageType: 'fruits' | 'twelve-animals' =
|
||||
Math.random() < 0.5 ? 'fruits' : 'twelve-animals';
|
||||
const maxImageIndex = imageType === 'fruits' ? 22 : 12;
|
||||
const imageIndex =
|
||||
Math.floor(Math.random() * maxImageIndex) + 1;
|
||||
|
||||
problems.push({
|
||||
whole,
|
||||
part1: showPart1 ? part1 : null,
|
||||
part2: showPart1 ? null : part2,
|
||||
imageIndex,
|
||||
imageType,
|
||||
});
|
||||
}
|
||||
} else if (mode === 'decompose') {
|
||||
// 分模式:15个题目,一行3个,总共5行
|
||||
const problemCount = 15;
|
||||
let attempts = 0;
|
||||
const maxAttempts = problemCount * 50; // 最大尝试次数
|
||||
|
||||
while (problems.length < problemCount && attempts < maxAttempts) {
|
||||
attempts++;
|
||||
|
||||
// 生成总数(2-10 或 11-20)
|
||||
const whole =
|
||||
Math.floor(Math.random() * (maxWhole - minWhole + 1)) +
|
||||
minWhole;
|
||||
// 随机选择一个部分(1 到 whole-1)
|
||||
const part1 = Math.floor(Math.random() * (whole - 1)) + 1;
|
||||
const part2 = whole - part1;
|
||||
|
||||
// 随机决定显示 part1 还是 part2(另一个为 null)
|
||||
const showPart1 = Math.random() < 0.5;
|
||||
|
||||
// 生成唯一标识(统一格式:part1 <= part2)
|
||||
const minPart = Math.min(part1, part2);
|
||||
const maxPart = Math.max(part1, part2);
|
||||
const key = `${whole}:${minPart}:${maxPart}:${showPart1}`;
|
||||
|
||||
// 检查是否已存在
|
||||
if (usedKeys.has(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
usedKeys.add(key);
|
||||
|
||||
problems.push({
|
||||
whole,
|
||||
part1: showPart1 ? part1 : null,
|
||||
part2: showPart1 ? null : part2,
|
||||
});
|
||||
}
|
||||
} else if (mode === 'compose') {
|
||||
// 组合模式:15个题目,一行3个,总共5行
|
||||
const problemCount = 15;
|
||||
let attempts = 0;
|
||||
const maxAttempts = problemCount * 50; // 最大尝试次数
|
||||
|
||||
while (problems.length < problemCount && attempts < maxAttempts) {
|
||||
attempts++;
|
||||
|
||||
// 生成总数(2-10 或 11-20)
|
||||
const whole =
|
||||
Math.floor(Math.random() * (maxWhole - minWhole + 1)) +
|
||||
minWhole;
|
||||
// 随机选择一个部分(1 到 whole-1)
|
||||
const part1 = Math.floor(Math.random() * (whole - 1)) + 1;
|
||||
const part2 = whole - part1;
|
||||
|
||||
// 生成唯一标识(统一格式:part1 <= part2)
|
||||
const minPart = Math.min(part1, part2);
|
||||
const maxPart = Math.max(part1, part2);
|
||||
const key = `${minPart}:${maxPart}`;
|
||||
|
||||
// 检查是否已存在
|
||||
if (usedKeys.has(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
usedKeys.add(key);
|
||||
|
||||
// 组合模式:两个部分都显示,根节点为 null
|
||||
problems.push({
|
||||
whole: null, // 根节点需要填写
|
||||
part1,
|
||||
part2,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.decomposeData = { problems, mode };
|
||||
},
|
||||
|
||||
/** 选择类型 */
|
||||
onSelectType(event: any) {
|
||||
const { name, value } = event.detail;
|
||||
this.setData({
|
||||
currentMode: value,
|
||||
currentModeName: name,
|
||||
});
|
||||
// 重新生成数据
|
||||
this.onRandom();
|
||||
},
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
<!-- 模版不能跨包引用 -->
|
||||
<import src="../shared/templates/canvas-page-template.wxml" />
|
||||
|
||||
<template
|
||||
is="canvasPageTemplate"
|
||||
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: true, currentModeName: currentModeName, typeActions: typeActions, adType: 'mathDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"navigationBarTitleText": "看数字,涂一涂",
|
||||
"navigationBarBackgroundColor": "#FFD719",
|
||||
"homeButton": true,
|
||||
"backgroundColor": "#F6F6F6",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"toy-button": "../../toy/button-v2/button",
|
||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
||||
"math-bottom-buttons": "../../components/math-bottom-buttons/math-bottom-buttons",
|
||||
"draw-ad": "../../components/draw-ad/draw-ad"
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
@import '../../base/baseDrawPage.wxss';
|
||||
|
||||
/* 数字选择区域 */
|
||||
.number-selection-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
margin-top: 40rpx;
|
||||
}
|
||||
|
||||
.number-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 40rpx;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.number-item {
|
||||
height: 90rpx;
|
||||
background: #ffffff;
|
||||
border: 2rpx solid #e8e8e8;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease-in-out;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08), 0 2rpx 4rpx rgba(0, 0, 0, 0.06);
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.1), 0 1rpx 2rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: #93d333;
|
||||
box-shadow: 0 4rpx 16rpx rgba(147, 211, 51, 0.25), 0 0 0 4rpx rgba(147, 211, 51, 0.15);
|
||||
}
|
||||
|
||||
.number-text {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #141414;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.number-column {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 40rpx;
|
||||
|
||||
.number-item {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.random-button {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
/* 随机按钮样式 */
|
||||
&.random-button {
|
||||
flex: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
|
||||
.random-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
.toy-icon {
|
||||
font-size: 32rpx;
|
||||
color: #141414;
|
||||
}
|
||||
}
|
||||
|
||||
.number-text {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: #93d333;
|
||||
box-shadow: 0 0 0 4rpx rgba(147, 211, 51, 0.2);
|
||||
|
||||
.random-icon .toy-icon {
|
||||
color: #93d333;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 底部按钮区域 */
|
||||
.bottom-btn-box {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 180rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background-color: #fff;
|
||||
box-shadow: 0px -3.9rpx 5.2rpx rgba(0, 0, 0, 0.15);
|
||||
padding: 24rpx;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
box-sizing: border-box;
|
||||
border-radius: 16rpx 16rpx 0 0;
|
||||
z-index: 100;
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
import NumberFindDraw from '../shared/service/numberFindDraw';
|
||||
import {
|
||||
createMathPage,
|
||||
CanvasDataState,
|
||||
} from '../shared/common/mathPageMixin';
|
||||
|
||||
createMathPage({
|
||||
canvas: null as Canvas | null,
|
||||
ctx: null as RenderingContext | null,
|
||||
boxHeight: 0,
|
||||
boxWidth: 0,
|
||||
drawService: null as NumberFindDraw | null,
|
||||
|
||||
data: {
|
||||
pageTitle: '看数字,涂一涂', // 默认标题
|
||||
selectedNumber: 0, // 选中的数字
|
||||
functionId: '', // 功能ID,用于判断标题
|
||||
numberList: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], // 数字列表
|
||||
hasContent: false,
|
||||
showShareDialog: false,
|
||||
} as CanvasDataState & {
|
||||
selectedNumber: number;
|
||||
numberList: number[];
|
||||
},
|
||||
|
||||
onLoad(options: { id?: string }) {
|
||||
const functionId = options.id || 'number-find';
|
||||
this.setData({
|
||||
functionId,
|
||||
});
|
||||
this.initPageInfo(functionId, '看数字,涂一涂');
|
||||
},
|
||||
|
||||
onReady() {
|
||||
this.initCanvas({
|
||||
createDrawService: (
|
||||
canvas: Canvas,
|
||||
ctx: RenderingContext,
|
||||
options?: Record<string, any>,
|
||||
) => {
|
||||
return new NumberFindDraw(canvas, ctx, options);
|
||||
},
|
||||
drawServiceOptions: {
|
||||
subTitle: '找一找下面相同的数字,涂上颜色',
|
||||
functionId: this.data.functionId,
|
||||
},
|
||||
onCanvasReady: () => {
|
||||
// 如果还没有选中数字,随机选择一个
|
||||
if (this.data.selectedNumber === 0) {
|
||||
const randomNumber = Math.floor(Math.random() * 10) + 1;
|
||||
this.setData({
|
||||
selectedNumber: randomNumber,
|
||||
});
|
||||
}
|
||||
|
||||
// 绘制初始内容
|
||||
this.drawCanvas();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 绘制Canvas内容
|
||||
*/
|
||||
async drawCanvas() {
|
||||
if (!this.ctx || !this.drawService) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.data.selectedNumber > 0) {
|
||||
try {
|
||||
await this.drawService.draw(this.data.selectedNumber);
|
||||
this.setData({ hasContent: true });
|
||||
} catch (error) {
|
||||
console.error('绘制失败:', error);
|
||||
this.setData({ hasContent: false });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 选择数字
|
||||
*/
|
||||
onSelectNumber(e: WechatMiniprogram.TouchEvent) {
|
||||
const number = parseInt(e.currentTarget.dataset.number);
|
||||
this.setData({
|
||||
selectedNumber: number,
|
||||
});
|
||||
this.drawCanvas();
|
||||
},
|
||||
|
||||
/**
|
||||
* 随机选择数字
|
||||
*/
|
||||
onRandom() {
|
||||
const randomNumber = Math.floor(Math.random() * 10) + 1;
|
||||
this.setData({
|
||||
selectedNumber: randomNumber,
|
||||
});
|
||||
this.drawCanvas();
|
||||
},
|
||||
});
|
||||
@@ -1,82 +0,0 @@
|
||||
<view class="page-container">
|
||||
<view class="wrapper">
|
||||
<text class="wrapper-title">预览打印效果</text>
|
||||
|
||||
<!-- 预览打印效果 -->
|
||||
<view id="canvasWrapper" class="canvas-wrapper">
|
||||
<canvas
|
||||
type="2d"
|
||||
id="canvasContent"
|
||||
class="canvas-content"
|
||||
style="width:{{boxWidth}}px;height:{{boxHeight}}px" />
|
||||
</view>
|
||||
|
||||
<!-- 数字选择区域 -->
|
||||
<view class="number-selection-area">
|
||||
<!-- 第一行:1-4 -->
|
||||
<view class="number-row">
|
||||
<view
|
||||
wx:for="{{numberList}}"
|
||||
wx:key="index"
|
||||
wx:for-item="number"
|
||||
wx:if="{{index < 4}}"
|
||||
class="number-item {{selectedNumber === number ? 'active' : ''}}"
|
||||
data-number="{{number}}"
|
||||
bind:tap="onSelectNumber">
|
||||
<view class="number-text">{{number}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 第二行:5-8 -->
|
||||
<view class="number-row">
|
||||
<view
|
||||
wx:for="{{numberList}}"
|
||||
wx:key="index"
|
||||
wx:for-item="number"
|
||||
wx:if="{{index >= 4 && index < 8}}"
|
||||
class="number-item {{selectedNumber === number ? 'active' : ''}}"
|
||||
data-number="{{number}}"
|
||||
bind:tap="onSelectNumber">
|
||||
<view class="number-text">{{number}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 第三行:9、10和随机按钮 -->
|
||||
<view class="number-row">
|
||||
<view class="number-column">
|
||||
<view
|
||||
wx:for="{{numberList}}"
|
||||
wx:key="index"
|
||||
wx:for-item="number"
|
||||
wx:if="{{index >= 8}}"
|
||||
class="number-item {{selectedNumber === number ? 'active' : ''}}"
|
||||
data-number="{{number}}"
|
||||
bind:tap="onSelectNumber">
|
||||
<view class="number-text">{{number}}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="number-column">
|
||||
<toy-button
|
||||
style="width: 100%"
|
||||
type="primary"
|
||||
bind:click="onRandom"
|
||||
width="100%"
|
||||
icon="refresh"
|
||||
icon-class-prefix="toy-icon">
|
||||
随机
|
||||
</toy-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<draw-ad type="mathDraw"></draw-ad>
|
||||
</view>
|
||||
<view class="empty"></view>
|
||||
</view>
|
||||
<math-bottom-buttons
|
||||
disabled="{{selectedNumber <= 0}}"
|
||||
bind:share="onShareAppMessage"
|
||||
bind:export="exportToPrint" />
|
||||
<share-guide-popup
|
||||
show="{{showShareDialog}}"
|
||||
bind:onClose="onCloseShareDialog"
|
||||
bind:onShareSuccess="onShareSuccess" />
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"navigationBarTitleText": "数物对应",
|
||||
"navigationBarBackgroundColor": "#FFD719",
|
||||
"homeButton": true,
|
||||
"backgroundColor": "#F6F6F6",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"toy-button": "../../toy/button-v2/button",
|
||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
||||
"math-bottom-buttons": "../../components/math-bottom-buttons/math-bottom-buttons",
|
||||
"draw-ad": "../../components/draw-ad/draw-ad",
|
||||
"math-type-selector": "../../components/math-type-selector/math-type-selector"
|
||||
}
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
import NumberObjectMatchDraw from '../shared/service/numberObjectMatchDraw';
|
||||
import {
|
||||
createMathPage,
|
||||
CanvasDataState,
|
||||
} from '../shared/common/mathPageMixin';
|
||||
import { NumberObjectMatchData } from '../shared/service/numberObjectMatchDraw';
|
||||
|
||||
createMathPage({
|
||||
canvas: null as Canvas | null,
|
||||
ctx: null as RenderingContext | null,
|
||||
boxHeight: 0,
|
||||
boxWidth: 0,
|
||||
drawService: null as NumberObjectMatchDraw | null,
|
||||
matchData: null as NumberObjectMatchData | null,
|
||||
|
||||
data: {
|
||||
pageTitle: '数物对应',
|
||||
functionId: '',
|
||||
hasContent: false,
|
||||
showShareDialog: false,
|
||||
showTypeSelector: true, // 控制是否显示类型选择器
|
||||
currentMode: 'match', // 'match' 连线 或 'fill' 填写
|
||||
currentModeName: '连线',
|
||||
typeActions: [
|
||||
{ name: '连线', value: 'match' },
|
||||
{ name: '填写', value: 'fill' },
|
||||
],
|
||||
} as CanvasDataState & {
|
||||
showTypeSelector: boolean;
|
||||
currentMode: string;
|
||||
currentModeName: string;
|
||||
typeActions: Array<{ name: string; value: string }>;
|
||||
},
|
||||
|
||||
onLoad(options: { id?: string; mode?: string }) {
|
||||
const functionId = options.id || 'number-object-match';
|
||||
this.initPageInfo(functionId, '数物对应');
|
||||
const isFill = functionId === 'number-object-fill';
|
||||
const currentMode = options.mode || (isFill ? 'fill' : 'match');
|
||||
this.setData({
|
||||
currentMode,
|
||||
currentModeName: currentMode === 'fill' ? '填写' : '连线',
|
||||
typeActions: [
|
||||
{ name: '连线', value: 'match' },
|
||||
{ name: '填写', value: 'fill' },
|
||||
],
|
||||
});
|
||||
},
|
||||
|
||||
onReady() {
|
||||
this.initCanvas({
|
||||
createDrawService: (
|
||||
canvas: Canvas,
|
||||
ctx: RenderingContext,
|
||||
options?: Record<string, any>,
|
||||
) => {
|
||||
return new NumberObjectMatchDraw(canvas, ctx, options);
|
||||
},
|
||||
drawServiceOptions: {},
|
||||
onCanvasReady: () => {
|
||||
// 初始随机生成
|
||||
this.onRandom();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 绘制Canvas内容
|
||||
*/
|
||||
async drawCanvas() {
|
||||
if (!this.ctx || !this.drawService || !this.matchData) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.drawService.draw(this.matchData, this.data.currentMode);
|
||||
this.setData({ hasContent: true });
|
||||
} catch (error) {
|
||||
console.error('绘制失败:', error);
|
||||
this.setData({ hasContent: false });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 随机生成
|
||||
*/
|
||||
onRandom() {
|
||||
// 从两个目录中随机选择4张不重复的图片
|
||||
// fruits: 1-22, twelve-animals: 1-12
|
||||
const fruitsIndices = Array.from({ length: 22 }, (_, i) => i + 1);
|
||||
const animalsIndices = Array.from({ length: 12 }, (_, i) => i + 1);
|
||||
|
||||
// 合并所有可用的图片索引
|
||||
const allImages: Array<{
|
||||
imageIndex: number;
|
||||
folder: 'fruits' | 'twelve-animals';
|
||||
}> = [
|
||||
...fruitsIndices.map((idx) => ({
|
||||
imageIndex: idx,
|
||||
folder: 'fruits' as const,
|
||||
})),
|
||||
...animalsIndices.map((idx) => ({
|
||||
imageIndex: idx,
|
||||
folder: 'twelve-animals' as const,
|
||||
})),
|
||||
];
|
||||
|
||||
// 随机打乱并选择4张不重复的图片
|
||||
const shuffled = [...allImages].sort(() => Math.random() - 0.5);
|
||||
const selectedImages = shuffled.slice(0, 4);
|
||||
|
||||
// 生成上面区域的16张图片(4行4列)
|
||||
// 每张图片随机出现,并添加随机偏移
|
||||
const gridImages: Array<{
|
||||
imageIndex: number;
|
||||
folder: 'fruits' | 'twelve-animals';
|
||||
offsetX: number;
|
||||
offsetY: number;
|
||||
}> = [];
|
||||
|
||||
const maxOffset = 15; // 最大偏移量
|
||||
for (let i = 0; i < 20; i++) {
|
||||
// 从选中的4张图片中随机选择一张
|
||||
const randomImage =
|
||||
selectedImages[
|
||||
Math.floor(Math.random() * selectedImages.length)
|
||||
];
|
||||
|
||||
// 生成随机偏移(-maxOffset 到 maxOffset)
|
||||
const offsetX = (Math.random() - 0.5) * 2 * maxOffset;
|
||||
const offsetY = (Math.random() - 0.5) * 2 * maxOffset;
|
||||
|
||||
gridImages.push({
|
||||
...randomImage,
|
||||
offsetX,
|
||||
offsetY,
|
||||
});
|
||||
}
|
||||
|
||||
// 下面区域第一行的4张图片(使用选中的4张图片)
|
||||
const bottomImages = [...selectedImages];
|
||||
|
||||
// 计算每个图片在上面区域出现的次数
|
||||
const imageCountMap = new Map<string, number>();
|
||||
gridImages.forEach((item) => {
|
||||
const key = `${item.folder}-${item.imageIndex}`;
|
||||
imageCountMap.set(key, (imageCountMap.get(key) || 0) + 1);
|
||||
});
|
||||
|
||||
// 获取每个图片的数量(按 selectedImages 的顺序)
|
||||
const imageCounts = selectedImages.map((img) => {
|
||||
const key = `${img.folder}-${img.imageIndex}`;
|
||||
return imageCountMap.get(key) || 0;
|
||||
});
|
||||
|
||||
// 将数量乱序排列
|
||||
const bottomNumbers = [...imageCounts].sort(() => Math.random() - 0.5);
|
||||
|
||||
this.matchData = {
|
||||
gridImages,
|
||||
bottomImages,
|
||||
bottomNumbers,
|
||||
};
|
||||
|
||||
this.drawCanvas();
|
||||
},
|
||||
|
||||
/** 选择类型 */
|
||||
onSelectType(event: any) {
|
||||
const { name, value } = event.detail;
|
||||
this.setData({
|
||||
currentMode: value,
|
||||
currentModeName: name,
|
||||
});
|
||||
// 重新生成数据
|
||||
this.onRandom();
|
||||
},
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
<!-- 模版不能跨包引用 -->
|
||||
<import src="../shared/templates/canvas-page-template.wxml" />
|
||||
|
||||
<template
|
||||
is="canvasPageTemplate"
|
||||
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: true, currentModeName: currentModeName, typeActions: typeActions, adType: 'mathDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
|
||||
@@ -1 +0,0 @@
|
||||
@import '../../base/baseDrawPage.wxss';
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"navigationBarTitleText": "数字排序",
|
||||
"navigationBarBackgroundColor": "#FFD719",
|
||||
"homeButton": true,
|
||||
"backgroundColor": "#F6F6F6",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"toy-button": "../../toy/button-v2/button",
|
||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
||||
"math-bottom-buttons": "../../components/math-bottom-buttons/math-bottom-buttons",
|
||||
"draw-ad": "../../components/draw-ad/draw-ad"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
@import '../../base/baseDrawPage.wxss';
|
||||
@@ -1,132 +0,0 @@
|
||||
import NumberSortDraw from '../shared/service/numberSortDraw';
|
||||
import {
|
||||
createMathPage,
|
||||
CanvasDataState,
|
||||
} from '../shared/common/mathPageMixin';
|
||||
import { NumberSortData } from '../shared/service/numberSortDraw';
|
||||
import { NUMBER_COLORS, getNumberColors } from '../../constants/colors';
|
||||
|
||||
createMathPage({
|
||||
canvas: null as Canvas | null,
|
||||
ctx: null as RenderingContext | null,
|
||||
boxHeight: 0,
|
||||
boxWidth: 0,
|
||||
drawService: null as NumberSortDraw | null,
|
||||
sortData: null as NumberSortData | null,
|
||||
|
||||
data: {
|
||||
pageTitle: '数字排序',
|
||||
functionId: '',
|
||||
hasContent: false,
|
||||
showShareDialog: false,
|
||||
} as CanvasDataState,
|
||||
|
||||
onLoad(options: { id?: string }) {
|
||||
const functionId = options.id || 'number-sort';
|
||||
this.setData({
|
||||
functionId,
|
||||
});
|
||||
this.initPageInfo(functionId, '数字排序');
|
||||
},
|
||||
|
||||
onReady() {
|
||||
this.initCanvas({
|
||||
createDrawService: (
|
||||
canvas: Canvas,
|
||||
ctx: RenderingContext,
|
||||
options?: Record<string, any>,
|
||||
) => {
|
||||
return new NumberSortDraw(canvas, ctx, options);
|
||||
},
|
||||
drawServiceOptions: {
|
||||
title: this.data.pageTitle,
|
||||
subTitle: '数字排序,写出正确顺序',
|
||||
functionId: this.data.functionId,
|
||||
},
|
||||
onCanvasReady: () => {
|
||||
// 初始随机生成
|
||||
this.onRandom();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 绘制Canvas内容
|
||||
*/
|
||||
async drawCanvas() {
|
||||
if (!this.ctx || !this.drawService || !this.sortData) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.drawService.draw(this.sortData);
|
||||
this.setData({ hasContent: true });
|
||||
} catch (error) {
|
||||
console.error('绘制失败:', error);
|
||||
this.setData({ hasContent: false });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 随机生成
|
||||
*/
|
||||
onRandom() {
|
||||
const groups: NumberSortData['groups'] = [];
|
||||
const availableColors = getNumberColors();
|
||||
|
||||
// 生成6组题目
|
||||
for (let i = 0; i < 6; i++) {
|
||||
// 随机选择4个不同的数字(范围1-9)
|
||||
const availableNumbers = Array.from({ length: 9 }, (_, i) => i + 1);
|
||||
const selectedNumbers: number[] = [];
|
||||
|
||||
// 随机选择4个数字
|
||||
for (let j = 0; j < 4; j++) {
|
||||
const randomIndex = Math.floor(
|
||||
Math.random() * availableNumbers.length,
|
||||
);
|
||||
selectedNumbers.push(
|
||||
availableNumbers.splice(randomIndex, 1)[0],
|
||||
);
|
||||
}
|
||||
|
||||
// 打乱顺序
|
||||
const shuffledNumbers = [...selectedNumbers];
|
||||
for (let j = shuffledNumbers.length - 1; j > 0; j--) {
|
||||
const k = Math.floor(Math.random() * (j + 1));
|
||||
[shuffledNumbers[j], shuffledNumbers[k]] = [
|
||||
shuffledNumbers[k],
|
||||
shuffledNumbers[j],
|
||||
];
|
||||
}
|
||||
|
||||
// 为每个数字分配颜色和倾斜角度
|
||||
const numbersWithColors = shuffledNumbers.map((number) => {
|
||||
// 使用数字对应的颜色,如果数字超出范围则随机选择
|
||||
const color =
|
||||
NUMBER_COLORS[number as keyof typeof NUMBER_COLORS] ||
|
||||
availableColors[
|
||||
Math.floor(Math.random() * availableColors.length)
|
||||
];
|
||||
// 生成随机倾斜角度(-30度到+30度,转换为弧度)
|
||||
const angleDegrees = (Math.random() - 0.5) * 60; // -30到+30度
|
||||
const angle = (angleDegrees * Math.PI) / 180; // 转换为弧度
|
||||
return {
|
||||
number,
|
||||
color,
|
||||
angle,
|
||||
};
|
||||
});
|
||||
|
||||
groups.push({
|
||||
numbers: numbersWithColors,
|
||||
});
|
||||
}
|
||||
|
||||
this.sortData = {
|
||||
groups,
|
||||
};
|
||||
|
||||
this.drawCanvas();
|
||||
},
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
<!-- 模版不能跨包引用 -->
|
||||
<import src="../shared/templates/canvas-page-template.wxml" />
|
||||
|
||||
<template
|
||||
is="canvasPageTemplate"
|
||||
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: false, adType: 'mathDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"navigationBarTitleText": "一位数加法",
|
||||
"navigationBarBackgroundColor": "#FFD719",
|
||||
"homeButton": true,
|
||||
"backgroundColor": "#F6F6F6",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"toy-button": "../../toy/button-v2/button",
|
||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
||||
"math-bottom-buttons": "../../components/math-bottom-buttons/math-bottom-buttons",
|
||||
"draw-ad": "../../components/draw-ad/draw-ad"
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
// 一位数加法练习页面样式
|
||||
|
||||
@import '../../base/baseDrawPage.wxss';
|
||||
@@ -1,118 +0,0 @@
|
||||
import OneDigitAdditionDraw from '../shared/service/oneDigitAdditionDraw';
|
||||
import {
|
||||
createMathPage,
|
||||
CanvasDataState,
|
||||
} from '../shared/common/mathPageMixin';
|
||||
|
||||
createMathPage({
|
||||
canvas: null as Canvas | null,
|
||||
ctx: null as RenderingContext | null,
|
||||
boxHeight: 0,
|
||||
boxWidth: 0,
|
||||
drawService: null as OneDigitAdditionDraw | null,
|
||||
oneDigitAdditionData: null as {
|
||||
problems: Array<{
|
||||
left: number;
|
||||
right: number;
|
||||
result: number;
|
||||
}>;
|
||||
} | null,
|
||||
|
||||
data: {
|
||||
pageTitle: '一位数加法',
|
||||
subTitle: '通过圆点学习一位数加法运算',
|
||||
functionId: '',
|
||||
hasContent: false,
|
||||
showShareDialog: false,
|
||||
} as CanvasDataState,
|
||||
|
||||
onLoad(options: { id?: string }) {
|
||||
const functionId = options.id || 'one-digit-addition';
|
||||
this.initPageInfo(functionId, '一位数加法');
|
||||
},
|
||||
|
||||
onReady() {
|
||||
this.initCanvas({
|
||||
createDrawService: (
|
||||
canvas: Canvas,
|
||||
ctx: RenderingContext,
|
||||
options?: Record<string, any>,
|
||||
) => {
|
||||
return new OneDigitAdditionDraw(canvas, ctx, options);
|
||||
},
|
||||
drawServiceOptions: {
|
||||
subTitle: this.data.subTitle,
|
||||
},
|
||||
onCanvasReady: () => {
|
||||
// 初始随机生成
|
||||
this.onRandom();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 绘制Canvas内容
|
||||
*/
|
||||
async drawCanvas() {
|
||||
if (!this.ctx || !this.drawService || !this.oneDigitAdditionData) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.drawService.draw(this.oneDigitAdditionData);
|
||||
this.setData({ hasContent: true });
|
||||
} catch (error) {
|
||||
console.error('绘制失败:', error);
|
||||
this.setData({ hasContent: false });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 随机生成
|
||||
*/
|
||||
onRandom() {
|
||||
const problems: Array<{
|
||||
left: number;
|
||||
right: number;
|
||||
result: number;
|
||||
}> = [];
|
||||
const usedProblems = new Set<string>(); // 用于记录已生成的题目,避免重复
|
||||
|
||||
// 生成6道不重复的题目(6行1列)
|
||||
let attempts = 0;
|
||||
const maxAttempts = 1000; // 最大尝试次数,避免无限循环
|
||||
|
||||
while (problems.length < 6 && attempts < maxAttempts) {
|
||||
attempts++;
|
||||
// 两个加数的和 <= 10
|
||||
// left >= 1, right >= 1, left + right <= 10
|
||||
const maxSum = 10;
|
||||
const left = Math.floor(Math.random() * (maxSum - 1)) + 1; // 1 到 9
|
||||
const maxRight = maxSum - left; // 确保 left + right <= 10
|
||||
const right = Math.floor(Math.random() * maxRight) + 1; // 1 到 maxRight
|
||||
const result = left + right;
|
||||
|
||||
// 使用 "left,right" 作为唯一标识,避免重复
|
||||
const problemKey = `${left},${right}`;
|
||||
if (usedProblems.has(problemKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
usedProblems.add(problemKey);
|
||||
problems.push({
|
||||
left,
|
||||
right,
|
||||
result,
|
||||
});
|
||||
}
|
||||
|
||||
if (problems.length < 6) {
|
||||
console.warn(
|
||||
`只生成了 ${problems.length} 道题目,可能符合条件的题目组合不足`,
|
||||
);
|
||||
}
|
||||
|
||||
this.oneDigitAdditionData = { problems };
|
||||
this.drawCanvas();
|
||||
},
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
<!-- 模版不能跨包引用 -->
|
||||
<import src="../shared/templates/canvas-page-template.wxml" />
|
||||
|
||||
<template
|
||||
is="canvasPageTemplate"
|
||||
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: false, adType: 'mathDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
|
||||
Reference in New Issue
Block a user