diff --git a/miniprogram/app.json b/miniprogram/app.json
index b372da1..413296e 100644
--- a/miniprogram/app.json
+++ b/miniprogram/app.json
@@ -31,7 +31,8 @@
"shapeSymbol/shapeSymbol",
"colorPattern/colorPattern",
"matchConnect/matchConnect",
- "lineRecognition/lineRecognition"
+ "lineRecognition/lineRecognition",
+ "gridReasoning/gridReasoning"
],
"independent": false
}
diff --git a/miniprogram/constants/focusFunctions.ts b/miniprogram/constants/focusFunctions.ts
index 9fbe576..da33173 100644
--- a/miniprogram/constants/focusFunctions.ts
+++ b/miniprogram/constants/focusFunctions.ts
@@ -56,6 +56,14 @@ export const FOCUS_FUNCTION_TYPES: FocusFunctionType[] = [
desc: '认识不同线条,画出颜色对应的线条',
icon: '📏',
},
+ // 方格推理
+ {
+ id: 'grid-reasoning',
+ page: 'gridReasoning',
+ title: '方格推理',
+ desc: '仔细观察,推理出合并方格并连线',
+ icon: '🧩',
+ },
// 格子仿画
{
id: 'grid-drawing-3x3',
diff --git a/miniprogram/focusPages/gridDrawing/gridDrawing.wxml b/miniprogram/focusPages/gridDrawing/gridDrawing.wxml
index 49dc28f..bd1d15c 100644
--- a/miniprogram/focusPages/gridDrawing/gridDrawing.wxml
+++ b/miniprogram/focusPages/gridDrawing/gridDrawing.wxml
@@ -17,8 +17,9 @@
type-actions="{{typeActions}}"
bind:select="onSelectType"
bind:random="onRandom" />
+
-
+
,
+ ) => {
+ return new GridReasoningDraw(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 });
+ }
+ },
+
+ /**
+ * 选择运算类型
+ */
+ onSelectType(event: any) {
+ const { name, value } = event.detail;
+ this.setData({
+ currentOperatorType: value,
+ currentOperatorTypeName: name,
+ });
+ // 重新生成题目
+ this.onRandom();
+ },
+
+ /**
+ * 随机生成
+ */
+ onRandom() {
+ // 随机选择一种颜色
+ const colorKeys = Object.keys(NUMBER_COLORS).map(Number);
+ const randomColorKey =
+ colorKeys[Math.floor(Math.random() * colorKeys.length)];
+ const selectedColor = NUMBER_COLORS[randomColorKey];
+
+ const rows: Array<{
+ grid1: GridPosition[];
+ grid2: GridPosition[];
+ operator: '+' | '-';
+ result: GridPosition[];
+ }> = [];
+
+ const operatorType = this.data.currentOperatorType;
+
+ // 生成5行的数据
+ for (let i = 0; i < 5; i++) {
+ // 根据选中的运算类型选择运算符
+ let operator: '+' | '-';
+ if (operatorType === 'addition') {
+ operator = '+';
+ } else if (operatorType === 'subtraction') {
+ operator = '-';
+ } else {
+ // 混合运算:随机选择运算符
+ operator = Math.random() > 0.5 ? '+' : '-';
+ }
+
+ // 生成第一个网格的涂色位置
+ // 如果是减法运算,第一个网格至少要有2个位置
+ let grid1Count: number;
+ if (operator === '-') {
+ grid1Count = Math.floor(Math.random() * 8) + 2; // 2-9个位置
+ } else {
+ grid1Count = Math.floor(Math.random() * 9) + 1; // 1-9个位置
+ }
+ const grid1Positions = this.generateRandomPositions(grid1Count);
+
+ let grid2Positions: GridPosition[];
+ let resultPositions: GridPosition[];
+
+ if (operator === '+') {
+ // 加法:生成第二个网格的涂色位置(至少1个)
+ const grid2Count = Math.floor(Math.random() * 9) + 1;
+ grid2Positions = this.generateRandomPositions(grid2Count);
+
+ // 结果 = 并集
+ resultPositions = this.unionPositions(
+ grid1Positions,
+ grid2Positions,
+ );
+ } else {
+ // 减法:生成第二个网格的涂色位置(必须少于第一个)
+ const maxGrid2Count = grid1Count - 1;
+ const grid2Count =
+ Math.floor(Math.random() * maxGrid2Count) + 1;
+ grid2Positions = this.generateRandomPositions(grid2Count);
+
+ // 结果 = 差集
+ resultPositions = this.subtractPositions(
+ grid1Positions,
+ grid2Positions,
+ );
+ }
+
+ rows.push({
+ grid1: grid1Positions,
+ grid2: grid2Positions,
+ operator: operator as '+' | '-',
+ result: resultPositions,
+ });
+ }
+
+ // 生成右侧答案选项
+ // 右侧展示左侧5行的结果(result),顺序随机
+ const answerOptions: Array<{
+ positions: GridPosition[];
+ rowIndex: number;
+ }> = [];
+
+ for (let i = 0; i < 5; i++) {
+ answerOptions.push({
+ positions: rows[i].result,
+ rowIndex: i,
+ });
+ }
+
+ // 随机打乱顺序
+ answerOptions.sort(() => Math.random() - 0.5);
+
+ this.gridData = {
+ rows,
+ answerOptions,
+ color: selectedColor,
+ };
+
+ this.drawCanvas();
+ },
+
+ /**
+ * 生成随机位置(不重复)
+ */
+ generateRandomPositions(count: number): GridPosition[] {
+ const allPositions: GridPosition[] = [];
+ for (let y = 0; y < 3; y++) {
+ for (let x = 0; x < 3; x++) {
+ allPositions.push({ x, y });
+ }
+ }
+
+ // 随机打乱
+ const shuffled = [...allPositions].sort(() => Math.random() - 0.5);
+
+ // 返回前count个
+ return shuffled.slice(0, count);
+ },
+
+ /**
+ * 计算位置的并集
+ */
+ unionPositions(pos1: GridPosition[], pos2: GridPosition[]): GridPosition[] {
+ const set = new Set();
+ const result: GridPosition[] = [];
+
+ // 添加第一个数组的位置
+ for (const pos of pos1) {
+ const key = `${pos.x},${pos.y}`;
+ if (!set.has(key)) {
+ set.add(key);
+ result.push(pos);
+ }
+ }
+
+ // 添加第二个数组的位置(去重)
+ for (const pos of pos2) {
+ const key = `${pos.x},${pos.y}`;
+ if (!set.has(key)) {
+ set.add(key);
+ result.push(pos);
+ }
+ }
+
+ return result;
+ },
+
+ /**
+ * 计算位置的差集(pos1 - pos2)
+ */
+ subtractPositions(
+ pos1: GridPosition[],
+ pos2: GridPosition[],
+ ): GridPosition[] {
+ const set2 = new Set();
+ for (const pos of pos2) {
+ set2.add(`${pos.x},${pos.y}`);
+ }
+
+ const result: GridPosition[] = [];
+ for (const pos of pos1) {
+ const key = `${pos.x},${pos.y}`;
+ if (!set2.has(key)) {
+ result.push(pos);
+ }
+ }
+
+ return result;
+ },
+});
diff --git a/miniprogram/focusPages/gridReasoning/gridReasoning.wxml b/miniprogram/focusPages/gridReasoning/gridReasoning.wxml
new file mode 100644
index 0000000..f63ead0
--- /dev/null
+++ b/miniprogram/focusPages/gridReasoning/gridReasoning.wxml
@@ -0,0 +1,31 @@
+
+
+ 预览打印效果
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/miniprogram/focusPages/gridReasoning/gridReasoning.wxss b/miniprogram/focusPages/gridReasoning/gridReasoning.wxss
new file mode 100644
index 0000000..ddc41cc
--- /dev/null
+++ b/miniprogram/focusPages/gridReasoning/gridReasoning.wxss
@@ -0,0 +1 @@
+@import '../../base/baseDrawPage.wxss';
diff --git a/miniprogram/focusPages/shared/service/gridReasoningDraw.ts b/miniprogram/focusPages/shared/service/gridReasoningDraw.ts
new file mode 100644
index 0000000..d9fea61
--- /dev/null
+++ b/miniprogram/focusPages/shared/service/gridReasoningDraw.ts
@@ -0,0 +1,226 @@
+/**
+ * 方格推理绘制服务
+ */
+
+import { BaseDrawService } from '../../../service/baseDraw';
+
+/**
+ * 3x3网格中的位置坐标
+ */
+export interface GridPosition {
+ x: number; // 0-2
+ y: number; // 0-2
+}
+
+/**
+ * 方格推理数据
+ */
+export interface GridReasoningData {
+ /** 每行的数据 */
+ rows: Array<{
+ /** 第一个方格涂色位置 */
+ grid1: GridPosition[];
+ /** 第二个方格涂色位置 */
+ grid2: GridPosition[];
+ /** 运算符:+ 或 - */
+ operator: '+' | '-';
+ /** 结果方格涂色位置 */
+ result: GridPosition[];
+ }>;
+ /** 右侧答案选项(随机排列的方格) */
+ answerOptions: Array<{
+ /** 方格涂色位置 */
+ positions: GridPosition[];
+ /** 对应的行索引(用于匹配答案) */
+ rowIndex: number;
+ }>;
+ /** 使用的颜色 */
+ color: string;
+}
+
+/**
+ * 方格推理绘制服务
+ */
+class GridReasoningDraw extends BaseDrawService {
+ gridData: GridReasoningData | null = null;
+
+ constructor(
+ canvas: Canvas,
+ ctx: RenderingContext,
+ options?: Record,
+ ) {
+ super(canvas, ctx, options);
+ }
+
+ /**
+ * 绘制方格推理内容
+ */
+ async draw(gridData: GridReasoningData) {
+ if (!gridData || !gridData.rows || !gridData.answerOptions) {
+ return;
+ }
+
+ this.setPrintConfig();
+ this.gridData = gridData;
+ this.clear();
+ this.setPaper();
+
+ // 绘制Header
+ if (this.headerType !== 'minimal') {
+ await this.drawHeader();
+ } else {
+ this.drawMiniHeader();
+ }
+
+ // 绘制内容区域
+ this.drawDivider();
+ await this.drawContent({
+ canvas: this.canvas,
+ ctx: this.ctx,
+ gridData: this.gridData,
+ canvasWidth: this.canvasWidth,
+ startY: this.currentY,
+ });
+ }
+
+ /**
+ * 绘制内容区域
+ */
+ private async drawContent(params: {
+ canvas: WechatMiniprogram.Canvas;
+ ctx: RenderingContext;
+ gridData: GridReasoningData;
+ canvasWidth: number;
+ startY: number;
+ }): Promise {
+ const { ctx, gridData, canvasWidth } = params;
+ let { startY } = params;
+
+ startY = startY + 30;
+
+ const margin = 40; // 左右边距
+ const gridSize = 3; // 3x3网格
+ const cellSize = 32; // 每个格子的宽度
+ const gridWidth = cellSize * gridSize;
+ const gridHeight = cellSize * gridSize;
+
+ // 左侧区域宽度(两个网格 + 运算符 + 间距)
+ const gridSpacing = 15; // 两个网格之间的间距
+ const operatorWidth = 40; // 运算符宽度
+ const dotRadius = 7; // 圆点半径
+ const dotSpacing = 20; // 圆点与网格的间距
+
+ // 右侧区域宽度(答案选项网格)
+ const rightAreaWidth = gridWidth;
+ const rightAreaStartX = canvasWidth - margin - rightAreaWidth;
+
+ // 行间距
+ const rowSpacing = 38;
+
+ // 打乱答案选项的顺序,实现随机乱序
+ const shuffledAnswers = [...gridData.answerOptions].sort(
+ () => Math.random() - 0.5,
+ );
+
+ // 绘制5行
+ for (let rowIndex = 0; rowIndex < 5; rowIndex++) {
+ const rowData = gridData.rows[rowIndex];
+ if (!rowData) continue;
+
+ const rowY = startY + rowIndex * (gridHeight + rowSpacing);
+
+ // 绘制左侧第一个网格
+ const grid1X = margin;
+ this.drawGrid(
+ ctx,
+ grid1X,
+ rowY,
+ cellSize,
+ gridSize,
+ rowData.grid1,
+ gridData.color,
+ );
+
+ // 绘制运算符
+ const operatorStartX = grid1X + gridWidth + gridSpacing;
+ const operatorX = operatorStartX + operatorWidth / 2;
+ const operatorY = rowY + gridHeight / 2;
+ const operatorSize = 30; // 运算符大小
+ this.drawSymbol(
+ operatorX,
+ operatorY,
+ rowData.operator,
+ operatorSize,
+ );
+
+ // 绘制左侧第二个网格
+ const grid2X = operatorStartX + operatorWidth + gridSpacing;
+ this.drawGrid(
+ ctx,
+ grid2X,
+ rowY,
+ cellSize,
+ gridSize,
+ rowData.grid2,
+ gridData.color,
+ );
+
+ // 绘制左侧圆点(垂直居中)
+ const leftDotX = grid2X + gridWidth + dotSpacing;
+ const leftDotY = rowY + gridHeight / 2;
+ this.drawDot(ctx, leftDotX, leftDotY, dotRadius);
+
+ // 绘制右侧答案选项(随机乱序)
+ const answerData = shuffledAnswers[rowIndex];
+ if (answerData) {
+ // 绘制右侧圆点(垂直居中)
+ const rightDotX = rightAreaStartX - dotSpacing - dotRadius * 2;
+ const rightDotY = rowY + gridHeight / 2;
+ this.drawDot(ctx, rightDotX, rightDotY, dotRadius);
+
+ // 绘制答案网格
+ this.drawGrid(
+ ctx,
+ rightAreaStartX,
+ rowY,
+ cellSize,
+ gridSize,
+ answerData.positions,
+ gridData.color,
+ );
+ }
+ }
+ }
+
+ /**
+ * 绘制3x3网格
+ */
+ private drawGrid(
+ ctx: RenderingContext,
+ x: number,
+ y: number,
+ cellSize: number,
+ gridSize: number,
+ filledPositions: GridPosition[],
+ fillColor: string,
+ ) {
+ // 绘制网格线
+ this.drawGridLines(x, y, cellSize, cellSize, gridSize, gridSize);
+
+ // 绘制填充的格子
+ ctx.fillStyle = fillColor;
+ for (const pos of filledPositions) {
+ const cellX = x + pos.x * cellSize;
+ const cellY = y + pos.y * cellSize;
+ ctx.fillRect(cellX, cellY, cellSize, cellSize);
+ }
+
+ // 重新绘制网格线(确保在填充色之上)
+ ctx.strokeStyle = '#000';
+ ctx.lineWidth = 1;
+ ctx.setLineDash([]);
+ this.drawGridLines(x, y, cellSize, cellSize, gridSize, gridSize);
+ }
+}
+
+export default GridReasoningDraw;
diff --git a/miniprogram/service/baseDraw.ts b/miniprogram/service/baseDraw.ts
index 77f8574..76d4c91 100644
--- a/miniprogram/service/baseDraw.ts
+++ b/miniprogram/service/baseDraw.ts
@@ -282,4 +282,103 @@ export class BaseDrawService {
// 重置为实线(避免影响后续绘制)
ctx.setLineDash([]);
}
+
+ /**
+ * 绘制符号(使用路径绘制)
+ * @param x 符号中心X坐标
+ * @param y 符号中心Y坐标
+ * @param symbol 符号类型:'+', '-', '×', '✓'
+ * @param size 符号大小
+ */
+ drawSymbol(x: number, y: number, symbol: string, size: number): void {
+ const { ctx } = this;
+ ctx.save();
+ ctx.translate(x, y);
+
+ const lineWidth = size * 0.15; // 线条宽度
+ const halfSize = size / 2;
+ const strokeLength = halfSize * 0.7; // 线条长度
+
+ ctx.strokeStyle = '#000';
+ ctx.lineWidth = lineWidth;
+ ctx.lineCap = 'round';
+ ctx.lineJoin = 'round';
+
+ switch (symbol) {
+ case '+':
+ // 加号:横线和竖线
+ ctx.beginPath();
+ // 横线
+ ctx.moveTo(-strokeLength, 0);
+ ctx.lineTo(strokeLength, 0);
+ // 竖线
+ ctx.moveTo(0, -strokeLength);
+ ctx.lineTo(0, strokeLength);
+ ctx.stroke();
+ break;
+
+ case '-':
+ // 减号:横线
+ ctx.beginPath();
+ ctx.moveTo(-strokeLength, 0);
+ ctx.lineTo(strokeLength, 0);
+ ctx.stroke();
+ break;
+
+ case '×':
+ // 乘号:两条斜线
+ ctx.beginPath();
+ // 左上到右下
+ ctx.moveTo(-strokeLength * 0.7, -strokeLength * 0.7);
+ ctx.lineTo(strokeLength * 0.7, strokeLength * 0.7);
+ // 右上到左下
+ ctx.moveTo(strokeLength * 0.7, -strokeLength * 0.7);
+ ctx.lineTo(-strokeLength * 0.7, strokeLength * 0.7);
+ ctx.stroke();
+ break;
+
+ case '✓':
+ // 对号:勾,整体更大,右侧的线更长
+ const checkScale = 1.2; // 对号整体放大1.2倍
+ ctx.beginPath();
+ const checkStartX = -strokeLength * 0.5 * checkScale;
+ const checkStartY = -strokeLength * 0.2 * checkScale;
+ const checkMidX = -strokeLength * 0.1 * checkScale;
+ const checkMidY = strokeLength * 0.3 * checkScale;
+ const checkEndX = strokeLength * 1.0 * checkScale; // 增加右侧长度
+ const checkEndY = -strokeLength * 0.4 * checkScale; // 稍微向上调整
+ ctx.moveTo(checkStartX, checkStartY);
+ ctx.lineTo(checkMidX, checkMidY);
+ ctx.lineTo(checkEndX, checkEndY);
+ ctx.stroke();
+ break;
+
+ default:
+ // 默认绘制加号
+ ctx.beginPath();
+ ctx.moveTo(-strokeLength, 0);
+ ctx.lineTo(strokeLength, 0);
+ ctx.moveTo(0, -strokeLength);
+ ctx.lineTo(0, strokeLength);
+ ctx.stroke();
+ }
+
+ ctx.restore();
+ }
+
+ /**
+ * 绘制圆点
+ */
+ drawDot(
+ ctx: RenderingContext,
+ x: number,
+ y: number,
+ radius: number,
+ fillColor?: string,
+ ) {
+ ctx.fillStyle = fillColor ?? '#93D333';
+ ctx.beginPath();
+ ctx.arc(x, y, radius, 0, Math.PI * 2);
+ ctx.fill();
+ }
}
diff --git a/project.private.config.json b/project.private.config.json
index 34fed26..a222ea1 100644
--- a/project.private.config.json
+++ b/project.private.config.json
@@ -23,12 +23,19 @@
"condition": {
"miniprogram": {
"list": [
+ {
+ "name": "focusPages/gridReasoning/gridReasoning",
+ "pathName": "focusPages/gridReasoning/gridReasoning",
+ "query": "id=grid-reasoning",
+ "scene": null,
+ "launchMode": "default"
+ },
{
"name": "focusPages/lineRecognition/lineRecognition",
"pathName": "focusPages/lineRecognition/lineRecognition",
"query": "id=line-recognition",
- "scene": null,
- "launchMode": "default"
+ "launchMode": "default",
+ "scene": null
},
{
"name": "focusPages/matchConnect/matchConnect",