feat:2.6.1增加 数物对应 和 根据颜色画图形

This commit is contained in:
R524809
2025-12-23 13:35:19 +08:00
parent 9b6a0a26cc
commit 20496c3cc8
28 changed files with 940 additions and 277 deletions
+4 -2
View File
@@ -18,7 +18,8 @@
"compare/compare",
"countingSelect/countingSelect",
"numberDecompose/numberDecompose",
"numberSort/numberSort"
"numberSort/numberSort",
"numberObjectMatch/numberObjectMatch"
],
"independent": false
},
@@ -34,7 +35,8 @@
"matchConnect/matchConnect",
"lineRecognition/lineRecognition",
"gridReasoning/gridReasoning",
"codeConnect/codeConnect"
"codeConnect/codeConnect",
"colorShapeMatch/colorShapeMatch"
],
"independent": false
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

+22
View File
@@ -127,6 +127,28 @@ export function getRandomNumberColor(): string {
return colors[Math.floor(Math.random() * colors.length)];
}
/**
* 从 NUMBER_COLORS 中随机获取指定数量的不重复颜色
* @param count 需要获取的颜色数量
* @returns 不重复的颜色数组
*/
export function getRandomUniqueNumberColors(count: number): string[] {
const allColors = getNumberColors();
if (count >= allColors.length) {
// 如果需要的数量大于等于总数,返回打乱后的所有颜色
return [...allColors].sort(() => Math.random() - 0.5);
}
// 使用 Set 确保不重复
const selectedColors = new Set<string>();
while (selectedColors.size < count) {
const randomIndex = Math.floor(Math.random() * allColors.length);
selectedColors.add(allColors[randomIndex]);
}
return Array.from(selectedColors);
}
export const PAPER_SIZE = {
A4: {
// width: 2480,
+17 -7
View File
@@ -9,14 +9,14 @@ export interface FocusFunctionType {
}
export const FOCUS_FUNCTION_TYPES: FocusFunctionType[] = [
// 形状识别
// 根据颜色画图
{
id: 'shape-recognition',
page: 'shape',
title: '识别形状',
desc: '识别形状,涂一涂',
icon: '🔍',
img: '/assets/focusEntrance/shape-recognition.png',
id: 'color-shape-match',
page: 'colorShapeMatch',
title: '根据颜色画图形',
desc: '根据颜色画出对应的图形',
icon: '🎯',
img: '/assets/focusEntrance/color-shape-match.png',
},
// 图形符号配对
{
@@ -27,6 +27,15 @@ export const FOCUS_FUNCTION_TYPES: FocusFunctionType[] = [
icon: '🔗',
img: '/assets/focusEntrance/shape-symbol.png',
},
// 形状识别
{
id: 'shape-recognition',
page: 'shape',
title: '识别形状',
desc: '识别形状,涂一涂',
icon: '🔍',
img: '/assets/focusEntrance/shape-recognition.png',
},
// 方位涂涂乐
{
id: 'position-coloring',
@@ -54,6 +63,7 @@ export const FOCUS_FUNCTION_TYPES: FocusFunctionType[] = [
icon: '🔗',
img: '/assets/focusEntrance/match-connect.png',
},
// 线条识别
{
id: 'line-recognition',
+9
View File
@@ -42,6 +42,15 @@ export const MATH_FUNCTION_TYPES: MathFunctionType[] = [
icon: '🔗',
img: '/assets/mathEntrance/count-match.png',
},
// 数物对应
{
id: 'number-object-match',
page: 'numberObjectMatch',
title: '数物对应',
desc: '连线相同数量的物品和数字',
icon: '🔗',
img: '/assets/mathEntrance/number-object-match.png',
},
{
id: 'counting-select',
page: 'countingSelect',
@@ -0,0 +1,13 @@
{
"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",
"draw-ad": "../../components/draw-ad/draw-ad"
}
}
@@ -0,0 +1,132 @@
import ColorShapeMatchDraw from '../shared/service/colorShapeMatchDraw';
import { createFocusPage } from '../shared/common/focusPageMixin';
import { ColorShapeMatchData } from '../shared/service/colorShapeMatchDraw';
import { SHAPE_SYMBOL_SHAPES } from '../shared/shapes/shapeSymbolShapes';
import { getRandomUniqueNumberColors } from '../../constants/colors';
createFocusPage({
canvas: null as Canvas | null,
ctx: null as RenderingContext | null,
boxHeight: 0,
boxWidth: 0,
drawService: null as ColorShapeMatchDraw | null,
gridData: null as ColorShapeMatchData | null,
data: {
pageTitle: '根据颜色画图形',
functionId: '',
hasContent: false,
showShareDialog: false,
},
onLoad(options: { id?: string }) {
const functionId = options.id || 'color-shape-match';
this.setData({
functionId,
});
this.initPageInfo(functionId, '根据颜色画图形');
},
onReady() {
this.initCanvas({
createDrawService: (
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, any>,
) => {
return new ColorShapeMatchDraw(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.gridData) {
return;
}
try {
await this.drawService.draw(this.gridData);
this.setData({ hasContent: true });
} catch (error) {
console.error('绘制失败:', error);
this.setData({ hasContent: false });
}
},
/**
* 随机生成
*/
onRandom() {
// 从所有图形中随机选择4个
const shapes = [...SHAPE_SYMBOL_SHAPES].filter(
(shape) => shape.id !== 'cross',
);
const selectedShapes = shapes
.sort(() => Math.random() - 0.5)
.slice(0, 4);
// 获取4个不重复的颜色
const colors = getRandomUniqueNumberColors(4);
// 创建阅览区域的数据,过滤掉 cross
const legendItems = selectedShapes.map((shape, index) => ({
shapeId: shape.id,
color: colors[index],
}));
// 创建练习区域的数据(5行5列,共25个格子)
// 随机填充这4种颜色,确保每种颜色至少出现一次
const practiceGrid: Array<Array<string | null>> = [];
const allColors: (string | null)[] = new Array(25).fill(null);
// 先随机选择4个位置,每个位置分配一种颜色(确保每种颜色至少出现一次)
const positions: number[] = [];
for (let i = 0; i < 25; i++) {
positions.push(i);
}
const shuffledPositions = [...positions].sort(
() => Math.random() - 0.5,
);
// 前4个位置分配4种颜色
for (let i = 0; i < 4; i++) {
allColors[shuffledPositions[i]] = colors[i];
}
// 剩余21个位置随机分配这4种颜色
for (let i = 4; i < 25; i++) {
const randomColor =
colors[Math.floor(Math.random() * colors.length)];
allColors[shuffledPositions[i]] = randomColor;
}
// 转换为5x5的二维数组
for (let row = 0; row < 5; row++) {
const practiceRow: Array<string | null> = [];
for (let col = 0; col < 5; col++) {
const index = row * 5 + col;
practiceRow.push(allColors[index]);
}
practiceGrid.push(practiceRow);
}
this.gridData = {
legendItems,
practiceGrid,
};
this.drawCanvas();
},
});
@@ -0,0 +1,6 @@
<!-- 模版不能跨包引用 -->
<import src="../shared/templates/canvas-page-template.wxml" />
<template
is="canvasPageTemplate"
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: false, adType: 'focusDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
@@ -0,0 +1 @@
@import '../../base/baseDrawPage.wxss';
@@ -3,13 +3,4 @@
<template
is="canvasPageTemplate"
data="{{
boxWidth: boxWidth,
boxHeight: boxHeight,
hasTypeSelector: true,
currentModeName: currentModeName,
typeActions: typeActions,
adType: 'focusDraw',
disabled: !hasContent,
showShareDialog: showShareDialog
}}" />
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: true, currentModeName: currentModeName, typeActions: typeActions, adType: 'focusDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
@@ -3,13 +3,4 @@
<template
is="canvasPageTemplate"
data="{{
boxWidth: boxWidth,
boxHeight: boxHeight,
hasTypeSelector: true,
currentModeName: currentOperatorTypeName,
typeActions: operatorTypeActions,
adType: 'focusDraw',
disabled: !hasContent,
showShareDialog: showShareDialog
}}" />
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: true, currentModeName: currentOperatorTypeName, typeActions: operatorTypeActions, adType: 'focusDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
@@ -0,0 +1,158 @@
/**
* 根据颜色画图形绘制服务
*/
import { BaseDrawService } from '../../../service/baseDraw';
import { drawShapeSymbolShape } from '../shapes/shapeSymbolShapes';
/**
* 根据颜色画图形数据
*/
export interface ColorShapeMatchData {
/** 阅览区域的4组数据 */
legendItems: Array<{
shapeId: string;
color: string;
}>;
/** 练习区域的5x5网格数据 */
practiceGrid: Array<Array<string | null>>; // 存储颜色,null表示空白
}
/**
* 根据颜色画图形绘制服务
*/
class ColorShapeMatchDraw extends BaseDrawService {
gridData: ColorShapeMatchData | null = null;
constructor(
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, any>,
) {
super(canvas, ctx, options);
}
/**
* 绘制根据颜色画图形内容
*/
async draw(gridData: ColorShapeMatchData) {
if (!gridData || !gridData.legendItems || !gridData.practiceGrid) {
return;
}
this.gridData = gridData;
this.prepareDraw();
await this.drawHeaderAndDivider();
this.drawContent({
ctx: this.ctx,
gridData: this.gridData,
canvasWidth: this.canvasWidth,
startY: this.currentY,
});
}
/**
* 绘制内容区域
*/
private drawContent(params: {
ctx: RenderingContext;
gridData: ColorShapeMatchData;
canvasWidth: number;
startY: number;
}): void {
const { ctx, gridData, canvasWidth } = params;
let { startY } = params;
startY = startY + 25;
const margin = 40; // 左右边距
const groupSpacing = 40; // 组间距
const groupSize = 80; // 每组的大小(图形外框大小)
const dotRadius = 15; // 中间圆点的半径
// 计算阅览区域的总宽度
const totalWidth = groupSize * 4 + groupSpacing * 3; // 4组 + 3个间距
const legendStartX =
margin + (canvasWidth - margin * 2 - totalWidth) / 2; // 居中
const legendStartY = startY;
// 绘制阅览区域的4组
gridData.legendItems.forEach((item, index) => {
const groupX = legendStartX + index * (groupSize + groupSpacing);
const groupY = legendStartY;
const centerX = groupX + groupSize / 2;
const centerY = groupY + groupSize / 2;
// 绘制外部图形(有边框,无填充)
drawShapeSymbolShape(
ctx,
item.shapeId,
centerX,
centerY,
groupSize * 1.1, // 图形大小略小于外框
{
fillColor: null,
strokeColor: '#000',
strokeWidth: 2,
},
);
// 绘制中间的圆点(无边线,有填充)
ctx.beginPath();
ctx.arc(centerX, centerY, dotRadius, 0, Math.PI * 2);
ctx.fillStyle = item.color;
ctx.fill();
});
// 绘制虚线分割线
const dividerY = legendStartY + groupSize + 30;
this.drawDashedDivider(dividerY, margin);
// 绘制练习区域(5行5列)
const practiceStartY = dividerY + 30;
const practiceCols = 5;
const practiceRows = 5;
const practiceCellSize = 100; // 每个格子的宽度
const practiceGridWidth = practiceCellSize * practiceCols;
// 计算练习区域起始X位置(居中)
const practiceStartX =
margin + (canvasWidth - margin * 2 - practiceGridWidth) / 2;
// // 绘制练习区域网格线
// this.drawGridLines(
// practiceStartX,
// practiceStartY,
// practiceCellSize,
// practiceCellSize,
// practiceCols,
// practiceRows,
// );
// 绘制练习区域的圆点
// const dotRadius = 15; // 圆点半径
for (let row = 0; row < practiceRows; row++) {
const practiceRow = gridData.practiceGrid[row];
if (!practiceRow) continue;
const rowY = practiceStartY + row * practiceCellSize;
const cellCenterY = rowY + practiceCellSize / 2;
for (let col = 0; col < practiceCols; col++) {
const color = practiceRow[col];
if (!color) continue;
const colX = practiceStartX + col * practiceCellSize;
const cellCenterX = colX + practiceCellSize / 2;
// 绘制圆点(无边线,有填充)
ctx.beginPath();
ctx.arc(cellCenterX, cellCenterY, dotRadius, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.fill();
}
}
}
}
export default ColorShapeMatchDraw;
@@ -145,12 +145,13 @@ export function drawShapeSymbolShape(
break;
case 'triangle':
// 三角形缩小为原来的0.9倍
const triangleRadius = radius * 0.9;
// 三角形缩小为原来的0.9倍,整体中心提高
// const triangleRadius = radius * 0.9;
const bottomY = radius * 0.7; // 减少底部Y坐标,使整体中心提高
ctx.beginPath();
ctx.moveTo(0, -triangleRadius);
ctx.lineTo(-triangleRadius, triangleRadius);
ctx.lineTo(triangleRadius, triangleRadius);
ctx.moveTo(0, -radius);
ctx.lineTo(-radius, bottomY);
ctx.lineTo(radius, bottomY);
ctx.closePath();
drawPath();
break;
@@ -176,9 +177,10 @@ export function drawShapeSymbolShape(
drawPath();
break;
// 椭圆
case 'ellipse':
ctx.beginPath();
ctx.ellipse(0, 0, radius * 0.8, radius * 0.6, 0, 0, Math.PI * 2);
ctx.ellipse(0, 0, radius, radius * 0.8, 0, 0, Math.PI * 2);
drawPath();
break;
@@ -287,17 +289,21 @@ export function drawShapeSymbolShape(
// 扇形
case 'sector':
// 扇形(90度扇形),圆心在格子中心点偏左二分之一的位置
const sectorCenterX = -radius / 2; // 向左偏移半径的一半
const sectorCenterY = 0;
const sectorRadius = radius * 1.5;
// 扇形(圆弧在正上方,圆点在正下方)
const sectorCenterX = 0; // 圆心在水平中心
const sectorCenterY = sectorRadius * 0.55; // 圆心在正下方
ctx.beginPath();
ctx.moveTo(sectorCenterX, sectorCenterY);
// 从左侧上方到右侧上方绘制圆弧(135度到225度)
ctx.arc(
sectorCenterX,
sectorCenterY,
radius,
sectorRadius,
(-3 * Math.PI) / 4,
-Math.PI / 4,
Math.PI / 4,
// (Math.PI * 3) / 4, // 左侧上方(135度)
// (Math.PI * 5) / 4, // 右侧上方(225度)
false,
);
ctx.lineTo(sectorCenterX, sectorCenterY);
+5 -31
View File
@@ -1,32 +1,6 @@
<view class="page-container">
<view class="wrapper">
<text class="wrapper-title">预览打印效果</text>
<!-- 模版不能跨包引用 -->
<import src="../shared/templates/canvas-page-template.wxml" />
<!-- 预览打印效果 -->
<view id="canvasWrapper" class="canvas-wrapper">
<canvas
type="2d"
id="canvasContent"
class="canvas-content"
style="width:{{boxWidth}}px;height:{{boxHeight}}px" />
</view>
<!-- 类型选择器和随机生成按钮 -->
<math-type-selector
current-type-name="{{currentModeName}}"
type-actions="{{typeActions}}"
bind:select="onSelectType"
bind:random="onRandom" />
<draw-ad type="mathDraw"></draw-ad>
</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" />
<template
is="canvasPageTemplate"
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: true, currentModeName: currentModeName, typeActions: typeActions, adType: 'mathDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
+5 -37
View File
@@ -1,38 +1,6 @@
<view class="page-container">
<view class="wrapper">
<text class="wrapper-title">预览打印效果</text>
<!-- 模版不能跨包引用 -->
<import src="../shared/templates/canvas-page-template.wxml" />
<!-- 预览打印效果 -->
<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>
<draw-ad type="mathDraw"></draw-ad>
</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" />
<template
is="canvasPageTemplate"
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: false, adType: 'mathDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
@@ -1,32 +1,6 @@
<view class="page-container">
<view class="wrapper">
<text class="wrapper-title">预览打印效果</text>
<!-- 模版不能跨包引用 -->
<import src="../shared/templates/canvas-page-template.wxml" />
<!-- 预览打印效果 -->
<view id="canvasWrapper" class="canvas-wrapper">
<canvas
type="2d"
id="canvasContent"
class="canvas-content"
style="width:{{boxWidth}}px;height:{{boxHeight}}px" />
</view>
<!-- 类型选择器和随机生成按钮 -->
<math-type-selector
wx:if="{{showTypeSelector}}"
current-type-name="{{currentModeName}}"
type-actions="{{typeActions}}"
bind:select="onSelectType"
bind:random="onRandom" />
<draw-ad type="mathDraw"></draw-ad>
</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" />
<template
is="canvasPageTemplate"
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: true, currentModeName: currentModeName, typeActions: typeActions, adType: 'mathDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
@@ -1,38 +1,6 @@
<view class="page-container">
<view class="wrapper">
<text class="wrapper-title">预览打印效果</text>
<!-- 模版不能跨包引用 -->
<import src="../shared/templates/canvas-page-template.wxml" />
<!-- 预览打印效果 -->
<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>
<draw-ad type="mathDraw"></draw-ad>
</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" />
<template
is="canvasPageTemplate"
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: false, adType: 'mathDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
@@ -1,32 +1,6 @@
<view class="page-container">
<view class="wrapper">
<text class="wrapper-title">预览打印效果</text>
<!-- 模版不能跨包引用 -->
<import src="../shared/templates/canvas-page-template.wxml" />
<!-- 预览打印效果 -->
<view id="canvasWrapper" class="canvas-wrapper">
<canvas
type="2d"
id="canvasContent"
class="canvas-content"
style="width:{{boxWidth}}px;height:{{boxHeight}}px" />
</view>
<!-- 类型选择器和随机生成按钮 -->
<math-type-selector
wx:if="{{showTypeSelector}}"
current-type-name="{{currentModeName}}"
type-actions="{{typeActions}}"
bind:select="onSelectType"
bind:random="onRandom" />
<draw-ad type="mathDraw"></draw-ad>
</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" />
<template
is="canvasPageTemplate"
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: true, currentModeName: currentModeName, typeActions: typeActions, adType: 'mathDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
@@ -1,32 +1,6 @@
<view class="page-container">
<view class="wrapper">
<text class="wrapper-title">预览打印效果</text>
<!-- 模版不能跨包引用 -->
<import src="../shared/templates/canvas-page-template.wxml" />
<!-- 预览打印效果 -->
<view id="canvasWrapper" class="canvas-wrapper">
<canvas
type="2d"
id="canvasContent"
class="canvas-content"
style="width:{{boxWidth}}px;height:{{boxHeight}}px" />
</view>
<!-- 类型选择器和随机生成按钮 -->
<math-type-selector
wx:if="{{showTypeSelector}}"
current-type-name="{{currentModeName}}"
type-actions="{{typeActions}}"
bind:select="onSelectType"
bind:random="onRandom" />
<draw-ad type="mathDraw"></draw-ad>
</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" />
<template
is="canvasPageTemplate"
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: true, currentModeName: currentModeName, typeActions: typeActions, adType: 'mathDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
@@ -0,0 +1,13 @@
{
"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",
"draw-ad": "../../components/draw-ad/draw-ad"
}
}
@@ -0,0 +1,145 @@
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,
} as CanvasDataState,
onLoad(options: { id?: string }) {
const functionId = options.id || 'number-object-match';
this.initPageInfo(functionId, '数物对应');
},
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.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();
},
});
@@ -0,0 +1,6 @@
<!-- 模版不能跨包引用 -->
<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 @@
@import '../../base/baseDrawPage.wxss';
@@ -1,38 +1,6 @@
<view class="page-container">
<view class="wrapper">
<text class="wrapper-title">预览打印效果</text>
<!-- 模版不能跨包引用 -->
<import src="../shared/templates/canvas-page-template.wxml" />
<!-- 预览打印效果 -->
<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>
<draw-ad type="mathDraw"></draw-ad>
</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" />
<template
is="canvasPageTemplate"
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: false, adType: 'mathDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
@@ -0,0 +1,265 @@
/**
* 数物对应绘制服务
*/
import { BaseDrawService } from '../../../service/baseDraw';
import { getImage } from '../../../utils/index';
/**
* 数物对应数据
*/
export interface NumberObjectMatchData {
/** 上面区域的图片配置(4行4列,共16张) */
gridImages: Array<{
imageIndex: number;
folder: 'fruits' | 'twelve-animals';
offsetX: number; // 随机左右偏移
offsetY: number; // 随机上下偏移
}>;
/** 下面区域第一行的4张图片配置 */
bottomImages: Array<{
imageIndex: number;
folder: 'fruits' | 'twelve-animals';
}>;
/** 第二行要显示的数量(乱序排列) */
bottomNumbers: number[];
}
/**
* 数物对应绘制服务
*/
class NumberObjectMatchDraw extends BaseDrawService {
matchData: NumberObjectMatchData | null = null;
constructor(
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, any>,
) {
super(canvas, ctx, options);
this.matchData = null;
}
/**
* 绘制数物对应内容
*/
async draw(matchData: NumberObjectMatchData) {
if (
!matchData ||
!matchData.gridImages ||
!matchData.bottomImages ||
!matchData.bottomNumbers
) {
return;
}
this.matchData = matchData;
this.prepareDraw();
await this.drawHeaderAndDivider();
await this.drawContent({
canvas: this.canvas,
ctx: this.ctx,
matchData: this.matchData,
canvasWidth: this.canvasWidth,
startY: this.currentY,
});
}
/**
* 绘制内容区域
*/
private async drawContent(params: {
canvas: WechatMiniprogram.Canvas;
ctx: RenderingContext;
matchData: NumberObjectMatchData;
canvasWidth: number;
startY: number;
}): Promise<void> {
const { canvas, ctx, matchData, canvasWidth } = params;
let { startY } = params;
startY = startY + 25;
const margin = 0; // 左右边距
const imagePadding = 10; // 图片内边距
const imageSize = 75; // 图片大小
const cellSize = imageSize + imagePadding * 2; // 每个格子的大小
// ========== 绘制上面区域(4行4列) ==========
const gridCols = 5;
const gridRows = 4;
const gridWidth = cellSize * gridCols;
const gridStartX = margin + (canvasWidth - margin * 2 - gridWidth) / 2; // 居中
const gridStartY = startY;
// 加载并绘制所有图片
const imagePromises = matchData.gridImages.map(async (item, index) => {
const row = Math.floor(index / gridCols);
const col = index % gridCols;
const baseX = gridStartX + col * cellSize + cellSize / 2;
const baseY = gridStartY + row * cellSize + cellSize / 2;
// 加上随机偏移
const imageX = baseX + item.offsetX;
const imageY = baseY + item.offsetY;
// 加载图片
let image: any = null;
try {
const imagePath = `/mathPages/shared/assets/${item.folder}/${item.imageIndex}.png`;
image = await getImage(canvas, imagePath);
} catch (error) {
console.error(
`加载图片失败: ${item.folder}/${item.imageIndex}`,
error,
);
return;
}
if (image) {
// 计算图片高度(等比例缩放)
// @ts-ignore - 微信小程序图片对象有 width 和 height 属性
const scaledHeight = (image.height / image.width) * imageSize;
// 绘制图片(居中)
ctx.drawImage(
image,
imageX - imageSize / 2,
imageY - scaledHeight / 2,
imageSize,
scaledHeight,
);
}
});
await Promise.all(imagePromises);
// ========== 绘制虚线分割线 ==========
const dividerY = gridStartY + gridRows * cellSize + 30;
this.drawDashedDivider(dividerY, margin);
// ========== 绘制下面区域 ==========
const bottomStartY = dividerY + 30;
const bottomImageSize = 50; // 下面图片大小
const bottomBoxSize = bottomImageSize + imagePadding * 2; // 图片框大小
const bottomSpacing = 40; // 图片间距
const bottomRowSpacing = 80; // 两行间距
// 计算下面区域的总宽度(4个图片框 + 3个间距)
const bottomTotalWidth = bottomBoxSize * 4 + bottomSpacing * 3;
const bottomStartX =
margin + (canvasWidth - margin * 2 - bottomTotalWidth) / 2; // 居中
// 第一行:绘制图片框和图片
for (let i = 0; i < matchData.bottomImages.length; i++) {
const item = matchData.bottomImages[i];
const boxX = bottomStartX + i * (bottomBoxSize + bottomSpacing);
const boxY = bottomStartY;
const imageX = boxX + imagePadding;
const imageY = boxY + imagePadding;
// 绘制方框(边线宽度2
ctx.strokeStyle = '#999';
ctx.lineWidth = 2;
ctx.strokeRect(boxX, boxY, bottomBoxSize, bottomBoxSize);
// 加载并绘制图片
let image: any = null;
try {
const imagePath = `/mathPages/shared/assets/${item.folder}/${item.imageIndex}.png`;
image = await getImage(canvas, imagePath);
} catch (error) {
console.error(
`加载图片失败: ${item.folder}/${item.imageIndex}`,
error,
);
continue;
}
if (image) {
// 计算图片高度(等比例缩放)
// @ts-ignore - 微信小程序图片对象有 width 和 height 属性
const scaledHeight =
(image.height / image.width) * bottomImageSize;
// 绘制图片(居中)
const drawImageX =
imageX + (bottomImageSize - bottomImageSize) / 2;
const drawImageY =
imageY + (bottomImageSize - scaledHeight) / 2;
ctx.drawImage(
image,
drawImageX,
drawImageY,
bottomImageSize,
scaledHeight,
);
}
}
// 绘制第一行方框下方的圆点(间隔5)
const dotRadius = 4; // 圆点半径
const dotSpacing = 10; // 圆点与方框的间距
const topDotY = bottomStartY + bottomBoxSize + dotSpacing;
for (let i = 0; i < matchData.bottomImages.length; i++) {
const boxX = bottomStartX + i * (bottomBoxSize + bottomSpacing);
const dotX = boxX + bottomBoxSize / 2; // 方框正中间
// 绘制圆点
ctx.fillStyle = '#93D333';
ctx.beginPath();
ctx.arc(dotX, topDotY, dotRadius, 0, Math.PI * 2);
ctx.fill();
}
// 第二行:绘制数字输入框(4个方框)
const numberBoxY = bottomStartY + bottomBoxSize + bottomRowSpacing;
const numberBoxSize = bottomBoxSize; // 数字框大小和图片框一样
// 绘制第二行方框上方的圆点(间隔5)
const bottomDotY = numberBoxY - dotSpacing;
// 先绘制所有圆点
for (let i = 0; i < 4; i++) {
const boxX = bottomStartX + i * (numberBoxSize + bottomSpacing);
const dotX = boxX + numberBoxSize / 2; // 方框正中间
// 绘制圆点
ctx.fillStyle = '#93D333';
ctx.beginPath();
ctx.arc(dotX, bottomDotY, dotRadius, 0, Math.PI * 2);
ctx.fill();
}
// 再绘制方框和数字
for (let i = 0; i < 4; i++) {
const boxX = bottomStartX + i * (numberBoxSize + bottomSpacing);
// 绘制方框(边线宽度2
ctx.strokeStyle = '#999';
ctx.lineWidth = 2;
ctx.strokeRect(boxX, numberBoxY, numberBoxSize, numberBoxSize);
// 绘制数字(如果存在)
if (
matchData.bottomNumbers &&
matchData.bottomNumbers[i] !== undefined
) {
const number = matchData.bottomNumbers[i];
ctx.fillStyle = '#141414';
ctx.font = `bold ${48}px "Microsoft Yahei"`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(
String(number),
boxX + numberBoxSize / 2,
numberBoxY + numberBoxSize / 2,
);
}
}
}
}
export default NumberObjectMatchDraw;
@@ -0,0 +1,78 @@
<!--
Canvas 页面模板
用于统一管理 Canvas 绘制页面的通用结构
使用方式:
<import src="../../templates/canvas-page-template.wxml" />
<template
is="canvasPageTemplate"
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: false, adType: 'focusDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
参数说明:
- boxWidth: Canvas 宽度
- boxHeight: Canvas 高度
- hasTypeSelector: 是否显示类型选择器(布尔值)
- showTypeSelector: 条件显示类型选择器(可选,用于 countMatch、missingNumber 等页面)
- currentModeName: 当前模式名称(类型选择器使用)
- typeActions: 类型选项数组(类型选择器使用)
- adType: 广告位类型('focusDraw' 或 'mathDraw'
- disabled: 底部按钮是否禁用
- showShareDialog: 是否显示分享弹窗
-->
<template name="canvasPageTemplate">
<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>
<!-- 类型选择器和随机生成按钮 -->
<!-- 支持两种方式:hasTypeSelector(布尔值)或 showTypeSelector(条件显示) -->
<math-type-selector
wx:if="{{hasTypeSelector || showTypeSelector}}"
current-type-name="{{currentModeName}}"
type-actions="{{typeActions}}"
bind:select="onSelectType"
bind:random="onRandom" />
<!-- 随机生成按钮(当没有类型选择器时显示) -->
<view
class="random-button-area"
wx:if="{{!hasTypeSelector && !showTypeSelector}}">
<toy-button
class="random-button"
type="primary"
bind:click="onRandom"
width="100%"
height="80rpx"
icon="refresh"
icon-class-prefix="toy-icon">
随机生成
</toy-button>
</view>
<!-- 广告位 -->
<draw-ad type="{{adType}}"></draw-ad>
</view>
<view class="empty"></view>
</view>
<!-- 底部按钮 -->
<math-bottom-buttons
disabled="{{disabled}}"
bind:share="onShareAppMessage"
bind:export="exportToPrint" />
<!-- 分享引导弹窗 -->
<share-guide-popup
show="{{showShareDialog}}"
bind:onClose="onCloseShareDialog"
bind:onShareSuccess="onShareSuccess" />
</template>