feat:v2.4.3版本,10以内的加减计算

This commit is contained in:
R524809
2025-12-04 10:56:49 +08:00
parent 0da5bf1758
commit 7f8a9b1c4d
32 changed files with 1170 additions and 25 deletions
+5 -1
View File
@@ -10,7 +10,11 @@
{ {
"root": "mathPages", "root": "mathPages",
"name": "mathPages", "name": "mathPages",
"pages": ["numberFind/numberFind", "countMatch/countMatch"], "pages": [
"numberFind/numberFind",
"countMatch/countMatch",
"addition/addition"
],
"independent": false "independent": false
} }
], ],
+5 -4
View File
@@ -1,6 +1,7 @@
export default { export default {
printHeader: { printHeader: {
header: 'wechat', // header: 'wechat',
appName: '涂鸦丫' header: 'LogoImage',
} appName: '涂鸦丫小程序',
} },
};
+28 -18
View File
@@ -35,6 +35,34 @@ export const MATH_FUNCTION_TYPES: MathFunctionType[] = [
desc: '通过连线配对数字和对应的数量图形', desc: '通过连线配对数字和对应的数量图形',
icon: '🔗', icon: '🔗',
}, },
{
id: 'addition-5',
page: 'addition',
title: '5以内加法',
desc: '练习5以内的加法运算,图形化展示',
icon: '',
},
{
id: 'addition-10',
page: 'addition',
title: '10以内加法',
desc: '练习10以内的加法运算,图形化展示',
icon: '',
},
{
id: 'subtraction-10',
page: 'addition',
title: '10以内减法',
desc: '练习10以内的减法运算,图形化展示',
icon: '',
},
{
id: 'addition-subtraction-10',
page: 'addition',
title: '10以内加减法',
desc: '练习10以内的加法和减法混合运算',
icon: '±',
},
{ {
id: 'missing-number', id: 'missing-number',
title: '填上缺少的数字', title: '填上缺少的数字',
@@ -59,22 +87,4 @@ export const MATH_FUNCTION_TYPES: MathFunctionType[] = [
desc: '数出物品数量,填写对应的数字', desc: '数出物品数量,填写对应的数字',
icon: '✏️', icon: '✏️',
}, },
{
id: 'addition-5',
title: '5以内加法',
desc: '练习5以内的加法运算,图形化展示',
icon: '',
},
{
id: 'addition-10',
title: '10以内加法',
desc: '练习10以内的加法运算,图形化展示',
icon: '',
},
{
id: 'addition-subtraction-10',
title: '10以内加减法',
desc: '练习10以内的加法和减法混合运算',
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",
"van-action-sheet": "../../miniprogram_npm/@vant/weapp/action-sheet/index"
}
}
@@ -0,0 +1,101 @@
page {
background-color: #f6f6f6;
}
.page-container {
background-color: #f6f6f6;
padding: 0 24rpx;
box-sizing: border-box;
padding-bottom: 160rpx; // 为底部按钮预留空间
.empty {
height: 50rpx;
}
}
.wrapper {
background-color: #ffffff;
border-radius: 20rpx;
padding: 36rpx 24rpx;
box-shadow: 0 6rpx 8rpx rgba(0, 0, 0, 0.15);
margin: 30rpx 0;
display: flex;
flex-direction: column;
.wrapper-title {
font-size: 32rpx;
color: #141414;
margin-bottom: 36rpx;
font-weight: bold;
text-align: center;
}
.canvas-wrapper {
display: flex;
justify-content: center;
align-items: center;
min-height: 400rpx;
background: #f8f9fa;
border-radius: 12rpx;
border: 2rpx dashed #dee2e6;
}
.canvas-content {
max-width: 100%;
border-radius: 8rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.random-button-area {
margin-top: 40rpx;
display: flex;
align-items: center;
gap: 20rpx;
.type-selector {
flex: 1; // 占据剩余空间
height: 80rpx;
background-color: #fff;
border: 2rpx solid #e5e5e5;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24rpx;
box-sizing: border-box;
.type-selector-text {
font-size: 28rpx;
color: #333;
}
.type-selector-arrow {
font-size: 20rpx;
color: #999;
}
}
.random-button {
flex: 2;
}
}
}
/* 底部按钮区域 */
.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: 2;
}
+298
View File
@@ -0,0 +1,298 @@
import { PAPER_SIZE } from '../../constants/colors';
import { checkAndSaveImage } from '../../utils/saveImage';
import { shouldShowShareGuide } from '../../utils/shareGuide';
import AdditionDraw from '../service/additionDraw';
import {
MATH_FUNCTION_TYPES,
MathFunctionType,
} from '../../constants/mathFunctions';
Page({
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: '加减法计算',
functionId: '',
hasContent: false,
showShareDialog: false,
showTypeSelector: false,
currentType: 'addition-5', // 'addition-5', 'addition-10', 'subtraction-10', 'addition-subtraction-10'
currentTypeName: '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
},
onLoad(options: { id?: string }) {
const functionId = options.id || 'addition-5';
this.setData({ functionId });
const functionItem = MATH_FUNCTION_TYPES.find(
(item: MathFunctionType) => item.id === functionId,
);
const pageTitle = functionItem?.title || '加减法计算';
this.setData({ pageTitle });
wx.setNavigationBarTitle({ title: pageTitle });
// 根据 functionId 设置默认类型
if (functionId === 'addition-5') {
this.setData({
currentType: 'addition-5',
currentTypeName: '5以内加法',
});
} else if (functionId === 'addition-10') {
this.setData({
currentType: 'addition-10',
currentTypeName: '10以内加法',
});
} else if (functionId === 'subtraction-10') {
this.setData({
currentType: 'subtraction-10',
currentTypeName: '10以内减法',
});
} else if (functionId === 'addition-subtraction-10') {
this.setData({
currentType: 'addition-subtraction-10',
currentTypeName: '10以内加减法',
});
}
},
onReady() {
this.initCanvas();
},
/**
* 初始化Canvas
*/
initCanvas() {
const query = wx.createSelectorQuery();
query
.select('#canvasWrapper')
.boundingClientRect((rect) => {
if (rect) {
const { width, height } = PAPER_SIZE['A4'];
const boxWidth = rect.width;
const boxHeight = boxWidth / (width / height);
this.boxHeight = boxHeight;
this.boxWidth = boxWidth;
this.setData({ boxWidth, boxHeight });
const canvas = wx
.createSelectorQuery()
.select('#canvasContent');
canvas.fields({ node: true, size: true }).exec((res) => {
if (res[0]) {
const canvasNode = res[0].node;
const ctx = canvasNode.getContext('2d');
const dpr = wx.getSystemInfoSync().pixelRatio;
canvasNode.width = boxWidth * dpr;
canvasNode.height = boxHeight * dpr;
ctx.scale(dpr, dpr);
this.canvas = canvasNode;
this.ctx = ctx;
this.drawService = new AdditionDraw(
canvasNode,
ctx,
{
title: this.data.currentTypeName,
subTitle: '通过图形化方式学习加减法运算',
},
);
// 初始随机生成
this.onRandom();
}
});
}
})
.exec();
},
/**
* 绘制Canvas内容
*/
async drawCanvas() {
if (!this.ctx || !this.drawService || !this.calculationData) {
return;
}
try {
// 更新 Header 的 Title
if (this.drawService) {
this.drawService.options.title = this.data.currentTypeName;
}
await this.drawService.draw(
this.calculationData,
this.data.currentType,
);
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.currentType;
// 生成6道题目
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();
},
/**
* 导出打印
*/
exportToPrint() {
if (!this.canvas || !this.data.hasContent) {
return;
}
if (shouldShowShareGuide()) {
this.setData({ showShareDialog: true });
return;
}
checkAndSaveImage(this.canvas);
},
/**
* 分享小程序
*/
onShareAppMessage() {
return {
title: '涂鸦丫-数学学习涂鸦卡',
path: `/mathPages/addition/addition?id=${this.data.functionId}`,
imageUrl:
'https://cdn.joeyone.cn/doodle/share-img/index-share-v2.png',
};
},
onShareTimeline() {
return {
title: '涂鸦丫-数学学习涂鸦卡',
query: `id=${this.data.functionId}`,
imageUrl:
'https://cdn.joeyone.cn/doodle/share-img/index-share-v2.png',
};
},
/** 关闭分享引导弹窗 */
onCloseShareDialog() {
this.setData({ showShareDialog: false });
},
/** 分享成功回调 */
onShareSuccess() {
this.setData({ showShareDialog: false });
if (this.canvas) {
checkAndSaveImage(this.canvas);
}
},
/** 显示类型选择器 */
onShowTypeSelector() {
this.setData({ showTypeSelector: true });
},
/** 关闭类型选择器 */
onCloseTypeSelector() {
this.setData({ showTypeSelector: false });
},
/** 选择类型 */
onSelectType(event: any) {
const { name, value } = event.detail;
this.setData({
currentType: value,
currentTypeName: name,
showTypeSelector: false,
});
// 重新生成数据
this.onRandom();
},
});
@@ -0,0 +1,69 @@
<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">
<view class="type-selector" bind:tap="onShowTypeSelector">
<text class="type-selector-text">{{currentTypeName}}</text>
<text class="type-selector-arrow">▼</text>
</view>
<toy-button
class="random-button"
type="primary"
bind:click="onRandom"
width="100%"
height="80rpx"
icon="refresh"
icon-class-prefix="toy-icon">
随机生成
</toy-button>
</view>
<van-action-sheet
show="{{showTypeSelector}}"
actions="{{typeActions}}"
bind:select="onSelectType"
bind:cancel="onCloseTypeSelector"
bind:close="onCloseTypeSelector"
cancel-text="取消"
custom-class="custom-action-sheet" />
</view>
<view class="empty"></view>
</view>
<view class="bottom-btn-box">
<toy-button
openType="share"
type="green"
flat="{{true}}"
bind:click="onShareAppMessage"
width="220rpx"
height="80rpx"
icon="wechat"
icon-class-prefix="toy-icon">
分享
</toy-button>
<toy-button
type="primary"
flat="{{true}}"
bind:click="exportToPrint"
width="420rpx"
height="80rpx"
disabled="{{!hasContent}}"
icon="download"
icon-class-prefix="toy-icon">
下载打印
</toy-button>
</view>
<share-guide-popup
show="{{showShareDialog}}"
bind:onClose="onCloseShareDialog"
bind:onShareSuccess="onShareSuccess" />
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

@@ -0,0 +1,576 @@
import { getImage } from '../../utils/index';
interface DrawAdditionContentParams {
canvas: WechatMiniprogram.Canvas;
ctx: RenderingContext;
problems: Array<{
type: 'addition' | 'subtraction';
left: number;
right: number;
result: number;
}>;
canvasWidth: number;
startY: number;
imageType?: string; // 'twelve-animals' 或 'fruits'
}
/**
* 绘制圆角矩形(虚线或实线边框)
*/
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([2, 2]); // 虚线
} else {
ctx.setLineDash([]); // 实线
}
ctx.stroke();
ctx.setLineDash([]); // 重置为实线
}
/**
* 绘制实线圆角方框(用于填写答案)
*/
function drawAnswerBox(
ctx: RenderingContext,
x: number,
y: number,
width: number,
height: number,
) {
const radius = 6; // 圆角大小
ctx.strokeStyle = '#000';
ctx.lineWidth = 1;
ctx.setLineDash([]); // 实线
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();
ctx.stroke();
}
/**
* 绘制右侧答案框
*/
function drawAnswerBoxArea(
ctx: RenderingContext,
params: {
rightBoxX: number;
boxY: number;
rightBoxWidth: number;
borderRadius: number;
boxHeight: number;
padding: number;
problemType: 'addition' | 'subtraction';
},
) {
const {
rightBoxX,
boxY,
rightBoxWidth,
borderRadius,
boxHeight,
padding,
problemType,
} = params;
const plusSignSize = 18; // 加号大小(缩小)
const minusSignSize = 18; // 减号大小(缩小)
const equalsSignSize = 18; // 等号大小(缩小)
const spacing = 8; // 加号左右间距(方便修改和调试)
const answerBoxWidth = 34; // 答案框宽度
const answerBoxHeight = 34; // 答案框高度
// 绘制右侧答案框边框
ctx.strokeStyle = '#999';
ctx.lineWidth = 1;
drawRoundedRect(
ctx,
rightBoxX,
boxY,
rightBoxWidth,
boxHeight,
borderRadius,
true,
);
const rightBoxCenterY = boxY + boxHeight / 2;
const rightBoxAvailableWidth = rightBoxWidth - padding * 2;
const answerStartY = rightBoxCenterY - answerBoxHeight / 2;
// 根据问题类型确定运算符号大小
const operatorSignSize =
problemType === 'addition' ? plusSignSize : minusSignSize;
// 计算总宽度并水平居中(加法和减法逻辑相同)
const totalAnswerWidth =
answerBoxWidth +
spacing +
operatorSignSize +
spacing +
answerBoxWidth +
spacing +
equalsSignSize +
spacing +
answerBoxWidth;
const answerStartX =
rightBoxX + padding + (rightBoxAvailableWidth - totalAnswerWidth) / 2;
// 计算各个元素的位置
const box1X = answerStartX;
const operatorX = box1X + answerBoxWidth + spacing;
const box2X = operatorX + plusSignSize + spacing;
const equalsX = box2X + answerBoxWidth + spacing;
const box3X = equalsX + equalsSignSize + spacing;
// 绘制第一个方框(圆角)
drawAnswerBox(ctx, box1X, answerStartY, answerBoxWidth, answerBoxHeight);
// 绘制运算符号(加号或减号)
ctx.strokeStyle = '#000';
ctx.lineWidth = 2;
ctx.beginPath();
if (problemType === 'addition') {
// 加号:横线和竖线
ctx.moveTo(operatorX, rightBoxCenterY);
ctx.lineTo(operatorX + operatorSignSize, rightBoxCenterY);
ctx.moveTo(
operatorX + operatorSignSize / 2,
rightBoxCenterY - operatorSignSize / 2,
);
ctx.lineTo(
operatorX + operatorSignSize / 2,
rightBoxCenterY + operatorSignSize / 2,
);
} else {
// 减号:只有横线
ctx.moveTo(operatorX, rightBoxCenterY);
ctx.lineTo(operatorX + operatorSignSize, rightBoxCenterY);
}
ctx.stroke();
// 绘制第二个方框(圆角)
drawAnswerBox(ctx, box2X, answerStartY, answerBoxWidth, answerBoxHeight);
// 绘制等号(缩短)
ctx.strokeStyle = '#000';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(equalsX, rightBoxCenterY - 3);
ctx.lineTo(equalsX + equalsSignSize, rightBoxCenterY - 3);
ctx.moveTo(equalsX, rightBoxCenterY + 3);
ctx.lineTo(equalsX + equalsSignSize, rightBoxCenterY + 3);
ctx.stroke();
// 绘制第三个方框(答案,圆角)
drawAnswerBox(ctx, box3X, answerStartY, answerBoxWidth, answerBoxHeight);
}
// 根据图片类型设置图片大小
function getImageSize(imageType: 'twelve-animals' | 'fruits'): number {
return imageType === 'twelve-animals' ? 40 : 36;
}
/**
* 绘制加减法计算内容区域
* 左侧框:展示图片(加法:加号左右展示图片;减法:总数图片,后几个加划线)
* 右侧框:填写运算方式(实线方框 + 实线方框 = 实线方框 或 实线方框 - 实线方框 = 实线方框)
*/
export async function drawAdditionContent({
canvas,
ctx,
problems,
canvasWidth,
startY,
imageType: _imageType = '', // 不再使用,每个运算会随机选择图片类型
}: DrawAdditionContentParams): Promise<void> {
// 根据图片类型确定图片目录和最大索引
const imageConfig = {
'twelve-animals': {
folder: 'twelve-animals',
maxIndex: 12,
},
fruits: {
folder: 'fruits',
maxIndex: 22,
},
};
const leftMargin = 24;
const rightMargin = 24;
const itemSpacing = 140;
// 调整左右框宽度比例:6:4 (约420:280)
const leftBoxWidth = 334; // 左侧图片框固定宽度
const rightBoxWidth = 196; // 右侧答案框固定宽度
const boxHeight = 110;
const borderRadius = 12;
const imageSpacing = 0; // 图片之间的间距(水平方向)
const rowSpacing = 4; // 第一行和第二行之间的间距(垂直方向)
const padding = 15; // 框内边距
const startYPos = startY + 20;
const plusSignSize = 20; // 加号大小(缩小)
const plusSignSpacing = 15; // 加号左右间距(方便修改和调试)
// 计算左侧和右侧的起始X位置
const leftBoxX = leftMargin;
const rightBoxX = canvasWidth - rightMargin - rightBoxWidth;
// 为每个题目随机选择图片类型和图片索引
const problemImageConfigs: Array<{
type: 'twelve-animals' | 'fruits';
imageIndex: number;
}> = [];
for (let i = 0; i < problems.length; i++) {
// 随机选择图片类型
const imageTypes: ('twelve-animals' | 'fruits')[] = [
'twelve-animals',
'fruits',
];
const randomType =
imageTypes[Math.floor(Math.random() * imageTypes.length)];
const config = imageConfig[randomType];
// 为当前题目生成唯一的图片索引
const availableImageIndices = Array.from(
{ length: config.maxIndex },
(_, i) => i + 1,
);
const randomIndex = Math.floor(
Math.random() * availableImageIndices.length,
);
const imageIndex = availableImageIndices[randomIndex];
problemImageConfigs.push({
type: randomType,
imageIndex,
});
}
// 绘制每一行的内容
for (let i = 0; i < problems.length; i++) {
const problem = problems[i];
const boxY = startYPos + i * itemSpacing;
const imageConfigItem = problemImageConfigs[i];
const config = imageConfig[imageConfigItem.type];
// 根据图片类型获取图片大小
const imageSize = getImageSize(imageConfigItem.type);
// 加载图片
let image: any = null;
try {
const imagePath = `/mathPages/assets/${config.folder}/${imageConfigItem.imageIndex}.png`;
image = await getImage(canvas, imagePath);
} catch (error) {
console.error('图片加载失败:', error);
}
// ========== 绘制左侧图片框 ==========
ctx.strokeStyle = '#999';
ctx.lineWidth = 1;
drawRoundedRect(
ctx,
leftBoxX,
boxY,
leftBoxWidth,
boxHeight,
borderRadius,
true,
);
const leftBoxCenterY = boxY + boxHeight / 2;
const leftBoxContentX = leftBoxX + padding;
if (problem.type === 'addition') {
// 加法:左侧图片 + 加号 + 右侧图片
const leftImageCount = problem.left;
const rightImageCount = problem.right;
/**
* 根据数量计算每行最多展示的图片数
*/
const getMaxImagesPerRow = (count: number): number => {
if (count <= 6) return 3;
if (count <= 8) return 4;
return 5; // 8-10
};
const leftMaxPerRow = getMaxImagesPerRow(leftImageCount);
const rightMaxPerRow = getMaxImagesPerRow(rightImageCount);
const leftRows = Math.min(
2,
Math.ceil(leftImageCount / leftMaxPerRow),
);
const rightRows = Math.min(
2,
Math.ceil(rightImageCount / rightMaxPerRow),
);
// 计算左侧图片区域的宽度和高度
// 需要根据实际行数计算每行的最大宽度
const leftFirstRowCount = Math.min(leftImageCount, leftMaxPerRow);
const leftSecondRowCount = Math.max(
0,
Math.min(leftMaxPerRow, leftImageCount - leftMaxPerRow),
);
const leftImagesWidth =
Math.max(
leftFirstRowCount * imageSize +
(leftFirstRowCount - 1) * imageSpacing,
leftSecondRowCount * imageSize +
(leftSecondRowCount - 1) * imageSpacing,
) || imageSize; // 至少为 imageSize
// 计算右侧图片区域的宽度和高度
const rightFirstRowCount = Math.min(
rightImageCount,
rightMaxPerRow,
);
const rightSecondRowCount = Math.max(
0,
Math.min(rightMaxPerRow, rightImageCount - rightMaxPerRow),
);
const rightImagesWidth =
Math.max(
rightFirstRowCount * imageSize +
(rightFirstRowCount - 1) * imageSpacing,
rightSecondRowCount * imageSize +
(rightSecondRowCount - 1) * imageSpacing,
) || imageSize; // 至少为 imageSize
// 获取图片实际高度(用于垂直居中计算)
let imageActualHeight = imageSize;
if (image) {
// @ts-ignore
imageActualHeight = (image.height / image.width) * imageSize;
}
// 计算左侧图片区域的实际高度(考虑图片的实际高度和行间距)
const leftImagesHeight =
leftRows * imageActualHeight + (leftRows - 1) * rowSpacing;
// 计算右侧图片区域的实际高度
const rightImagesHeight =
rightRows * imageActualHeight + (rightRows - 1) * rowSpacing;
// 计算加号位置(居中,包含左右间距)
const totalContentWidth =
leftImagesWidth +
plusSignSpacing +
plusSignSize +
plusSignSpacing +
rightImagesWidth;
const availableWidth = leftBoxWidth - padding * 2;
const contentStartX =
leftBoxContentX + (availableWidth - totalContentWidth) / 2;
const leftImagesStartX = contentStartX;
// 垂直居中:使用图片实际高度计算
const leftImagesStartY = leftBoxCenterY - leftImagesHeight / 2;
// 绘制左侧图片(最多两行)
for (let j = 0; j < Math.min(leftImageCount, 10); j++) {
const row = Math.floor(j / leftMaxPerRow);
const col = j % leftMaxPerRow;
// 如果超过两行,停止绘制
if (row >= 2) break;
const imagesInThisRow = Math.min(
leftMaxPerRow,
leftImageCount - row * leftMaxPerRow,
);
const rowWidth =
imagesInThisRow * imageSize +
(imagesInThisRow - 1) * imageSpacing;
const rowStartX =
leftImagesStartX + (leftImagesWidth - rowWidth) / 2;
const imageX = rowStartX + col * (imageSize + imageSpacing);
// 使用图片实际高度和行间距计算垂直位置
const imageY =
leftImagesStartY + row * (imageActualHeight + rowSpacing);
if (image) {
// @ts-ignore
const scaledHeight =
(image.height / image.width) * imageSize;
ctx.drawImage(
image,
imageX,
imageY,
imageSize,
scaledHeight,
);
}
}
// 绘制加号(左右有间距)
const plusX =
leftImagesStartX +
leftImagesWidth +
plusSignSpacing +
plusSignSize / 2;
const plusY = leftBoxCenterY;
ctx.strokeStyle = '#000';
ctx.lineWidth = 2;
ctx.beginPath();
// 横线
ctx.moveTo(plusX - plusSignSize / 2, plusY);
ctx.lineTo(plusX + plusSignSize / 2, plusY);
// 竖线
ctx.moveTo(plusX, plusY - plusSignSize / 2);
ctx.lineTo(plusX, plusY + plusSignSize / 2);
ctx.stroke();
const rightImagesStartX =
plusX + plusSignSize / 2 + plusSignSpacing;
// 垂直居中:使用图片实际高度计算
const rightImagesStartY = leftBoxCenterY - rightImagesHeight / 2;
// 绘制右侧图片(最多两行)
for (let j = 0; j < Math.min(rightImageCount, 10); j++) {
const row = Math.floor(j / rightMaxPerRow);
const col = j % rightMaxPerRow;
// 如果超过两行,停止绘制
if (row >= 2) break;
const imagesInThisRow = Math.min(
rightMaxPerRow,
rightImageCount - row * rightMaxPerRow,
);
const rowWidth =
imagesInThisRow * imageSize +
(imagesInThisRow - 1) * imageSpacing;
const rowStartX =
rightImagesStartX + (rightImagesWidth - rowWidth) / 2;
const imageX = rowStartX + col * (imageSize + imageSpacing);
// 使用图片实际高度和行间距计算垂直位置
const imageY =
rightImagesStartY + row * (imageActualHeight + rowSpacing);
if (image) {
// @ts-ignore
const scaledHeight =
(image.height / image.width) * imageSize;
ctx.drawImage(
image,
imageX,
imageY,
imageSize,
scaledHeight,
);
}
}
} else {
// 减法:展示总数量的图片,后几个加划线
const totalCount = problem.left; // 总数
const subtractCount = problem.right; // 要减去的数量
// 获取图片实际高度(用于垂直居中计算)
let imageActualHeight = imageSize;
if (image) {
// @ts-ignore
imageActualHeight = (image.height / image.width) * imageSize;
}
// 计算图片布局
const imagesPerRow = 5;
const totalRows = Math.ceil(totalCount / imagesPerRow);
// 使用图片实际高度和行间距计算总高度
const totalHeight =
totalRows * imageActualHeight + (totalRows - 1) * rowSpacing;
const imagesStartY = leftBoxCenterY - totalHeight / 2;
// 绘制所有图片
for (let j = 0; j < totalCount; j++) {
const row = Math.floor(j / imagesPerRow);
const col = j % imagesPerRow;
const imagesInThisRow = Math.min(
imagesPerRow,
totalCount - row * imagesPerRow,
);
const rowWidth =
imagesInThisRow * imageSize +
(imagesInThisRow - 1) * imageSpacing;
const rowStartX =
leftBoxContentX +
(leftBoxWidth - padding * 2 - rowWidth) / 2;
const imageX = rowStartX + col * (imageSize + imageSpacing);
// 使用图片实际高度和行间距计算垂直位置
const imageY =
imagesStartY + row * (imageActualHeight + rowSpacing);
if (image) {
// @ts-ignore
const scaledHeight =
(image.height / image.width) * imageSize;
ctx.drawImage(
image,
imageX,
imageY,
imageSize,
scaledHeight,
);
// 如果是后几个图片,绘制虚线删除线(表示减去)
if (j >= totalCount - subtractCount) {
ctx.strokeStyle = '#000';
ctx.lineWidth = 2;
ctx.setLineDash([4, 3]); // 设置虚线样式
ctx.beginPath();
// 绘制第一条虚线对角线(从左上到右下)
ctx.moveTo(imageX, imageY);
ctx.lineTo(imageX + imageSize, imageY + scaledHeight);
// // 绘制第二条虚线对角线(从右上到左下)
// ctx.moveTo(imageX + imageSize, imageY);
// ctx.lineTo(imageX, imageY + scaledHeight);
ctx.stroke();
ctx.setLineDash([]); // 重置为实线,防止影响后续绘制
}
}
}
}
// ========== 绘制右侧答案框 ==========
drawAnswerBoxArea(ctx, {
rightBoxX,
boxY,
rightBoxWidth,
borderRadius,
boxHeight,
padding,
problemType: problem.type,
});
}
}
@@ -0,0 +1,67 @@
import { BaseMathDrawService } from './baseMathDraw';
import { drawAdditionContent } from './additionContentDraw';
/**
* 加减法计算绘制服务
* 组合使用基础绘制服务和内容区域绘制服务
*/
class AdditionDraw extends BaseMathDrawService {
calculationData: {
problems: Array<{
type: 'addition' | 'subtraction';
left: number;
right: number;
result: number;
}>;
} | null;
constructor(
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, any>,
) {
super(canvas, ctx, options);
this.calculationData = null;
}
async draw(
calculationData: {
problems: Array<{
type: 'addition' | 'subtraction';
left: number;
right: number;
result: number;
}>;
},
_calculationType: string = 'addition-5', // 保留参数以保持接口兼容性
_imageType?: string, // 可选,不再使用,每个运算会随机选择图片类型
) {
if (!calculationData || !calculationData.problems) {
return;
}
this.setPrintConfig();
this.calculationData = calculationData;
this.clear();
this.setPaper();
// 绘制Header
if (this.headerType !== 'minimal') {
await this.drawHeader();
} else {
this.drawMiniHeader();
}
// 绘制内容区域(加减法计算)
this.drawDivider();
await drawAdditionContent({
canvas: this.canvas,
ctx: this.ctx,
problems: this.calculationData.problems,
canvasWidth: this.canvasWidth,
startY: this.currentY,
});
}
}
export default AdditionDraw;
+9 -2
View File
@@ -23,12 +23,19 @@
"condition": { "condition": {
"miniprogram": { "miniprogram": {
"list": [ "list": [
{
"name": "mathPages/addition/addition",
"pathName": "mathPages/addition/addition",
"query": "id=addition-5",
"scene": null,
"launchMode": "default"
},
{ {
"name": "mathPages/countMatch/countMatch", "name": "mathPages/countMatch/countMatch",
"pathName": "mathPages/countMatch/countMatch", "pathName": "mathPages/countMatch/countMatch",
"query": "id=number-coloring", "query": "id=number-coloring",
"scene": null, "launchMode": "default",
"launchMode": "default" "scene": null
}, },
{ {
"name": "pages/mathIndex/mathIndex", "name": "pages/mathIndex/mathIndex",