feat:添加埋点,createPage 重构

This commit is contained in:
R524809
2025-12-09 17:33:58 +08:00
parent e193369478
commit 2c32235568
10 changed files with 1249 additions and 1279 deletions
+1 -1
View File
@@ -60,7 +60,7 @@
"pagePath": "pages/mathIndex/mathIndex", "pagePath": "pages/mathIndex/mathIndex",
"iconPath": "assets/tabBar/icon-math.png", "iconPath": "assets/tabBar/icon-math.png",
"selectedIconPath": "assets/tabBar/icon-math-active.png", "selectedIconPath": "assets/tabBar/icon-math-active.png",
"text": "数" "text": "数感启蒙"
}, },
{ {
"pagePath": "pages/index/index", "pagePath": "pages/index/index",
+4 -4
View File
@@ -2,19 +2,19 @@ import config from './config/config';
const defaultPrintConfig: PrintConfig = config.printHeader as PrintConfig; const defaultPrintConfig: PrintConfig = config.printHeader as PrintConfig;
// app.ts // app.ts
App<IAppOption>({ App<IAppOption>({
globalData: { globalData: {
env: 'release', env: 'release',
printConfig: defaultPrintConfig printConfig: defaultPrintConfig,
}, },
onLaunch() { onLaunch() {
const accountInfo = wx.getAccountInfoSync(); const accountInfo = wx.getAccountInfoSync();
const env = accountInfo.miniProgram.envVersion || 'release'; const env = accountInfo.miniProgram.envVersion || 'release';
this.globalData.env = env; this.globalData.env = env;
if (env !== 'release') { if (env !== 'release') {
const printConfig = wx.getStorageSync('printConfig') || defaultPrintConfig; const printConfig =
wx.getStorageSync('printConfig') || defaultPrintConfig;
this.globalData.printConfig = printConfig; this.globalData.printConfig = printConfig;
} }
}, },
@@ -34,5 +34,5 @@ App<IAppOption>({
setPrintConfig(config: PrintConfig) { setPrintConfig(config: PrintConfig) {
this.globalData.printConfig = config; this.globalData.printConfig = config;
wx.setStorageSync('printConfig', config); wx.setStorageSync('printConfig', config);
} },
}); });
+212 -268
View File
@@ -1,286 +1,230 @@
import { PAPER_SIZE } from '../../constants/colors';
import { checkAndSaveImage } from '../../utils/saveImage';
import { shouldShowShareGuide } from '../../utils/shareGuide';
import AdditionDraw from '../service/additionDraw'; import AdditionDraw from '../service/additionDraw';
import { import { applyMathPageMixin, CanvasDataState } from '../common/mathPageMixin';
MATH_FUNCTION_TYPES,
MathFunctionType,
} from '../../constants/mathFunctions';
Page({ Page(
canvas: null as Canvas | null, applyMathPageMixin(
ctx: null as RenderingContext | null, {
boxHeight: 0, canvas: null as Canvas | null,
boxWidth: 0, ctx: null as RenderingContext | null,
drawService: null as AdditionDraw | null, boxHeight: 0,
calculationData: null as { boxWidth: 0,
problems: Array<{ drawService: null as AdditionDraw | null,
type: 'addition' | 'subtraction'; calculationData: null as {
left: number; problems: Array<{
right: number; type: 'addition' | 'subtraction';
result: number; left: number;
}>; right: number;
} | null, result: number;
}>;
} | null,
data: { data: {
pageTitle: '加减法计算', pageTitle: '加减法计算',
functionId: '', subTitle: '通过图形化方式学习加减法运算',
hasContent: false, functionId: '',
showShareDialog: false, hasContent: false,
currentType: 'addition-5', // 'addition-5', 'addition-10', 'subtraction-10', 'addition-subtraction-10' showShareDialog: false,
currentTypeName: '5以内加法', currentType: 'addition-5', // 'addition-5', 'addition-10', 'subtraction-10', 'addition-subtraction-10'
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以内加法', currentTypeName: '5以内加法',
}); typeActions: [
} else if (functionId === 'addition-10') { { name: '5以内加法', value: 'addition-5' },
this.setData({ { name: '10以内加法', value: 'addition-10' },
currentType: 'addition-10', { name: '10以内减法', value: 'subtraction-10' },
currentTypeName: '10以内加法', { name: '10以内加法', value: 'addition-subtraction-10' },
}); ],
} else if (functionId === 'subtraction-10') { imageType: 'twelve-animals', // 图片类型:twelve-animals 或 fruits
this.setData({ } as CanvasDataState & {
currentType: 'subtraction-10', currentType: string;
currentTypeName: '10以内减法', currentTypeName: string;
}); typeActions: Array<{ name: string; value: string }>;
} else if (functionId === 'addition-subtraction-10') { imageType: string;
this.setData({ },
currentType: 'addition-subtraction-10',
currentTypeName: '10以内加减法',
});
}
},
onReady() { onLoad(options: { id?: string }) {
this.initCanvas(); const functionId = options.id || 'addition-5';
}, this.initPageInfo(functionId, '加减法计算');
/** // 根据 functionId 设置默认类型
* 初始化Canvas if (functionId === 'addition-5') {
*/ this.setData({
initCanvas() { currentType: 'addition-5',
const query = wx.createSelectorQuery(); currentTypeName: '5以内加法',
query });
.select('#canvasWrapper') } else if (functionId === 'addition-10') {
.boundingClientRect((rect) => { this.setData({
if (rect) { currentType: 'addition-10',
const { width, height } = PAPER_SIZE['A4']; currentTypeName: '10以内加法',
const boxWidth = rect.width; });
const boxHeight = boxWidth / (width / height); } else if (functionId === 'subtraction-10') {
this.setData({
this.boxHeight = boxHeight; currentType: 'subtraction-10',
this.boxWidth = boxWidth; currentTypeName: '10以内减法',
});
this.setData({ boxWidth, boxHeight }); } else if (functionId === 'addition-subtraction-10') {
this.setData({
const canvas = wx currentType: 'addition-subtraction-10',
.createSelectorQuery() currentTypeName: '10以内加减法',
.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();
},
/** onReady() {
* 绘制Canvas内容 this.initCanvas({
*/ createDrawService: (
async drawCanvas() { canvas: Canvas,
if (!this.ctx || !this.drawService || !this.calculationData) { ctx: RenderingContext,
return; options?: Record<string, any>,
} ) => {
return new AdditionDraw(canvas, ctx, options);
},
drawServiceOptions: {
subTitle: this.data.subTitle,
},
onCanvasReady: () => {
// 初始随机生成
this.onRandom();
},
});
},
try { /**
// 更新 Header 的 Title * 绘制Canvas内容
if (this.drawService) { */
this.drawService.options.title = this.data.currentTypeName; async drawCanvas() {
} if (!this.ctx || !this.drawService || !this.calculationData) {
return;
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 }; try {
this.drawCanvas(); // 更新 Header 的 Title
}, if (this.drawService) {
this.drawService.options.title =
this.data.currentTypeName;
}
/** await this.drawService.draw(
* 导出打印 this.calculationData,
*/ this.data.currentType,
exportToPrint() { );
if (!this.canvas || !this.data.hasContent) { this.setData({ hasContent: true });
return; } catch (error) {
} console.error('绘制失败:', error);
this.setData({ hasContent: false });
}
},
if (shouldShowShareGuide()) { /**
this.setData({ showShareDialog: true }); * 随机生成
return; */
} onRandom() {
const problems: Array<{
type: 'addition' | 'subtraction';
left: number;
right: number;
result: number;
}> = [];
checkAndSaveImage(this.canvas); const type = this.data.currentType;
},
/** // 生成5道题目
* 分享小程序 for (let i = 0; i < 5; i++) {
*/ if (type === 'addition-5') {
onShareAppMessage() { // 5以内加法:和 ≤ 5
return { // left >= 1, right >= 1, left + right <= 5
title: '涂鸦丫-数学学习涂鸦卡', const maxSum = 5;
path: `/mathPages/addition/addition?id=${this.data.functionId}`, const left =
imageUrl: Math.floor(Math.random() * (maxSum - 1)) + 1; // 1 到 4
'https://cdn.joeyone.cn/doodle/share-img/index-share-v2.png', 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,
});
}
}
}
onShareTimeline() { this.calculationData = { problems };
return { this.drawCanvas();
title: '涂鸦丫-数学学习涂鸦卡', },
query: `id=${this.data.functionId}`,
imageUrl:
'https://cdn.joeyone.cn/doodle/share-img/index-share-v2.png',
};
},
/** 关闭分享引导弹窗 */ /** 选择类型 */
onCloseShareDialog() { onSelectType(event: any) {
this.setData({ showShareDialog: false }); const { name, value } = event.detail;
}, this.setData({
currentType: value,
/** 分享成功回调 */ currentTypeName: name,
onShareSuccess() { });
this.setData({ showShareDialog: false }); // 重新生成数据
if (this.canvas) { this.onRandom();
checkAndSaveImage(this.canvas); },
} },
}, {
pagePath: 'addition/addition',
/** 选择类型 */ },
onSelectType(event: any) { ),
const { name, value } = event.detail; );
this.setData({
currentType: value,
currentTypeName: name,
});
// 重新生成数据
this.onRandom();
},
});
+62 -8
View File
@@ -6,6 +6,7 @@ import {
MATH_FUNCTION_TYPES, MATH_FUNCTION_TYPES,
MathFunctionType, MathFunctionType,
} from '../../constants/mathFunctions'; } from '../../constants/mathFunctions';
import tracker from '../../utils/tracker';
/** /**
* Canvas 相关的页面实例属性 * Canvas 相关的页面实例属性
@@ -16,6 +17,7 @@ export interface MathPageCanvasInstance {
boxHeight: number; boxHeight: number;
boxWidth: number; boxWidth: number;
drawService: BaseMathDrawService | null; drawService: BaseMathDrawService | null;
setData(data: any, callback?: () => void): void;
} }
/** /**
@@ -63,11 +65,16 @@ export interface ShareOptions {
pagePath: string; pagePath: string;
} }
const shareConfig = {
title: '涂鸦丫-数学学习涂鸦卡',
imageUrl: 'https://cdn.joeyone.cn/doodle/share-img/index-share-v2.png',
};
/** /**
* 获取数学页面的公共方法 * 获取数学页面的公共方法
* 这些方法可以在所有数学页面中复用 * 这些方法可以在所有数学页面中复用
*/ */
export function getMathPageCommonMethods(shareOptions: ShareOptions) { export function getMathPageCommonMethods() {
return { return {
/** /**
* 初始化 Canvas * 初始化 Canvas
@@ -76,6 +83,7 @@ export function getMathPageCommonMethods(shareOptions: ShareOptions) {
this: MathPageCanvasInstance & { data: CanvasDataState }, this: MathPageCanvasInstance & { data: CanvasDataState },
options: InitCanvasOptions, options: InitCanvasOptions,
) { ) {
console.log('initCanvas this', this);
const query = wx.createSelectorQuery(); const query = wx.createSelectorQuery();
query query
.select('#canvasWrapper') .select('#canvasWrapper')
@@ -145,6 +153,9 @@ export function getMathPageCommonMethods(shareOptions: ShareOptions) {
return; return;
} }
// 上报下载埋点
tracker.reportDownload(this.data.pageTitle);
checkAndSaveImage(this.canvas); checkAndSaveImage(this.canvas);
}, },
@@ -154,11 +165,13 @@ export function getMathPageCommonMethods(shareOptions: ShareOptions) {
onShareAppMessage( onShareAppMessage(
this: MathPageCanvasInstance & { data: CanvasDataState }, this: MathPageCanvasInstance & { data: CanvasDataState },
) { ) {
console.log('onShareAppMessage');
// 上报分享埋点
tracker.reportShare(this.data.pageTitle);
return { return {
title: '涂鸦丫-数学学习涂鸦卡', ...shareConfig,
path: `/mathPages/${shareOptions.pagePath}?id=${this.data.functionId}`, query: `id=${this.data.functionId}`,
imageUrl:
'https://cdn.joeyone.cn/doodle/share-img/index-share-v2.png',
}; };
}, },
@@ -168,11 +181,13 @@ export function getMathPageCommonMethods(shareOptions: ShareOptions) {
onShareTimeline( onShareTimeline(
this: MathPageCanvasInstance & { data: CanvasDataState }, this: MathPageCanvasInstance & { data: CanvasDataState },
) { ) {
console.log('onShareTimeline');
// 上报分享埋点
tracker.reportShare(this.data.pageTitle);
return { return {
title: '涂鸦丫-数学学习涂鸦卡', ...shareConfig,
query: `id=${this.data.functionId}`, query: `id=${this.data.functionId}`,
imageUrl:
'https://cdn.joeyone.cn/doodle/share-img/index-share-v2.png',
}; };
}, },
@@ -193,6 +208,8 @@ export function getMathPageCommonMethods(shareOptions: ShareOptions) {
) { ) {
this.setData({ showShareDialog: false }); this.setData({ showShareDialog: false });
if (this.canvas) { if (this.canvas) {
// 上报下载埋点(分享成功后下载)
tracker.reportDownload(this.data.pageTitle);
checkAndSaveImage(this.canvas); checkAndSaveImage(this.canvas);
} }
}, },
@@ -222,3 +239,40 @@ export function getMathPageCommonMethods(shareOptions: ShareOptions) {
}, },
}; };
} }
/**
* 应用数学页面公共方法的辅助函数
* 自动混入公共方法,简化页面代码
* @param pageOptions 页面选项
* @param shareOptions 分享配置选项
* @returns 合并后的页面选项
*/
export function applyMathPageMixin(pageOptions: any) {
const commonMethods = getMathPageCommonMethods();
// 提取 pageOptions 中的 data(如果有)
const pageData = pageOptions.data || {};
// 合并页面选项和公共方法
// 注意:pageOptions 放在后面,这样页面可以覆盖公共方法
const mergedOptions: any = {
...commonMethods,
...pageOptions,
// 处理 data 的合并(需要特殊处理,避免覆盖)
data: {
...pageData,
},
};
return mergedOptions;
}
/**
* 创建数学页面的便捷函数
* 自动应用公共方法并注册为页面
* 页面路径从函数调用栈自动推断(从调用文件路径提取)
* @param pageOptions 页面选项
*/
export function createMathPage(pageOptions: any) {
Page(applyMathPageMixin(pageOptions));
}
+175 -254
View File
@@ -1,279 +1,200 @@
import { PAPER_SIZE } from '../../constants/colors';
import { checkAndSaveImage } from '../../utils/saveImage';
import { shouldShowShareGuide } from '../../utils/shareGuide';
import CountMatchDraw from '../service/countMatchDraw'; import CountMatchDraw from '../service/countMatchDraw';
import NumberColorDraw from '../service/numberColorDraw'; import NumberColorDraw from '../service/numberColorDraw';
import { import { applyMathPageMixin, CanvasDataState } from '../common/mathPageMixin';
MATH_FUNCTION_TYPES,
MathFunctionType,
} from '../../constants/mathFunctions';
Page({ Page(
canvas: null as Canvas | null, applyMathPageMixin(
ctx: null as RenderingContext | null, {
boxHeight: 0, canvas: null as Canvas | null,
boxWidth: 0, ctx: null as RenderingContext | null,
drawService: null as CountMatchDraw | NumberColorDraw | null, boxHeight: 0,
matchData: null as { boxWidth: 0,
leftNumbers: number[]; drawService: null as CountMatchDraw | NumberColorDraw | null,
rightNumbers: number[]; matchData: null as {
} | null, leftNumbers: number[];
colorData: null as { rightNumbers: number[];
numbers: number[]; } | null,
} | null, colorData: null as {
numbers: number[];
} | null,
data: { data: {
pageTitle: '数一数,连一连', pageTitle: '数一数,连一连',
functionId: '', functionId: '',
hasContent: false, hasContent: false,
showShareDialog: false, showShareDialog: false,
showTypeSelector: true, // 控制是否显示类型选择器 showTypeSelector: true, // 控制是否显示类型选择器
currentType: 'twelve-animals', // 'twelve-animals' 或 'fruits' 或 'circle' 或 'caterpillar' currentType: 'twelve-animals', // 'twelve-animals' 或 'fruits' 或 'circle' 或 'caterpillar'
currentTypeName: '十二生肖', currentTypeName: '十二生肖',
typeActions: [
{ name: '十二生肖', value: 'twelve-animals' },
{ name: '水果', value: 'fruits' },
],
},
onLoad(options: { id?: string }) {
const functionId = options.id || 'counting-matching';
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 === 'number-coloring') {
this.setData({
currentType: 'caterpillar',
currentTypeName: '毛毛虫',
typeActions: [ typeActions: [
{ name: '毛毛虫', value: 'caterpillar' }, { name: '十二生肖', value: 'twelve-animals' },
{ name: '圆圈', value: 'circle' }, { name: '水果', value: 'fruits' },
], ],
}); } as CanvasDataState & {
} showTypeSelector: boolean;
}, currentType: string;
currentTypeName: string;
typeActions: Array<{ name: string; value: string }>;
},
onReady() { onLoad(options: { id?: string }) {
this.initCanvas(); const functionId = options.id || 'counting-matching';
}, this.setData({ functionId });
/** const functionItem =
* 初始化Canvas require('../../constants/mathFunctions').MATH_FUNCTION_TYPES.find(
*/ (item: any) => item.id === functionId,
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; const pageTitle = functionItem?.title || '数一数,连一连';
this.boxWidth = boxWidth; this.setData({ pageTitle });
this.initPageInfo(functionId, pageTitle);
this.setData({ boxWidth, boxHeight }); // 根据 functionId 设置不同的类型选择器
if (functionId === 'number-coloring') {
const canvas = wx this.setData({
.createSelectorQuery() currentType: 'caterpillar',
.select('#canvasContent'); currentTypeName: '毛毛虫',
canvas.fields({ node: true, size: true }).exec((res) => { typeActions: [
if (res[0]) { { name: '毛毛虫', value: 'caterpillar' },
const canvasNode = res[0].node; { name: '圆圈', value: 'circle' },
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;
// 根据 functionId 创建不同的绘制服务
if (this.data.functionId === 'number-coloring') {
this.drawService = new NumberColorDraw(
canvasNode,
ctx,
{
title: this.data.pageTitle,
subTitle: '按数字给相应的圆圈涂上颜色',
},
);
} else {
this.drawService = new CountMatchDraw(
canvasNode,
ctx,
{
title: this.data.pageTitle,
subTitle:
'通过连线配对数字和对应的数量图形',
},
);
}
// 初始随机生成
this.onRandom();
}
}); });
} }
}) },
.exec();
},
/** onReady() {
* 绘制Canvas内容 this.initCanvas({
*/ createDrawService: (
async drawCanvas() { canvas: Canvas,
if (!this.ctx || !this.drawService) { ctx: RenderingContext,
return; options?: Record<string, any>,
} ) => {
// 根据 functionId 创建不同的绘制服务
if (this.data.functionId === 'number-coloring') {
return new NumberColorDraw(canvas, ctx, options);
} else {
return new CountMatchDraw(canvas, ctx, options);
}
},
drawServiceOptions: {
subTitle:
this.data.functionId === 'number-coloring'
? '按数字给相应的圆圈涂上颜色'
: '通过连线配对数字和对应的数量图形',
},
onCanvasReady: () => {
// 初始随机生成
this.onRandom();
},
});
},
try { /**
if (this.data.functionId === 'number-coloring') { * 绘制Canvas内容
if (!this.colorData) { */
async drawCanvas() {
if (!this.ctx || !this.drawService) {
return; return;
} }
await (this.drawService as NumberColorDraw).draw(
this.colorData, try {
this.data.currentType, if (this.data.functionId === 'number-coloring') {
); if (!this.colorData) {
} else { return;
if (!this.matchData) { }
return; await (this.drawService as NumberColorDraw).draw(
this.colorData,
this.data.currentType,
);
} else {
if (!this.matchData) {
return;
}
await (this.drawService as CountMatchDraw).draw(
this.matchData,
this.data.currentType,
);
}
this.setData({ hasContent: true });
} catch (error) {
console.error('绘制失败:', error);
this.setData({ hasContent: false });
} }
await (this.drawService as CountMatchDraw).draw( },
this.matchData,
this.data.currentType,
);
}
this.setData({ hasContent: true });
} catch (error) {
console.error('绘制失败:', error);
this.setData({ hasContent: false });
}
},
/** /**
* 随机生成 * 随机生成
*/ */
onRandom() { onRandom() {
if (this.data.functionId === 'number-coloring') { if (this.data.functionId === 'number-coloring') {
// 按数字涂颜色模式:生成6个随机数字(1-10) // 按数字涂颜色模式:生成6个随机数字(1-10)
const availableNumbers = Array.from( const availableNumbers = Array.from(
{ length: 10 }, { length: 10 },
(_, i) => i + 1, (_, i) => i + 1,
); );
const numbers: number[] = []; const numbers: number[] = [];
for (let i = 0; i < 6; i++) { for (let i = 0; i < 6; i++) {
const randomIndex = Math.floor( const randomIndex = Math.floor(
Math.random() * availableNumbers.length, Math.random() * availableNumbers.length,
); );
const number = availableNumbers.splice(randomIndex, 1)[0]; const number = availableNumbers.splice(
numbers.push(number); randomIndex,
} 1,
)[0];
numbers.push(number);
}
this.colorData = { numbers }; this.colorData = { numbers };
} else { } else {
// 数一数连一连模式:生成5个不同的数字(1-10) // 数一数连一连模式:生成5个不同的数字(1-10)
const availableNumbers = Array.from( const availableNumbers = Array.from(
{ length: 10 }, { length: 10 },
(_, i) => i + 1, (_, i) => i + 1,
); );
const leftNumbers: number[] = []; const leftNumbers: number[] = [];
for (let i = 0; i < 5; i++) { for (let i = 0; i < 5; i++) {
const randomIndex = Math.floor( const randomIndex = Math.floor(
Math.random() * availableNumbers.length, Math.random() * availableNumbers.length,
); );
const number = availableNumbers.splice(randomIndex, 1)[0]; const number = availableNumbers.splice(
leftNumbers.push(number); randomIndex,
} 1,
)[0];
leftNumbers.push(number);
}
// 复制数字数组并打乱顺序,作为右侧显示的数字 // 复制数字数组并打乱顺序,作为右侧显示的数字
const rightNumbers = [...leftNumbers]; const rightNumbers = [...leftNumbers];
for (let i = rightNumbers.length - 1; i > 0; i--) { for (let i = rightNumbers.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1)); const j = Math.floor(Math.random() * (i + 1));
[rightNumbers[i], rightNumbers[j]] = [ [rightNumbers[i], rightNumbers[j]] = [
rightNumbers[j], rightNumbers[j],
rightNumbers[i], rightNumbers[i],
]; ];
} }
this.matchData = { leftNumbers, rightNumbers }; this.matchData = { leftNumbers, rightNumbers };
} }
this.drawCanvas(); this.drawCanvas();
}, },
/** /** 选择类型 */
* 导出打印 onSelectType(event: any) {
*/ const { name, value } = event.detail;
exportToPrint() { this.setData({
if (!this.canvas || !this.data.hasContent) { currentType: value,
return; currentTypeName: name,
} });
// 重新生成数据
if (shouldShowShareGuide()) { this.onRandom();
this.setData({ showShareDialog: true }); },
return; },
} {
pagePath: 'countMatch/countMatch',
checkAndSaveImage(this.canvas); },
}, ),
);
/**
* 分享小程序
*/
onShareAppMessage() {
return {
title: '涂鸦丫-数学学习涂鸦卡',
path: `/mathPages/countMatch/countMatch?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);
}
},
/** 选择类型 */
onSelectType(event: any) {
const { name, value } = event.detail;
this.setData({
currentType: value,
currentTypeName: name,
});
// 重新生成数据
this.onRandom();
},
});
@@ -1,161 +1,168 @@
import CountingSelectDraw from '../service/countingSelectDraw'; import CountingSelectDraw from '../service/countingSelectDraw';
import { import { applyMathPageMixin, CanvasDataState } from '../common/mathPageMixin';
getMathPageCommonMethods,
CanvasDataState,
} from '../common/mathPageMixin';
// 获取公共方法 Page(
const commonMethods = getMathPageCommonMethods({ applyMathPageMixin(
pagePath: 'countingSelect/countingSelect', {
}); canvas: null as Canvas | null,
ctx: null as RenderingContext | null,
boxHeight: 0,
boxWidth: 0,
drawService: null as CountingSelectDraw | null,
countingSelectData: null as {
problems: Array<{
count: number; // 图片数量(正确答案)
imageIndex: number; // 图片索引
imageType: 'fruits' | 'twelve-animals'; // 图片类型
options?: number[]; // 三个数字选项(选一选模式需要)
correctIndex?: number; // 正确答案在options中的索引(选一选模式需要)
}>;
} | null,
Page({ data: {
canvas: null as Canvas | null, pageTitle: '数一数,选一选',
ctx: null as RenderingContext | null, subTitle: '数出物品数量,从多个选项中选择正确答案',
boxHeight: 0, functionId: '',
boxWidth: 0, hasContent: false,
drawService: null as CountingSelectDraw | null, showShareDialog: false,
countingSelectData: null as { } as CanvasDataState & {
problems: Array<{ showTypeSelector?: boolean;
count: number; // 图片数量(正确答案) currentType?: string;
imageIndex: number; // 图片索引 currentTypeName?: string;
imageType: 'fruits' | 'twelve-animals'; // 图片类型 typeActions?: Array<{ name: string; value: any }>;
options?: number[]; // 三个数字选项(选一选模式需要)
correctIndex?: number; // 正确答案在options中的索引(选一选模式需要)
}>;
} | null,
data: {
pageTitle: '数一数,选一选',
subTitle: '数出物品数量,从多个选项中选择正确答案',
functionId: '',
hasContent: false,
showShareDialog: false,
} as CanvasDataState,
onLoad(options: { id?: string }) {
const functionId = options.id || 'counting-select';
// 初始化页面信息(会从 mathFunctions.ts 读取 title 和 desc
this.initPageInfo(functionId, '数一数,选一选');
},
onReady() {
this.initCanvas({
createDrawService: (canvas, ctx, options) => {
return new CountingSelectDraw(canvas, ctx, options);
}, },
drawServiceOptions: {
subTitle: this.data.subTitle, onLoad(options: { id?: string }) {
const functionId = options.id || 'counting-select';
// 初始化页面信息(会从 mathFunctions.ts 读取 title 和 desc
this.initPageInfo(functionId, '数一数,选一选');
}, },
onCanvasReady: () => {
// Canvas 初始化完成后,生成初始数据 onReady() {
this.onRandom(); this.initCanvas({
createDrawService: (
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, any>,
) => {
return new CountingSelectDraw(canvas, ctx, options);
},
drawServiceOptions: {
subTitle: this.data.subTitle,
},
onCanvasReady: () => {
// Canvas 初始化完成后,生成初始数据
this.onRandom();
},
});
}, },
});
},
/** /**
* 绘制Canvas内容 * 绘制Canvas内容
*/ */
async drawCanvas() { async drawCanvas() {
if (!this.ctx || !this.drawService || !this.countingSelectData) { if (
return; !this.ctx ||
} !this.drawService ||
!this.countingSelectData
try { ) {
// 判断是选一选还是填一填模式 return;
const mode =
this.data.functionId === 'counting-fill' ? 'fill' : 'select';
await this.drawService.draw(this.countingSelectData, mode);
this.setData({ hasContent: true });
} catch (error) {
console.error('绘制失败:', error);
this.setData({ hasContent: false });
}
},
/**
* 随机生成
*/
onRandom() {
this.generateCountingSelectData();
this.drawCanvas();
},
/**
* 生成数一数选一选/填一填数据
*/
generateCountingSelectData() {
const isFillMode = this.data.functionId === 'counting-fill';
const problems: Array<{
count: number;
imageIndex: number;
imageType: 'fruits' | 'twelve-animals';
options?: number[];
correctIndex?: number;
}> = [];
// 生成9道题目
for (let i = 0; i < 9; i++) {
// 随机选择图片类型
const imageType: 'fruits' | 'twelve-animals' =
Math.random() < 0.5 ? 'fruits' : 'twelve-animals';
// 根据图片类型确定最大索引
const maxImageIndex = imageType === 'fruits' ? 22 : 12;
// 生成图片数量(1-10
const count = Math.floor(Math.random() * 10) + 1;
// 随机选择图片索引
const imageIndex = Math.floor(Math.random() * maxImageIndex) + 1;
const problem: {
count: number;
imageIndex: number;
imageType: 'fruits' | 'twelve-animals';
options?: number[];
correctIndex?: number;
} = {
count,
imageIndex,
imageType,
};
// 选一选模式:生成三个选项
if (!isFillMode) {
const options: number[] = [];
const correctIndex = Math.floor(Math.random() * 3);
for (let j = 0; j < 3; j++) {
if (j === correctIndex) {
options.push(count); // 正确答案
} else {
// 生成错误答案(与正确答案不同)
let wrongAnswer: number;
do {
wrongAnswer = Math.floor(Math.random() * 10) + 1;
} while (wrongAnswer === count);
options.push(wrongAnswer);
}
} }
problem.options = options; try {
problem.correctIndex = correctIndex; // 判断是选一选还是填一填模式
} const mode =
this.data.functionId === 'counting-fill'
? 'fill'
: 'select';
await this.drawService.draw(this.countingSelectData, mode);
this.setData({ hasContent: true });
} catch (error) {
console.error('绘制失败:', error);
this.setData({ hasContent: false });
}
},
problems.push(problem); /**
} * 随机生成
*/
onRandom() {
this.generateCountingSelectData();
this.drawCanvas();
},
this.countingSelectData = { problems }; /**
}, * 生成数一数选一选/填一填数据
*/
generateCountingSelectData() {
const isFillMode = this.data.functionId === 'counting-fill';
const problems: Array<{
count: number;
imageIndex: number;
imageType: 'fruits' | 'twelve-animals';
options?: number[];
correctIndex?: number;
}> = [];
// ========== 使用公共方法 ========== // 生成9道题目
initCanvas: commonMethods.initCanvas, for (let i = 0; i < 9; i++) {
exportToPrint: commonMethods.exportToPrint, // 随机选择图片类型
onShareAppMessage: commonMethods.onShareAppMessage, const imageType: 'fruits' | 'twelve-animals' =
onShareTimeline: commonMethods.onShareTimeline, Math.random() < 0.5 ? 'fruits' : 'twelve-animals';
onCloseShareDialog: commonMethods.onCloseShareDialog,
onShareSuccess: commonMethods.onShareSuccess, // 根据图片类型确定最大索引
initPageInfo: commonMethods.initPageInfo, const maxImageIndex = imageType === 'fruits' ? 22 : 12;
});
// 生成图片数量(1-10
const count = Math.floor(Math.random() * 10) + 1;
// 随机选择图片索引
const imageIndex =
Math.floor(Math.random() * maxImageIndex) + 1;
const problem: {
count: number;
imageIndex: number;
imageType: 'fruits' | 'twelve-animals';
options?: number[];
correctIndex?: number;
} = {
count,
imageIndex,
imageType,
};
// 选一选模式:生成三个选项
if (!isFillMode) {
const options: number[] = [];
const correctIndex = Math.floor(Math.random() * 3);
for (let j = 0; j < 3; j++) {
if (j === correctIndex) {
options.push(count); // 正确答案
} else {
// 生成错误答案(与正确答案不同)
let wrongAnswer: number;
do {
wrongAnswer =
Math.floor(Math.random() * 10) + 1;
} while (wrongAnswer === count);
options.push(wrongAnswer);
}
}
problem.options = options;
problem.correctIndex = correctIndex;
}
problems.push(problem);
}
this.countingSelectData = { problems };
},
},
{
pagePath: 'countingSelect/countingSelect',
},
),
);
@@ -1,203 +1,198 @@
import { getRandomNumberColor } from '../../constants/colors'; import { getRandomNumberColor } from '../../constants/colors';
import MissingNumberDraw from '../service/missingNumberDraw'; import MissingNumberDraw from '../service/missingNumberDraw';
import { import { applyMathPageMixin, CanvasDataState } from '../common/mathPageMixin';
getMathPageCommonMethods,
CanvasDataState,
} from '../common/mathPageMixin';
// 获取公共方法 Page(
const commonMethods = getMathPageCommonMethods({ applyMathPageMixin(
pagePath: 'missingNumber/missingNumber', {
}); canvas: null as Canvas | null,
ctx: null as RenderingContext | null,
boxHeight: 0,
boxWidth: 0,
drawService: null as MissingNumberDraw | null,
missingNumberData: null as {
grids: Array<{
numbers: (number | null)[];
colors: (string | null)[];
}>;
maxNumber: number;
} | null,
Page({ data: {
canvas: null as Canvas | null, pageTitle: '填上缺少的数字',
ctx: null as RenderingContext | null, subTitle: '在数字序列中找出并填写缺失的数字',
boxHeight: 0, functionId: '',
boxWidth: 0, hasContent: false,
drawService: null as MissingNumberDraw | null, showShareDialog: false,
missingNumberData: null as { showTypeSelector: true,
grids: Array<{ currentType: 10,
numbers: (number | null)[]; currentTypeName: '10以内',
colors: (string | null)[]; typeActions: [
}>; { name: '10以内', value: 10 },
maxNumber: number; { name: '20以内', value: 20 },
} | null, { name: '40以内', value: 40 },
{ name: '50以内', value: 50 },
data: { { name: '80以内', value: 80 },
pageTitle: '填上缺少的数字', { name: '100以内', value: 100 },
subTitle: '在数字序列中找出并填写缺失的数字', { name: '120以内', value: 120 },
functionId: '', ],
hasContent: false, } as CanvasDataState & {
showShareDialog: false, showTypeSelector: boolean;
showTypeSelector: true, currentType: number;
currentType: 10, currentTypeName: string;
currentTypeName: '10以内', typeActions: Array<{ name: string; value: number }>;
typeActions: [
{ name: '10以内', value: 10 },
{ name: '20以内', value: 20 },
{ name: '40以内', value: 40 },
{ name: '50以内', value: 50 },
{ name: '80以内', value: 80 },
{ name: '100以内', value: 100 },
{ name: '120以内', value: 120 },
],
} as CanvasDataState & {
showTypeSelector: boolean;
currentType: number;
currentTypeName: string;
typeActions: Array<{ name: string; value: number }>;
},
onLoad(options: { id?: string }) {
const functionId = options.id || 'missing-number';
this.initPageInfo(functionId, '填上缺少的数字');
},
onReady() {
this.initCanvas({
createDrawService: (canvas, ctx, options) => {
return new MissingNumberDraw(canvas, ctx, options);
}, },
drawServiceOptions: {
subTitle: this.data.subTitle, onLoad(options: { id?: string }) {
const functionId = options.id || 'missing-number';
this.initPageInfo(functionId, '填上缺少的数字');
}, },
onCanvasReady: () => {
// Canvas 初始化完成后,生成初始数据 onReady() {
this.initCanvas({
createDrawService: (
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, any>,
) => {
return new MissingNumberDraw(canvas, ctx, options);
},
drawServiceOptions: {
subTitle: this.data.subTitle,
},
onCanvasReady: () => {
// Canvas 初始化完成后,生成初始数据
this.onRandom();
},
});
},
/**
* 绘制Canvas内容
*/
async drawCanvas() {
if (!this.ctx || !this.drawService || !this.missingNumberData) {
return;
}
try {
await this.drawService.draw(
this.missingNumberData,
String(this.data.currentType),
);
this.setData({ hasContent: true });
} catch (error) {
console.error('绘制失败:', error);
this.setData({ hasContent: false });
}
},
/**
* 随机生成
*/
onRandom() {
const maxNumber = this.data.currentType;
this.generateMissingNumberData(maxNumber);
this.drawCanvas();
},
/**
* 生成缺失数字数据
*/
generateMissingNumberData(maxNumber: number) {
const grids: Array<{
numbers: (number | null)[];
colors: (string | null)[];
}> = [];
const gridMap = {
10: {
gridCount: 3,
},
20: {
gridCount: 2,
},
40: {
gridCount: 2,
},
50: {
gridCount: 2,
},
80: {
gridCount: 1,
},
100: {
gridCount: 1,
},
120: {
gridCount: 1,
},
};
const { gridCount } =
gridMap[maxNumber as keyof typeof gridMap];
// 生成多个网格
for (let gridIndex = 0; gridIndex < gridCount; gridIndex++) {
let startNumber = 1;
let endNumber = maxNumber;
const actualNumbersPerGrid = endNumber - startNumber + 1;
const numbers: number[] = Array.from(
{ length: actualNumbersPerGrid },
(_, i) => startNumber + i,
);
// 随机隐藏一部分数字(隐藏40-60%)
const hideCount = Math.floor(
actualNumbersPerGrid * (0.4 + Math.random() * 0.2),
);
const hiddenIndices = new Set<number>();
while (hiddenIndices.size < hideCount) {
const randomIndex = Math.floor(
Math.random() * actualNumbersPerGrid,
);
hiddenIndices.add(randomIndex);
}
const gridNumbers: (number | null)[] = numbers.map(
(num, index) => (hiddenIndices.has(index) ? null : num),
);
// 为每个数字分配颜色(包括null位置)
const gridColors: (string | null)[] = gridNumbers.map(
(num) => (num ? this.getRandomNumberColor() : null),
);
grids.push({ numbers: gridNumbers, colors: gridColors });
}
this.missingNumberData = {
grids,
maxNumber,
};
},
/**
* 获取随机数字颜色
*/
getRandomNumberColor(): string {
return getRandomNumberColor();
},
/** 选择类型 */
onSelectType(event: any) {
const { name, value } = event.detail;
this.setData({
currentType: value,
currentTypeName: name,
});
// 重新生成数据
this.onRandom(); this.onRandom();
}, },
}); },
}, {
pagePath: 'missingNumber/missingNumber',
/** },
* 绘制Canvas内容 ),
*/ );
async drawCanvas() {
if (!this.ctx || !this.drawService || !this.missingNumberData) {
return;
}
try {
await this.drawService.draw(
this.missingNumberData,
String(this.data.currentType),
);
this.setData({ hasContent: true });
} catch (error) {
console.error('绘制失败:', error);
this.setData({ hasContent: false });
}
},
/**
* 随机生成
*/
onRandom() {
const maxNumber = this.data.currentType;
this.generateMissingNumberData(maxNumber);
this.drawCanvas();
},
/**
* 生成缺失数字数据
*/
generateMissingNumberData(maxNumber: number) {
const grids: Array<{
numbers: (number | null)[];
colors: (string | null)[];
}> = [];
const gridMap = {
10: {
gridCount: 3,
},
20: {
gridCount: 2,
},
40: {
gridCount: 2,
},
50: {
gridCount: 2,
},
80: {
gridCount: 1,
},
100: {
gridCount: 1,
},
120: {
gridCount: 1,
},
};
const { gridCount } = gridMap[maxNumber as keyof typeof gridMap];
// 生成多个网格
for (let gridIndex = 0; gridIndex < gridCount; gridIndex++) {
let startNumber = 1;
let endNumber = maxNumber;
const actualNumbersPerGrid = endNumber - startNumber + 1;
const numbers: number[] = Array.from(
{ length: actualNumbersPerGrid },
(_, i) => startNumber + i,
);
// 随机隐藏一部分数字(隐藏40-60%)
const hideCount = Math.floor(
actualNumbersPerGrid * (0.4 + Math.random() * 0.2),
);
const hiddenIndices = new Set<number>();
while (hiddenIndices.size < hideCount) {
const randomIndex = Math.floor(
Math.random() * actualNumbersPerGrid,
);
hiddenIndices.add(randomIndex);
}
const gridNumbers: (number | null)[] = numbers.map((num, index) =>
hiddenIndices.has(index) ? null : num,
);
// 为每个数字分配颜色(包括null位置)
const gridColors: (string | null)[] = gridNumbers.map((num) =>
num ? this.getRandomNumberColor() : null,
);
grids.push({ numbers: gridNumbers, colors: gridColors });
}
this.missingNumberData = {
grids,
maxNumber,
};
},
/**
* 获取随机数字颜色
*/
getRandomNumberColor(): string {
return getRandomNumberColor();
},
// ========== 使用公共方法 ==========
initCanvas: commonMethods.initCanvas,
exportToPrint: commonMethods.exportToPrint,
onShareAppMessage: commonMethods.onShareAppMessage,
onShareTimeline: commonMethods.onShareTimeline,
onCloseShareDialog: commonMethods.onCloseShareDialog,
onShareSuccess: commonMethods.onShareSuccess,
initPageInfo: commonMethods.initPageInfo,
/** 选择类型 */
onSelectType(event: any) {
const { name, value } = event.detail;
this.setData({
currentType: value,
currentTypeName: name,
});
// 重新生成数据
this.onRandom();
},
});
@@ -1,264 +1,270 @@
import NumberDecomposeDraw from '../service/numberDecomposeDraw'; import NumberDecomposeDraw from '../service/numberDecomposeDraw';
import { import { applyMathPageMixin, CanvasDataState } from '../common/mathPageMixin';
getMathPageCommonMethods,
CanvasDataState,
} from '../common/mathPageMixin';
// 获取公共方法
const commonMethods = getMathPageCommonMethods({
pagePath: 'numberDecompose/numberDecompose',
});
type DecomposeMode = 'with-image' | 'decompose' | 'compose'; type DecomposeMode = 'with-image' | 'decompose' | 'compose';
Page({ Page(
canvas: null as Canvas | null, applyMathPageMixin(
ctx: null as RenderingContext | null, {
boxHeight: 0, canvas: null as Canvas | null,
boxWidth: 0, ctx: null as RenderingContext | null,
drawService: null as NumberDecomposeDraw | null, boxHeight: 0,
decomposeData: null as { boxWidth: 0,
problems: Array<{ drawService: null as NumberDecomposeDraw | null,
whole: number | null; // 总数(null表示组合模式需要填写) decomposeData: null as {
part1: number | null; // 第一个部分(null表示需要填写) problems: Array<{
part2: number | null; // 第二个部分null表示需要填写) whole: number | null; // 总数null表示组合模式需要填写)
imageIndex?: number; // 图片索引(有图片模式需要 part1: number | null; // 第一个部分(null表示需要填写
imageType?: 'fruits' | 'twelve-animals'; // 图片类型 part2: number | null; // 第二个部分(null表示需要填写)
}>; imageIndex?: number; // 图片索引(有图片模式需要)
mode: DecomposeMode; imageType?: 'fruits' | 'twelve-animals'; // 图片类型
} | null, }>;
mode: DecomposeMode;
} | null,
data: { data: {
pageTitle: '10以内数的分与合', pageTitle: '10以内数的分与合',
subTitle: '学习数的分解与组合', subTitle: '学习数的分解与组合',
functionId: '', functionId: '',
hasContent: false, hasContent: false,
showShareDialog: false, showShareDialog: false,
showTypeSelector: true, showTypeSelector: true,
currentType: 'with-image', currentType: 'with-image',
currentTypeName: '有图片模式', currentTypeName: '有图片模式',
typeActions: [ typeActions: [
{ name: '有图片模式', value: 'with-image' }, { name: '有图片模式', value: 'with-image' },
{ name: '分模式', value: 'decompose' }, { name: '分模式', value: 'decompose' },
{ name: '组合模式', value: 'compose' }, { name: '组合模式', value: 'compose' },
], ],
} as CanvasDataState & { } as CanvasDataState & {
showTypeSelector: boolean; showTypeSelector: boolean;
currentType: DecomposeMode; currentType: DecomposeMode;
currentTypeName: string; currentTypeName: string;
typeActions: Array<{ name: string; value: DecomposeMode }>; typeActions: Array<{ name: string; value: DecomposeMode }>;
},
onLoad(options: { id?: string; mode?: DecomposeMode }) {
const functionId = options.id || 'number-decompose';
const mode = options.mode || 'with-image';
const currentTypeName =
mode === 'with-image'
? '有图片模式'
: mode === 'decompose'
? '分模式'
: '组合模式';
this.setData({
currentType: mode,
currentTypeName,
});
this.initPageInfo(functionId, '10以内数的分与合');
},
onReady() {
this.initCanvas({
createDrawService: (canvas, ctx, options) => {
return new NumberDecomposeDraw(canvas, ctx, options);
}, },
drawServiceOptions: {
subTitle: this.data.subTitle, onLoad(options: { id?: string; mode?: DecomposeMode }) {
const functionId = options.id || 'number-decompose';
const mode = options.mode || 'with-image';
const currentTypeName =
mode === 'with-image'
? '有图片模式'
: mode === 'decompose'
? '分模式'
: '组合模式';
this.setData({
currentType: mode,
currentTypeName,
});
this.initPageInfo(functionId, '10以内数的分与合');
}, },
onCanvasReady: () => {
// Canvas 初始化完成后,生成初始数据 onReady() {
this.initCanvas({
createDrawService: (
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, any>,
) => {
return new NumberDecomposeDraw(canvas, ctx, options);
},
drawServiceOptions: {
subTitle: this.data.subTitle,
},
onCanvasReady: () => {
// Canvas 初始化完成后,生成初始数据
this.onRandom();
},
});
},
/**
* 绘制Canvas内容
*/
async drawCanvas() {
if (!this.ctx || !this.drawService || !this.decomposeData) {
return;
}
try {
await this.drawService.draw(this.decomposeData);
this.setData({ hasContent: true });
} catch (error) {
console.error('绘制失败:', error);
this.setData({ hasContent: false });
}
},
/**
* 随机生成
*/
onRandom() {
this.generateDecomposeData();
this.drawCanvas();
},
/**
* 生成分解数据(确保不重复)
*/
generateDecomposeData() {
const mode = this.data.currentType;
const problems: Array<{
whole: number | null;
part1: number | null;
part2: number | null;
imageIndex?: number;
imageType?: 'fruits' | 'twelve-animals';
}> = [];
// 用于去重的 Set,存储题目唯一标识
// 格式:对于分解模式 "whole:part1:part2:showPart1",对于组合模式 "part1:part2"
const usedKeys = new Set<string>();
if (mode === 'with-image') {
// 有图片模式:9个题目,一行三列,总共三行
const problemCount = 9;
let attempts = 0;
const maxAttempts = problemCount * 50; // 最大尝试次数
while (
problems.length < problemCount &&
attempts < maxAttempts
) {
attempts++;
// 生成总数(2-10
const whole = Math.floor(Math.random() * 9) + 2;
// 随机选择一个部分(1 到 whole-1)
const part1 =
Math.floor(Math.random() * (whole - 1)) + 1;
const part2 = whole - part1;
// 随机决定显示 part1 还是 part2(另一个为 null
const showPart1 = Math.random() < 0.5;
// 生成唯一标识(统一格式:part1 <= part2
const minPart = Math.min(part1, part2);
const maxPart = Math.max(part1, part2);
const key = `${whole}:${minPart}:${maxPart}:${showPart1}`;
// 检查是否已存在
if (usedKeys.has(key)) {
continue;
}
usedKeys.add(key);
// 随机选择图片类型和索引
const imageType: 'fruits' | 'twelve-animals' =
Math.random() < 0.5 ? 'fruits' : 'twelve-animals';
const maxImageIndex = imageType === 'fruits' ? 22 : 12;
const imageIndex =
Math.floor(Math.random() * maxImageIndex) + 1;
problems.push({
whole,
part1: showPart1 ? part1 : null,
part2: showPart1 ? null : part2,
imageIndex,
imageType,
});
}
} else if (mode === 'decompose') {
// 分模式:15个题目,一行3个,总共5行
const problemCount = 15;
let attempts = 0;
const maxAttempts = problemCount * 50; // 最大尝试次数
while (
problems.length < problemCount &&
attempts < maxAttempts
) {
attempts++;
// 生成总数(2-10
const whole = Math.floor(Math.random() * 9) + 2;
// 随机选择一个部分(1 到 whole-1)
const part1 =
Math.floor(Math.random() * (whole - 1)) + 1;
const part2 = whole - part1;
// 随机决定显示 part1 还是 part2(另一个为 null
const showPart1 = Math.random() < 0.5;
// 生成唯一标识(统一格式:part1 <= part2
const minPart = Math.min(part1, part2);
const maxPart = Math.max(part1, part2);
const key = `${whole}:${minPart}:${maxPart}:${showPart1}`;
// 检查是否已存在
if (usedKeys.has(key)) {
continue;
}
usedKeys.add(key);
problems.push({
whole,
part1: showPart1 ? part1 : null,
part2: showPart1 ? null : part2,
});
}
} else if (mode === 'compose') {
// 组合模式:15个题目,一行3个,总共5行
const problemCount = 15;
let attempts = 0;
const maxAttempts = problemCount * 50; // 最大尝试次数
while (
problems.length < problemCount &&
attempts < maxAttempts
) {
attempts++;
// 生成总数(2-10
const whole = Math.floor(Math.random() * 9) + 2;
// 随机选择一个部分(1 到 whole-1)
const part1 =
Math.floor(Math.random() * (whole - 1)) + 1;
const part2 = whole - part1;
// 生成唯一标识(统一格式:part1 <= part2
const minPart = Math.min(part1, part2);
const maxPart = Math.max(part1, part2);
const key = `${minPart}:${maxPart}`;
// 检查是否已存在
if (usedKeys.has(key)) {
continue;
}
usedKeys.add(key);
// 组合模式:两个部分都显示,根节点为 null
problems.push({
whole: null, // 根节点需要填写
part1,
part2,
});
}
}
this.decomposeData = { problems, mode };
},
/** 选择类型 */
onSelectType(event: any) {
const { name, value } = event.detail;
this.setData({
currentType: value,
currentTypeName: name,
});
// 重新生成数据
this.onRandom(); this.onRandom();
}, },
}); },
}, {
pagePath: 'numberDecompose/numberDecompose',
/** },
* 绘制Canvas内容 ),
*/ );
async drawCanvas() {
if (!this.ctx || !this.drawService || !this.decomposeData) {
return;
}
try {
await this.drawService.draw(this.decomposeData);
this.setData({ hasContent: true });
} catch (error) {
console.error('绘制失败:', error);
this.setData({ hasContent: false });
}
},
/**
* 随机生成
*/
onRandom() {
this.generateDecomposeData();
this.drawCanvas();
},
/**
* 生成分解数据(确保不重复)
*/
generateDecomposeData() {
const mode = this.data.currentType;
const problems: Array<{
whole: number | null;
part1: number | null;
part2: number | null;
imageIndex?: number;
imageType?: 'fruits' | 'twelve-animals';
}> = [];
// 用于去重的 Set,存储题目唯一标识
// 格式:对于分解模式 "whole:part1:part2:showPart1",对于组合模式 "part1:part2"
const usedKeys = new Set<string>();
if (mode === 'with-image') {
// 有图片模式:9个题目,一行三列,总共三行
const problemCount = 9;
let attempts = 0;
const maxAttempts = problemCount * 50; // 最大尝试次数
while (problems.length < problemCount && attempts < maxAttempts) {
attempts++;
// 生成总数(2-10
const whole = Math.floor(Math.random() * 9) + 2;
// 随机选择一个部分(1 到 whole-1)
const part1 = Math.floor(Math.random() * (whole - 1)) + 1;
const part2 = whole - part1;
// 随机决定显示 part1 还是 part2(另一个为 null
const showPart1 = Math.random() < 0.5;
// 生成唯一标识(统一格式:part1 <= part2
const minPart = Math.min(part1, part2);
const maxPart = Math.max(part1, part2);
const key = `${whole}:${minPart}:${maxPart}:${showPart1}`;
// 检查是否已存在
if (usedKeys.has(key)) {
continue;
}
usedKeys.add(key);
// 随机选择图片类型和索引
const imageType: 'fruits' | 'twelve-animals' =
Math.random() < 0.5 ? 'fruits' : 'twelve-animals';
const maxImageIndex = imageType === 'fruits' ? 22 : 12;
const imageIndex =
Math.floor(Math.random() * maxImageIndex) + 1;
problems.push({
whole,
part1: showPart1 ? part1 : null,
part2: showPart1 ? null : part2,
imageIndex,
imageType,
});
}
} else if (mode === 'decompose') {
// 分模式:15个题目,一行3个,总共5行
const problemCount = 15;
let attempts = 0;
const maxAttempts = problemCount * 50; // 最大尝试次数
while (problems.length < problemCount && attempts < maxAttempts) {
attempts++;
// 生成总数(2-10
const whole = Math.floor(Math.random() * 9) + 2;
// 随机选择一个部分(1 到 whole-1)
const part1 = Math.floor(Math.random() * (whole - 1)) + 1;
const part2 = whole - part1;
// 随机决定显示 part1 还是 part2(另一个为 null
const showPart1 = Math.random() < 0.5;
// 生成唯一标识(统一格式:part1 <= part2
const minPart = Math.min(part1, part2);
const maxPart = Math.max(part1, part2);
const key = `${whole}:${minPart}:${maxPart}:${showPart1}`;
// 检查是否已存在
if (usedKeys.has(key)) {
continue;
}
usedKeys.add(key);
problems.push({
whole,
part1: showPart1 ? part1 : null,
part2: showPart1 ? null : part2,
});
}
} else if (mode === 'compose') {
// 组合模式:15个题目,一行3个,总共5行
const problemCount = 15;
let attempts = 0;
const maxAttempts = problemCount * 50; // 最大尝试次数
while (problems.length < problemCount && attempts < maxAttempts) {
attempts++;
// 生成总数(2-10
const whole = Math.floor(Math.random() * 9) + 2;
// 随机选择一个部分(1 到 whole-1)
const part1 = Math.floor(Math.random() * (whole - 1)) + 1;
const part2 = whole - part1;
// 生成唯一标识(统一格式:part1 <= part2
const minPart = Math.min(part1, part2);
const maxPart = Math.max(part1, part2);
const key = `${minPart}:${maxPart}`;
// 检查是否已存在
if (usedKeys.has(key)) {
continue;
}
usedKeys.add(key);
// 组合模式:两个部分都显示,根节点为 null
problems.push({
whole: null, // 根节点需要填写
part1,
part2,
});
}
}
this.decomposeData = { problems, mode };
},
// ========== 使用公共方法 ==========
initCanvas: commonMethods.initCanvas,
exportToPrint: commonMethods.exportToPrint,
onShareAppMessage: commonMethods.onShareAppMessage,
onShareTimeline: commonMethods.onShareTimeline,
onCloseShareDialog: commonMethods.onCloseShareDialog,
onShareSuccess: commonMethods.onShareSuccess,
initPageInfo: commonMethods.initPageInfo,
/** 选择类型 */
onSelectType(event: any) {
const { name, value } = event.detail;
this.setData({
currentType: value,
currentTypeName: name,
});
// 重新生成数据
this.onRandom();
},
});
+32 -144
View File
@@ -1,13 +1,7 @@
import { PAPER_SIZE } from '../../constants/colors';
import { checkAndSaveImage } from '../../utils/saveImage';
import { shouldShowShareGuide } from '../../utils/shareGuide';
import NumberFindDraw from '../service/numberFindDraw'; import NumberFindDraw from '../service/numberFindDraw';
import { import { createMathPage, CanvasDataState } from '../common/mathPageMixin';
MATH_FUNCTION_TYPES,
MathFunctionType,
} from '../../constants/mathFunctions';
Page({ createMathPage({
canvas: null as Canvas | null, canvas: null as Canvas | null,
ctx: null as RenderingContext | null, ctx: null as RenderingContext | null,
boxHeight: 0, boxHeight: 0,
@@ -19,102 +13,47 @@ Page({
selectedNumber: 0, // 选中的数字 selectedNumber: 0, // 选中的数字
functionId: '', // 功能ID,用于判断标题 functionId: '', // 功能ID,用于判断标题
numberList: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], // 数字列表 numberList: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], // 数字列表
showShareDialog: false, // 显示分享引导弹窗 hasContent: false,
showShareDialog: false,
} as CanvasDataState & {
selectedNumber: number;
numberList: number[];
}, },
onLoad(options: { id?: string }) { onLoad(options: { id?: string }) {
// 根据id参数判断页面标题
const functionId = options.id || 'number-find'; const functionId = options.id || 'number-find';
this.setData({ this.setData({
functionId, functionId,
}); });
this.initPageInfo(functionId, '看数字,涂一涂');
// 根据functionId设置页面标题
const functionItem = MATH_FUNCTION_TYPES.find(
(item: MathFunctionType) => item.id === functionId,
);
const pageTitle = functionItem?.title || '看数字,涂一涂';
this.setData({
pageTitle,
});
// 设置导航栏标题
wx.setNavigationBarTitle({
title: pageTitle,
});
}, },
onReady() { onReady() {
// 初始化canvas this.initCanvas({
this.initCanvas(); createDrawService: (
}, canvas: Canvas,
ctx: RenderingContext,
/** options?: Record<string, any>,
* 初始化Canvas ) => {
*/ return new NumberFindDraw(canvas, ctx, options);
initCanvas() { },
const query = wx.createSelectorQuery(); drawServiceOptions: {
query subTitle: '找一找下面相同的数字,涂上颜色',
.select('#canvasWrapper') functionId: this.data.functionId,
.boundingClientRect((rect) => { },
if (rect) { onCanvasReady: () => {
const { width, height } = PAPER_SIZE['A4']; // 如果还没有选中数字,随机选择一个
const boxWidth = rect.width; if (this.data.selectedNumber === 0) {
const boxHeight = boxWidth / (width / height); const randomNumber = Math.floor(Math.random() * 10) + 1;
this.boxHeight = boxHeight;
this.boxWidth = boxWidth;
this.setData({ this.setData({
boxWidth, selectedNumber: randomNumber,
boxHeight,
});
// 初始化canvas上下文
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 NumberFindDraw(
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();
}
}); });
} }
})
.exec(); // 绘制初始内容
this.drawCanvas();
},
});
}, },
/** /**
@@ -128,8 +67,10 @@ Page({
if (this.data.selectedNumber > 0) { if (this.data.selectedNumber > 0) {
try { try {
await this.drawService.draw(this.data.selectedNumber); await this.drawService.draw(this.data.selectedNumber);
this.setData({ hasContent: true });
} catch (error) { } catch (error) {
console.error('绘制失败:', error); console.error('绘制失败:', error);
this.setData({ hasContent: false });
} }
} }
}, },
@@ -155,57 +96,4 @@ Page({
}); });
this.drawCanvas(); this.drawCanvas();
}, },
/**
* 导出打印
*/
exportToPrint() {
if (!this.canvas || this.data.selectedNumber <= 0) {
return;
}
// 检查是否需要显示分享引导
if (shouldShowShareGuide()) {
this.setData({ showShareDialog: true });
return;
}
// 直接下载
checkAndSaveImage(this.canvas);
},
/**
* 分享小程序
*/
onShareAppMessage() {
return {
title: '涂鸦丫-数学学习涂鸦卡',
path: `/mathPages/numberFind/numberFind?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);
}
},
}); });
+155
View File
@@ -0,0 +1,155 @@
/**
* 埋点服务
* 提供分享和下载打印事件的上报功能
*/
// 生成唯一 UUID
function generateUUID(): string {
const timestamp = Date.now();
const random = Math.floor(Math.random() * 1000000);
return `${timestamp}-${random}`;
}
// 格式化时间为 yyyy-MM-dd HH:mm
function formatTime(date: Date = new Date()): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
}
/**
* 获取今天的日期字符串(yyyy-MM-dd)
*/
function getTodayString(): string {
const date = new Date();
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
/**
* 获取事件计数(当天该事件上报的次数)
* 使用本地存储持久化,每天重置计数
*/
function getEventCount(eventName: string): number {
try {
const storageKey = `tracker_count_${eventName}`;
const dateKey = `tracker_date_${eventName}`;
const today = getTodayString();
// 获取上次记录的日期
const lastDate = wx.getStorageSync(dateKey);
let currentCount = 0;
// 如果是今天,获取上次的计数;否则重置为0
if (lastDate === today) {
currentCount = wx.getStorageSync(storageKey) || 0;
}
// 递增计数
const newCount = currentCount + 1;
// 保存计数和日期
wx.setStorageSync(storageKey, newCount);
wx.setStorageSync(dateKey, today);
return newCount;
} catch (error) {
console.error('获取事件计数失败:', error);
return 1; // 如果存储失败,返回 1
}
}
/**
* 埋点追踪器
*/
class Tracker {
private uuid: string;
private openLog: boolean;
constructor() {
// 初始化时获取或生成 UUID(持久化存储)
this.uuid = this.getOrCreateUUID();
this.openLog = true;
}
/**
* 获取或创建 UUID
*/
private getOrCreateUUID(): string {
const uuidKey = 'tracker_uuid';
let storedUUID = wx.getStorageSync(uuidKey);
if (!storedUUID) {
storedUUID = generateUUID();
wx.setStorageSync(uuidKey, storedUUID);
}
return storedUUID;
}
printLog(tip: string, message?: string | object): void {
if (this.openLog) {
if (message) {
if (typeof message === 'object') {
console.log(tip, ':', JSON.stringify(message, null, 2));
} else {
console.log(tip, ':', message as string);
}
} else {
console.log(tip);
}
}
}
/**
* 上报分享点击事件
* @param pageName 页面名称
*/
reportShare(pageName: string): void {
try {
const time = formatTime();
const count = getEventCount('share_click');
const params = {
count,
time,
uuid: this.uuid,
page_name: pageName,
};
this.printLog('分享事件', params);
wx.reportEvent('share_click', params);
} catch (error) {
console.error('上报分享事件失败:', error);
}
}
/**
* 上报下载打印事件
* @param pageName 页面名称
*/
reportDownload(pageName: string): void {
try {
const time = formatTime();
const count = getEventCount('download');
const params = {
count,
time,
uuid: this.uuid,
page_name: pageName,
};
this.printLog('下载事件', params);
wx.reportEvent('download', params);
} catch (error) {
console.error('上报下载事件失败:', error);
}
}
}
// 创建并导出 tracker 实例
const tracker = new Tracker();
export default tracker;