feat:数字学习卡

This commit is contained in:
R524809
2025-11-26 18:03:29 +08:00
parent 61e7781bf8
commit 698109e6f5
37 changed files with 872 additions and 158 deletions
+19 -7
View File
@@ -9,9 +9,21 @@
"subPackages": [
{
"root": "mathPages",
"pages": ["numberColor/numberColor"]
"name": "mathPages",
"pages": ["numberColor/numberColor"],
"independent": false
}
],
"preloadRule": {
"pages/index/index": {
"network": "all",
"packages": ["mathPages"]
},
"pages/mathIndex/mathIndex": {
"network": "all",
"packages": ["mathPages"]
}
},
"window": {},
"style": "v2",
"rendererOptions": {
@@ -48,17 +60,17 @@
"selectedIconPath": "assets/tabBar/icon-math-active.png",
"text": "数学"
},
{
"pagePath": "pages/copyBook/copyBook",
"iconPath": "assets/tabBar/icon-edit.png",
"selectedIconPath": "assets/tabBar/icon-edit-active.png",
"text": "练字"
},
{
"pagePath": "pages/shape/index",
"iconPath": "assets/tabBar/icon-shape.png",
"selectedIconPath": "assets/tabBar/icon-shape-active.png",
"text": "图形"
},
{
"pagePath": "pages/copyBook/copyBook",
"iconPath": "assets/tabBar/icon-edit.png",
"selectedIconPath": "assets/tabBar/icon-edit-active.png",
"text": "练字"
}
]
}
+78
View File
@@ -0,0 +1,78 @@
export interface MathFunctionType {
id: string;
page?: string;
title: string;
desc: string;
icon: string;
}
export const MATH_FUNCTION_TYPES: MathFunctionType[] = [
{
id: 'number-find',
page: 'numberColor',
title: '看数字,涂一涂',
desc: '看数字,找一找下面相同的数字,涂上颜色',
icon: '🔍',
},
{
id: 'number-write',
page: 'numberColor',
title: '看数字,写一写',
desc: '看数字,按笔画顺序写一写',
icon: '✏️',
},
{
id: 'number-coloring',
title: '数字涂色卡',
desc: '一边写数字、一边学数数,数字和卡通图片一一对应',
icon: '🎨',
},
{
id: 'counting-matching',
title: '数一数,连一连',
desc: '通过连线配对数字和对应的数量图形',
icon: '🔗',
},
{
id: 'missing-number',
title: '填上缺少的数字',
desc: '在数字序列中找出并填写缺失的数字',
icon: '❓',
},
{
id: 'compare',
title: '数一数,比大小',
desc: '通过数数比较两组物品的数量大小',
icon: '⚖️',
},
{
id: 'counting-select',
title: '数一数,选一选',
desc: '数出物品数量,从多个选项中选择正确答案',
icon: '✓',
},
{
id: 'counting-fill',
title: '数一数,填一填',
desc: '数出物品数量,填写对应的数字',
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: '±',
},
];
Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 241 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 254 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 252 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 276 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

@@ -47,22 +47,6 @@ page {
overflow: hidden;
}
// .canvas-wrapper {
// width: 100%;
// margin-bottom: 40rpx;
// display: flex;
// justify-content: center;
// align-items: center;
// background: #f8f8f8;
// border-radius: 12rpx;
// padding: 20rpx;
// box-sizing: border-box;
// }
// .canvas-content {
// width: 100%;
// height: 100%;
// }
}
/* 数字选择区域 */
@@ -70,17 +54,18 @@ page {
display: flex;
flex-direction: column;
gap: 20rpx;
margin-top: 40rpx;
}
.number-row {
display: flex;
flex-direction: row;
gap: 60rpx;
gap: 40rpx;
width: 100%;
margin-top: 40rpx;
}
.number-item {
height: 90rpx;
background: #ffffff;
border: 2rpx solid #e8e8e8;
border-radius: 16rpx;
@@ -90,26 +75,43 @@ page {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
box-sizing: border-box;
min-width: 0;
position: relative;
flex: 1;
min-width: 0;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08), 0 2rpx 4rpx rgba(0, 0, 0, 0.06);
&:active {
transform: scale(0.98);
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.1), 0 1rpx 2rpx rgba(0, 0, 0, 0.08);
}
&.active {
border-color: #93d333;
box-shadow: 0 0 0 4rpx rgba(147, 211, 51, 0.2);
box-shadow: 0 4rpx 16rpx rgba(147, 211, 51, 0.25), 0 0 0 4rpx rgba(147, 211, 51, 0.15);
}
.number-text {
font-size: 28rpx;
color: #141414;
font-size: 36rpx;
font-weight: bold;
color: #141414;
text-align: center;
}
}
.number-column {
flex: 1;
display: flex;
flex-direction: row;
gap: 40rpx;
.number-item {
flex: 1;
}
.random-button {
flex: 2;
}
/* 随机按钮样式 */
&.random-button {
@@ -134,7 +136,7 @@ page {
}
.number-text {
// flex: 0;
flex-shrink: 0;
}
&.active {
@@ -154,13 +156,12 @@ page {
left: 0;
bottom: 0;
width: 100%;
height: 200rpx;
height: 180rpx;
display: flex;
justify-content: space-between;
align-items: center;
background-color: #fff;
box-shadow: 0px -3.9rpx 5.2rpx rgba(0, 0, 0, 0.15);
padding: 0 24rpx;
padding: 24rpx;
padding-bottom: env(safe-area-inset-bottom);
box-sizing: border-box;
border-radius: 16rpx 16rpx 0 0;
@@ -1,12 +1,18 @@
import { PAPER_SIZE } from '../../constants/colors';
import { checkAndSaveImage } from '../../utils/saveImage';
import { shouldShowShareGuide } from '../../utils/shareGuide';
import NumberColorDraw from '../service/numberColorDraw';
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 NumberColorDraw | null,
data: {
pageTitle: '看数字,涂一涂', // 默认标题
@@ -18,26 +24,17 @@ Page({
onLoad(options: { id?: string }) {
// 根据id参数判断页面标题
const functionId = options.id || 'number-find-color';
const functionId = options.id || 'number-find';
this.setData({
functionId,
});
// 根据functionId设置页面标题
const titleMap: Record<string, string> = {
'number-find-color': '看数字,涂一涂',
'number-coloring': '数字涂色卡',
'counting-matching': '数一数,连一连',
'missing-number': '填上缺少的数字',
compare: '数一数,比大小',
'counting-select': '数一数,选一选',
'counting-fill': '数一数,填一填',
'addition-5': '5以内加法',
'addition-10': '10以内加法',
'addition-subtraction-10': '10以内加减法',
};
const functionItem = MATH_FUNCTION_TYPES.find(
(item: MathFunctionType) => item.id === functionId,
);
const pageTitle = titleMap[functionId] || '看数字,涂一涂';
const pageTitle = functionItem?.title || '看数字,涂一涂';
this.setData({
pageTitle,
});
@@ -91,6 +88,26 @@ Page({
this.canvas = canvasNode;
this.ctx = ctx;
// 初始化绘制服务
this.drawService = new NumberColorDraw(
canvasNode,
ctx,
{
title: this.data.pageTitle,
subTitle: '找一找下面相同的数字,涂上颜色',
functionId: this.data.functionId,
},
);
// 如果还没有选中数字,随机选择一个
if (this.data.selectedNumber === 0) {
const randomNumber =
Math.floor(Math.random() * 10) + 1;
this.setData({
selectedNumber: randomNumber,
});
}
// 绘制初始内容
this.drawCanvas();
}
@@ -103,34 +120,17 @@ Page({
/**
* 绘制Canvas内容
*/
drawCanvas() {
if (!this.ctx) {
async drawCanvas() {
if (!this.ctx || !this.drawService) {
return;
}
const ctx = this.ctx;
const width = this.boxWidth;
const height = this.boxHeight;
// 清空画布
ctx.clearRect(0, 0, width, height);
// 设置背景色
ctx.fillStyle = '#FFFFFF';
ctx.fillRect(0, 0, width, height);
// TODO: 根据选中的数字绘制内容
if (this.data.selectedNumber > 0) {
// 绘制示例:显示选中的数字
ctx.fillStyle = '#141414';
ctx.font = 'bold 120px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(
String(this.data.selectedNumber),
width / 2,
height / 2,
);
try {
await this.drawService.draw(this.data.selectedNumber);
} catch (error) {
console.error('绘制失败:', error);
}
}
},
@@ -43,6 +43,7 @@
<!-- 第三行:9、10和随机按钮 -->
<view class="number-row">
<view class="number-column">
<view
wx:for="{{numberList}}"
wx:key="index"
@@ -53,11 +54,17 @@
bind:tap="onSelectNumber">
<view class="number-text">{{number}}</view>
</view>
<view class="number-item random-button" bind:tap="onRandom">
<view class="random-icon">
<text class="toy-icon toy-icon-refresh"></text>
</view>
<view class="number-text">随机</view>
<view class="number-column">
<toy-button
style="width: 100%"
type="primary"
bind:click="onRandom"
width="100%"
icon="refresh"
icon-class-prefix="toy-icon">
随机
</toy-button>
</view>
</view>
</view>
@@ -0,0 +1,142 @@
import { PAPER_SIZE } from '../../constants/colors';
import { drawMathHeader, drawMathMiniHeader } from './mathHeaderDraw';
/**
* 基础数学绘制服务
* 包含Paper设置和Header绘制功能,可被其他绘制服务复用
*/
export class BaseMathDrawService {
headerType: PrintHeader = 'wechat';
canvas: WechatMiniprogram.Canvas;
ctx: RenderingContext;
options: Record<string, any>;
paperSize: PaperSize;
currentX: number;
currentY: number;
canvasWidth: number; // 逻辑像素宽度
canvasHeight: number; // 逻辑像素高度
constructor(
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, any>,
) {
options = options || {};
this.canvas = canvas;
this.ctx = ctx;
this.paperSize = 'A4';
this.options = {
appName: '涂鸦丫小程序',
appHint: '在玩耍中学习数学',
title: '看数字,涂一涂',
subTitle: '找一找下面相同的数字,涂上颜色',
...options,
};
this.currentX = 0;
this.currentY = 0;
this.canvasWidth = 0;
this.canvasHeight = 0;
this.setPrintConfig();
}
setPrintConfig() {
const printConfig = getApp().getPrintConfig();
this.headerType = printConfig.header;
this.options.appName = printConfig.appName;
}
/**
* 设置Paper(逻辑像素,尺寸除以3)
*/
setPaper() {
const { ctx, canvas } = this;
const { pixelRatio: dpr } = wx.getWindowInfo();
let { width, height } = PAPER_SIZE[this.paperSize];
console.log('width', width);
console.log('height', height);
console.log('dpr', dpr);
this.canvasWidth = width;
this.canvasHeight = height;
// 设置 canvas 为物理像素尺寸(用于高分辨率显示)
const physicalWidth = width * dpr;
const physicalHeight = height * dpr;
canvas.width = physicalWidth;
canvas.height = physicalHeight;
// 重置 transform 并 scale 到逻辑像素
ctx.setTransform(1, 0, 0, 1, 0, 0); // 重置 transform
ctx.scale(dpr, dpr); // scale 到逻辑像素,后续绘制都使用逻辑像素
this.clear();
ctx.fillStyle = '#fff';
// 使用逻辑像素尺寸填充
ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight);
}
/**
* 清除画布
*/
clear() {
const canvas = this.canvas;
this.ctx.clearRect(0, 0, canvas.width, canvas.height);
}
/**
* 绘制Header(逻辑像素,尺寸除以3)
*/
async drawHeader() {
this.currentX = 24;
this.currentY = 24;
await drawMathHeader({
canvas: this.canvas,
ctx: this.ctx,
headerType: this.headerType,
options: {
appName: this.options.appName || '涂鸦丫小程序',
appHint: this.options.appHint || '识字|识图|练字|打印',
title: this.options.title || '看数字,涂一涂',
subTitle:
this.options.subTitle || '找一找下面相同的数字,涂上颜色',
},
onHeaderDrawn: (currentY) => {
this.currentY = currentY;
},
});
}
/**
* 绘制迷你Header(逻辑像素,尺寸除以3)
*/
drawMiniHeader() {
drawMathMiniHeader({
ctx: this.ctx,
canvasWidth: this.canvasWidth,
options: {
appName: this.options.appName || '涂鸦丫小程序',
title: this.options.title || '看数字,涂一涂',
},
onHeaderDrawn: (currentY) => {
this.currentY = currentY;
},
});
}
/**
* 绘制分割线(逻辑像素,尺寸除以3)
*/
drawDivider() {
const { ctx, canvasWidth } = this;
const dividerY = this.currentY + 10; // 10px间距
ctx.strokeStyle = '#000';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(24, dividerY);
ctx.lineTo(canvasWidth - 24, dividerY);
ctx.stroke();
this.currentY = dividerY + 10; // 分割线下方10px间距
}
}
@@ -0,0 +1,129 @@
import { getMiniCodeImage, getImage } from '../../utils/index';
/**
* 绘制数学模块页眉的参数接口(尺寸除以3)
*/
interface DrawMathHeaderParams {
canvas: WechatMiniprogram.Canvas;
ctx: RenderingContext;
headerType: PrintHeader;
options: {
appName: string;
appHint: string;
title: string;
subTitle: string;
};
onHeaderDrawn?: (currentY: number) => void;
}
/**
* 绘制数学模块完整页眉(尺寸除以3)
*/
export async function drawMathHeader({
canvas,
ctx,
headerType,
options,
onHeaderDrawn,
}: DrawMathHeaderParams): Promise<void> {
const { appName, appHint, title, subTitle } = options;
let titleX = 108; // 约109.33
const titleY = 26; // 约26.67
const logoX = 24; // 约26.67
const logoY = 20; // 20
const logoWidth = 65; // 约66.67
const logoHeight = 65; // 约66.67
// 根据 headerType 绘制 Logo 或调整标题位置
switch (headerType) {
case 'LogoImage': {
const image = await getImage(
canvas,
'/assets/imgs/doodle-logo.png',
);
ctx.drawImage(image, logoX, logoY, logoWidth, logoHeight);
break;
}
case 'noLogoImage': {
titleX = 40;
break;
}
case 'minimal': {
titleX = 40;
break;
}
default: {
const image = await getMiniCodeImage(canvas);
ctx.drawImage(image, logoX, logoY, logoWidth, logoHeight);
break;
}
}
// 绘制应用名称(字体大小除以3
ctx.font = 'bold 22px "Microsoft Yahei"';
ctx.fillStyle = '#000';
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
ctx.fillText(appName, titleX, titleY);
// 绘制应用提示(字体大小除以3
ctx.font = '16px "Microsoft Yahei"';
ctx.fillStyle = '#666';
ctx.fillText(appHint, titleX, 66);
// 绘制标题(字体大小除以3
ctx.font = 'bold 22px "Microsoft Yahei"';
ctx.fillStyle = '#000';
ctx.fillText(title, 280, titleY);
// 绘制副标题(字体大小除以3
ctx.font = '16px "Microsoft Yahei"';
ctx.fillStyle = '#666';
ctx.fillText(subTitle, 280, 66);
// 调用回调函数
if (onHeaderDrawn) {
onHeaderDrawn(100);
}
}
/**
* 绘制数学模块迷你页眉的参数接口(尺寸除以3)
*/
interface DrawMathMiniHeaderParams {
// canvas: WechatMiniprogram.Canvas;
ctx: RenderingContext;
options: {
appName: string;
title: string;
};
canvasWidth: number; // 逻辑像素宽度(已除以3
onHeaderDrawn?: (currentY: number) => void;
}
/**
* 绘制数学模块迷你页眉(尺寸除以3)
*/
export function drawMathMiniHeader({
ctx,
options,
canvasWidth,
onHeaderDrawn,
}: DrawMathMiniHeaderParams): void {
const { appName, title } = options;
const titleY = 46;
const centerX = canvasWidth / 2;
// 字体大小除以3
ctx.font = 'bold 24px "Microsoft Yahei"'; // 64/3
ctx.fillStyle = '#000';
ctx.textAlign = 'center';
ctx.fillText(appName + ' ' + title, centerX, titleY);
// 调用回调函数,传递除以3后的currentY
if (onHeaderDrawn) {
onHeaderDrawn(64); // 约66.67
}
}
@@ -0,0 +1,76 @@
import { BaseMathDrawService } from './baseMathDraw';
import { drawNumberPreview } from './numberPreviewDraw';
import { drawNumberContent } from './numberContentDraw';
import { drawNumberWriteContent } from './numberWriteDraw';
/**
* 数字涂色绘制服务
* 组合使用基础绘制服务、预览区域绘制服务和内容区域绘制服务
*/
class NumberColorDraw extends BaseMathDrawService {
selectedNumber: number;
functionId: string; // 功能ID,用于判断绘制类型
constructor(
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, any>,
) {
super(canvas, ctx, options);
this.selectedNumber = 0;
this.functionId = options?.functionId || 'number-find';
}
async draw(selectedNumber: number) {
if (selectedNumber <= 0 || selectedNumber > 10) {
return;
}
this.setPrintConfig();
this.selectedNumber = selectedNumber;
this.clear();
this.setPaper();
// 绘制Header
if (this.headerType !== 'minimal') {
await this.drawHeader();
} else {
this.drawMiniHeader();
}
// 绘制预览区域
this.drawDivider();
await drawNumberPreview({
canvas: this.canvas,
ctx: this.ctx,
selectedNumber: this.selectedNumber,
canvasWidth: this.canvasWidth,
startY: this.currentY,
onDrawn: (currentY) => {
this.currentY = currentY;
},
});
// 绘制内容区域(根据 functionId 选择不同的绘制方法)
this.drawDivider();
if (this.functionId === 'number-write') {
// 书写类型:绘制书写行
drawNumberWriteContent({
ctx: this.ctx,
selectedNumber: this.selectedNumber,
canvasWidth: this.canvasWidth,
startY: this.currentY,
});
} else {
// 默认类型(number-find):绘制数字涂色内容
drawNumberContent({
ctx: this.ctx,
selectedNumber: this.selectedNumber,
canvasWidth: this.canvasWidth,
startY: this.currentY,
});
}
}
}
export default NumberColorDraw;
@@ -0,0 +1,78 @@
/**
* 绘制内容区域的参数接口
*/
interface DrawNumberContentParams {
ctx: RenderingContext;
selectedNumber: number;
canvasWidth: number; // 逻辑像素宽度(已除以3
startY: number; // 起始Y坐标
}
/**
* 绘制数字涂色内容区域服务
* 随机绘制1-10的数字,尺寸除以3
*/
export function drawNumberContent({
ctx,
selectedNumber,
canvasWidth,
startY,
}: DrawNumberContentParams): void {
const rows = 6;
const radius = 26; // 约26.67
const fontSize = 24; // 24
const startYPos = startY + 10 + radius; // 约47.33
const startX1 = 100 + radius; // 约112.22
const startX2 = 60 + radius; // 约75.56
const spaceWidthFirst = (canvasWidth - startX1 * 2) / (5 - 1);
const spaceWidthSecond = (canvasWidth - startX2 * 2) / (6 - 1);
const spaceHeight = 18; // 18
ctx.moveTo(startX1, startYPos);
for (let i = 0; i < rows; i++) {
const cols = i % 2 === 0 ? 5 : 6;
for (let j = 0; j < cols; j++) {
const y = startYPos + i * (spaceHeight + radius * 2);
const x =
i % 2 === 0
? startX1 + j * spaceWidthFirst
: startX2 + j * spaceWidthSecond;
// 随机生成1-10的数字,但确保包含选中的数字
let number: number;
if (i === 0 && j === 0) {
// 第一个位置固定为选中的数字
number = selectedNumber;
} else {
// 其他位置随机,但增加选中数字出现的概率
const random = Math.random();
if (random < 0.3) {
// 30%概率是选中的数字
number = selectedNumber;
} else {
// 70%概率是其他随机数字
number = Math.floor(Math.random() * 10) + 1;
}
}
// 绘制圆形背景(线宽除以3
ctx.fillStyle = '#fff';
ctx.strokeStyle = '#000';
ctx.lineWidth = 1; // 约1.33
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.closePath();
// 绘制数字(字体大小除以3
ctx.fillStyle = '#333';
ctx.font = `bold ${fontSize}px "Microsoft Yahei"`;
ctx.textBaseline = 'middle';
ctx.textAlign = 'center';
ctx.fillText(String(number), x, y, radius * 2);
}
}
}
@@ -0,0 +1,138 @@
import { getImage } from '../../utils/index';
/**
* 数字到英文单词的映射
*/
const NUMBER_WORDS: Record<number, string> = {
1: 'one',
2: 'two',
3: 'three',
4: 'four',
5: 'five',
6: 'six',
7: 'seven',
8: 'eight',
9: 'nine',
10: 'ten',
};
const NUMBER_COLORS: Record<number, string> = {
1: '#9FC558',
2: '#CDB984',
3: '#B393B8',
4: '#ED7E97',
5: '#53B5B9',
6: '#B3B3B3',
7: '#EE8B7A',
8: '#F6C644',
9: '#99C1F4',
10: '#F5D25C',
};
/**
* 绘制预览区域的参数接口
*/
interface DrawNumberPreviewParams {
canvas: WechatMiniprogram.Canvas;
ctx: RenderingContext;
selectedNumber: number;
canvasWidth: number; // 逻辑像素宽度(已除以3
startY: number; // 起始Y坐标
onDrawn?: (currentY: number) => void; // 绘制完成后的回调
}
/**
* 绘制数字预览区域服务
* 左侧:animal-number 图片
* 右侧:两个框,第一个框是finger图片,第二个框是英文单词
* 所有尺寸除以3
*/
export async function drawNumberPreview({
canvas,
ctx,
selectedNumber,
canvasWidth,
startY,
onDrawn,
}: DrawNumberPreviewParams): Promise<void> {
// 左侧 animal-number 图片位置
const animalImageWidth = 360;
const animalImageHeight = 236;
const leftMargin = 35;
const animalImageX = leftMargin;
const animalImageY = startY + 10;
// 加载并绘制 animal-number 图片
try {
const animalImagePath = `/mathPages/assets/animal-number/animal-number-${selectedNumber}.png`;
const animalImage = await getImage(canvas, animalImagePath);
ctx.drawImage(
animalImage,
animalImageX,
animalImageY,
animalImageWidth,
animalImageHeight,
);
} catch (error) {
console.error('加载animal-number图片失败:', error);
}
// 右侧两个框的位置(逻辑像素,除以3)
const boxWidth = 130;
const boxHeight = 100;
const rightMargin = 35;
const boxSpacing = 30;
const boxX = canvasWidth - rightMargin - boxWidth;
// 第一个框:finger 图片
const fingerBoxY = animalImageY;
ctx.strokeStyle = '#000';
ctx.lineWidth = 1;
ctx.strokeRect(boxX, fingerBoxY, boxWidth, boxHeight);
try {
const fingerImagePath = `/mathPages/assets/finger/finger-${selectedNumber}.png`;
const fingerImage = await getImage(canvas, fingerImagePath);
// 计算图片在框内的位置(居中显示)
const imagePadding = 10;
const imageWidth = boxWidth - imagePadding * 2;
const imageHeight = boxHeight - imagePadding * 2;
ctx.drawImage(
fingerImage,
boxX + imagePadding,
fingerBoxY + imagePadding,
imageWidth,
imageHeight,
);
} catch (error) {
console.error('加载finger图片失败:', error);
}
// 第二个框:英文单词
const wordBoxY = fingerBoxY + boxHeight + boxSpacing;
ctx.strokeRect(boxX, wordBoxY, boxWidth, boxHeight);
// 绘制英文单词(字体大小除以3
const word = NUMBER_WORDS[selectedNumber] || '';
const color = NUMBER_COLORS[selectedNumber] || '#000';
ctx.fillStyle = color;
// 推荐使用系统内常见的圆体或幼圆体字体,更适合儿童视觉风格
ctx.font =
'bold 32px "HarmonyOS Sans", "PingFang SC", "YouYuan", "Microsoft Yahei", sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(
word,
boxX + boxWidth / 2,
wordBoxY + boxHeight / 2,
boxWidth - 20,
);
// 更新 currentY 为预览区域底部
const finalY =
Math.max(animalImageY + animalImageHeight, wordBoxY + boxHeight) + 10;
if (onDrawn) {
onDrawn(finalY);
}
}
@@ -0,0 +1,99 @@
/**
* 绘制书写行内容区域的参数接口
*/
interface DrawNumberWriteContentParams {
ctx: RenderingContext;
selectedNumber: number;
canvasWidth: number; // 逻辑像素宽度
startY: number; // 起始Y坐标
}
/**
* 绘制数字书写内容区域服务
* 绘制三条书写行,每条行有三条线(上下实线,中间虚线)
* 第一行和第二行有数字(第一个黑色实线,后续灰色虚线),第三行空白
*/
export function drawNumberWriteContent({
ctx,
selectedNumber,
canvasWidth,
startY,
}: DrawNumberWriteContentParams): void {
const lineSpacing = 30; // 行间距(三条线之间的间距)
const rowHeight = lineSpacing * 2; // 每行的高度(两条实线之间的距离)
const rowMargin = 24; // 行与行之间的间距
const leftMargin = 24; // 左边距
const rightMargin = 24; // 右边距
const numberIndent = selectedNumber === 10 ? 12 : 24; // 数字缩进(避免贴边)
const numberSpacing = 90; // 数字之间的间距
const numberCount = 6; // 每行数字的数量(第一个实线 + 6个虚线)
const fontSize = 76; // 数字字体大小(调整为行高的83%,避免超出上下边线)
// 计算每行的起始Y坐标
const rowStartY = startY + 20; // 顶部间距
// 绘制三条书写行
for (let rowIndex = 0; rowIndex < 5; rowIndex++) {
const rowY = rowStartY + rowIndex * (rowHeight + rowMargin);
const topLineY = rowY;
const middleLineY = rowY + lineSpacing;
const bottomLineY = rowY + rowHeight;
// 绘制上实线(蓝色)
ctx.strokeStyle = '#4A90E2'; // 蓝色
ctx.lineWidth = 1;
ctx.setLineDash([]); // 实线
ctx.beginPath();
ctx.moveTo(leftMargin, topLineY);
ctx.lineTo(canvasWidth - rightMargin, topLineY);
ctx.stroke();
// 绘制中间虚线(橙色)
ctx.strokeStyle = '#FF8C42'; // 橙色
ctx.lineWidth = 1;
ctx.setLineDash([12, 12]); // 虚线模式
ctx.beginPath();
ctx.moveTo(leftMargin, middleLineY);
ctx.lineTo(canvasWidth - rightMargin, middleLineY);
ctx.stroke();
ctx.setLineDash([]); // 恢复实线模式
// 绘制下实线(蓝色)
ctx.strokeStyle = '#4A90E2'; // 蓝色
ctx.lineWidth = 1;
ctx.setLineDash([]); // 实线
ctx.beginPath();
ctx.moveTo(leftMargin, bottomLineY);
ctx.lineTo(canvasWidth - rightMargin, bottomLineY);
ctx.stroke();
// 第一行和第二行绘制数字
if (rowIndex < 3) {
const centerY = rowY + lineSpacing; // 数字中心Y坐标(中间虚线位置)
for (let i = 0; i < numberCount; i++) {
const numberX = leftMargin + numberIndent + i * numberSpacing;
if (i === 0) {
// 第一个数字:黑色实线
ctx.fillStyle = '#000000';
ctx.font = `bold ${fontSize}px "Microsoft Yahei"`;
ctx.textBaseline = 'middle';
ctx.textAlign = 'left';
ctx.fillText(String(selectedNumber), numberX, centerY);
} else {
// 后续数字:灰色虚线(使用灰色填充配合透明度模拟虚线效果)
const savedAlpha = ctx.globalAlpha;
ctx.globalAlpha = 0.6; // 设置透明度模拟虚线效果
ctx.fillStyle = '#999999'; // 灰色
ctx.font = `${fontSize}px "Microsoft Yahei"`;
ctx.textBaseline = 'middle';
ctx.textAlign = 'left';
ctx.fillText(String(selectedNumber), numberX, centerY);
ctx.globalAlpha = savedAlpha; // 恢复透明度
}
}
}
// 第三行不绘制数字(空白行)
}
}
+8 -1
View File
@@ -161,5 +161,12 @@ page {
font-size: 24rpx;
color: #666666;
line-height: 1.5;
white-space: pre-line;
view {
margin-bottom: 8rpx;
&:last-child {
margin-bottom: 0;
}
}
}
+5 -65
View File
@@ -1,68 +1,8 @@
import { MATH_FUNCTION_TYPES } from '../../constants/mathFunctions';
Page({
data: {
functionList: [
{
id: 'number-find-color',
page: 'numberColor',
title: '看数字,涂一涂',
desc: '给一个数字,找一找下面相同的数字,涂上颜色',
icon: '🔍',
},
{
id: 'number-coloring',
title: '数字涂色卡',
desc: '一边写数字、一边学数数,数字和卡通图片一一对应',
icon: '🎨',
},
{
id: 'counting-matching',
title: '数一数,连一连',
desc: '通过连线配对数字和对应的数量图形',
icon: '🔗',
},
{
id: 'missing-number',
title: '填上缺少的数字',
desc: '在数字序列中找出并填写缺失的数字',
icon: '❓',
},
{
id: 'compare',
title: '数一数,比大小',
desc: '通过数数比较两组物品的数量大小',
icon: '⚖️',
},
{
id: 'counting-select',
title: '数一数,选一选',
desc: '数出物品数量,从多个选项中选择正确答案',
icon: '✓',
},
{
id: 'counting-fill',
title: '数一数,填一填',
desc: '数出物品数量,填写对应的数字',
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: '±',
},
],
functionList: MATH_FUNCTION_TYPES,
},
onLoad() {
@@ -89,9 +29,9 @@ Page({
});
} else {
wx.showToast({
title: `即将打开:${functionItem.title}`,
title: `功能开发中,敬请期待`,
icon: 'none',
duration: 2000,
duration: 2500,
});
}
},
+4 -3
View File
@@ -32,9 +32,10 @@
<view class="hint-text">
<view class="hint-title">使用提示</view>
<view class="hint-desc">
1. 点击上方功能卡片选择要生成的涂鸦卡类型\n 2.
选择数字范围、图形类型等参数\n 3. 预览生成的涂鸦卡效果\n 4.
下载A4格式PNG图片,打印后使用
<view>1. 点击上方功能卡片选择要生成的涂鸦卡类型</view>
<view>2. 选择数字等参数</view>
<view>3. 预览生成的涂鸦卡效果</view>
<view>4. 下载A4格式PNG图片,打印后使用</view>
</view>
</view>
</view>
+7 -1
View File
@@ -99,6 +99,7 @@ interface DrawMiniHeaderParams {
appName: string;
title: string;
};
canvasWidth?: number; // 逻辑像素宽度(可选,如果提供则使用,否则从 canvas.width 计算)
onHeaderDrawn?: (currentY: number) => void; // 绘制完成后的回调,用于设置 currentY 和绘制分割线
}
@@ -109,15 +110,20 @@ export function drawMiniHeader({
canvas,
ctx,
options,
canvasWidth,
onHeaderDrawn,
}: DrawMiniHeaderParams): void {
const { appName, title } = options;
const titleY = 120;
// 如果提供了逻辑宽度,使用它;否则使用 canvas.width(假设 ctx 未 scale
const centerX =
canvasWidth !== undefined ? canvasWidth / 2 : canvas.width / 2;
ctx.font = 'bold 64px "Microsoft Yahei"';
ctx.fillStyle = '#000';
ctx.textAlign = 'center';
ctx.fillText(appName + ' ' + title, canvas.width / 2, titleY);
ctx.fillText(appName + ' ' + title, centerX, titleY);
// 调用回调函数,让调用者设置 currentY 并绘制分割线
if (onHeaderDrawn) {
+1 -1
View File
@@ -62,7 +62,7 @@ export function recordShareSuccess(): void {
*/
export function shouldShowShareGuide(): boolean {
// TODO: 开启分享引导
// return true;
return false;
// 如果今天已经分享过,不需要显示引导
if (hasSharedToday()) {
return false;
+1 -1
View File
@@ -26,7 +26,7 @@
{
"name": "mathPages/numberColor/numberColor",
"pathName": "mathPages/numberColor/numberColor",
"query": "id=number-find-color",
"query": "id=number-write",
"scene": null,
"launchMode": "default"
},