diff --git a/miniprogram/app.json b/miniprogram/app.json index 5d35ad5..b2f4855 100644 --- a/miniprogram/app.json +++ b/miniprogram/app.json @@ -15,7 +15,8 @@ "countMatch/countMatch", "addition/addition", "missingNumber/missingNumber", - "compare/compare" + "compare/compare", + "countingSelect/countingSelect" ], "independent": false } diff --git a/miniprogram/constants/mathFunctions.ts b/miniprogram/constants/mathFunctions.ts index 11ecbdb..f73b9b0 100644 --- a/miniprogram/constants/mathFunctions.ts +++ b/miniprogram/constants/mathFunctions.ts @@ -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: '✏️', - }, ]; diff --git a/miniprogram/mathPages/countingSelect/countingSelect.json b/miniprogram/mathPages/countingSelect/countingSelect.json new file mode 100644 index 0000000..0746d06 --- /dev/null +++ b/miniprogram/mathPages/countingSelect/countingSelect.json @@ -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" + } +} diff --git a/miniprogram/mathPages/countingSelect/countingSelect.less b/miniprogram/mathPages/countingSelect/countingSelect.less new file mode 100644 index 0000000..12602d2 --- /dev/null +++ b/miniprogram/mathPages/countingSelect/countingSelect.less @@ -0,0 +1 @@ +@import '../common/mathPage.less'; \ No newline at end of file diff --git a/miniprogram/mathPages/countingSelect/countingSelect.ts b/miniprogram/mathPages/countingSelect/countingSelect.ts new file mode 100644 index 0000000..1022356 --- /dev/null +++ b/miniprogram/mathPages/countingSelect/countingSelect.ts @@ -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, +}); diff --git a/miniprogram/mathPages/countingSelect/countingSelect.wxml b/miniprogram/mathPages/countingSelect/countingSelect.wxml new file mode 100644 index 0000000..5d3cbdb --- /dev/null +++ b/miniprogram/mathPages/countingSelect/countingSelect.wxml @@ -0,0 +1,37 @@ + + + 预览打印效果 + + + + + + + + + + 随机生成 + + + + + + + diff --git a/miniprogram/mathPages/service/countingSelectContentDraw.ts b/miniprogram/mathPages/service/countingSelectContentDraw.ts new file mode 100644 index 0000000..1654e31 --- /dev/null +++ b/miniprogram/mathPages/service/countingSelectContentDraw.ts @@ -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 { + 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); + } + } +} diff --git a/miniprogram/mathPages/service/countingSelectDraw.ts b/miniprogram/mathPages/service/countingSelectDraw.ts new file mode 100644 index 0000000..f1ce25f --- /dev/null +++ b/miniprogram/mathPages/service/countingSelectDraw.ts @@ -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, + ) { + 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; diff --git a/project.private.config.json b/project.private.config.json index 9734f26..bc824fc 100644 --- a/project.private.config.json +++ b/project.private.config.json @@ -23,12 +23,19 @@ "condition": { "miniprogram": { "list": [ + { + "name": "mathPages/countingSelect/countingSelect", + "pathName": "mathPages/countingSelect/countingSelect", + "query": "id=counting-fill", + "scene": null, + "launchMode": "default" + }, { "name": "mathPages/compare/compare", "pathName": "mathPages/compare/compare", "query": "id=compare", - "scene": null, - "launchMode": "default" + "launchMode": "default", + "scene": null }, { "name": "mathPages/missingNumber/missingNumber",