feat:2.4.5 数一数填一填和数一数选一选开发

This commit is contained in:
R524809
2025-12-08 10:53:07 +08:00
parent a06e1bdc3d
commit 791a8bc373
9 changed files with 646 additions and 36 deletions
+2 -1
View File
@@ -15,7 +15,8 @@
"countMatch/countMatch",
"addition/addition",
"missingNumber/missingNumber",
"compare/compare"
"compare/compare",
"countingSelect/countingSelect"
],
"independent": false
}
+39 -33
View File
@@ -7,6 +7,7 @@ export interface MathFunctionType {
}
export const MATH_FUNCTION_TYPES: MathFunctionType[] = [
// 第一阶段:认识数字(最基础)
{
id: 'number-find',
page: 'numberFind',
@@ -14,13 +15,6 @@ export const MATH_FUNCTION_TYPES: MathFunctionType[] = [
desc: '找一找下面相同的数字,涂上颜色',
icon: '🔍',
},
{
id: 'number-coloring',
page: 'countMatch',
title: '按数字,涂颜色',
desc: '按数字给相应的圆圈涂上颜色',
icon: '🎨',
},
{
id: 'number-write',
page: 'numberFind',
@@ -28,6 +22,21 @@ export const MATH_FUNCTION_TYPES: MathFunctionType[] = [
desc: '看数字,按笔画顺序写一写',
icon: '✏️',
},
{
id: 'number-coloring',
page: 'countMatch',
title: '按数字,涂颜色',
desc: '按数字给相应的圆圈涂上颜色',
icon: '🎨',
},
// 第二阶段:数数与计数
{
id: 'counting-select',
page: 'countingSelect',
title: '数一数,选一选',
desc: '数出物品数量,圈出正确答案',
icon: '✓',
},
{
id: 'counting-matching',
page: 'countMatch',
@@ -35,6 +44,29 @@ export const MATH_FUNCTION_TYPES: MathFunctionType[] = [
desc: '通过连线配对数字和对应的数量图形',
icon: '🔗',
},
{
id: 'counting-fill',
page: 'countingSelect',
title: '数一数,填一填',
desc: '数出物品数量,填写对应的数字',
icon: '✏️',
},
{
id: 'compare',
page: 'compare',
title: '数一数,比大小',
desc: '比较数量大小,在 ⭕️ 中填入>、<、=',
icon: '⚖️',
},
// 第三阶段:数字序列
{
id: 'missing-number',
page: 'missingNumber',
title: '填上缺少的数字',
desc: '在数字序列中找出并填写缺失的数字',
icon: '❓',
},
// 第四阶段:简单运算(从易到难)
{
id: 'addition-5',
page: 'addition',
@@ -63,30 +95,4 @@ export const MATH_FUNCTION_TYPES: MathFunctionType[] = [
desc: '练习10以内的加法和减法混合运算',
icon: '±',
},
{
id: 'missing-number',
page: 'missingNumber',
title: '填上缺少的数字',
desc: '在数字序列中找出并填写缺失的数字',
icon: '❓',
},
{
id: 'compare',
page: 'compare',
title: '数一数,比大小',
desc: '比较数量大小,在 ⭕️ 中填入>、<、=',
icon: '⚖️',
},
{
id: 'counting-select',
title: '数一数,选一选',
desc: '数出物品数量,从多个选项中选择正确答案',
icon: '✓',
},
{
id: 'counting-fill',
title: '数一数,填一填',
desc: '数出物品数量,填写对应的数字',
icon: '✏️',
},
];
@@ -0,0 +1,12 @@
{
"navigationBarTitleText": "数一数,选一选",
"navigationBarBackgroundColor": "#FFD719",
"homeButton": true,
"backgroundColor": "#F6F6F6",
"enablePullDownRefresh": false,
"usingComponents": {
"toy-button": "../../ui/button/button",
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
"math-bottom-buttons": "../components/math-bottom-buttons/math-bottom-buttons"
}
}
@@ -0,0 +1 @@
@import '../common/mathPage.less';
@@ -0,0 +1,161 @@
import CountingSelectDraw from '../service/countingSelectDraw';
import {
getMathPageCommonMethods,
CanvasDataState,
} from '../common/mathPageMixin';
// 获取公共方法
const commonMethods = getMathPageCommonMethods({
pagePath: 'countingSelect/countingSelect',
});
Page({
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, ctx, options) => {
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 };
},
// ========== 使用公共方法 ==========
initCanvas: commonMethods.initCanvas,
exportToPrint: commonMethods.exportToPrint,
onShareAppMessage: commonMethods.onShareAppMessage,
onShareTimeline: commonMethods.onShareTimeline,
onCloseShareDialog: commonMethods.onCloseShareDialog,
onShareSuccess: commonMethods.onShareSuccess,
initPageInfo: commonMethods.initPageInfo,
});
@@ -0,0 +1,37 @@
<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="random-button-area">
<toy-button
class="random-button"
type="primary"
bind:click="onRandom"
width="100%"
height="80rpx"
icon="refresh"
icon-class-prefix="toy-icon">
随机生成
</toy-button>
</view>
</view>
<view class="empty"></view>
</view>
<math-bottom-buttons
disabled="{{!hasContent}}"
bind:share="onShareAppMessage"
bind:export="exportToPrint" />
<share-guide-popup
show="{{showShareDialog}}"
bind:onClose="onCloseShareDialog"
bind:onShareSuccess="onShareSuccess" />
@@ -0,0 +1,312 @@
import { getImage } from '../../utils/index';
import { getRandomNumberColor } from '../../constants/colors';
interface DrawCountingSelectContentParams {
canvas: WechatMiniprogram.Canvas;
ctx: RenderingContext;
countingData: {
problems: Array<{
count: number;
imageIndex: number;
imageType: 'fruits' | 'twelve-animals';
options?: number[]; // 选一选模式需要,填一填模式不需要
correctIndex?: number; // 选一选模式需要
}>;
};
mode: 'select' | 'fill'; // 模式:'select' 显示选项,'fill' 显示括号
canvasWidth: number;
startY: number;
}
/**
* 绘制圆角矩形(虚线边框)
*/
function drawRoundedRect(
ctx: RenderingContext,
x: number,
y: number,
width: number,
height: number,
radius: number,
isDashed: boolean = true,
) {
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.lineTo(x + width - radius, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
ctx.lineTo(x + width, y + height - radius);
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
ctx.lineTo(x + radius, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
ctx.lineTo(x, y + radius);
ctx.quadraticCurveTo(x, y, x + radius, y);
ctx.closePath();
if (isDashed) {
ctx.setLineDash([6 / 3, 6 / 3]); // 约2px的虚线
} else {
ctx.setLineDash([]);
}
ctx.stroke();
ctx.setLineDash([]); // 重置为实线
}
/**
* 绘制数一数选一选/填一填内容区域
* 3x3网格布局,每个框上面是图片,下面根据模式显示选项或括号
*/
export async function drawCountingSelectContent({
canvas,
ctx,
countingData,
mode,
canvasWidth,
startY,
}: DrawCountingSelectContentParams): Promise<void> {
const { problems } = countingData;
// 布局参数
const leftMargin = 30;
const rightMargin = 30;
const topMargin = 20;
const boxSpacing = 20; // 框之间的间距
// 计算每个框的尺寸
const availableWidth = canvasWidth - leftMargin - rightMargin;
const boxWidth = (availableWidth - boxSpacing * 2) / 3; // 3列,2个间距
const boxHeight = 210; // 框的高度(增加)
// 图片区域高度
const imageAreaHeight = 160;
// 底部区域高度(选项表格或括号区域)
const bottomAreaHeight = boxHeight - imageAreaHeight;
// 选项表格相关(仅 select 模式使用)
const optionCellWidth = boxWidth / 3; // 每列宽度(均匀分三列)
let currentY = startY + topMargin;
// 绘制3x3网格
for (let row = 0; row < 3; row++) {
for (let col = 0; col < 3; col++) {
const problemIndex = row * 3 + col;
if (problemIndex >= problems.length) {
continue;
}
const problem = problems[problemIndex];
// 计算框的位置
const boxX = leftMargin + col * (boxWidth + boxSpacing);
const boxY = currentY + row * (boxHeight + boxSpacing);
// 绘制一个完整的题目框
await drawProblemBox(
ctx,
canvas,
problem,
boxX,
boxY,
boxWidth,
boxHeight,
imageAreaHeight,
bottomAreaHeight,
optionCellWidth,
mode,
);
}
}
}
/**
* 绘制一个题目框(图片 + 选项表格 或 括号)
*/
async function drawProblemBox(
ctx: RenderingContext,
canvas: WechatMiniprogram.Canvas,
problem: {
count: number;
imageIndex: number;
imageType: 'fruits' | 'twelve-animals';
options?: number[];
correctIndex?: number;
},
boxX: number,
boxY: number,
boxWidth: number,
boxHeight: number,
imageAreaHeight: number,
bottomAreaHeight: number,
optionCellWidth: number,
mode: 'select' | 'fill',
) {
const borderRadius = 12;
// 绘制实线框
ctx.strokeStyle = '#333';
ctx.lineWidth = 1;
drawRoundedRect(ctx, boxX, boxY, boxWidth, boxHeight, borderRadius, false);
// 图片区域
const imageAreaX = boxX;
const imageAreaY = boxY;
// 绘制图片
await drawImagesInBox(
ctx,
canvas,
problem.count,
problem.imageIndex,
problem.imageType,
imageAreaX,
imageAreaY,
boxWidth,
imageAreaHeight,
);
// 底部区域
const bottomAreaX = boxX;
const bottomAreaY = boxY + imageAreaHeight;
// 绘制底部区域的顶部横线(两种模式都需要)
ctx.strokeStyle = '#333';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(bottomAreaX, bottomAreaY);
ctx.lineTo(bottomAreaX + boxWidth, bottomAreaY);
ctx.stroke();
if (mode === 'select' && problem.options) {
// 选一选模式:绘制三个数字选项(表格样式)
// 绘制三个单元格(均匀分三列)
for (let i = 0; i < problem.options.length; i++) {
const cellX = bottomAreaX + i * optionCellWidth;
const cellY = bottomAreaY;
// 绘制单元格边框(右边框,除了最后一个)
if (i < problem.options.length - 1) {
ctx.beginPath();
ctx.moveTo(cellX + optionCellWidth, cellY);
ctx.lineTo(cellX + optionCellWidth, cellY + bottomAreaHeight);
ctx.stroke();
}
// 绘制数字(居中)
const numberX = cellX + optionCellWidth / 2;
const numberY = cellY + bottomAreaHeight / 2;
ctx.fillStyle = getRandomNumberColor();
ctx.font = `bold ${28}px "Microsoft Yahei"`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(String(problem.options[i]), numberX, numberY);
}
} else if (mode === 'fill') {
// 填一填模式:绘制括号 "( )"
const bracketFontSize = 28;
const bracketSpacing = 46; // 左右括号之间的间距(留空区域)
// 计算括号位置(居中)
const bracketY = bottomAreaY + bottomAreaHeight / 2;
const centerX = boxX + boxWidth / 2;
// 左括号
ctx.fillStyle = '#333';
ctx.font = `${bracketFontSize}px "Microsoft Yahei"`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const leftBracketX = centerX - bracketSpacing / 2;
ctx.fillText('(', leftBracketX, bracketY);
// 右括号
const rightBracketX = centerX + bracketSpacing / 2;
ctx.fillText(')', rightBracketX, bracketY);
}
}
/**
* 在框内绘制多张图片
*/
async function drawImagesInBox(
ctx: RenderingContext,
canvas: WechatMiniprogram.Canvas,
count: number,
imageIndex: number,
imageType: 'fruits' | 'twelve-animals',
boxX: number,
boxY: number,
boxWidth: number,
boxHeight: number,
) {
// 图片配置
const imageConfig = {
'twelve-animals': {
folder: 'twelve-animals',
maxIndex: 12,
},
fruits: {
folder: 'fruits',
maxIndex: 22,
},
};
const config = imageConfig[imageType] || imageConfig['twelve-animals'];
const padding = 8; // 框内边距
const availableWidth = boxWidth - padding * 2;
const availableHeight = boxHeight - padding * 2;
// 根据数量确定每行的图片数和图片大小
let imagesPerRow: number;
let imageSize: number;
if (count <= 4) {
imagesPerRow = count <= 2 ? count : 2;
imageSize =
Math.min(availableWidth / imagesPerRow, availableHeight / 2) - 4;
} else if (count <= 6) {
imagesPerRow = 3;
imageSize = Math.min(availableWidth / 3, availableHeight / 2) - 4;
} else {
imagesPerRow = 3;
imageSize = Math.min(availableWidth / 3, availableHeight / 3) - 4;
}
const rows = Math.ceil(count / imagesPerRow);
const imageSpacing =
(availableWidth - imageSize * imagesPerRow) / (imagesPerRow + 1);
const rowSpacing =
rows > 1 ? (availableHeight - imageSize * rows) / (rows + 1) : 0;
// 加载图片
let boxImage: any = null;
try {
const imagePath = `/mathPages/assets/${config.folder}/${imageIndex}.png`;
boxImage = await getImage(canvas, imagePath);
} catch (error) {
console.error(`加载${config.folder}/${imageIndex}图片失败:`, error);
return;
}
// 绘制图片
for (let i = 0; i < count; i++) {
const row = Math.floor(i / imagesPerRow);
const col = i % imagesPerRow;
const imageX =
boxX + padding + imageSpacing + col * (imageSize + imageSpacing);
const imageY =
boxY +
padding +
(rows > 1 ? rowSpacing : (availableHeight - imageSize) / 2) +
row * (imageSize + (rows > 1 ? rowSpacing : 0));
if (boxImage) {
// 计算图片高度(等比例缩放)
// @ts-ignore - 微信小程序图片对象有 width 和 height 属性
const scaledHeight = (boxImage.height / boxImage.width) * imageSize;
ctx.drawImage(boxImage, imageX, imageY, imageSize, scaledHeight);
}
}
}
@@ -0,0 +1,73 @@
import { BaseMathDrawService } from './baseMathDraw';
import { drawCountingSelectContent } from './countingSelectContentDraw';
/**
* 数一数选一选/填一填绘制服务
* 组合使用基础绘制服务和内容区域绘制服务
* 支持两种模式:'select'(选一选)和 'fill'(填一填)
*/
class CountingSelectDraw extends BaseMathDrawService {
countingData: {
problems: Array<{
count: number;
imageIndex: number;
imageType: 'fruits' | 'twelve-animals';
options?: number[];
correctIndex?: number;
}>;
} | null;
mode: 'select' | 'fill';
constructor(
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, any>,
) {
super(canvas, ctx, options);
this.countingData = null;
this.mode = 'select'; // 默认选一选模式
}
async draw(
countingData: {
problems: Array<{
count: number;
imageIndex: number;
imageType: 'fruits' | 'twelve-animals';
options?: number[];
correctIndex?: number;
}>;
},
mode: 'select' | 'fill' = 'select',
) {
if (!countingData || !countingData.problems) {
return;
}
this.setPrintConfig();
this.countingData = countingData;
this.mode = mode;
this.clear();
this.setPaper();
// 绘制Header
if (this.headerType !== 'minimal') {
await this.drawHeader();
} else {
this.drawMiniHeader();
}
// 绘制内容区域
this.drawDivider();
await drawCountingSelectContent({
canvas: this.canvas,
ctx: this.ctx,
countingData: this.countingData,
mode: this.mode,
canvasWidth: this.canvasWidth,
startY: this.currentY,
});
}
}
export default CountingSelectDraw;