feat:架构代码优化

This commit is contained in:
R524809
2025-12-11 13:02:44 +08:00
parent 2c32235568
commit 3c0e2af10d
89 changed files with 2774 additions and 2816 deletions
+13 -3
View File
@@ -1,17 +1,27 @@
import config from './config/config';
const defaultPrintConfig: PrintConfig = config.printHeader as PrintConfig;
import { defaultPrintConfig } from './config/config';
import { generateUUID, getAppUUID, setAppUUID } from './utils/uuid';
// app.ts
App<IAppOption>({
globalData: {
env: 'release',
uuid: '',
printConfig: defaultPrintConfig,
},
onLaunch() {
const accountInfo = wx.getAccountInfoSync();
const env = accountInfo.miniProgram.envVersion || 'release';
this.globalData.env = env;
// 从 localStorage 读取 UUID,如果没有则生成并存储
let uuid = getAppUUID();
if (!uuid) {
uuid = generateUUID();
setAppUUID(uuid);
console.log('生成并存储 UUID', uuid);
}
this.globalData.uuid = uuid;
if (env !== 'release') {
const printConfig =
wx.getStorageSync('printConfig') || defaultPrintConfig;
@@ -0,0 +1,201 @@
# 页面 Mixin 重构说明
## 重构目标
`mathPageMixin.ts` 改造为通用的页面基类,支持数学学习、专注力等多个模块共用。
## 改造内容
### 1. 创建通用基类
**文件:** `base/pageMixin.ts`
- 提供了通用的页面公共方法
- 抽象了 `initPageInfo` 方法,支持两种调用方式:
1. `initPageInfo(functionId, defaultTitle)` - 可以配合 `pageInfoLookup` 函数使用
2. `initPageInfo({ title, desc, functionId })` - 直接提供页面信息
- 支持通过配置传入模块特定的信息查找函数和分享配置
### 2. 重构数学模块 Mixin
**文件:** `mathPages/common/mathPageMixin.ts`
- 基于通用 `pageMixin` 创建数学模块的包装器
- 自动配置 `MATH_FUNCTION_TYPES` 查找函数
- 配置数学模块的分享信息
- **保持完全向后兼容**,所有现有的数学页面无需修改
### 3. 创建专注力模块示例
**文件:** `focusPages/common/focusPageMixin.ts`
- 展示如何为专注力模块创建专用的 mixin
- 可以配置 `FOCUS_FUNCTION_TYPES` 查找函数
- 配置专注力模块的分享信息
## 关键改进
### initPageInfo 方法优化
**之前:**
```typescript
initPageInfo(functionId: string, defaultTitle?: string) {
const functionItem = MATH_FUNCTION_TYPES.find(...); // 硬编码
// ...
}
```
**现在:**
```typescript
// 方式1:配合 pageInfoLookup 使用(数学模块自动配置)
initPageInfo(functionId: string, defaultTitle?: string)
// 方式2:直接提供信息(专注力等模块可以使用)
initPageInfo({ title: string, desc?: string, functionId: string })
```
### 配置化的页面信息查找
通过 `PageCommonMethodsConfig` 可以传入:
- `shareConfig`: 模块特定的分享配置
- `pageInfoLookup`: 页面信息查找函数(可选)
这样不同模块可以使用不同的查找逻辑,而不需要硬编码。
## 使用方式
### 数学模块(无需修改)
所有现有的数学页面继续使用原有方式:
```typescript
import { createMathPage, CanvasDataState } from '../common/mathPageMixin';
createMathPage({
// ...
onLoad(options: { id?: string }) {
const functionId = options.id || 'addition-5';
// 自动从 MATH_FUNCTION_TYPES 中查找
this.initPageInfo(functionId, '默认标题');
},
});
```
### 专注力模块(新)
```typescript
import { createFocusPage } from '../common/focusPageMixin';
createFocusPage({
// ...
onLoad(options: { id?: string }) {
const functionId = options.id || 'grid-drawing';
// 自动从 FOCUS_FUNCTION_TYPES 中查找
this.initPageInfo(functionId, '格子仿画');
},
});
```
### 其他模块(通用方式)
```typescript
import { createPage, PageCommonMethodsConfig } from '../../base/pageMixin';
const myConfig: PageCommonMethodsConfig = {
shareConfig: {
title: '我的模块',
imageUrl: 'https://example.com/share.png',
},
pageInfoLookup: (functionId) => {
// 自定义查找逻辑
return { title: '...', desc: '...' };
},
};
createPage(
{
// ...
onLoad(options: { id?: string }) {
// 方式1:使用 lookup 函数
this.initPageInfo(options.id || 'default', '默认标题');
// 方式2:直接提供信息
this.initPageInfo({
functionId: options.id || 'default',
title: '页面标题',
desc: '页面描述',
});
},
},
myConfig,
);
```
## 文件结构
```
miniprogram/
├── base/
│ ├── pageMixin.ts # 通用页面基类(新增)
│ └── pageMixin.README.md # 使用文档(新增)
├── mathPages/
│ └── common/
│ └── mathPageMixin.ts # 数学模块包装器(重构)
└── focusPages/
└── common/
└── focusPageMixin.ts # 专注力模块示例(新增)
```
## 向后兼容性
**所有现有的数学页面无需修改**
因为:
- `mathPageMixin.ts` 保持了相同的导出接口
- `createMathPage` 函数签名不变
- `initPageInfo` 方法调用方式不变
- 所有类型定义都重新导出
## 受影响的文件
### 无需修改的文件
以下数学页面文件无需修改,因为它们使用相同的接口:
- `mathPages/addition/addition.ts`
- `mathPages/compare/compare.ts`
- `mathPages/countMatch/countMatch.ts`
- `mathPages/countingSelect/countingSelect.ts`
- `mathPages/missingNumber/missingNumber.ts`
- `mathPages/numberDecompose/numberDecompose.ts`
- `mathPages/numberFind/numberFind.ts`
### 修改的文件
-`base/pageMixin.ts` - 新增
-`mathPages/common/mathPageMixin.ts` - 重构(保持向后兼容)
## 测试建议
1. 验证所有数学页面功能正常
2. 验证页面信息(title, desc)正确显示
3. 验证分享功能正常
4. 验证 Canvas 初始化正常
5. 验证图片导出功能正常
## 后续扩展
其他模块(如专注力模块)可以:
1. 创建自己的 `XXXPageMixin.ts` 文件
2. 定义自己的函数类型和列表
3. 配置自己的分享信息
4. 使用 `createXXXPage` 创建页面
这样可以保持代码的模块化和可维护性。
+229
View File
@@ -0,0 +1,229 @@
# 通用页面基类 (pageMixin)
## 概述
`pageMixin.ts` 提供了一个通用的页面基类,可以被不同模块(数学学习、专注力等)共用。它抽象了 Canvas 绘制页面的通用功能。
## 核心功能
- Canvas 初始化和配置
- 分享功能(小程序分享、朋友圈分享)
- 图片导出和打印
- 页面信息初始化
## 使用方法
### 1. 基础用法(不依赖特定的页面信息查找)
```typescript
import { createPage } from '../../base/pageMixin';
createPage({
canvas: null as Canvas | null,
ctx: null as RenderingContext | null,
// ... 其他属性
data: {
pageTitle: '页面标题',
functionId: 'my-function-id',
// ... 其他数据
},
onLoad(options: { id?: string }) {
const functionId = options.id || 'my-function-id';
// 方式1:直接提供页面信息
this.initPageInfo({
functionId,
title: '页面标题',
desc: '页面描述',
});
// 或者方式2:使用默认标题(需要传入 defaultTitle
this.initPageInfo(functionId, '默认标题');
},
});
```
### 2. 自定义页面信息查找函数
如果你有多个页面需要共享同一个查找逻辑,可以创建自己的 mixin:
```typescript
import { createPage, PageCommonMethodsConfig } from '../../base/pageMixin';
// 定义自己的页面信息查找函数
function myPageInfoLookup(functionId: string) {
// 从你的常量或配置中查找
const pageInfo = MY_FUNCTION_TYPES.find((item) => item.id === functionId);
if (pageInfo) {
return {
title: pageInfo.title,
desc: pageInfo.desc,
};
}
return undefined;
}
// 使用配置创建页面
const config: PageCommonMethodsConfig = {
shareConfig: {
title: '我的模块',
imageUrl: 'https://example.com/share.png',
},
pageInfoLookup: myPageInfoLookup,
};
createPage(
{
// ... 页面配置
onLoad(options: { id?: string }) {
const functionId = options.id || 'default-id';
// 现在可以使用简化的调用方式
this.initPageInfo(functionId, '默认标题');
// 如果有 pageInfoLookup,会自动查找并设置 title 和 desc
},
},
config,
);
```
### 3. 数学模块的用法(向后兼容)
数学模块已经封装好了专用的 `mathPageMixin.ts`,可以直接使用:
```typescript
import { createMathPage, CanvasDataState } from '../common/mathPageMixin';
createMathPage({
// ... 页面配置
onLoad(options: { id?: string }) {
const functionId = options.id || 'addition-5';
// 自动从 MATH_FUNCTION_TYPES 中查找
this.initPageInfo(functionId, '默认标题');
},
});
```
## API 说明
### createPage(pageOptions, config?)
创建并注册一个页面。
**参数:**
- `pageOptions`: 页面配置对象
- `config`: 可选,公共方法配置(见 `PageCommonMethodsConfig`
### initPageInfo(functionIdOrOptions, defaultTitle?)
初始化页面信息,支持两种调用方式:
**方式1**
```typescript
this.initPageInfo(functionId: string, defaultTitle?: string)
```
- 如果有配置 `pageInfoLookup`,会尝试查找
- 否则使用 `defaultTitle`
**方式2**
```typescript
this.initPageInfo({ title: string, desc?: string, functionId: string })
```
- 直接提供页面信息,忽略 `pageInfoLookup`
### initCanvas(options)
初始化 Canvas。
**参数:**
- `options.createDrawService`: 创建绘制服务的工厂函数
- `options.drawServiceOptions`: 绘制服务的选项
- `options.onCanvasReady`: Canvas 初始化完成后的回调
### exportToPrint()
导出并保存图片(用于打印)。
### getShareOptions()
获取分享配置。
### onShareAppMessage()
微信小程序分享处理函数。
### onShareTimeline()
微信朋友圈分享处理函数。
## 专注力模块使用示例
```typescript
import { createPage, PageCommonMethodsConfig } from '../../base/pageMixin';
// 专注力模块的函数类型定义
interface FocusFunctionType {
id: string;
title: string;
desc: string;
}
// 专注力模块的函数列表
const FOCUS_FUNCTION_TYPES: FocusFunctionType[] = [
{
id: 'grid-drawing',
title: '格子仿画',
desc: '在格子中绘制颜色',
},
// ... 更多
];
// 专注力模块的页面信息查找函数
function focusPageInfoLookup(functionId: string) {
const functionItem = FOCUS_FUNCTION_TYPES.find(
(item) => item.id === functionId,
);
if (functionItem) {
return {
title: functionItem.title,
desc: functionItem.desc,
};
}
return undefined;
}
// 专注力模块的配置
const focusConfig: PageCommonMethodsConfig = {
shareConfig: {
title: '涂鸦丫-专注力训练',
imageUrl: 'https://cdn.joeyone.cn/doodle/share-img/focus-share.png',
},
pageInfoLookup: focusPageInfoLookup,
};
// 创建专注力页面
createPage(
{
canvas: null as Canvas | null,
// ... 其他配置
onLoad(options: { id?: string }) {
const functionId = options.id || 'grid-drawing';
// 自动从 FOCUS_FUNCTION_TYPES 中查找
this.initPageInfo(functionId, '格子仿画');
},
},
focusConfig,
);
```
## 向后兼容性
所有现有的数学页面无需修改,因为 `mathPageMixin.ts` 保持了相同的接口。
@@ -1,23 +1,22 @@
import { PAPER_SIZE } from '../../constants/colors';
import { checkAndSaveImage } from '../../utils/saveImage';
import { shouldShowShareGuide } from '../../utils/shareGuide';
import { BaseMathDrawService } from '../service/baseMathDraw';
import {
MATH_FUNCTION_TYPES,
MathFunctionType,
} from '../../constants/mathFunctions';
import tracker from '../../utils/tracker';
import { PAPER_SIZE } from '../constants/colors';
import { checkAndSaveImage } from '../utils/saveImage';
import { shouldShowShareGuide } from '../utils/shareGuide';
import { BaseDrawService } from '../service/baseDraw';
import tracker from '../utils/tracker';
/**
* Canvas
*/
export interface MathPageCanvasInstance {
export interface PageCanvasInstance {
data: CanvasDataState;
canvas: Canvas | null;
ctx: RenderingContext | null;
boxHeight: number;
boxWidth: number;
drawService: BaseMathDrawService | null;
drawService: BaseDrawService | null;
setData(data: any, callback?: () => void): void;
route: string;
getShareOptions(): ShareOptions;
}
/**
@@ -29,8 +28,12 @@ export interface CanvasDataState {
boxWidth: number;
boxHeight: number;
functionId: string;
currentMode?: string;
pageTitle: string;
subTitle?: string;
data: any;
setData(data: any, callback?: () => void): void;
route: string;
}
/**
@@ -44,7 +47,7 @@ export interface InitCanvasOptions {
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, any>,
) => BaseMathDrawService;
) => BaseDrawService;
/**
*
*/
@@ -62,28 +65,60 @@ export interface ShareOptions {
/**
* mathPages 'missingNumber/missingNumber'
*/
pagePath: string;
path: string;
title: string;
imageUrl: string;
}
const shareConfig = {
title: '涂鸦丫-数学学习涂鸦卡',
/**
*
*/
export interface PageInfo {
title: string;
desc?: string;
}
/**
*
*/
export type PageInfoLookup = (functionId: string) => PageInfo | undefined;
/**
*
*/
export interface PageCommonMethodsConfig {
/**
*
*/
shareConfig?: {
title: string;
imageUrl: string;
};
/**
*
* initPageInfo 使
*/
pageInfoLookup?: PageInfoLookup;
}
const defaultShareConfig = {
title: '涂鸦丫',
imageUrl: 'https://cdn.joeyone.cn/doodle/share-img/index-share-v2.png',
};
/**
*
*
*
* Canvas绘制页面中复用
* @param config
*/
export function getMathPageCommonMethods() {
export function getPageCommonMethods(config: PageCommonMethodsConfig = {}) {
const { shareConfig = defaultShareConfig, pageInfoLookup } = config;
return {
/**
* Canvas
*/
initCanvas(
this: MathPageCanvasInstance & { data: CanvasDataState },
options: InitCanvasOptions,
) {
console.log('initCanvas this', this);
initCanvas(this: PageCanvasInstance, options: InitCanvasOptions) {
const query = wx.createSelectorQuery();
query
.select('#canvasWrapper')
@@ -141,9 +176,7 @@ export function getMathPageCommonMethods() {
/**
*
*/
exportToPrint(
this: MathPageCanvasInstance & { data: CanvasDataState },
) {
exportToPrint(this: PageCanvasInstance) {
if (!this.canvas || !this.data.hasContent) {
return;
}
@@ -154,58 +187,55 @@ export function getMathPageCommonMethods() {
}
// 上报下载埋点
tracker.reportDownload(this.data.pageTitle);
tracker.reportDownload(this.data.pageTitle, this.data.currentMode);
checkAndSaveImage(this.canvas);
},
getShareOptions(this: PageCanvasInstance): ShareOptions {
const route = this.route || '';
const { functionId, currentMode } = this.data;
const currentModeQuery = currentMode ? `&mode=${currentMode}` : '';
const path = `${route}?id=${functionId}${currentModeQuery}`;
console.log('getShareOptions path', path);
return {
...shareConfig,
path,
};
},
/**
*
*/
onShareAppMessage(
this: MathPageCanvasInstance & { data: CanvasDataState },
) {
console.log('onShareAppMessage');
onShareAppMessage(this: PageCanvasInstance) {
// 上报分享埋点
tracker.reportShare(this.data.pageTitle);
return {
...shareConfig,
query: `id=${this.data.functionId}`,
};
return this.getShareOptions();
},
/**
*
*/
onShareTimeline(
this: MathPageCanvasInstance & { data: CanvasDataState },
) {
onShareTimeline(this: PageCanvasInstance) {
console.log('onShareTimeline');
// 上报分享埋点
tracker.reportShare(this.data.pageTitle);
return {
...shareConfig,
query: `id=${this.data.functionId}`,
};
return this.getShareOptions();
},
/**
*
*/
onCloseShareDialog(
this: MathPageCanvasInstance & { data: CanvasDataState },
) {
onCloseShareDialog(this: PageCanvasInstance) {
this.setData({ showShareDialog: false });
},
/**
*
*/
onShareSuccess(
this: MathPageCanvasInstance & { data: CanvasDataState },
) {
onShareSuccess(this: PageCanvasInstance) {
this.setData({ showShareDialog: false });
if (this.canvas) {
// 上报下载埋点(分享成功后下载)
@@ -215,19 +245,43 @@ export function getMathPageCommonMethods() {
},
/**
* functionId
*
*
*
* 1. initPageInfo(functionId, defaultTitle) - pageInfoLookup使 defaultTitle
* 2. initPageInfo({ title, desc, functionId }) -
*/
initPageInfo(
this: MathPageCanvasInstance & { data: CanvasDataState },
functionId: string,
this: PageCanvasInstance,
functionIdOrOptions:
| string
| { title: string; desc?: string; functionId: string },
defaultTitle?: string,
) {
const functionItem = MATH_FUNCTION_TYPES.find(
(item: MathFunctionType) => item.id === functionId,
);
let functionId: string;
let title: string;
let desc: string = '';
const title = functionItem?.title || defaultTitle || '';
const desc = functionItem?.desc || '';
// 判断调用方式
if (typeof functionIdOrOptions === 'string') {
// 方式1: initPageInfo(functionId, defaultTitle)
functionId = functionIdOrOptions;
title = defaultTitle || '';
// 如果有 pageInfoLookup,尝试查找
if (pageInfoLookup) {
const pageInfo = pageInfoLookup(functionId);
if (pageInfo) {
title = pageInfo.title;
desc = pageInfo.desc || '';
}
}
} else {
// 方式2: initPageInfo({ title, desc, functionId })
functionId = functionIdOrOptions.functionId;
title = functionIdOrOptions.title;
desc = functionIdOrOptions.desc || '';
}
this.setData({
pageTitle: title,
@@ -241,14 +295,17 @@ export function getMathPageCommonMethods() {
}
/**
*
*
*
* @param pageOptions
* @param shareOptions
* @param config
* @returns
*/
export function applyMathPageMixin(pageOptions: any) {
const commonMethods = getMathPageCommonMethods();
export function applyPageMixin(
pageOptions: any,
config: PageCommonMethodsConfig = {},
) {
const commonMethods = getPageCommonMethods(config);
// 提取 pageOptions 中的 data(如果有)
const pageData = pageOptions.data || {};
@@ -268,11 +325,14 @@ export function applyMathPageMixin(pageOptions: any) {
}
/**
* 便
* 便
*
*
* @param pageOptions
* @param config
*/
export function createMathPage(pageOptions: any) {
Page(applyMathPageMixin(pageOptions));
export function createPage(
pageOptions: any,
config: PageCommonMethodsConfig = {},
) {
Page(applyPageMixin(pageOptions, config));
}
+12 -6
View File
@@ -1,7 +1,13 @@
export default {
printHeader: {
// header: 'wechat',
header: 'LogoImage',
appName: '涂鸦丫小程序',
},
// export default {
// printHeader: {
// // header: 'wechat',
// header: 'LogoImage',
// appName: '涂鸦丫小程序',
// },
// };
export const defaultPrintConfig: PrintConfig = {
header: 'LogoImage',
appName: '涂鸦丫小程序',
appHint: '数学|专注|练字|涂鸦',
};
+18
View File
@@ -0,0 +1,18 @@
export interface FocusFunctionType {
id: string;
page?: string;
title: string;
desc: string;
icon: string;
}
export const FOCUS_FUNCTION_TYPES: FocusFunctionType[] = [
// 第一阶段:认识数字(最基础)
{
id: 'number-find',
page: 'numberFind',
title: '找数字,涂一涂',
desc: '找一找下面相同的数字,涂上颜色',
icon: '🔍',
},
];
+7
View File
@@ -102,4 +102,11 @@ export const MATH_FUNCTION_TYPES: MathFunctionType[] = [
desc: '练习10以内的加法和减法混合运算',
icon: '±',
},
{
id: 'number-decompose-20',
page: 'numberDecompose',
title: '20以内数的分与合',
desc: '把数字分一分,合一合',
icon: '🔢',
},
];
@@ -0,0 +1,76 @@
/**
* 专注力页面专用的 Mixin
* 基于通用 pageMixin,提供专注力模块特定的配置
*/
import { createPage, PageCommonMethodsConfig } from '../../base/pageMixin';
/**
* 专注力模块的函数类型定义
*/
export interface FocusFunctionType {
id: string;
title: string;
desc: string;
}
/**
* 专注力模块的函数列表
* TODO: 根据实际需求补充完整的功能列表
*/
export const FOCUS_FUNCTION_TYPES: FocusFunctionType[] = [
{
id: 'grid-drawing',
title: '格子仿画',
desc: '在格子中绘制颜色,形成各种形状',
},
// 可以在这里添加更多专注力相关的功能
// {
// id: 'pattern-memory',
// title: '图案记忆',
// desc: '记住并重现图案',
// },
];
/**
* 专注力模块的分享配置
*/
const focusShareConfig = {
title: '涂鸦丫-专注力训练',
imageUrl: 'https://cdn.joeyone.cn/doodle/share-img/focus-share.png',
};
/**
* 专注力模块的页面信息查找函数
*/
function focusPageInfoLookup(functionId: string) {
const functionItem = FOCUS_FUNCTION_TYPES.find(
(item) => item.id === functionId,
);
if (functionItem) {
return {
title: functionItem.title,
desc: functionItem.desc,
};
}
return undefined;
}
/**
* 专注力模块的配置
*/
const focusConfig: PageCommonMethodsConfig = {
shareConfig: focusShareConfig,
pageInfoLookup: focusPageInfoLookup,
};
/**
* 创建专注力页面的便捷函数
* 自动应用公共方法并注册为页面
* @param pageOptions 页面选项
*/
export function createFocusPage(pageOptions: any) {
createPage(pageOptions, focusConfig);
}
+162
View File
@@ -0,0 +1,162 @@
# mathPages 目录重构方案
## 当前目录结构
```
mathPages/
├── addition/ # 页面
├── compare/ # 页面
├── countingSelect/ # 页面
├── countMatch/ # 页面
├── missingNumber/ # 页面
├── numberDecompose/ # 页面
├── numberFind/ # 页面
├── assets/ # 静态资源
├── common/ # 公共文件(mixin等)
├── components/ # 组件
└── service/ # 服务(Draw相关)
```
## 问题
- 页面和服务放在同一级,不易区分
- 目录较多,查找页面不够直观
- 公共服务散落在不同目录
## 推荐方案:使用 `shared/` 目录
### 重构后的目录结构
```
mathPages/
├── addition/ # 页面
├── compare/ # 页面
├── countingSelect/ # 页面
├── countMatch/ # 页面
├── missingNumber/ # 页面
├── numberDecompose/ # 页面
├── numberFind/ # 页面
├── assets/ # 静态资源
└── shared/ # 公共服务目录(新建)
├── service/ # 服务(从 service/ 移动)
├── common/ # 公共文件(从 common/ 移动)
└── components/ # 组件(从 components/ 移动)
```
### 优点
1.**语义清晰**`shared/` 明确表示共享代码
2.**结构清晰**:页面和公共服务分离,一目了然
3.**易于维护**:所有公共服务集中在一个目录下
4.**符合常见实践**`shared/` 在很多项目中都有使用
5.**扩展性好**:未来可以添加 `shared/utils/``shared/types/`
### 需要修改的导入路径
#### 页面文件中的导入
**之前:**
```typescript
import AdditionDraw from '../service/additionDraw';
import { createMathPage } from '../common/mathPageMixin';
import MathBottomButtons from '../components/math-bottom-buttons/math-bottom-buttons';
```
**之后:**
```typescript
import AdditionDraw from '../shared/service/additionDraw';
import { createMathPage } from '../shared/common/mathPageMixin';
import MathBottomButtons from '../shared/components/math-bottom-buttons/math-bottom-buttons';
```
#### service 文件之间的导入
**之前:**
```typescript
import { BaseDrawService } from './baseMathDraw';
```
**之后:**
```typescript
import { BaseDrawService } from './baseMathDraw';
// 或者如果跨目录
import { BaseDrawService } from '../baseMathDraw';
```
## 备选方案:使用 `lib/` 目录
如果不想用 `shared/`,也可以考虑 `lib/`
```
mathPages/
├── addition/
├── compare/
├── ...
├── assets/
└── lib/ # 公共服务目录
├── service/
├── common/
└── components/
```
**优点:**
- 简洁
- 表示库文件
**缺点:**
- `lib/` 通常用于第三方库或编译产物
- 语义上不如 `shared/` 清晰
## 备选方案:使用 `core/` 目录
```
mathPages/
├── addition/
├── compare/
├── ...
├── assets/
└── core/ # 核心功能目录
├── service/
├── common/
└── components/
```
**优点:**
- 表示核心功能
**缺点:**
- `core/` 通常用于框架核心,不太适合业务代码
## 迁移步骤
1. **创建 `shared/` 目录**
2. **移动目录**
- `service/``shared/service/`
- `common/``shared/common/`
- `components/``shared/components/`
3. **批量更新导入路径**
- 所有页面文件中的 `../service/``../shared/service/`
- 所有页面文件中的 `../common/``../shared/common/`
- 所有页面文件中的 `../components/``../shared/components/`
- 检查 `shared/` 内部文件的相互导入
4. **更新配置文件**
- 检查是否有路径配置需要更新(如 `app.json` 中的组件路径)
5. **测试验证**
- 确保所有页面功能正常
- 确保导入路径正确
## 推荐使用 `shared/` 目录
综合考虑,**推荐使用 `shared/` 目录**,因为:
- 语义最清晰
- 符合常见实践
- 易于理解和维护
+2 -2
View File
@@ -6,7 +6,7 @@
"enablePullDownRefresh": false,
"usingComponents": {
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
"math-type-selector": "../components/math-type-selector/math-type-selector",
"math-bottom-buttons": "../components/math-bottom-buttons/math-bottom-buttons"
"math-type-selector": "../shared/components/math-type-selector/math-type-selector",
"math-bottom-buttons": "../shared/components/math-bottom-buttons/math-bottom-buttons"
}
}
+1 -1
View File
@@ -1 +1 @@
@import '../common/mathPage.less';
@import '../shared/common/mathPage.less';
+213 -224
View File
@@ -1,230 +1,219 @@
import AdditionDraw from '../service/additionDraw';
import { applyMathPageMixin, CanvasDataState } from '../common/mathPageMixin';
import AdditionDraw from '../shared/service/additionDraw';
import {
createMathPage,
CanvasDataState,
} from '../shared/common/mathPageMixin';
Page(
applyMathPageMixin(
{
canvas: null as Canvas | null,
ctx: null as RenderingContext | null,
boxHeight: 0,
boxWidth: 0,
drawService: null as AdditionDraw | null,
calculationData: null as {
problems: Array<{
type: 'addition' | 'subtraction';
left: number;
right: number;
result: number;
}>;
} | null,
createMathPage({
canvas: null as Canvas | null,
ctx: null as RenderingContext | null,
boxHeight: 0,
boxWidth: 0,
drawService: null as AdditionDraw | null,
calculationData: null as {
problems: Array<{
type: 'addition' | 'subtraction';
left: number;
right: number;
result: number;
}>;
} | null,
data: {
pageTitle: '加减法计算',
subTitle: '通过图形化方式学习加减法运算',
functionId: '',
hasContent: false,
showShareDialog: false,
currentType: 'addition-5', // 'addition-5', 'addition-10', 'subtraction-10', 'addition-subtraction-10'
currentTypeName: '5以内加法',
typeActions: [
{ name: '5以内加法', value: 'addition-5' },
{ name: '10以内加法', value: 'addition-10' },
{ name: '10以内减法', value: 'subtraction-10' },
{ name: '10以内加减法', value: 'addition-subtraction-10' },
],
imageType: 'twelve-animals', // 图片类型:twelve-animals 或 fruits
} as CanvasDataState & {
currentType: string;
currentTypeName: string;
typeActions: Array<{ name: string; value: string }>;
imageType: string;
data: {
pageTitle: '加减法计算',
subTitle: '通过图形化方式学习加减法运算',
functionId: '',
hasContent: false,
showShareDialog: false,
currentMode: 'addition-5', // 'addition-5', 'addition-10', 'subtraction-10', 'addition-subtraction-10'
currentModeName: '5以内加法',
typeActions: [
{ name: '5以内加法', value: 'addition-5' },
{ name: '10以内加法', value: 'addition-10' },
{ name: '10以内减法', value: 'subtraction-10' },
{ name: '10以内加减法', value: 'addition-subtraction-10' },
],
imageType: 'twelve-animals', // 图片类型:twelve-animals 或 fruits
} as CanvasDataState & {
currentMode: string;
currentModeName: string;
typeActions: Array<{ name: string; value: string }>;
imageType: string;
},
onLoad(options: { id?: string }) {
const functionId = options.id || 'addition-5';
this.initPageInfo(functionId, '加减法计算');
// 根据 functionId 设置默认类型
if (functionId === 'addition-5') {
this.setData({
currentMode: 'addition-5',
currentModeName: '5以内加法',
});
} else if (functionId === 'addition-10') {
this.setData({
currentMode: 'addition-10',
currentModeName: '10以内加法',
});
} else if (functionId === 'subtraction-10') {
this.setData({
currentMode: 'subtraction-10',
currentModeName: '10以内减法',
});
} else if (functionId === 'addition-subtraction-10') {
this.setData({
currentMode: 'addition-subtraction-10',
currentModeName: '10以内加减法',
});
}
},
onReady() {
this.initCanvas({
createDrawService: (
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, any>,
) => {
return new AdditionDraw(canvas, ctx, options);
},
onLoad(options: { id?: string }) {
const functionId = options.id || 'addition-5';
this.initPageInfo(functionId, '加减法计算');
// 根据 functionId 设置默认类型
if (functionId === 'addition-5') {
this.setData({
currentType: 'addition-5',
currentTypeName: '5以内加法',
});
} else if (functionId === 'addition-10') {
this.setData({
currentType: 'addition-10',
currentTypeName: '10以内加法',
});
} else if (functionId === 'subtraction-10') {
this.setData({
currentType: 'subtraction-10',
currentTypeName: '10以内减法',
});
} else if (functionId === 'addition-subtraction-10') {
this.setData({
currentType: 'addition-subtraction-10',
currentTypeName: '10以内加减法',
});
}
drawServiceOptions: {
subTitle: this.data.subTitle,
},
onReady() {
this.initCanvas({
createDrawService: (
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, any>,
) => {
return new AdditionDraw(canvas, ctx, options);
},
drawServiceOptions: {
subTitle: this.data.subTitle,
},
onCanvasReady: () => {
// 初始随机生成
this.onRandom();
},
});
},
/**
* 绘制Canvas内容
*/
async drawCanvas() {
if (!this.ctx || !this.drawService || !this.calculationData) {
return;
}
try {
// 更新 Header 的 Title
if (this.drawService) {
this.drawService.options.title =
this.data.currentTypeName;
}
await this.drawService.draw(
this.calculationData,
this.data.currentType,
);
this.setData({ hasContent: true });
} catch (error) {
console.error('绘制失败:', error);
this.setData({ hasContent: false });
}
},
/**
* 随机生成
*/
onRandom() {
const problems: Array<{
type: 'addition' | 'subtraction';
left: number;
right: number;
result: number;
}> = [];
const type = this.data.currentType;
// 生成5道题目
for (let i = 0; i < 5; i++) {
if (type === 'addition-5') {
// 5以内加法:和 ≤ 5
// left >= 1, right >= 1, left + right <= 5
const maxSum = 5;
const left =
Math.floor(Math.random() * (maxSum - 1)) + 1; // 1 到 4
const maxRight = maxSum - left; // 确保 left + right <= 5
const right = Math.floor(Math.random() * maxRight) + 1; // 1 到 maxRight
const result = left + right;
problems.push({
type: 'addition',
left,
right,
result,
});
} else if (type === 'addition-10') {
// 10以内加法:和 ≤ 10
// left >= 1, right >= 1, left + right <= 10
const maxSum = 10;
const left =
Math.floor(Math.random() * (maxSum - 1)) + 1; // 1 到 9
const maxRight = maxSum - left; // 确保 left + right <= 10
const right = Math.floor(Math.random() * maxRight) + 1; // 1 到 maxRight
const result = left + right;
problems.push({
type: 'addition',
left,
right,
result,
});
} else if (type === 'subtraction-10') {
// 10以内减法:被减数 ≤ 10
// left <= 10, left - right = result, result >= 1
const maxLeft = 10;
const left = Math.floor(Math.random() * maxLeft) + 1; // 1 到 10
const maxRight = left - 1; // 确保 result >= 1
const right = Math.floor(Math.random() * maxRight) + 1; // 1 到 maxRight
const result = left - right;
problems.push({
type: 'subtraction',
left,
right,
result,
});
} else if (type === 'addition-subtraction-10') {
// 加减法混合
if (Math.random() < 0.5) {
// 加法:和 ≤ 10
const maxSum = 10;
const left =
Math.floor(Math.random() * (maxSum - 1)) + 1; // 1 到 9
const maxRight = maxSum - left; // 确保 left + right <= 10
const right =
Math.floor(Math.random() * maxRight) + 1; // 1 到 maxRight
const result = left + right;
problems.push({
type: 'addition',
left,
right,
result,
});
} else {
// 减法:被减数 ≤ 10
const maxLeft = 10;
const left =
Math.floor(Math.random() * maxLeft) + 1; // 1 到 10
const maxRight = left - 1; // 确保 result >= 1
const right =
Math.floor(Math.random() * maxRight) + 1; // 1 到 maxRight
const result = left - right;
problems.push({
type: 'subtraction',
left,
right,
result,
});
}
}
}
this.calculationData = { problems };
this.drawCanvas();
},
/** 选择类型 */
onSelectType(event: any) {
const { name, value } = event.detail;
this.setData({
currentType: value,
currentTypeName: name,
});
// 重新生成数据
onCanvasReady: () => {
// 初始随机生成
this.onRandom();
},
},
{
pagePath: 'addition/addition',
},
),
);
});
},
/**
* 绘制Canvas内容
*/
async drawCanvas() {
if (!this.ctx || !this.drawService || !this.calculationData) {
return;
}
try {
// 更新 Header 的 Title
if (this.drawService) {
this.drawService.options.title = this.data.currentModeName;
}
await this.drawService.draw(
this.calculationData,
this.data.currentMode,
);
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.currentMode;
// 生成5道题目
for (let i = 0; i < 5; i++) {
if (type === 'addition-5') {
// 5以内加法:和 ≤ 5
// left >= 1, right >= 1, left + right <= 5
const maxSum = 5;
const left = Math.floor(Math.random() * (maxSum - 1)) + 1; // 1 到 4
const maxRight = maxSum - left; // 确保 left + right <= 5
const right = Math.floor(Math.random() * maxRight) + 1; // 1 到 maxRight
const result = left + right;
problems.push({
type: 'addition',
left,
right,
result,
});
} else if (type === 'addition-10') {
// 10以内加法:和 ≤ 10
// left >= 1, right >= 1, left + right <= 10
const maxSum = 10;
const left = Math.floor(Math.random() * (maxSum - 1)) + 1; // 1 到 9
const maxRight = maxSum - left; // 确保 left + right <= 10
const right = Math.floor(Math.random() * maxRight) + 1; // 1 到 maxRight
const result = left + right;
problems.push({
type: 'addition',
left,
right,
result,
});
} else if (type === 'subtraction-10') {
// 10以内减法:被减数 ≤ 10
// left <= 10, left - right = result, result >= 1
const maxLeft = 10;
const left = Math.floor(Math.random() * maxLeft) + 1; // 1 到 10
const maxRight = left - 1; // 确保 result >= 1
const right = Math.floor(Math.random() * maxRight) + 1; // 1 到 maxRight
const result = left - right;
problems.push({
type: 'subtraction',
left,
right,
result,
});
} else if (type === 'addition-subtraction-10') {
// 加减法混合
if (Math.random() < 0.5) {
// 加法:和 ≤ 10
const maxSum = 10;
const left = Math.floor(Math.random() * (maxSum - 1)) + 1; // 1 到 9
const maxRight = maxSum - left; // 确保 left + right <= 10
const right = Math.floor(Math.random() * maxRight) + 1; // 1 到 maxRight
const result = left + right;
problems.push({
type: 'addition',
left,
right,
result,
});
} else {
// 减法:被减数 ≤ 10
const maxLeft = 10;
const left = Math.floor(Math.random() * maxLeft) + 1; // 1 到 10
const maxRight = left - 1; // 确保 result >= 1
const right = Math.floor(Math.random() * maxRight) + 1; // 1 到 maxRight
const result = left - right;
problems.push({
type: 'subtraction',
left,
right,
result,
});
}
}
}
this.calculationData = { problems };
this.drawCanvas();
},
/** 选择类型 */
onSelectType(event: any) {
const { name, value } = event.detail;
this.setData({
currentMode: value,
currentModeName: name,
});
// 重新生成数据
this.onRandom();
},
});
+1 -1
View File
@@ -13,7 +13,7 @@
<!-- 类型选择器和随机生成按钮 -->
<math-type-selector
current-type-name="{{currentTypeName}}"
current-type-name="{{currentModeName}}"
type-actions="{{typeActions}}"
bind:select="onSelectType"
bind:random="onRandom" />
+1 -1
View File
@@ -7,6 +7,6 @@
"usingComponents": {
"toy-button": "../../ui/button/button",
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
"math-bottom-buttons": "../components/math-bottom-buttons/math-bottom-buttons"
"math-bottom-buttons": "../shared/components/math-bottom-buttons/math-bottom-buttons"
}
}
+1 -1
View File
@@ -1 +1 @@
@import '../common/mathPage.less';
@import '../shared/common/mathPage.less';
+10 -18
View File
@@ -1,15 +1,12 @@
import CompareDraw from '../service/compareDraw';
import CompareDraw from '../shared/service/compareDraw';
import {
getMathPageCommonMethods,
createMathPage,
CanvasDataState,
} from '../common/mathPageMixin';
} from '../shared/common/mathPageMixin';
// 获取公共方
const commonMethods = getMathPageCommonMethods({
pagePath: 'compare/compare',
});
// 获取公共方
Page({
createMathPage({
canvas: null as Canvas | null,
ctx: null as RenderingContext | null,
boxHeight: 0,
@@ -40,7 +37,11 @@ Page({
onReady() {
this.initCanvas({
createDrawService: (canvas, ctx, options) => {
createDrawService: (
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, any>,
) => {
return new CompareDraw(canvas, ctx, options);
},
drawServiceOptions: {
@@ -120,13 +121,4 @@ Page({
this.compareData = { problems };
},
// ========== 使用公共方法 ==========
initCanvas: commonMethods.initCanvas,
exportToPrint: commonMethods.exportToPrint,
onShareAppMessage: commonMethods.onShareAppMessage,
onShareTimeline: commonMethods.onShareTimeline,
onCloseShareDialog: commonMethods.onCloseShareDialog,
onShareSuccess: commonMethods.onShareSuccess,
initPageInfo: commonMethods.initPageInfo,
});
@@ -1,7 +0,0 @@
{
"component": true,
"usingComponents": {
"toy-button": "../../../ui/button/button",
"van-action-sheet": "../../../miniprogram_npm/@vant/weapp/action-sheet/index"
}
}
@@ -6,7 +6,7 @@
"enablePullDownRefresh": false,
"usingComponents": {
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
"math-type-selector": "../components/math-type-selector/math-type-selector",
"math-bottom-buttons": "../components/math-bottom-buttons/math-bottom-buttons"
"math-type-selector": "../shared/components/math-type-selector/math-type-selector",
"math-bottom-buttons": "../shared/components/math-bottom-buttons/math-bottom-buttons"
}
}
@@ -1 +1 @@
@import '../common/mathPage.less';
@import '../shared/common/mathPage.less';
+181 -187
View File
@@ -1,200 +1,194 @@
import CountMatchDraw from '../service/countMatchDraw';
import NumberColorDraw from '../service/numberColorDraw';
import { applyMathPageMixin, CanvasDataState } from '../common/mathPageMixin';
import CountMatchDraw from '../shared/service/countMatchDraw';
import NumberColorDraw from '../shared/service/numberColorDraw';
import {
createMathPage,
CanvasDataState,
} from '../shared/common/mathPageMixin';
Page(
applyMathPageMixin(
{
canvas: null as Canvas | null,
ctx: null as RenderingContext | null,
boxHeight: 0,
boxWidth: 0,
drawService: null as CountMatchDraw | NumberColorDraw | null,
matchData: null as {
leftNumbers: number[];
rightNumbers: number[];
} | null,
colorData: null as {
numbers: number[];
} | null,
createMathPage({
canvas: null as Canvas | null,
ctx: null as RenderingContext | null,
boxHeight: 0,
boxWidth: 0,
drawService: null as CountMatchDraw | NumberColorDraw | null,
matchData: null as {
leftNumbers: number[];
rightNumbers: number[];
} | null,
colorData: null as {
numbers: number[];
} | null,
data: {
pageTitle: '数一数,连一连',
functionId: '',
hasContent: false,
showShareDialog: false,
showTypeSelector: true, // 控制是否显示类型选择器
currentType: 'twelve-animals', // 'twelve-animals' 或 'fruits' 或 'circle' 或 'caterpillar'
currentTypeName: '十二生肖',
data: {
pageTitle: '数一数,连一连',
functionId: '',
hasContent: false,
showShareDialog: false,
showTypeSelector: true, // 控制是否显示类型选择器
currentMode: 'twelve-animals', // 'twelve-animals' 或 'fruits' 或 'circle' 或 'caterpillar'
currentModeName: '十二生肖',
typeActions: [
{ name: '十二生肖', value: 'twelve-animals' },
{ name: '水果', value: 'fruits' },
],
} as CanvasDataState & {
showTypeSelector: boolean;
currentMode: string;
currentModeName: string;
typeActions: Array<{ name: string; value: string }>;
},
onLoad(options: { id?: string; mode?: string }) {
const functionId = options.id || 'counting-matching';
this.initPageInfo(functionId, '数一数,连一连');
// 根据 functionId 设置不同的类型选择器
if (functionId === 'number-coloring') {
const currentMode = options.mode || 'caterpillar';
this.setData({
currentMode: currentMode,
currentModeName:
currentMode === 'caterpillar' ? '毛毛虫' : '圆圈',
typeActions: [
{ name: '毛毛虫', value: 'caterpillar' },
{ name: '圆圈', value: 'circle' },
],
});
} else {
const currentMode = options.mode || 'twelve-animals';
this.setData({
currentMode,
currentModeName:
currentMode === 'twelve-animals' ? '十二生肖' : '水果',
typeActions: [
{ name: '十二生肖', value: 'twelve-animals' },
{ name: '水果', value: 'fruits' },
],
} as CanvasDataState & {
showTypeSelector: boolean;
currentType: string;
currentTypeName: string;
typeActions: Array<{ name: string; value: string }>;
},
});
}
},
onLoad(options: { id?: string }) {
const functionId = options.id || 'counting-matching';
this.setData({ functionId });
const functionItem =
require('../../constants/mathFunctions').MATH_FUNCTION_TYPES.find(
(item: any) => item.id === functionId,
);
const pageTitle = functionItem?.title || '数一数,连一连';
this.setData({ pageTitle });
this.initPageInfo(functionId, pageTitle);
// 根据 functionId 设置不同的类型选择器
if (functionId === 'number-coloring') {
this.setData({
currentType: 'caterpillar',
currentTypeName: '毛毛虫',
typeActions: [
{ name: '毛毛虫', value: 'caterpillar' },
{ name: '圆圈', value: 'circle' },
],
});
}
},
onReady() {
this.initCanvas({
createDrawService: (
canvas: Canvas,
ctx: RenderingContext,
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();
},
});
},
/**
* 绘制Canvas内容
*/
async drawCanvas() {
if (!this.ctx || !this.drawService) {
return;
}
try {
if (this.data.functionId === 'number-coloring') {
if (!this.colorData) {
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 });
}
},
/**
* 随机生成
*/
onRandom() {
onReady() {
this.initCanvas({
createDrawService: (
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, any>,
) => {
// 根据 functionId 创建不同的绘制服务
if (this.data.functionId === 'number-coloring') {
// 按数字涂颜色模式:生成6个随机数字(1-10)
const availableNumbers = Array.from(
{ length: 10 },
(_, i) => i + 1,
);
const numbers: number[] = [];
for (let i = 0; i < 6; i++) {
const randomIndex = Math.floor(
Math.random() * availableNumbers.length,
);
const number = availableNumbers.splice(
randomIndex,
1,
)[0];
numbers.push(number);
}
this.colorData = { numbers };
return new NumberColorDraw(canvas, ctx, options);
} else {
// 数一数连一连模式:生成5个不同的数字(1-10)
const availableNumbers = Array.from(
{ length: 10 },
(_, i) => i + 1,
);
const leftNumbers: number[] = [];
for (let i = 0; i < 5; i++) {
const randomIndex = Math.floor(
Math.random() * availableNumbers.length,
);
const number = availableNumbers.splice(
randomIndex,
1,
)[0];
leftNumbers.push(number);
}
// 复制数字数组并打乱顺序,作为右侧显示的数字
const rightNumbers = [...leftNumbers];
for (let i = rightNumbers.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[rightNumbers[i], rightNumbers[j]] = [
rightNumbers[j],
rightNumbers[i],
];
}
this.matchData = { leftNumbers, rightNumbers };
return new CountMatchDraw(canvas, ctx, options);
}
this.drawCanvas();
},
/** 选择类型 */
onSelectType(event: any) {
const { name, value } = event.detail;
this.setData({
currentType: value,
currentTypeName: name,
});
// 重新生成数据
drawServiceOptions: {
subTitle:
this.data.functionId === 'number-coloring'
? '按数字给相应的圆圈涂上颜色'
: '通过连线配对数字和对应的数量图形',
},
onCanvasReady: () => {
// 初始随机生成
this.onRandom();
},
},
{
pagePath: 'countMatch/countMatch',
},
),
);
});
},
/**
* 绘制Canvas内容
*/
async drawCanvas() {
if (!this.ctx || !this.drawService) {
return;
}
try {
if (this.data.functionId === 'number-coloring') {
if (!this.colorData) {
return;
}
await (this.drawService as NumberColorDraw).draw(
this.colorData,
this.data.currentMode,
);
} else {
if (!this.matchData) {
return;
}
await (this.drawService as CountMatchDraw).draw(
this.matchData,
this.data.currentMode,
);
}
this.setData({ hasContent: true });
} catch (error) {
console.error('绘制失败:', error);
this.setData({ hasContent: false });
}
},
/**
* 随机生成
*/
onRandom() {
if (this.data.functionId === 'number-coloring') {
// 按数字涂颜色模式:生成6个随机数字(1-10)
const availableNumbers = Array.from(
{ length: 10 },
(_, i) => i + 1,
);
const numbers: number[] = [];
for (let i = 0; i < 6; i++) {
const randomIndex = Math.floor(
Math.random() * availableNumbers.length,
);
const number = availableNumbers.splice(randomIndex, 1)[0];
numbers.push(number);
}
this.colorData = { numbers };
} else {
// 数一数连一连模式:生成5个不同的数字(1-10)
const availableNumbers = Array.from(
{ length: 10 },
(_, i) => i + 1,
);
const leftNumbers: number[] = [];
for (let i = 0; i < 5; i++) {
const randomIndex = Math.floor(
Math.random() * availableNumbers.length,
);
const number = availableNumbers.splice(randomIndex, 1)[0];
leftNumbers.push(number);
}
// 复制数字数组并打乱顺序,作为右侧显示的数字
const rightNumbers = [...leftNumbers];
for (let i = rightNumbers.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[rightNumbers[i], rightNumbers[j]] = [
rightNumbers[j],
rightNumbers[i],
];
}
this.matchData = { leftNumbers, rightNumbers };
}
this.drawCanvas();
},
/** 选择类型 */
onSelectType(event: any) {
const { name, value } = event.detail;
this.setData({
currentMode: value,
currentModeName: name,
});
// 重新生成数据
this.onRandom();
},
});
@@ -14,7 +14,7 @@
<!-- 类型选择器和随机生成按钮 -->
<math-type-selector
wx:if="{{showTypeSelector}}"
current-type-name="{{currentTypeName}}"
current-type-name="{{currentModeName}}"
type-actions="{{typeActions}}"
bind:select="onSelectType"
bind:random="onRandom" />
@@ -7,6 +7,6 @@
"usingComponents": {
"toy-button": "../../ui/button/button",
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
"math-bottom-buttons": "../components/math-bottom-buttons/math-bottom-buttons"
"math-bottom-buttons": "../shared/components/math-bottom-buttons/math-bottom-buttons"
}
}
@@ -1 +1 @@
@import '../common/mathPage.less';
@import '../shared/common/mathPage.less';
@@ -1,168 +1,151 @@
import CountingSelectDraw from '../service/countingSelectDraw';
import { applyMathPageMixin, CanvasDataState } from '../common/mathPageMixin';
import CountingSelectDraw from '../shared/service/countingSelectDraw';
import {
createMathPage,
CanvasDataState,
} from '../shared/common/mathPageMixin';
Page(
applyMathPageMixin(
{
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,
createMathPage({
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,
data: {
pageTitle: '数一数,选一选',
subTitle: '数出物品数量,从多个选项中选择正确答案',
functionId: '',
hasContent: false,
showShareDialog: false,
} as CanvasDataState & {
showTypeSelector?: boolean;
currentType?: string;
currentTypeName?: string;
typeActions?: Array<{ name: string; value: any }>;
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: Canvas,
ctx: RenderingContext,
options?: Record<string, any>,
) => {
return new CountingSelectDraw(canvas, ctx, options);
},
onLoad(options: { id?: string }) {
const functionId = options.id || 'counting-select';
// 初始化页面信息(会从 mathFunctions.ts 读取 title 和 desc
this.initPageInfo(functionId, '数一数,选一选');
drawServiceOptions: {
subTitle: this.data.subTitle,
},
onReady() {
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();
},
});
onCanvasReady: () => {
// Canvas 初始化完成后,生成初始数据
this.onRandom();
},
});
},
/**
* 绘制Canvas内容
*/
async drawCanvas() {
if (
!this.ctx ||
!this.drawService ||
!this.countingSelectData
) {
return;
}
/**
* 绘制Canvas内容
*/
async drawCanvas() {
if (!this.ctx || !this.drawService || !this.countingSelectData) {
return;
}
try {
// 判断是选一选还是填一填模式
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 });
}
},
try {
// 判断是选一选还是填一填模式
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();
},
/**
* 随机生成
*/
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;
}> = [];
/**
* 生成数一数选一选/填一填数据
*/
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';
// 生成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;
// 根据图片类型确定最大索引
const maxImageIndex = imageType === 'fruits' ? 22 : 12;
// 生成图片数量(1-10
const count = Math.floor(Math.random() * 10) + 1;
// 生成图片数量(1-10
const count = Math.floor(Math.random() * 10) + 1;
// 随机选择图片索引
const imageIndex =
Math.floor(Math.random() * maxImageIndex) + 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,
};
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);
// 选一选模式:生成三个选项
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;
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);
}
problems.push(problem);
}
this.countingSelectData = { problems };
},
},
{
pagePath: 'countingSelect/countingSelect',
},
),
);
problem.options = options;
problem.correctIndex = correctIndex;
}
problems.push(problem);
}
this.countingSelectData = { problems };
},
});
@@ -6,7 +6,7 @@
"enablePullDownRefresh": false,
"usingComponents": {
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
"math-type-selector": "../components/math-type-selector/math-type-selector",
"math-bottom-buttons": "../components/math-bottom-buttons/math-bottom-buttons"
"math-type-selector": "../shared/components/math-type-selector/math-type-selector",
"math-bottom-buttons": "../shared/components/math-bottom-buttons/math-bottom-buttons"
}
}
@@ -1 +1 @@
@import '../common/mathPage.less';
@import '../shared/common/mathPage.less';
@@ -1,198 +1,198 @@
import { getRandomNumberColor } from '../../constants/colors';
import MissingNumberDraw from '../service/missingNumberDraw';
import { applyMathPageMixin, CanvasDataState } from '../common/mathPageMixin';
import MissingNumberDraw from '../shared/service/missingNumberDraw';
import {
createMathPage,
CanvasDataState,
} from '../shared/common/mathPageMixin';
Page(
applyMathPageMixin(
{
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,
createMathPage({
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,
data: {
pageTitle: '填上缺少的数字',
subTitle: '在数字序列中找出并填写缺失的数字',
functionId: '',
hasContent: false,
showShareDialog: false,
showTypeSelector: true,
currentType: 10,
currentTypeName: '10以内',
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 }>;
data: {
pageTitle: '填上缺少的数字',
subTitle: '在数字序列中找出并填写缺失的数字',
functionId: '',
hasContent: false,
showShareDialog: false,
showTypeSelector: true,
currentMode: 10,
currentModeName: '10以内',
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;
currentMode: number;
currentModeName: string;
typeActions: Array<{ name: string; value: number }>;
},
onLoad(options: { id?: string; mode?: number }) {
const functionId = options.id || 'missing-number';
this.initPageInfo(functionId, '填上缺少的数字');
this.setData({
currentMode: options.mode || 10,
currentModeName: options.mode ? `${options.mode}以内` : '10以内',
});
},
onReady() {
this.initCanvas({
createDrawService: (
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, any>,
) => {
return new MissingNumberDraw(canvas, ctx, options);
},
onLoad(options: { id?: string }) {
const functionId = options.id || 'missing-number';
this.initPageInfo(functionId, '填上缺少的数字');
drawServiceOptions: {
subTitle: this.data.subTitle,
},
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,
});
// 重新生成数据
onCanvasReady: () => {
// Canvas 初始化完成后,生成初始数据
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.currentMode),
);
this.setData({ hasContent: true });
} catch (error) {
console.error('绘制失败:', error);
this.setData({ hasContent: false });
}
},
/**
* 随机生成
*/
onRandom() {
const maxNumber = this.data.currentMode;
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({
currentMode: value,
currentModeName: name,
});
// 重新生成数据
this.onRandom();
},
});
@@ -14,7 +14,7 @@
<!-- 类型选择器和随机生成按钮 -->
<math-type-selector
wx:if="{{showTypeSelector}}"
current-type-name="{{currentTypeName}}"
current-type-name="{{currentModeName}}"
type-actions="{{typeActions}}"
bind:select="onSelectType"
bind:random="onRandom" />
@@ -6,7 +6,7 @@
"enablePullDownRefresh": false,
"usingComponents": {
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
"math-bottom-buttons": "../components/math-bottom-buttons/math-bottom-buttons",
"math-type-selector": "../components/math-type-selector/math-type-selector"
"math-bottom-buttons": "../shared/components/math-bottom-buttons/math-bottom-buttons",
"math-type-selector": "../shared/components/math-type-selector/math-type-selector"
}
}
@@ -1 +1 @@
@import '../common/mathPage.less';
@import '../shared/common/mathPage.less';
@@ -1,270 +1,300 @@
import NumberDecomposeDraw from '../service/numberDecomposeDraw';
import { applyMathPageMixin, CanvasDataState } from '../common/mathPageMixin';
import NumberDecomposeDraw from '../shared/service/numberDecomposeDraw';
import {
createMathPage,
CanvasDataState,
} from '../shared/common/mathPageMixin';
type DecomposeMode = 'with-image' | 'decompose' | 'compose';
Page(
applyMathPageMixin(
{
canvas: null as Canvas | null,
ctx: null as RenderingContext | null,
boxHeight: 0,
boxWidth: 0,
drawService: null as NumberDecomposeDraw | null,
decomposeData: null as {
problems: Array<{
whole: number | null; // 总数null表示组合模式需要填写)
part1: number | null; // 第一个部分(null表示需要填写
part2: number | null; // 第二个部分(null表示需要填写)
imageIndex?: number; // 图片索引(有图片模式需要)
imageType?: 'fruits' | 'twelve-animals'; // 图片类型
}>;
mode: DecomposeMode;
} | null,
createMathPage({
canvas: null as Canvas | null,
ctx: null as RenderingContext | null,
boxHeight: 0,
boxWidth: 0,
drawService: null as NumberDecomposeDraw | null,
decomposeData: null as {
problems: Array<{
whole: number | null; // 总数(null表示组合模式需要填写)
part1: number | null; // 第一个部分(null表示需要填写)
part2: number | null; // 第二个部分null表示需要填写)
imageIndex?: number; // 图片索引(有图片模式需要
imageType?: 'fruits' | 'twelve-animals'; // 图片类型
}>;
mode: DecomposeMode;
} | null,
data: {
pageTitle: '10以内数的分与合',
subTitle: '学习数的分解与组合',
functionId: '',
hasContent: false,
showShareDialog: false,
showTypeSelector: true,
currentType: 'with-image',
currentTypeName: '有图片模式',
typeActions: [
{ name: '有图片模式', value: 'with-image' },
{ name: '分模式', value: 'decompose' },
{ name: '组合模式', value: 'compose' },
],
} as CanvasDataState & {
showTypeSelector: boolean;
currentType: DecomposeMode;
currentTypeName: string;
typeActions: Array<{ name: string; value: DecomposeMode }>;
data: {
pageTitle: '10以内数的分与合',
subTitle: '学习数的分解与组合',
functionId: '',
hasContent: false,
showShareDialog: false,
showTypeSelector: true,
currentMode: 'with-image',
currentModeName: '有图片模式',
typeActions: [
{ name: '有图片模式', value: 'with-image' },
{ name: '分模式', value: 'decompose' },
{ name: '组合模式', value: 'compose' },
],
maxNumber: 10, // 最大数字:10 或 20
} as CanvasDataState & {
showTypeSelector: boolean;
currentMode: DecomposeMode;
currentModeName: string;
typeActions: Array<{ name: string; value: DecomposeMode }>;
maxNumber: number;
},
onLoad(options: { id?: string; mode?: DecomposeMode }) {
const functionId = options.id || 'number-decompose';
const is20Within = functionId === 'number-decompose-20';
const maxNumber = is20Within ? 20 : 10;
// 20以内只有两种模式,10以内有三种模式
let defaultMode: DecomposeMode;
let typeActions: Array<{ name: string; value: DecomposeMode }>;
let defaultTypeName: string;
if (is20Within) {
// 20以内:只有分模式和组合模式
defaultMode = options.mode || 'decompose';
typeActions = [
{ name: '20以内的分解', value: 'decompose' },
{ name: '20以内的组合', value: 'compose' },
];
defaultTypeName =
defaultMode === 'decompose' ? '20以内的分解' : '20以内的组合';
} else {
// 10以内:有图片模式、分模式、组合模式
defaultMode = options.mode || 'with-image';
typeActions = [
{ name: '有图片模式', value: 'with-image' },
{ name: '分模式', value: 'decompose' },
{ name: '组合模式', value: 'compose' },
];
defaultTypeName =
defaultMode === 'with-image'
? '有图片模式'
: defaultMode === 'decompose'
? '分模式'
: '组合模式';
}
this.setData({
currentMode: defaultMode,
currentModeName: defaultTypeName,
typeActions,
maxNumber,
});
this.initPageInfo(
functionId,
is20Within ? '20以内数的分与合' : '10以内数的分与合',
);
},
onReady() {
this.initCanvas({
createDrawService: (
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, any>,
) => {
return new NumberDecomposeDraw(canvas, ctx, options);
},
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以内数的分与合');
drawServiceOptions: {
subTitle: this.data.subTitle,
},
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,
});
// 重新生成数据
onCanvasReady: () => {
// Canvas 初始化完成后,生成初始数据
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.currentMode;
const maxNumber = this.data.maxNumber;
const is20Within = maxNumber === 20;
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>();
// 确定根节点的数字范围
const minWhole = is20Within ? 11 : 2; // 20以内从11开始,10以内从2开始
const maxWhole = maxNumber;
if (mode === 'with-image') {
// 有图片模式:9个题目,一行三列,总共三行(仅10以内)
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() * (maxWhole - minWhole + 1)) +
minWhole;
// 随机选择一个部分(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 或 11-20
const whole =
Math.floor(Math.random() * (maxWhole - minWhole + 1)) +
minWhole;
// 随机选择一个部分(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 或 11-20
const whole =
Math.floor(Math.random() * (maxWhole - minWhole + 1)) +
minWhole;
// 随机选择一个部分(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({
currentMode: value,
currentModeName: name,
});
// 重新生成数据
this.onRandom();
},
});
@@ -14,7 +14,7 @@
<!-- 类型选择器和随机生成按钮 -->
<math-type-selector
wx:if="{{showTypeSelector}}"
current-type-name="{{currentTypeName}}"
current-type-name="{{currentModeName}}"
type-actions="{{typeActions}}"
bind:select="onSelectType"
bind:random="onRandom" />
@@ -7,6 +7,6 @@
"usingComponents": {
"toy-button": "../../ui/button/button",
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
"math-bottom-buttons": "../components/math-bottom-buttons/math-bottom-buttons"
"math-bottom-buttons": "../shared/components/math-bottom-buttons/math-bottom-buttons"
}
}
@@ -1,4 +1,4 @@
@import '../common/mathPage.less';
@import '../shared/common/mathPage.less';
/* 数字选择区域 */
.number-selection-area {
@@ -1,5 +1,8 @@
import NumberFindDraw from '../service/numberFindDraw';
import { createMathPage, CanvasDataState } from '../common/mathPageMixin';
import NumberFindDraw from '../shared/service/numberFindDraw';
import {
createMathPage,
CanvasDataState,
} from '../shared/common/mathPageMixin';
createMathPage({
canvas: null as Canvas | null,
@@ -0,0 +1,97 @@
/**
* 数学页面专用的 Mixin
* 基于通用 pageMixin,提供数学模块特定的配置
*/
import {
getPageCommonMethods,
applyPageMixin,
createPage,
CanvasDataState,
PageCanvasInstance,
InitCanvasOptions,
ShareOptions,
PageCommonMethodsConfig,
} from '../../../base/pageMixin';
import {
MATH_FUNCTION_TYPES,
MathFunctionType,
} from '../../../constants/mathFunctions';
/**
* 数学模块的页面实例类型(向后兼容)
*/
export type MathPageCanvasInstance = PageCanvasInstance;
/**
* 数学模块的分享配置
*/
const mathShareConfig = {
title: '涂鸦丫-数学学习涂鸦卡',
imageUrl: 'https://cdn.joeyone.cn/doodle/share-img/index-share-v2.png',
};
/**
* 数学模块的页面信息查找函数
*/
function mathPageInfoLookup(functionId: string) {
const functionItem = MATH_FUNCTION_TYPES.find(
(item: MathFunctionType) => item.id === functionId,
);
if (functionItem) {
return {
title: functionItem.title,
desc: functionItem.desc,
};
}
return undefined;
}
/**
* 获取数学页面的公共方法
* 这些方法可以在所有数学页面中复用
* @deprecated 使用 getMathPageCommonMethods() 即可,会自动应用数学模块配置
*/
export function getMathPageCommonMethods() {
const config: PageCommonMethodsConfig = {
shareConfig: mathShareConfig,
pageInfoLookup: mathPageInfoLookup,
};
return getPageCommonMethods(config);
}
/**
* 应用数学页面公共方法的辅助函数
* 自动混入公共方法,简化页面代码
* @param pageOptions 页面选项
* @returns 合并后的页面选项
* @deprecated 使用 applyMathPageMixin() 即可,会自动应用数学模块配置
*/
export function applyMathPageMixin(pageOptions: any) {
const config: PageCommonMethodsConfig = {
shareConfig: mathShareConfig,
pageInfoLookup: mathPageInfoLookup,
};
return applyPageMixin(pageOptions, config);
}
/**
* 创建数学页面的便捷函数
* 自动应用公共方法并注册为页面
* @param pageOptions 页面选项
*/
export function createMathPage(pageOptions: any) {
const config: PageCommonMethodsConfig = {
shareConfig: mathShareConfig,
pageInfoLookup: mathPageInfoLookup,
};
createPage(pageOptions, config);
}
// 导出类型,保持向后兼容
export type { CanvasDataState, InitCanvasOptions, ShareOptions };
@@ -1,6 +1,6 @@
{
"component": true,
"usingComponents": {
"toy-button": "../../../ui/button/button"
"toy-button": "../../../../ui/button/button"
}
}
@@ -0,0 +1,7 @@
{
"component": true,
"usingComponents": {
"toy-button": "../../../../ui/button/button",
"van-action-sheet": "../../../../miniprogram_npm/@vant/weapp/action-sheet/index"
}
}
@@ -1,4 +1,4 @@
import { getImage } from '../../utils/index';
import { getImage } from '../../../utils/index';
interface DrawAdditionContentParams {
canvas: WechatMiniprogram.Canvas;
@@ -1,11 +1,11 @@
import { BaseMathDrawService } from './baseMathDraw';
import { BaseDrawService } from '../../../service/baseDraw';
import { drawAdditionContent } from './additionContentDraw';
/**
*
* 使
*/
class AdditionDraw extends BaseMathDrawService {
class AdditionDraw extends BaseDrawService {
calculationData: {
problems: Array<{
type: 'addition' | 'subtraction';
@@ -1,4 +1,4 @@
import { getImage } from '../../utils/index';
import { getImage } from '../../../utils/index';
interface DrawCompareContentParams {
canvas: WechatMiniprogram.Canvas;
@@ -1,11 +1,11 @@
import { BaseMathDrawService } from './baseMathDraw';
import { BaseDrawService } from '../../../service/baseDraw';
import { drawCompareContent } from './compareContentDraw';
/**
*
* 使
*/
class CompareDraw extends BaseMathDrawService {
class CompareDraw extends BaseDrawService {
compareData: {
problems: Array<{
leftCount: number;
@@ -1,5 +1,5 @@
import { getImage } from '../../utils/index';
import { getRandomNumberColor } from '../../constants/colors';
import { getImage } from '../../../utils/index';
import { getRandomNumberColor } from '../../../constants/colors';
interface DrawCountMatchContentParams {
canvas: WechatMiniprogram.Canvas;
@@ -1,11 +1,11 @@
import { BaseMathDrawService } from './baseMathDraw';
import { BaseDrawService } from '../../../service/baseDraw';
import { drawCountMatchContent } from './countMatchContentDraw';
/**
*
* 使
*/
class CountMatchDraw extends BaseMathDrawService {
class CountMatchDraw extends BaseDrawService {
matchData: {
leftNumbers: number[];
rightNumbers: number[]; // 打乱顺序后的数字数组
@@ -1,5 +1,5 @@
import { getImage } from '../../utils/index';
import { getRandomNumberColor } from '../../constants/colors';
import { getImage } from '../../../utils/index';
import { getRandomNumberColor } from '../../../constants/colors';
interface DrawCountingSelectContentParams {
canvas: WechatMiniprogram.Canvas;
@@ -1,4 +1,4 @@
import { BaseMathDrawService } from './baseMathDraw';
import { BaseDrawService } from '../../../service/baseDraw';
import { drawCountingSelectContent } from './countingSelectContentDraw';
/**
@@ -6,7 +6,7 @@ import { drawCountingSelectContent } from './countingSelectContentDraw';
* 使
* 'select' 'fill'
*/
class CountingSelectDraw extends BaseMathDrawService {
class CountingSelectDraw extends BaseDrawService {
countingData: {
problems: Array<{
count: number;
@@ -1,11 +1,11 @@
import { BaseMathDrawService } from './baseMathDraw';
import { BaseDrawService } from '../../../service/baseDraw';
import { drawMissingNumberContent } from './missingNumberContentDraw';
/**
*
* 使
*/
class MissingNumberDraw extends BaseMathDrawService {
class MissingNumberDraw extends BaseDrawService {
missingNumberData: {
grids: Array<{
numbers: (number | null)[];
@@ -1,4 +1,4 @@
import { getNumberColors } from '../../constants/colors';
import { getNumberColors } from '../../../constants/colors';
interface DrawNumberColorContentParams {
canvas: WechatMiniprogram.Canvas;
@@ -1,11 +1,11 @@
import { BaseMathDrawService } from './baseMathDraw';
import { BaseDrawService } from '../../../service/baseDraw';
import { drawNumberColorContent } from './numberColorContentDraw';
/**
*
* 使
*/
class NumberColorDraw extends BaseMathDrawService {
class NumberColorDraw extends BaseDrawService {
colorData: {
numbers: number[];
} | null;
@@ -1,5 +1,5 @@
import { getImage } from '../../utils/index';
import { getRandomNumberColor } from '../../constants/colors';
import { getImage } from '../../../utils/index';
import { getRandomNumberColor } from '../../../constants/colors';
interface DrawNumberDecomposeContentParams {
canvas: WechatMiniprogram.Canvas;
@@ -1,11 +1,11 @@
import { BaseMathDrawService } from './baseMathDraw';
import { BaseDrawService } from '../../../service/baseDraw';
import { drawNumberDecomposeContent } from './numberDecomposeContentDraw';
/**
* 10
* 使
*/
class NumberDecomposeDraw extends BaseMathDrawService {
class NumberDecomposeDraw extends BaseDrawService {
decomposeData: {
problems: Array<{
whole: number | null;
@@ -1,4 +1,4 @@
import { BaseMathDrawService } from './baseMathDraw';
import { BaseDrawService } from '../../../service/baseDraw';
import { drawNumberPreview } from './numberPreviewDraw';
import { drawNumberContent } from './numberContentDraw';
import { drawNumberWriteContent } from './numberWriteDraw';
@@ -7,7 +7,7 @@ import { drawNumberWriteContent } from './numberWriteDraw';
*
* 使
*/
class NumberFindDraw extends BaseMathDrawService {
class NumberFindDraw extends BaseDrawService {
selectedNumber: number;
functionId: string; // 功能ID,用于判断绘制类型
@@ -1,4 +1,4 @@
import { getImage } from '../../utils/index';
import { getImage } from '../../../utils/index';
/**
*
+10
View File
@@ -5,6 +5,7 @@ import { PAPER_SIZE } from '../../constants/colors';
import { getWordsSvgData } from '../../utils/getWordsSvgJson';
import { WORDS } from '../../constants/words';
import { CharacterItem } from '../../types/characterType';
import tracker from '../../utils/tracker';
Page({
canvas: null as Canvas | null,
@@ -360,6 +361,9 @@ Page({
return;
}
// 上报下载埋点
tracker.reportDownload('练字贴');
// 直接下载
checkAndSaveImage(this.canvas);
},
@@ -371,6 +375,9 @@ Page({
// 分享功能
onShareAppMessage() {
// 上报分享埋点
tracker.reportShare('练字贴');
return {
title: '涂鸦丫-涂色|练字|识字|打印',
path: '/pages/copyBook/copyBook',
@@ -379,6 +386,9 @@ Page({
};
},
onShareTimeline() {
// 上报分享埋点
tracker.reportShare('练字贴');
return {
title: '涂鸦丫-涂色|练字|识字|打印',
query: '/pages/copyBook/copyBook',
+4 -6
View File
@@ -1,11 +1,9 @@
import config from '../../config/config';
const defaultPrintConfig: PrintConfig = config.printHeader as PrintConfig;
import { defaultPrintConfig } from '../../config/config';
Page({
data: {
printConfig: defaultPrintConfig,
enableDebug: false
enableDebug: false,
},
onLoad() {
@@ -41,5 +39,5 @@ Page({
toggleDebug() {
const enableDebug = !this.data.enableDebug;
this.setData({ enableDebug });
}
});
},
});
-199
View File
@@ -1,199 +0,0 @@
/**index.less**/
page {
height: 100vh;
display: flex;
flex-direction: column;
background-color: #f5f5f5;
}
.container {
padding: 20rpx;
height: 100vh;
overflow-y: auto;
}
/* 输入区域样式 */
.input-section {
background-color: #fff;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
}
.input-row {
display: flex;
align-items: center;
gap: 20rpx;
}
.word-input {
flex: 1;
height: 80rpx;
border: 2rpx solid #e0e0e0;
border-radius: 40rpx;
padding: 0 30rpx;
font-size: 32rpx;
background-color: #fafafa;
}
.generate-btn {
width: 160rpx;
height: 80rpx;
background-color: #07c160;
color: #fff;
border-radius: 40rpx;
font-size: 28rpx;
border: none;
display: flex;
align-items: center;
justify-content: center;
}
/* 通用区域样式 */
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.clear-btn {
background-color: #ff4757;
color: #fff;
border-radius: 20rpx;
font-size: 24rpx;
padding: 10rpx 20rpx;
border: none;
}
/* 已选择的汉字区域 */
.selected-section {
background-color: #fff;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
}
.word-list {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
}
.word-item.selected {
background-color: #07c160;
color: #fff;
border-radius: 50rpx;
padding: 20rpx 30rpx;
position: relative;
min-width: 80rpx;
text-align: center;
}
.word-text {
font-size: 32rpx;
font-weight: bold;
}
.remove-btn {
position: absolute;
top: -10rpx;
right: -10rpx;
width: 40rpx;
height: 40rpx;
background-color: #ff4757;
color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
font-weight: bold;
cursor: pointer;
}
/* 可选汉字区域 */
.available-section {
background-color: #fff;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
}
.word-grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 20rpx;
}
.word-item.available {
background-color: #f0f0f0;
color: #333;
border-radius: 50rpx;
padding: 20rpx;
text-align: center;
border: 2rpx solid #e0e0e0;
transition: all 0.3s ease;
}
.word-item.available:active {
background-color: #e0e0e0;
transform: scale(0.95);
}
.word-item.available.disabled {
background-color: #ccc;
color: #999;
cursor: not-allowed;
}
.word-item.available.disabled:active {
background-color: #ccc;
transform: none;
}
/* 功能模块区域 */
.cards-section {
background-color: #fff;
border-radius: 20rpx;
padding: 30rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
}
.scroll-view {
height: 400rpx;
}
.card {
margin: 10rpx;
padding: 20rpx;
background-color: #f8f9fa;
border-radius: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
width: calc(33.33% - 20rpx);
box-sizing: border-box;
display: inline-block;
vertical-align: top;
transition: all 0.3s ease;
}
.card:active {
transform: scale(0.95);
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.2);
}
.card-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
text-align: center;
}
-141
View File
@@ -1,141 +0,0 @@
Page({
data: {
inputWord: '', // 输入框中的汉字
wordList: [] as string[], // 选中的汉字列表
availableWords: [] as string[], // 从grade3.json中提取的前30个汉字
cards: [
{
key: 0,
title: '文本绘制',
url: '/demoPages/textPaint/textPaint',
},
{
key: 1,
title: '图形绘制',
url: '/demoPages/shapePrint/index',
},
{
key: 2,
title: '文本绘制',
url: '/demoPages/textPaint/textPaint',
},
{
key: 3,
title: '文本绘制',
url: '/demoPages/textPaint/textPaint',
},
],
},
onLoad() {
this.loadGrade3Words();
},
// 加载grade3.json中的前30个汉字
loadGrade3Words() {
const fs = wx.getFileSystemManager();
try {
const fileContent = fs.readFileSync('grade3.json', 'utf8') as string;
const grade3Data = JSON.parse(fileContent);
const words = Object.keys(grade3Data).slice(0, 30);
this.setData({
availableWords: words
});
} catch (error) {
console.error('加载grade3.json失败:', error);
// 如果文件读取失败,使用一些示例汉字
const fallbackWords = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '人', '口', '手', '足', '目', '耳', '鼻', '舌', '心', '肝', '脾', '肺', '肾', '胃', '肠', '胆', '膀', '胱', '皮', '毛'];
this.setData({
availableWords: fallbackWords
});
}
},
// 输入框输入事件
onInputChange(e: WechatMiniprogram.Input) {
this.setData({
inputWord: e.detail.value
});
},
// 生成按钮点击事件
onGenerateClick() {
const { inputWord, wordList } = this.data;
if (inputWord && inputWord.trim()) {
// 检查是否已经存在
if (!wordList.includes(inputWord.trim())) {
const newWordList = [...wordList, inputWord.trim()];
this.setData({
wordList: newWordList,
inputWord: '' // 清空输入框
});
wx.showToast({
title: '已添加到列表',
icon: 'success'
});
} else {
wx.showToast({
title: '该汉字已存在',
icon: 'none'
});
}
} else {
wx.showToast({
title: '请输入汉字',
icon: 'none'
});
}
},
// 选择汉字点击事件
onWordSelect(e: WechatMiniprogram.TouchEvent) {
const { word } = e.currentTarget.dataset;
const { wordList } = this.data;
if (!wordList.includes(word)) {
const newWordList = [...wordList, word];
this.setData({
wordList: newWordList
});
wx.showToast({
title: '已选择',
icon: 'success'
});
} else {
wx.showToast({
title: '已选择过',
icon: 'none'
});
}
},
// 从列表中移除汉字
onRemoveWord(e: WechatMiniprogram.TouchEvent) {
const { index } = e.currentTarget.dataset;
const { wordList } = this.data;
const newWordList = wordList.filter((_, i) => i !== index);
this.setData({
wordList: newWordList
});
wx.showToast({
title: '已移除',
icon: 'success'
});
},
// 清空所有选中的汉字
onClearAll() {
this.setData({
wordList: []
});
wx.showToast({
title: '已清空',
icon: 'success'
});
},
tapCard(e: WechatMiniprogram.TouchEvent) {
const { url } = e.currentTarget.dataset;
wx.navigateTo({ url });
},
});
-81
View File
@@ -1,81 +0,0 @@
<!--index.wxml-->
<view class="container">
<!-- 输入区域 -->
<view class="input-section">
<view class="input-row">
<input
class="word-input"
placeholder="请输入汉字"
value="{{inputWord}}"
bindinput="onInputChange"
maxlength="10" />
<button class="generate-btn" bindtap="onGenerateClick">生成</button>
</view>
</view>
<!-- 已选择的汉字列表 -->
<view class="selected-section" wx:if="{{wordList.length > 0}}">
<view class="section-header">
<text class="section-title"
>已选择的汉字 ({{wordList.length}})</text
>
<button class="clear-btn" bindtap="onClearAll" size="mini">
清空
</button>
</view>
<view class="word-list">
<view
class="word-item selected"
wx:for="{{wordList}}"
wx:key="*this"
wx:for-item="word"
wx:for-index="index">
<text class="word-text">{{word}}</text>
<text
class="remove-btn"
bindtap="onRemoveWord"
data-index="{{index}}"
>×</text
>
</view>
</view>
</view>
<!-- 可选汉字区域 -->
<view class="available-section">
<view class="section-header">
<text class="section-title"
>可选汉字 ({{availableWords.length}})</text
>
</view>
<view class="word-grid">
<view
class="word-item available {{wordList.includes(word) ? 'disabled' : ''}}"
wx:for="{{availableWords}}"
wx:key="*this"
wx:for-item="word"
bindtap="onWordSelect"
data-word="{{word}}">
<text class="word-text">{{word}}</text>
</view>
</view>
</view>
<!-- 原有的卡片区域 -->
<view class="cards-section">
<view class="section-header">
<text class="section-title">功能模块</text>
</view>
<scroll-view class="scroll-view" scroll-y type="list">
<view
class="card"
wx:for="{{cards}}"
wx:for-item="card"
wx:key="*this"
bindtap="tapCard"
data-url="{{card.url}}">
<view class="card-title">{{card.title}}</view>
</view>
</scroll-view>
</view>
</view>
+18 -3
View File
@@ -7,6 +7,7 @@ import {
} from '../../service/drawServiceFactory';
import { checkAndSaveImage } from '../../utils/saveImage';
import { shouldShowShareGuide } from '../../utils/shareGuide';
import tracker from '../../utils/tracker';
Page({
canvas: null as Canvas | null,
@@ -29,7 +30,12 @@ Page({
showShareDialog: false, // 显示分享引导弹窗
},
onLoad() {
onLoad(options: { template?: string }) {
const { template } = options;
this.setData({
selectedTemplate: template || 'grid',
});
const hasShowIntroduction =
wx.getStorageSync('hasShowIntroduction') || false;
@@ -292,6 +298,9 @@ Page({
return;
}
// 上报下载埋点
tracker.reportDownload('涂色识字', this.data.selectedTemplate);
// 直接下载
checkAndSaveImage(this.canvas);
},
@@ -344,17 +353,23 @@ Page({
},
onShareAppMessage() {
// 上报分享埋点
tracker.reportShare('涂色识字');
return {
title: '涂鸦丫-涂色|识字|画画|打印',
path: '/pages/index/index',
path: `/pages/index/index?template=${this.data.selectedTemplate}`,
imageUrl:
'https://cdn.joeyone.cn/doodle/share-img/index-share-v2.png',
};
},
onShareTimeline() {
// 上报分享埋点
tracker.reportShare('涂色识字');
return {
title: '涂鸦丫-涂色|识字|画画|打印',
query: '/pages/index/index',
query: `/pages/index/index?template=${this.data.selectedTemplate}`,
imageUrl:
'https://cdn.joeyone.cn/doodle/share-img/index-share-v2.png',
};
+10
View File
@@ -3,6 +3,7 @@ import { WATER_COLORS, PAPER_SIZE } from '../../constants/colors';
import ShapeDrawService from '../../service/shapeDrawService';
import { checkAndSaveImage } from '../../utils/saveImage';
import { shouldShowShareGuide } from '../../utils/shareGuide';
import tracker from '../../utils/tracker';
Page({
canvas: null as Canvas | null,
@@ -296,6 +297,9 @@ Page({
return;
}
// 上报下载埋点
tracker.reportDownload('图形涂色');
// 直接下载
checkAndSaveImage(this.canvas);
},
@@ -306,6 +310,9 @@ Page({
},
onShareAppMessage() {
// 上报分享埋点
tracker.reportShare('图形涂色');
return {
title: '涂鸦丫-涂色|识字|图形|打印',
path: '/pages/shape/index',
@@ -314,6 +321,9 @@ Page({
};
},
onShareTimeline() {
// 上报分享埋点
tracker.reportShare('图形涂色');
return {
title: '涂鸦丫-涂色|识字|图形|打印',
query: '/pages/shape/index',
-89
View File
@@ -1,89 +0,0 @@
# 汉字选择页面 (wordDemo)
## 功能描述
这是一个汉字输入和选择的页面,主要包含以下功能:
### 上部区域 - 汉字输入
- 输入框:可以输入任意汉字(最多10个字符)
- 生成按钮:点击后将输入的汉字添加到已选择列表中
- **汉字校验**:自动校验输入内容是否为汉字,非汉字会提示错误
- **智能分割**:支持输入多个汉字,自动分割为单个汉字存储
### 中部区域 - 已选择的汉字
- 显示所有已选择的汉字
- 每个汉字都有删除按钮(×)
- 清空按钮:一键清空所有已选择的汉字
- 显示已选择汉字的数量
### 下部区域 - 可选汉字
-`demo/grade3.json` 文件中提取前30个汉字
- 以6列网格形式展示
- 已选择的汉字会显示为禁用状态
- 点击未选择的汉字可以添加到列表中
## 核心特性
### 🔍 **汉字校验**
- 使用Unicode范围 `\u4e00-\u9fff` 校验汉字
- 非汉字输入会显示Toast提示"请输入汉字"
- 确保 `wordList` 中只存储纯汉字
### ✂️ **智能分割**
- 支持输入多个汉字(如:"你好世界")
- 自动分割为单个汉字:["你", "好", "世", "界"]
- 过滤重复汉字,避免重复添加
- 显示实际添加的汉字数量
### 💾 **数据管理**
- `wordList` 中每个元素都是单个汉字
- 自动去重,避免重复存储
- 支持批量添加和单个删除
## 数据结构
- `inputWord`: 输入框中的汉字
- `wordList`: 已选择的汉字列表(string[]类型,每个元素为单个汉字)
- `availableWords`: 从grade3.json中提取的前30个汉字
## 使用方法
1. 在输入框中输入汉字(支持多个汉字)
2. 点击"生成"按钮,系统自动校验和分割
3. 点击下方网格中的汉字进行选择
4. 已选择的汉字会显示在上方,可以单独删除或一键清空
5. 所有选择的汉字都存储在 `this.data.wordList`
## 技术实现
### 汉字校验
```typescript
isChineseText(text: string): boolean {
const chineseRegex = /^[\u4e00-\u9fff]+$/;
return chineseRegex.test(text);
}
```
### 文本分割
```typescript
splitToSingleChars(text: string): string[] {
const chineseRegex = /[\u4e00-\u9fff]/g;
const matches = text.match(chineseRegex);
return matches || [];
}
```
## 文件结构
- `index.ts`: 页面逻辑文件
- `index.wxml`: 页面模板文件
- `index.less`: 页面样式文件
- `index.json`: 页面配置文件
-6
View File
@@ -1,6 +0,0 @@
{
"navigationBarTitleText": "汉字选择",
"navigationBarBackgroundColor": "#d2e7d8",
"backgroundColor": "#f5f5f5",
"enablePullDownRefresh": false
}
-237
View File
@@ -1,237 +0,0 @@
/**index.less**/
page {
height: 100vh;
background-color: #f5f5f5;
}
.container {
padding: 20rpx;
height: 100vh;
overflow-y: auto;
}
.preview-section {
margin-top: 24rpx;
background: #fff;
border-radius: 20rpx;
padding: 24rpx;
box-shadow: 0 6rpx 8rpx rgba(0, 0, 0, 0.15);
}
.section-title {
display: block;
text-align: center;
font-size: 32rpx;
font-weight: bold;
margin-bottom: 24rpx;
}
.tianzige {
display: grid;
grid-auto-rows: min-content;
row-gap: 12rpx;
}
.tzg-row {
display: grid;
grid-template-columns: repeat(12, 1fr);
column-gap: 12rpx;
}
.tzg-cell {
position: relative;
background: #fff;
border: 4rpx solid #333;
height: 120rpx;
border-radius: 6rpx;
overflow: hidden;
}
.tzg-word {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
font-size: 72rpx;
color: #222;
font-family: 'Kaiti', 'KaiTi', 'STKaiti', 'FZKai-Z03S', serif;
}
.mid-line {
position: absolute;
background: repeating-linear-gradient(to right,
rgba(0, 0, 0, 0.25),
rgba(0, 0, 0, 0.25) 2rpx,
transparent 2rpx,
transparent 6rpx);
}
.mid-line.h {
left: 0;
right: 0;
top: 50%;
height: 2rpx;
transform: translateY(-50%);
}
.mid-line.v {
top: 0;
bottom: 0;
left: 50%;
width: 2rpx;
transform: translateX(-50%);
}
.empty-tip {
text-align: center;
color: #999;
font-size: 28rpx;
}
/* 输入区域样式 */
.input-section {
background-color: #fff;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
}
.input-row {
display: flex;
align-items: center;
gap: 20rpx;
}
.word-input {
flex: 1;
height: 80rpx;
border: 2rpx solid #e0e0e0;
border-radius: 40rpx;
padding: 0 30rpx;
font-size: 32rpx;
background-color: #fafafa;
}
.generate-btn {
width: 160rpx;
height: 80rpx;
background-color: #07c160;
color: #fff;
border-radius: 40rpx;
font-size: 28rpx;
border: none;
display: flex;
align-items: center;
justify-content: center;
}
/* 通用区域样式 */
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.clear-btn {
background-color: #ff4757;
color: #fff;
border-radius: 20rpx;
font-size: 24rpx;
padding: 10rpx 20rpx;
border: none;
}
/* 已选择的汉字区域 */
.selected-section {
background-color: #fff;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
}
.word-list {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
}
.word-item.selected {
background-color: #07c160;
color: #fff;
border-radius: 50rpx;
padding: 20rpx 30rpx;
position: relative;
min-width: 80rpx;
text-align: center;
}
.word-text {
font-size: 32rpx;
font-weight: bold;
}
.remove-btn {
position: absolute;
top: -10rpx;
right: -10rpx;
width: 40rpx;
height: 40rpx;
background-color: #ff4757;
color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24rpx;
font-weight: bold;
cursor: pointer;
}
/* 可选汉字区域 */
.available-section {
background-color: #fff;
border-radius: 20rpx;
padding: 30rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
}
.word-grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 20rpx;
}
.word-item.available {
background-color: #f0f0f0;
color: #333;
border-radius: 50rpx;
padding: 20rpx;
text-align: center;
border: 2rpx solid #e0e0e0;
transition: all 0.3s ease;
}
.word-item.available:active {
background-color: #e0e0e0;
transform: scale(0.95);
}
.word-item.available.disabled {
background-color: #ccc;
color: #999;
cursor: not-allowed;
}
.word-item.available.disabled:active {
background-color: #ccc;
transform: none;
}
-179
View File
@@ -1,179 +0,0 @@
Page({
data: {
inputWord: '', // 输入框中的汉字
wordList: [] as string[], // 选中的汉字列表
availableWords: [] as string[], // 从grade3.json中提取的前30个汉字
// 田字格配置
rows: 10,
cols: 12,
rowsArray: [] as number[],
colsArray: [] as number[],
rowHeaderWords: [] as string[],
},
onLoad() {
this.loadGrade3Words();
this.initGrid();
},
// 加载grade3.json中的前30个汉字
loadGrade3Words() {
const fs = wx.getFileSystemManager();
try {
const fileContent = fs.readFileSync('demo/grade3.json', 'utf8') as string;
const grade3Data = JSON.parse(fileContent);
const words = Object.keys(grade3Data).slice(0, 30);
this.setData({
availableWords: words
});
} catch (error) {
console.error('加载grade3.json失败:', error);
// 如果文件读取失败,使用一些示例汉字
const fallbackWords = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '人', '口', '手', '足', '目', '耳', '鼻', '舌', '心', '肝', '脾', '肺', '肾', '胃', '肠', '胆', '膀', '胱', '皮', '毛'];
this.setData({
availableWords: fallbackWords
});
}
},
// 输入框输入事件
onInputChange(e: WechatMiniprogram.Input) {
this.setData({
inputWord: e.detail.value
});
},
// 生成按钮点击事件
onGenerateClick() {
const { inputWord, wordList } = this.data;
if (inputWord && inputWord.trim()) {
const inputText = inputWord.trim();
// 校验输入是否为汉字
if (!this.isChineseText(inputText)) {
wx.showToast({
title: '请输入汉字',
icon: 'none'
});
return;
}
// 将输入文本分割为单个汉字
const singleChars = this.splitToSingleChars(inputText);
// 过滤掉已存在的汉字
const newChars = singleChars.filter(char => !wordList.includes(char));
if (newChars.length === 0) {
wx.showToast({
title: '所有汉字都已存在',
icon: 'none'
});
return;
}
// 添加到列表中
const newWordList = [...wordList, ...newChars];
this.setData({
wordList: newWordList,
inputWord: '' // 清空输入框
}, () => {
this.updateRowHeaderWords();
});
wx.showToast({
title: `已添加${newChars.length}个汉字`,
icon: 'success'
});
} else {
wx.showToast({
title: '请输入汉字',
icon: 'none'
});
}
},
// 校验文本是否为汉字
isChineseText(text: string): boolean {
// 汉字Unicode范围:\u4e00-\u9fff
const chineseRegex = /^[\u4e00-\u9fff]+$/;
return chineseRegex.test(text);
},
// 将文本分割为单个汉字
splitToSingleChars(text: string): string[] {
// 使用正则表达式匹配每个汉字
const chineseRegex = /[\u4e00-\u9fff]/g;
const matches = text.match(chineseRegex);
return matches || [];
},
// 选择汉字点击事件
onWordSelect(e: WechatMiniprogram.TouchEvent) {
const { word } = e.currentTarget.dataset;
const { wordList } = this.data;
if (!wordList.includes(word)) {
const newWordList = [...wordList, word];
this.setData({
wordList: newWordList
});
wx.showToast({
title: '已选择',
icon: 'success'
});
} else {
wx.showToast({
title: '已选择过',
icon: 'none'
});
}
},
// 从列表中移除汉字
onRemoveWord(e: WechatMiniprogram.TouchEvent) {
const { index } = e.currentTarget.dataset;
const { wordList } = this.data;
const newWordList = wordList.filter((_, i) => i !== index);
this.setData({
wordList: newWordList
}, () => {
this.updateRowHeaderWords();
});
wx.showToast({
title: '已移除',
icon: 'success'
});
},
// 清空所有选中的汉字
onClearAll() {
this.setData({
wordList: []
}, () => {
this.updateRowHeaderWords();
});
wx.showToast({
title: '已清空',
icon: 'success'
});
},
// 初始化田字格行列数组
initGrid() {
const { rows, cols } = this.data as any;
const rowsArray = Array.from({ length: rows }, (_, i) => i);
const colsArray = Array.from({ length: cols }, (_, i) => i);
this.setData({ rowsArray, colsArray });
},
// 根据选中的汉字生成每行首字数组
updateRowHeaderWords() {
const { rows, wordList } = this.data as any;
const rowHeaderWords: string[] = [];
for (let i = 0; i < rows; i++) {
rowHeaderWords.push(wordList[i] || '');
}
this.setData({ rowHeaderWords });
},
});
-88
View File
@@ -1,88 +0,0 @@
<!--index.wxml-->
<view class="container">
<!-- 上部:输入区域 -->
<view class="input-section">
<view class="input-row">
<input
class="word-input"
placeholder="请输入汉字(支持多个汉字)"
value="{{inputWord}}"
bindinput="onInputChange"
maxlength="20" />
<button class="generate-btn" bindtap="onGenerateClick">生成</button>
</view>
</view>
<!-- 已选择的汉字列表 -->
<view class="selected-section" wx:if="{{wordList.length > 0}}">
<view class="section-header">
<text class="section-title"
>已选择的汉字 ({{wordList.length}})</text
>
<button class="clear-btn" bindtap="onClearAll" size="mini">
清空
</button>
</view>
<view class="word-list">
<view
class="word-item selected"
wx:for="{{wordList}}"
wx:key="*this"
wx:for-item="word"
wx:for-index="index">
<text class="word-text">{{word}}</text>
<text
class="remove-btn"
bindtap="onRemoveWord"
data-index="{{index}}"
>×</text
>
</view>
</view>
</view>
<!-- 下部:可选汉字区域 -->
<view class="available-section">
<view class="section-header">
<text class="section-title"
>可选汉字 ({{availableWords.length}})</text
>
</view>
<view class="word-grid">
<view
class="word-item available {{wordList.includes(word) ? 'disabled' : ''}}"
wx:for="{{availableWords}}"
wx:key="*this"
wx:for-item="word"
bindtap="onWordSelect"
data-word="{{word}}">
<text class="word-text">{{word}}</text>
</view>
</view>
</view>
</view>
<!-- 预览:田字格 -->
<view class="preview-section">
<text class="section-title">预览练字田字格</text>
<view wx:if="{{rowHeaderWords.length > 0}}" class="tianzige">
<view
class="tzg-row"
wx:for="{{rowsArray}}"
wx:for-index="rowIndex"
wx:key="rowIndex">
<view
class="tzg-cell"
wx:for="{{colsArray}}"
wx:for-index="colIndex"
wx:key="{{rowIndex}}-{{colIndex}}">
<text wx:if="{{colIndex === 0}}" class="tzg-word"
>{{rowHeaderWords[rowIndex]}}</text
>
<view class="mid-line h"></view>
<view class="mid-line v"></view>
</view>
</view>
</view>
<view wx:else class="empty-tip">请选择或输入汉字后生成田字格</view>
</view>
+172
View File
@@ -0,0 +1,172 @@
# Service 目录绘制服务改造完成总结
## 改造目标
将所有绘制服务统一使用 `baseDraw.ts``baseHeaderDraw.ts` 作为基类,提取公共部分并统一 Header 绘制。
## 完成的工作
### 1. ✅ 统一基础服务类
所有绘制服务现在都继承自 `BaseDrawService`(位于 `service/baseDraw.ts`):
-`TextDrawService` - 文字涂色服务
-`ShapeDrawService` - 图形涂色服务
-`FindWordDrawService` - 找字涂色服务
-`WordDrawService` - 田字格练字服务
### 2. ✅ 统一 Header 绘制
所有服务使用统一的 Header 绘制功能(位于 `service/baseHeaderDraw.ts`):
- `drawBaseHeader()` - 完整 Header(包含 Logo、应用名称、标题、副标题)
- `drawBaseMiniHeader()` - 迷你 Header(简化版本)
### 3. ✅ 统一使用逻辑像素
所有服务现在使用逻辑像素(尺寸除以3),与数学模块保持一致:
- Canvas 尺寸通过 `setPaper()` 统一处理 DPRdevice pixel ratio
- 所有绘制代码中的硬编码尺寸都已转换为逻辑像素
- 坐标计算统一使用 `this.canvasWidth``this.canvasHeight`
### 4. ✅ 清理重复代码
删除了以下重复方法,统一使用基类实现:
- `setPrintConfig()` - 统一在基类中实现
- `setPaper()` - 统一在基类中实现
- `clear()` - 统一在基类中实现
- `drawHeader()` / `drawMiniHeader()` - 统一在基类中实现
- `drawDivider()` - 统一在基类中实现
## 改造详情
### TextDrawService
**改造内容:**
- 继承 `BaseDrawService`
- 移除自定义的 `setPaper()`, `clear()`, `drawHeader()`, `drawMiniHeader()`
- 所有尺寸转换为逻辑像素:
- 半径:80 → 27
- 字体:72px → 24px, 48px → 16px
- 边距:80 → 27, 220 → 73
- 坐标:200 → 67, 304 → 101
**关键方法:**
- `drawLegend()` - 绘制示例区域
- `drawContent()` - 绘制内容区域
### ShapeDrawService
**改造内容:**
- 继承 `BaseDrawService`
- 移除自定义的绘制方法
- 所有尺寸转换为逻辑像素:
- 图形大小:200 → 67, 180 → 60
- 字体:36px → 12px
- 间距:120 → 40, 160 → 53
**关键方法:**
- `drawLegend()` - 绘制示例区域
- `drawContent()` - 绘制内容区域
### FindWordDrawService
**改造内容:**
- 继承 `BaseDrawService`
- 移除自定义的绘制方法
- 所有尺寸转换为逻辑像素:
- 半径:80 → 27
- 字体:72px → 24px
- 模板坐标转换:`pos.x / 3`, `pos.y / 3`
**关键方法:**
- `drawContent()` - 绘制找字内容(中心大字 + 四周字符)
**特殊处理:**
- `getPositionsFromTemplate()` 函数中增加了坐标转换逻辑,将模板中的原始像素坐标转换为逻辑像素
### WordDrawService
**改造内容:**
- 继承 `BaseDrawService`
- 移除自定义的绘制方法
- 所有尺寸转换为逻辑像素:
- 田字格大小:140 → 47
- 边距:120 → 40, 50 → 17
- 间距:24 → 8, 36 → 12
**关键方法:**
- `drawLayout()` - 绘制页眉和基础布局
- `drawContent()` - 绘制田字格和练字内容
- `drawPracticeContent()` - 绘制练习内容
- `getMaxGridLayout()` - 计算最大网格布局
## 架构优势
1.**代码复用**:公共逻辑集中在 `BaseDrawService` 基类中
2.**统一 Header**:所有服务使用相同的 Header 绘制逻辑
3.**统一像素标准**:所有服务使用逻辑像素,保持一致
4.**易于维护**:修改 Header 或基础功能只需修改一个地方
5.**类型安全**TypeScript 类型支持完整
## 注意事项
### 坐标转换
所有硬编码的尺寸值都已转换为逻辑像素(除以3):
```typescript
// 原始像素 → 逻辑像素
const radius = 27; // 80/3≈27
const fontSize = 24; // 72/3=24
const margin = 40; // 120/3=40
```
### 模板坐标转换
`FindWordDrawService` 中使用的 `POSITION_TEMPLATES` 坐标是基于原始像素的,需要在使用时转换:
```typescript
const x = centerX + pos.x / 3; // 转换为逻辑像素
const y = centerY + pos.y / 3; // 转换为逻辑像素
```
### Canvas 尺寸
所有服务现在统一使用:
- `this.canvasWidth` - 逻辑像素宽度
- `this.canvasHeight` - 逻辑像素高度
这些值在 `setPaper()` 中设置,已经处理了 DPR。
## 向后兼容性
-`drawServiceFactory.ts` 接口无需修改
- ✅ 所有服务的公共接口保持不变
- ✅ 只改变了内部实现方式
## 后续优化建议
1. 可以考虑将 `findWordTemplate.ts` 中的坐标模板也转换为逻辑像素,避免运行时转换
2. 可以考虑创建更多的基础绘制工具函数(如绘制表格、绘制网格等)
3. 可以考虑支持更多 Paper 尺寸(目前仅支持 A4)
## 测试建议
1. 验证所有绘制服务功能正常
2. 验证 Header 正确显示
3. 验证尺寸和比例正确
4. 验证不同 Header 类型(wechat, LogoImage, noLogoImage, minimal
5. 验证打印输出质量
+218
View File
@@ -0,0 +1,218 @@
# 绘制服务重构总结
## 重构目标
基于 `baseDraw.ts``baseHeaderDraw.ts` 统一所有绘制服务的公共部分,提取并统一 Header 绘制功能。
## 完成的工作
### 1. ✅ 统一基础服务类
所有绘制服务现在都继承自 `BaseDrawService`(位于 `service/baseDraw.ts`):
-`AdditionDraw` - 加减法计算
-`CompareDraw` - 数一数比大小
-`CountMatchDraw` - 数一数连一连
-`CountingSelectDraw` - 数一数选一选/填一填
-`MissingNumberDraw` - 填上缺少的数字
-`NumberColorDraw` - 按数字涂颜色
-`NumberDecomposeDraw` - 10以内数的分与合
-`NumberFindDraw` - 数字涂色(找数字、写数字)
### 2. ✅ 统一 Header 绘制
所有服务使用统一的 Header 绘制功能(位于 `service/baseHeaderDraw.ts`):
- `drawBaseHeader()` - 完整 Header(包含 Logo、应用名称、标题、副标题)
- `drawBaseMiniHeader()` - 迷你 Header(简化版本)
### 3. ✅ 修复相关导入
- ✅ 修复 `base/pageMixin.ts` 中的导入路径
-`../mathPages/service/baseMathDraw` 改为 `../service/baseDraw`
### 4. ✅ 清理旧文件
- ✅ 删除 `mathPages/shared/service/baseMathDraw.ts`(已被 `BaseDrawService` 替代)
- ✅ 删除 `mathPages/shared/service/mathHeaderDraw.ts`(已被 `baseHeaderDraw.ts` 替代)
## 架构设计
### 基础服务类 (`BaseDrawService`)
提供以下公共功能:
```typescript
class BaseDrawService {
// Canvas 和上下文
canvas: Canvas;
ctx: RenderingContext;
// 配置
options: Record<string, any>;
paperSize: PaperSize;
headerType: PrintHeader;
// 坐标管理
currentX: number;
currentY: number;
canvasWidth: number;
canvasHeight: number;
// 公共方法
setPrintConfig(); // 设置打印配置
setPaper(); // 设置 Paper 尺寸
clear(); // 清除画布
drawHeader(); // 绘制完整 Header
drawMiniHeader(); // 绘制迷你 Header
drawDivider(); // 绘制分割线
}
```
### Header 绘制 (`baseHeaderDraw.ts`)
提供统一的 Header 绘制函数:
- `drawBaseHeader()` - 支持多种 Header 类型:
- `wechat` - 微信小程序码(默认)
- `LogoImage` - Logo 图片
- `noLogoImage` - 无 Logo
- `minimal` - 极简模式
- `drawBaseMiniHeader()` - 迷你版本,居中显示
### 具体服务类
每个具体服务类:
1. **继承 `BaseDrawService`**
2. **实现 `draw()` 方法**,通常包含以下步骤:
```typescript
async draw(data: any) {
// 1. 设置配置
this.setPrintConfig();
// 2. 清除并设置 Paper
this.clear();
this.setPaper();
// 3. 绘制 Header
if (this.headerType !== 'minimal') {
await this.drawHeader();
} else {
this.drawMiniHeader();
}
// 4. 绘制分割线
this.drawDivider();
// 5. 绘制内容(调用 ContentDraw 函数)
await drawXXXContent({
canvas: this.canvas,
ctx: this.ctx,
data: data,
canvasWidth: this.canvasWidth,
startY: this.currentY,
});
}
```
3. **使用 ContentDraw 函数**绘制具体内容(例如 `drawAdditionContent`
## 文件结构
```
service/
├── baseDraw.ts # 基础绘制服务类 ✅
├── baseHeaderDraw.ts # Header 绘制函数 ✅
└── ...
mathPages/shared/service/
├── additionDraw.ts # 加减法绘制服务 ✅
├── compareDraw.ts # 比较绘制服务 ✅
├── countMatchDraw.ts # 连线绘制服务 ✅
├── countingSelectDraw.ts # 选择绘制服务 ✅
├── missingNumberDraw.ts # 缺失数字绘制服务 ✅
├── numberColorDraw.ts # 数字涂色绘制服务 ✅
├── numberDecomposeDraw.ts # 分解绘制服务 ✅
├── numberFindDraw.ts # 数字查找绘制服务 ✅
├── additionContentDraw.ts # 加减法内容绘制
├── compareContentDraw.ts # 比较内容绘制
├── ... # 其他 ContentDraw 函数
└── (已删除)
├── baseMathDraw.ts # ❌ 已删除
└── mathHeaderDraw.ts # ❌ 已删除
```
## 使用示例
### 创建新的绘制服务
```typescript
import { BaseDrawService } from '../../../service/baseDraw';
import { drawMyContent } from './myContentDraw';
class MyDraw extends BaseDrawService {
myData: any;
constructor(
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, any>,
) {
super(canvas, ctx, options);
this.myData = null;
}
async draw(myData: any) {
if (!myData) return;
this.setPrintConfig();
this.myData = myData;
this.clear();
this.setPaper();
// 绘制 Header
if (this.headerType !== 'minimal') {
await this.drawHeader();
} else {
this.drawMiniHeader();
}
// 绘制内容
this.drawDivider();
await drawMyContent({
canvas: this.canvas,
ctx: this.ctx,
data: this.myData,
canvasWidth: this.canvasWidth,
startY: this.currentY,
});
}
}
export default MyDraw;
```
## 优势
1. ✅ **代码复用**:公共逻辑集中在基类中
2. ✅ **统一 Header**:所有服务使用相同的 Header 绘制逻辑
3. ✅ **易于维护**:修改 Header 只需修改一个地方
4. ✅ **类型安全**TypeScript 类型支持完整
5. ✅ **扩展性好**:新增服务只需继承基类并实现 `draw()` 方法
## 注意事项
1. **ContentDraw 函数**:具体内容绘制函数通常以 `drawXXXContent` 命名,接收参数包括 `canvas`、`ctx`、数据、`canvasWidth`、`startY` 等
2. **坐标管理**:使用 `this.currentY` 管理垂直坐标,ContentDraw 函数需要更新该值
3. **Header 类型**:通过 `this.headerType` 判断使用完整 Header 还是迷你 Header
4. **逻辑像素**:所有尺寸都已除以 3,使用逻辑像素
## 后续优化建议
1. 考虑将 ContentDraw 函数也提取到 `service/` 目录下的统一位置
2. 可以考虑创建更多的基础绘制工具函数(如绘制表格、绘制网格等)
3. 可以考虑支持更多 Paper 尺寸(目前仅支持 A4)
@@ -1,11 +1,18 @@
import { PAPER_SIZE } from '../../constants/colors';
import { drawMathHeader, drawMathMiniHeader } from './mathHeaderDraw';
import { PAPER_SIZE } from '../constants/colors';
import { drawBaseHeader, drawBaseMiniHeader } from './baseHeaderDraw';
/**
*
* Paper设置和Header绘制功能
*
* Paper设置和Header绘制功能
*
*
* - Canvas
* - Paper A4
* - Header Header Header
* - 线
* -
*/
export class BaseMathDrawService {
export class BaseDrawService {
headerType: PrintHeader = 'wechat';
canvas: WechatMiniprogram.Canvas;
ctx: RenderingContext;
@@ -22,14 +29,16 @@ export class BaseMathDrawService {
options?: Record<string, any>,
) {
options = options || {};
const { appName, appHint } = getApp().getPrintConfig();
this.canvas = canvas;
this.ctx = ctx;
this.paperSize = 'A4';
this.options = {
appName: '涂鸦丫小程序',
appHint: '在玩耍中学习数学',
appName,
appHint,
title: '看数字,涂一涂',
subTitle: '找一找下面相同的数字,涂上颜色',
...options,
};
this.currentX = 0;
@@ -86,7 +95,7 @@ export class BaseMathDrawService {
async drawHeader() {
this.currentX = 25;
this.currentY = 25;
await drawMathHeader({
await drawBaseHeader({
canvas: this.canvas,
ctx: this.ctx,
headerType: this.headerType,
@@ -107,7 +116,7 @@ export class BaseMathDrawService {
* Header3
*/
drawMiniHeader() {
drawMathMiniHeader({
drawBaseMiniHeader({
ctx: this.ctx,
canvasWidth: this.canvasWidth,
options: {
@@ -1,9 +1,9 @@
import { getMiniCodeImage, getImage } from '../../utils/index';
import { getMiniCodeImage, getImage } from '../utils/index';
/**
* 3
*/
interface DrawMathHeaderParams {
interface drawBaseHeaderParams {
canvas: WechatMiniprogram.Canvas;
ctx: RenderingContext;
headerType: PrintHeader;
@@ -19,13 +19,13 @@ interface DrawMathHeaderParams {
/**
* 3
*/
export async function drawMathHeader({
export async function drawBaseHeader({
canvas,
ctx,
headerType,
options,
onHeaderDrawn,
}: DrawMathHeaderParams): Promise<void> {
}: drawBaseHeaderParams): Promise<void> {
const { appName, appHint, title, subTitle } = options;
let titleX = 108; // 约109.33
@@ -85,14 +85,14 @@ export async function drawMathHeader({
// 调用回调函数
if (onHeaderDrawn) {
onHeaderDrawn(110);
onHeaderDrawn(104);
}
}
/**
* 3
*/
interface DrawMathMiniHeaderParams {
interface drawBaseMiniHeaderParams {
// canvas: WechatMiniprogram.Canvas;
ctx: RenderingContext;
options: {
@@ -106,12 +106,12 @@ interface DrawMathMiniHeaderParams {
/**
* 3
*/
export function drawMathMiniHeader({
export function drawBaseMiniHeader({
ctx,
options,
canvasWidth,
onHeaderDrawn,
}: DrawMathMiniHeaderParams): void {
}: drawBaseMiniHeaderParams): void {
const { appName, title } = options;
const titleY = 46;
const centerX = canvasWidth / 2;
@@ -124,6 +124,6 @@ export function drawMathMiniHeader({
// 调用回调函数,传递除以3后的currentY
if (onHeaderDrawn) {
onHeaderDrawn(66); // 约66.67
onHeaderDrawn(60); // 约66.67
}
}
+28 -113
View File
@@ -1,8 +1,4 @@
import { PAPER_SIZE } from '../constants/colors';
import {
drawHeader as drawHeaderCommon,
drawMiniHeader as drawMiniHeaderCommon,
} from './headerDrawService';
import { BaseDrawService } from './baseDraw';
import { POSITION_TEMPLATES } from './findWordTemplate';
// ==================== Debug 开关 ====================
@@ -35,13 +31,11 @@ function selectTemplate(): { templateIndex: number } {
/**
* 从指定模板中获取位置
* @param templateIndex 模板索引
* @param centerX 中心X坐标
* @param centerY 中心Y坐标
* @param padding 边距
* @param canvasWidth 画布宽度
* @param canvasHeight 画布高度
* @param radius 字符圆半径
* @returns 位置数组
* @param centerX 中心X坐标(逻辑像素)
* @param centerY 中心Y坐标(逻辑像素)
* @returns 位置数组(逻辑像素)
*
* 注意:POSITION_TEMPLATES 中的坐标是基于原始像素的,需要转换为逻辑像素(除以3)
*/
function getPositionsFromTemplate(
templateIndex: number,
@@ -50,25 +44,18 @@ function getPositionsFromTemplate(
): Array<{ x: number; y: number }> {
const template = POSITION_TEMPLATES[templateIndex];
// 转换为绝对坐标并检查边界
// 转换为绝对坐标(模板坐标除以3转换为逻辑像素)
const positions: Array<{ x: number; y: number }> = [];
for (const pos of template) {
const x = centerX + pos.x;
const y = centerY + pos.y;
const x = centerX + pos.x / 3; // 转换为逻辑像素
const y = centerY + pos.y / 3; // 转换为逻辑像素
positions.push({ x, y });
}
return positions;
}
class FindWordDrawService {
headerType: PrintHeader = 'wechat';
canvas: WechatMiniprogram.Canvas;
ctx: RenderingContext;
options: Record<string, any>;
paperSize: PaperSize;
currentX: number;
currentY: number;
class FindWordDrawService extends BaseDrawService {
colors: string[];
characters: string[];
debug: boolean = false; // Debug模式开关
@@ -79,29 +66,15 @@ class FindWordDrawService {
options?: Record<string, any>,
) {
options = options || {};
this.canvas = canvas;
this.ctx = ctx;
this.paperSize = 'A4';
this.options = {
appName: '涂鸦丫小程序',
appHint: '识字|识图|练字|打印',
super(canvas, ctx, {
title: '找一找 涂 色',
subTitle: '找出相同的文字涂色',
...options,
};
});
// 从options中读取debug参数,如果没有则使用全局DEBUG常量
this.debug = options.debug === true || DEBUG;
this.currentX = 0;
this.currentY = 0;
this.colors = ['#000'];
this.characters = ['王'];
this.setPrintConfig();
}
setPrintConfig() {
const printConfig = getApp().getPrintConfig();
this.headerType = printConfig.header;
this.options.appName = printConfig.appName;
}
async draw(list: Array<{ color: string; word: string }>) {
@@ -111,6 +84,8 @@ class FindWordDrawService {
this.characters = list.map((item) => item.word || '日');
this.clear();
this.setPaper();
// 绘制Header
if (this.headerType !== 'minimal') {
await this.drawHeader();
} else {
@@ -118,64 +93,30 @@ class FindWordDrawService {
}
// 找字模板没有 drawLegend 部分
this.drawDivider();
this.drawContent();
}
async drawHeader() {
this.currentX = 80;
this.currentY = 80;
await drawHeaderCommon({
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;
this.drawLine(this.currentY);
},
});
}
drawMiniHeader() {
drawMiniHeaderCommon({
canvas: this.canvas,
ctx: this.ctx,
options: {
appName: this.options.appName || '涂鸦丫小程序',
title: this.options.title || '找一找 涂 色',
},
onHeaderDrawn: (currentY) => {
this.currentY = currentY;
this.drawLine(this.currentY);
},
});
}
drawContent() {
const { canvas, ctx, characters, colors } = this;
const { ctx, characters, colors } = this;
if (characters.length <= 0) return;
// 第一个字作为中心大字
const firstChar = characters[0];
const firstColor = colors[0];
// 计算内容区域
const contentTop = this.currentY + 50;
const contentBottom = canvas.height - 80;
// 计算内容区域(逻辑像素)
const contentTop = this.currentY + 17; // 50/3≈17
const contentBottom = this.canvasHeight - 27; // 80/3≈27
const contentHeight = contentBottom - contentTop;
// 中心大字的参数
const centerX = canvas.width / 2;
const centerY = contentTop + contentHeight / 2; // 内容区域垂直居中
const centerX = this.canvasWidth / 2;
const centerY = contentTop + contentHeight / 2;
// 普通圆的参数(和textDrawService一致
const radius = 80;
const fontSize = 72;
// 普通圆的参数(逻辑像素
const radius = 27; // 80/3≈27
const fontSize = 24; // 72/3=24
// 先选择模板,获取模板的实际位置数量
const { templateIndex } = selectTemplate();
@@ -242,12 +183,12 @@ class FindWordDrawService {
// 绘制四周的字符
positions.forEach((pos, index) => {
const item = charsToDraw[index];
const y = pos.y; // 已经是绝对坐标,不需要再加contentTop
const y = pos.y; // 已经是绝对坐标
// 绘制圆
ctx.fillStyle = '#fff';
ctx.strokeStyle = '#000';
ctx.lineWidth = 4;
ctx.lineWidth = 1; // 4/3≈1
ctx.beginPath();
ctx.arc(pos.x, y, radius, 0, Math.PI * 2);
ctx.fill();
@@ -265,35 +206,9 @@ class FindWordDrawService {
});
}
// drawLine 已由基类的 drawDivider 替代,保留此方法以保持兼容
drawLine(linY: number) {
const { canvas, ctx } = this;
ctx.strokeStyle = '#000';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(80, linY);
ctx.lineTo(canvas.width - 80, linY);
ctx.stroke();
}
setPaper() {
const { ctx, canvas } = this;
const { pixelRatio: dpr } = wx.getWindowInfo();
let { width, height } = PAPER_SIZE[this.paperSize];
width = width * dpr;
height = height * dpr;
canvas.width = width;
canvas.height = height;
this.clear();
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, width, height);
}
clear() {
const canvas = this.canvas;
this.ctx.clearRect(0, 0, canvas.width, canvas.height);
this.drawDivider();
}
}
+35 -181
View File
@@ -1,69 +1,8 @@
import { PAPER_SIZE } from '../constants/colors';
import { BaseDrawService } from './baseDraw';
import { ShapeCard } from '../constants/shapes';
import { drawShape } from './drawShape';
import {
drawHeader as drawHeaderCommon,
drawMiniHeader as drawMiniHeaderCommon,
} from './headerDrawService';
/**
* 计算图形在画布上的位置,避免重叠
* @param canvasWidth 画布宽度
* @param canvasHeight 画布高度
* @param shapeCount 图形数量
* @param shapeSize 图形大小
* @returns 图形位置数组
*/
function calculateShapePositions(
canvasWidth: number,
canvasHeight: number,
shapeCount: number,
shapeSize: number,
) {
const positions = [];
const padding = 80;
const minSpacing = shapeSize * 1.5; // 最小间距为图形大小的1.5倍
const availableWidth = canvasWidth - 2 * padding;
const availableHeight = canvasHeight - 2 * padding;
// 计算网格布局
const cols = Math.ceil(Math.sqrt(shapeCount));
const rows = Math.ceil(shapeCount / cols);
const cellWidth = availableWidth / cols;
const cellHeight = availableHeight / rows;
for (let i = 0; i < shapeCount; i++) {
const row = Math.floor(i / cols);
const col = i % cols;
// 在单元格内随机位置
const x =
padding +
col * cellWidth +
(cellWidth - shapeSize) / 2 +
(Math.random() - 0.5) * (cellWidth - shapeSize) * 0.3;
const y =
padding +
row * cellHeight +
(cellHeight - shapeSize) / 2 +
(Math.random() - 0.5) * (cellHeight - shapeSize) * 0.3;
positions.push({ x, y });
}
return positions;
}
class ShapeDrawService {
headerType: PrintHeader;
canvas: WechatMiniprogram.Canvas;
ctx: RenderingContext;
options: Record<string, any>;
paperSize: PaperSize;
currentX: number;
currentY: number;
class ShapeDrawService extends BaseDrawService {
shapes: ShapeCard[];
constructor(
@@ -72,116 +11,61 @@ class ShapeDrawService {
options?: Record<string, any>,
) {
options = options || {};
this.canvas = canvas;
this.ctx = ctx;
this.paperSize = 'A4';
this.options = {
appName: '涂鸦丫小程序',
appHint: '涂色|识字|画画|打印',
super(canvas, ctx, {
title: '找一找 涂 色',
subTitle: '给图形涂上相同的颜色',
...options,
};
this.currentX = 0;
this.currentY = 0;
});
this.shapes = [];
this.headerType = 'wechat'; // 默认值
this.setPrintConfig();
}
setPrintConfig() {
const printConfig = getApp().getPrintConfig();
this.headerType = printConfig.header;
this.options.appName = printConfig.appName;
}
draw(shapes: ShapeCard[]) {
async draw(shapes: ShapeCard[]) {
this.setPrintConfig();
this.shapes = shapes.slice(0, 6); // 最多6个图形
this.clear();
this.setPaper();
// 绘制Header
if (this.headerType !== 'minimal') {
this.drawHeader();
await this.drawHeader();
} else {
this.drawMiniHeader();
await this.drawMiniHeader();
}
this.drawDivider();
this.drawLegend();
this.drawContent();
}
async drawHeader() {
this.currentX = 80;
this.currentY = 80;
await drawHeaderCommon({
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;
this.drawLine(this.currentY);
},
});
}
drawMiniHeader() {
drawMiniHeaderCommon({
canvas: this.canvas,
ctx: this.ctx,
options: {
appName: this.options.appName || '涂鸦丫小程序',
title: this.options.title || '找一找 涂 色',
},
onHeaderDrawn: (currentY) => {
this.currentY = currentY;
this.drawLine(this.currentY);
},
});
}
drawLegend() {
const { canvas, ctx, shapes } = this;
const { ctx, shapes } = this;
if (shapes.length <= 0) return;
this.currentY = this.headerType === 'minimal' ? 200 : 304;
const shapeSize = 200;
const rectWidth = 180;
const rectHeight = 80;
// 固定图例的Y位置,不依赖shapeSize
const startY = this.currentY + 125; // 固定距离,不依赖shapeSize
// 逻辑像素尺寸(原始尺寸除以3
this.currentY = this.headerType === 'minimal' ? 75 : 110; // 200/3≈67, 304/3≈101
const shapeSize = 67; // 200/3≈67
const rectWidth = 60; // 180/3=60
const rectHeight = 27; // 80/3≈27
const startY = this.currentY + 42; // 125/3≈42
const len = shapes.length;
const canvasWidth = canvas.width;
// 计算示例图形的间距
const totalWidth = len * shapeSize + (len - 1) * 40;
const startX = (canvasWidth - totalWidth) / 2 + shapeSize / 2;
const totalWidth = len * shapeSize + (len - 1) * 13; // 40/3≈13
const startX = (this.canvasWidth - totalWidth) / 2 + shapeSize / 2;
// 绘制所有图形
shapes.forEach((shape: ShapeCard, index: number) => {
const x = startX + index * (shapeSize + 40);
const x = startX + index * (shapeSize + 13); // 40/3≈13
const y = startY;
// 绘制示例图形
drawShape(ctx, shape, x, y, shapeSize, shape.fillColor);
// // 绘制长方形
// ctx.fillStyle = '#fff';
// ctx.strokeStyle = '#000';
// ctx.lineWidth = 4;
const rectangleX = x - rectWidth / 2;
const rectangleY = y + shapeSize / 2 + 10;
// ctx.strokeRect(rectangleX, rectangleY, rectWidth, rectHeight);
const rectangleY = y + shapeSize / 2 + 3; // 10/3≈3
// 绘制图形名称
ctx.fillStyle = '#333';
ctx.font = 'bold 36px "Microsoft Yahei"';
ctx.font = 'bold 12px "Microsoft Yahei"'; // 36/3=12
ctx.textBaseline = 'middle';
ctx.textAlign = 'center';
ctx.fillText(
@@ -192,33 +76,29 @@ class ShapeDrawService {
);
});
this.currentY = this.headerType === 'minimal' ? 532 : 622;
this.drawLine(this.currentY);
this.currentY = this.headerType === 'minimal' ? 180 : 220; // 532/3≈177, 622/3≈207
this.drawDivider();
}
drawContent() {
const { canvas, ctx, shapes } = this;
const { ctx, shapes } = this;
if (shapes.length <= 0) return;
// 固定可渲染的总行数
// 逻辑像素尺寸(原始尺寸除以3
const ROW_COUNT = 6;
// 每行之间的垂直间距
const VERTICAL_GAP = 120;
// 图形大小
const shapeSize = 180;
// 左右边距
const leftMargin = 160;
const rightMargin = 160;
// 以 drawLegend 画完后的分割线作为基准,内容区顶部间距 50
const VERTICAL_GAP = 40; // 120/3=40
const shapeSize = 60; // 180/3=60
const leftMargin = 53; // 160/3≈53
const rightMargin = 53; // 160/3≈53
const legendBottomY = this.currentY;
const topGap = 60;
const topGap = 20; // 60/3=20
const contentTop = legendBottomY + topGap;
// 可用宽度
const contentWidth = canvas.width - leftMargin - rightMargin;
const contentWidth = this.canvasWidth - leftMargin - rightMargin;
// 每行最多能放多少个图形(考虑最小间距40
const minGap = 40;
// 每行最多能放多少个图形(考虑最小间距,逻辑像素
const minGap = 13; // 40/3≈13
const maxPerRow = Math.floor(
(contentWidth + minGap) / (shapeSize + minGap),
);
@@ -300,35 +180,9 @@ class ShapeDrawService {
});
}
// drawLine 已由基类的 drawDivider 替代,保留此方法以保持兼容
drawLine(linY: number) {
const { canvas, ctx } = this;
ctx.strokeStyle = '#000';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(80, linY);
ctx.lineTo(canvas.width - 80, linY);
ctx.stroke();
}
setPaper() {
const { ctx, canvas } = this;
const { pixelRatio: dpr } = wx.getWindowInfo();
let { width, height } = PAPER_SIZE[this.paperSize];
width = width * dpr;
height = height * dpr;
canvas.width = width;
canvas.height = height;
this.clear();
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, width, height);
}
clear() {
const canvas = this.canvas;
this.ctx.clearRect(0, 0, canvas.width, canvas.height);
this.drawDivider();
}
}
+54 -136
View File
@@ -1,16 +1,10 @@
// import { PAPER_SIZE } from './constant';
import { PAPER_SIZE } from '../constants/colors';
import {
drawHeader as drawHeaderCommon,
drawMiniHeader as drawMiniHeaderCommon,
} from './headerDrawService';
import { BaseDrawService } from './baseDraw';
/**
* 计算示例区域圆的中心点
* @param canvasWidth 画布宽度
* @param canvasWidth 画布宽度(逻辑像素)
* @param circleCount 圆的数量
* @param padding 圆的间距
* @param radius 圆的半径
* @param padding 圆的间距(逻辑像素)
* @param radius 圆的半径(逻辑像素)
* @returns 圆的中心点
*/
function calculateCircleCenters(
@@ -24,7 +18,6 @@ function calculateCircleCenters(
const centers = [];
if (circleCount === 1) {
// 单个圆圈直接居中
centers.push(canvasWidth / 2);
} else {
const requiredSpace = 2 * radius * circleCount;
@@ -32,7 +25,6 @@ function calculateCircleCenters(
(availableWidth - requiredSpace) / (circleCount - 1);
if (spaceBetween > maxSpacing) {
// 超过最大间距时,固定间距并居中对齐
const totalWidth = 2 * radius + (circleCount - 1) * maxSpacing;
const startX = (canvasWidth - totalWidth) / 2 + radius;
@@ -40,7 +32,6 @@ function calculateCircleCenters(
centers.push(startX + i * maxSpacing);
}
} else {
// 正常均匀分布
const startX = padding + radius;
for (let i = 0; i < circleCount; i++) {
@@ -52,14 +43,7 @@ function calculateCircleCenters(
return centers;
}
class TextDrawService {
headerType: PrintHeader = 'wechat';
canvas: WechatMiniprogram.Canvas;
ctx: RenderingContext;
options: Record<string, any>;
paperSize: PaperSize;
currentX: number;
currentY: number;
class TextDrawService extends BaseDrawService {
colors: string[];
characters: string[];
@@ -69,27 +53,13 @@ class TextDrawService {
options?: Record<string, any>,
) {
options = options || {};
this.canvas = canvas;
this.ctx = ctx;
this.paperSize = 'A4';
this.options = {
appName: '涂鸦丫小程序',
appHint: '识字|识图|练字|打印',
super(canvas, ctx, {
title: '找一找 涂 色',
subTitle: '给文字涂上相同的颜色',
...options,
};
this.currentX = 0;
this.currentY = 0;
});
this.colors = ['#000'];
this.characters = ['王'];
this.setPrintConfig();
}
setPrintConfig() {
const printConfig = getApp().getPrintConfig();
this.headerType = printConfig.header;
this.options.appName = printConfig.appName;
}
async draw(list: Array<{ color: string; word: string }>) {
@@ -99,82 +69,58 @@ class TextDrawService {
this.characters = list.map((item) => item.word || '日');
this.clear();
this.setPaper();
// 绘制Header
if (this.headerType !== 'minimal') {
await this.drawHeader();
} else {
this.drawMiniHeader();
await this.drawMiniHeader();
}
this.drawDivider();
this.drawLegend();
this.drawContent();
}
async drawHeader() {
this.currentX = 80;
this.currentY = 80;
await drawHeaderCommon({
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;
this.drawLine(this.currentY);
},
});
}
drawMiniHeader() {
drawMiniHeaderCommon({
canvas: this.canvas,
ctx: this.ctx,
options: {
appName: this.options.appName || '涂鸦丫小程序',
title: this.options.title || '找一找 涂 色',
},
onHeaderDrawn: (currentY) => {
this.currentY = currentY;
this.drawLine(this.currentY);
},
});
}
/** 绘制示例
* 矩形: x、y为左上角
* 圆形:x、y为圆心
* 文字:x textAlign 为 start,文本左边缘对齐
* y textBaseline 为 alphabeticy 对应字母基线(类似左下角)
*
* 注意:所有尺寸已转换为逻辑像素(除以3)
* */
drawLegend() {
const { canvas, ctx, colors, characters } = this;
const { ctx, colors, characters } = this;
if (characters.length <= 0) return;
// const { colors, characters } = options;
this.currentY = this.headerType === 'minimal' ? 200 : 304;
const radius = 65;
const rectWidth = 180;
const rectHeight = 80;
const startX = 260 + radius; // 圆形的X坐标是圆心
const startY = this.currentY + 30 + radius; // 圆形的Y坐标是圆心
// 逻辑像素尺寸(原始尺寸除以3
this.currentY = this.headerType === 'minimal' ? 68 : 110; // 200/3≈67, 304/3≈101
const radius = 22; // 65/3≈22
const rectWidth = 60; // 180/3=60
const rectHeight = 27; // 80/3≈27
const startX = 87 + radius; // (260+65)/3≈108
const startY = this.currentY + 10 + radius; // (30+65)/3≈32
const len = characters.length || 4;
const canvasWidth = canvas.width;
const centers = calculateCircleCenters(canvasWidth, len, 220, 65);
/** 计算每个圆之间的间距 俩个60,一个是页面右侧边距,另一个是圆离右边距地边距*/
const spaceWidth = len > 3 ? (canvasWidth - startX * 2) / (len - 1) : 0;
const centers = calculateCircleCenters(
this.canvasWidth,
len,
73,
radius,
); // 220/3≈73
/** 计算每个圆之间的间距 */
const spaceWidth =
len > 3 ? (this.canvasWidth - startX * 2) / (len - 1) : 0;
ctx.moveTo(startX, startY);
colors.forEach((color: string, index: number) => {
const x = centers[index] || startX + index * spaceWidth; // 计算圆的X坐标
const y = startY; // 计算圆的Y坐标
const x = centers[index] || startX + index * spaceWidth;
const y = startY;
// 绘制圆
ctx.strokeStyle = '#000';
ctx.fillStyle = color;
ctx.lineWidth = 4;
ctx.lineWidth = 1; // 4/3≈1
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fill();
@@ -182,16 +128,16 @@ class TextDrawService {
ctx.closePath();
// 绘制长方形
ctx.fillStyle = '#fff'; // 长方形背景色
ctx.fillStyle = '#fff';
ctx.strokeStyle = '#000';
ctx.lineWidth = 4;
const rectangleX = x - rectWidth / 2; // 从圆形移动一半的长方形的长
const rectangleY = y + radius + 43; // 从圆形下移一个半径,再加上43的间距
ctx.strokeRect(rectangleX, rectangleY, rectWidth, rectHeight); // 绘制长方形
ctx.lineWidth = 1; // 4/3≈1
const rectangleX = x - rectWidth / 2;
const rectangleY = y + radius + 14; // 43/3≈14
ctx.strokeRect(rectangleX, rectangleY, rectWidth, rectHeight);
// 绘制字符
ctx.fillStyle = '#333';
ctx.font = 'bold 48px "Microsoft Yahei"';
ctx.font = 'bold 16px "Microsoft Yahei"'; // 48/3=16
ctx.textBaseline = 'middle';
ctx.textAlign = 'center';
ctx.fillText(
@@ -202,26 +148,25 @@ class TextDrawService {
);
});
this.currentY = this.headerType === 'minimal' ? 522 : 612; // 计算分割线的Y坐标,距离示例图20px
this.drawLine(this.currentY);
this.currentY = this.headerType === 'minimal' ? 178 : 230; // 522/3=174, 612/3=204
this.drawDivider();
}
drawContent() {
const { canvas, ctx, characters } = this;
const { ctx, characters } = this;
if (characters.length <= 0) return;
const len = characters.length;
const rows = this.headerType === 'minimal' ? 9 : 8;
const radius = 80;
const fontSize = 72;
const radius = 27; // 80/3≈27
const fontSize = 24; // 72/3=24
const startY = this.currentY + 62 + radius;
const startX1 = 310 + radius;
const startX2 = 200 + radius;
const canvasWidth = canvas.width;
const spaceWidthFrist = (canvasWidth - startX1 * 2) / (5 - 1);
const spaceWidthSecond = (canvasWidth - startX2 * 2) / (6 - 1);
const spaceHeight = 54;
const startY = this.currentY + 10 + radius; // 62/3≈21
const startX1 = 103 + radius; // 310/3≈103
const startX2 = 67 + radius; // 200/3≈67
const spaceWidthFrist = (this.canvasWidth - startX1 * 2) / (5 - 1);
const spaceWidthSecond = (this.canvasWidth - startX2 * 2) / (6 - 1);
const spaceHeight = 18; // 54/3=18
ctx.moveTo(startX1, startY);
for (let i = 0; i < rows; i++) {
@@ -238,7 +183,7 @@ class TextDrawService {
ctx.fillStyle = '#fff';
ctx.strokeStyle = '#000';
ctx.lineWidth = 4;
ctx.lineWidth = 1; // 4/3≈1
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fill();
@@ -254,36 +199,9 @@ class TextDrawService {
}
}
// drawLine 已由基类的 drawDivider 替代,保留此方法以保持兼容
drawLine(linY: number) {
const { canvas, ctx } = this;
ctx.strokeStyle = '#000';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(80, linY);
ctx.lineTo(canvas.width - 80, linY);
ctx.stroke();
}
setPaper() {
const { ctx, canvas } = this;
const { pixelRatio: dpr } = wx.getWindowInfo();
// const { width, height } = { width: 595 * dpr, height: 842 * dpr };
let { width, height } = PAPER_SIZE[this.paperSize];
width = width * dpr;
height = height * dpr;
canvas.width = width;
canvas.height = height;
this.clear();
ctx.fillStyle = '#fff'; // 设置背景色为白色
ctx.fillRect(0, 0, width, height);
}
clear() {
const canvas = this.canvas;
this.ctx.clearRect(0, 0, canvas.width, canvas.height);
this.drawDivider();
}
}
+42 -134
View File
@@ -1,9 +1,5 @@
import { PAPER_SIZE } from '../constants/colors';
import { BaseDrawService } from './baseDraw';
import { CharacterItem } from '../types/characterType';
import {
drawHeader as drawHeaderCommon,
drawMiniHeader as drawMiniHeaderCommon,
} from './headerDrawService';
/**
* 仅支持 M/L/Z 的简单 SVG 路径解析与绘制
@@ -197,14 +193,14 @@ function drawTianZiGrid({
lineColor = '#e0e0e0',
boldColor = '#cccccc',
}: DrawTianZiGridParams) {
// 外框
// 外框(逻辑像素)
ctx.strokeStyle = boldColor;
ctx.lineWidth = 2;
ctx.lineWidth = 1; // 2/3≈1,逻辑像素
ctx.strokeRect(x - size / 2, y - size / 2, size, size);
// 中线
// 中线(逻辑像素)
ctx.strokeStyle = lineColor;
ctx.lineWidth = 1;
ctx.lineWidth = 1; // 逻辑像素
ctx.beginPath();
// 竖线
ctx.moveTo(x, y - size / 2);
@@ -215,7 +211,7 @@ function drawTianZiGrid({
ctx.stroke();
ctx.closePath();
// 对角线(淡)
// 对角线(淡,逻辑像素
ctx.strokeStyle = '#eeeeee';
ctx.lineWidth = 1;
ctx.beginPath();
@@ -227,40 +223,18 @@ function drawTianZiGrid({
ctx.closePath();
}
class WordDrawService {
headerType: PrintHeader = 'wechat';
canvas: WechatMiniprogram.Canvas;
ctx: RenderingContext;
options: Record<string, any>;
paperSize: PaperSize;
currentX: number;
currentY: number;
class WordDrawService extends BaseDrawService {
constructor(
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, any>,
) {
options = options || {};
this.canvas = canvas;
this.ctx = ctx;
this.paperSize = 'A4';
this.options = {
appName: '涂鸦丫小程序',
appHint: '识字|识图|练字|打印',
super(canvas, ctx, {
title: '田字格 练 字 贴',
subTitle: '按笔画临摹练习',
...options,
};
this.currentX = 0;
this.currentY = 0;
this.setPrintConfig();
}
setPrintConfig() {
const printConfig = getApp().getPrintConfig();
this.headerType = printConfig.header;
this.options.appName = printConfig.appName;
});
}
/**
@@ -271,12 +245,14 @@ class WordDrawService {
this.clear();
this.setPaper();
// 等待页眉绘制完成,确保 this.currentY 被正确设置
// 绘制Header
if (this.headerType !== 'minimal') {
await this.drawHeader();
} else {
await this.drawMiniHeader();
this.drawMiniHeader();
}
this.drawDivider();
this.drawContentEmpty();
}
@@ -301,60 +277,19 @@ class WordDrawService {
* 清空内容区域(页眉以下的部分)
*/
private clearContentArea() {
const { ctx, canvas } = this;
const { ctx } = this;
const contentStartY = this.currentY;
// 清空页眉以下的所有内容
// 清空页眉以下的所有内容(使用逻辑像素)
ctx.fillStyle = '#fff';
ctx.fillRect(
0,
contentStartY,
canvas.width,
canvas.height - contentStartY,
this.canvasWidth,
this.canvasHeight - contentStartY,
);
}
async drawHeader() {
this.currentX = 80;
this.currentY = 80;
await drawHeaderCommon({
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: () => {
// wordDrawService 使用不同的计算方式
const titleY = 80;
const headerHeight = Math.max(titleY + 64 + 48 + 48) + 0; // 64px字体 + 48px间距 + 48px字体 + 20px边距
this.currentY = 80 + headerHeight;
this.drawDivider(this.currentY);
},
});
}
drawMiniHeader() {
drawMiniHeaderCommon({
canvas: this.canvas,
ctx: this.ctx,
options: {
appName: this.options.appName || '涂鸦丫小程序',
title: this.options.title || '田字格 练 字 贴',
},
onHeaderDrawn: () => {
// wordDrawService 使用不同的计算方式
const titleY = 120;
const miniHeaderHeight = titleY + 64 + 20; // 64px字体 + 20px边距
this.currentY = miniHeaderHeight;
this.drawDivider(this.currentY);
},
});
}
/**
* 绘制正文内容:两阶段绘制 - 先绘制空田字格,再绘制练字内容
*/
@@ -368,18 +303,18 @@ class WordDrawService {
* rowGap(行间距)用于田字格竖直方向(每一行田字格之间的间隔)的初始最小值。它防止行与行的田字格上下贴得太紧,增加一定的可视呼吸空间。之后同样会结合剩余空间动态调整为实际间距 actualRowGap。
*/
drawContent(characterData: CharacterItem[] | null) {
const { canvas, ctx } = this;
const { ctx } = this;
// 布局参数
const topGap = 50; // 与页眉分割线的距离
const leftMargin = 120;
const rightMargin = 120;
const bottomMargin = 120;
// 布局参数(逻辑像素,原始尺寸除以3
const topGap = 17; // 50/3≈17
const leftMargin = 40; // 120/3=40
const rightMargin = 40; // 120/3=40
const bottomMargin = 40; // 120/3=40
const contentTop = this.currentY + topGap;
const contentWidth = canvas.width - leftMargin - rightMargin;
const contentHeight = canvas.height - contentTop - bottomMargin;
const contentWidth = this.canvasWidth - leftMargin - rightMargin;
const contentHeight = this.canvasHeight - contentTop - bottomMargin;
const cellSize = 140;
const cellSize = 47; // 140/3≈47
// 统一通过 getMaxGridLayout 获取最大行列数
const { maxRow, maxCol } = this.getMaxGridLayout();
@@ -423,20 +358,18 @@ class WordDrawService {
* 获取当前页面可绘制田字格的最大行数与列数(与绘制使用同一套计算规则)
*/
getMaxGridLayout(): { maxRow: number; maxCol: number } {
const { canvas } = this;
// 布局参数需与 drawContent 保持一致
const topGap = 50;
const leftMargin = 120;
const rightMargin = 120;
const bottomMargin = 120;
// 布局参数需与 drawContent 保持一致(逻辑像素)
const topGap = 17; // 50/3≈17
const leftMargin = 40; // 120/3=40
const rightMargin = 40; // 120/3=40
const bottomMargin = 40; // 120/3=40
const contentTop = this.currentY + topGap;
const contentWidth = canvas.width - leftMargin - rightMargin;
const contentHeight = canvas.height - contentTop - bottomMargin;
const contentWidth = this.canvasWidth - leftMargin - rightMargin;
const contentHeight = this.canvasHeight - contentTop - bottomMargin;
const cellSize = 140;
const minGap = 24;
const rowGap = 36;
const cellSize = 47; // 140/3≈47
const minGap = 8; // 24/3=8
const rowGap = 12; // 36/3=12
const maxCol = Math.max(
1,
@@ -596,7 +529,7 @@ class WordDrawService {
uptoInclusive: strokes.length - 1,
fillStyle: 'rgb(0,0,0)', // 黑色填充
strokeStyle: 'rgb(0,0,0)', // 黑色描边
lineWidth: 4, // 较粗的线条
lineWidth: 1, // 较粗的线条4/3≈1,逻辑像素)
});
}
@@ -631,7 +564,7 @@ class WordDrawService {
// console.log(`练习格 [${rowIndex}, ${columnIndex}] 坐标: (${x}, ${y}), 笔画: ${strokeIndex + 1}/${strokes.length}`);
// 绘制到指定笔画的汉字(色,中等粗细)
// 绘制到指定笔画的汉字(色,中等粗细)
drawStrokes({
ctx,
strokes,
@@ -641,38 +574,13 @@ class WordDrawService {
uptoInclusive: strokeIndex,
fillStyle: '#ccc',
strokeStyle: '#ccc',
lineWidth: 3, // 中等粗细
lineWidth: 1, // 中等粗细3/3=1,逻辑像素)
});
}
drawDivider(linY: number) {
const { canvas, ctx } = this;
ctx.strokeStyle = '#000';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(80, linY);
ctx.lineTo(canvas.width - 80, linY);
ctx.stroke();
}
setPaper() {
const { ctx, canvas } = this;
const { pixelRatio: dpr } = wx.getWindowInfo();
let { width, height } = PAPER_SIZE[this.paperSize];
width = width * dpr;
height = height * dpr;
canvas.width = width;
canvas.height = height;
this.clear();
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, width, height);
}
clear() {
const canvas = this.canvas;
this.ctx.clearRect(0, 0, canvas.width, canvas.height);
// drawDivider 已在基类中实现,此方法保留以保持兼容
drawDivider(linY?: number) {
super.drawDivider();
}
}
+38 -38
View File
@@ -3,12 +3,7 @@
* 提供分享和下载打印事件的上报功能
*/
// 生成唯一 UUID
function generateUUID(): string {
const timestamp = Date.now();
const random = Math.floor(Math.random() * 1000000);
return `${timestamp}-${random}`;
}
import { getAppUUID } from './uuid';
// 格式化时间为 yyyy-MM-dd HH:mm
function formatTime(date: Date = new Date()): string {
@@ -65,29 +60,24 @@ function getEventCount(eventName: string): number {
}
/**
* 埋点追踪器
* 埋点追踪器(单例模式)
*/
class Tracker {
private uuid: string;
private static instance: Tracker | null = null;
private openLog: boolean;
constructor() {
// 初始化时获取或生成 UUID(持久化存储)
this.uuid = this.getOrCreateUUID();
private constructor() {
this.openLog = true;
}
/**
* 获取或创建 UUID
* 获取 Tracker 单例实例
*/
private getOrCreateUUID(): string {
const uuidKey = 'tracker_uuid';
let storedUUID = wx.getStorageSync(uuidKey);
if (!storedUUID) {
storedUUID = generateUUID();
wx.setStorageSync(uuidKey, storedUUID);
public static getInstance(): Tracker {
if (!Tracker.instance) {
Tracker.instance = new Tracker();
}
return storedUUID;
return Tracker.instance;
}
printLog(tip: string, message?: string | object): void {
@@ -104,20 +94,34 @@ class Tracker {
}
}
/**
* 生成公共埋点属性
* @param eventName 事件名称
* @param pageName 页面名称
* @param extraParams 额外的参数对象
* @returns 包含公共属性和额外参数的埋点参数对象
*/
private getCommonEventParams(
eventName: string,
pageName: string,
extraParams?: Record<string, any>,
): Record<string, any> {
return {
count: getEventCount(eventName),
date_time: formatTime(),
uuid: getAppUUID(),
page_name: pageName,
...extraParams,
};
}
/**
* 上报分享点击事件
* @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,
};
const params = this.getCommonEventParams('share_click', pageName);
this.printLog('分享事件', params);
wx.reportEvent('share_click', params);
@@ -129,17 +133,13 @@ class Tracker {
/**
* 上报下载打印事件
* @param pageName 页面名称
* @param mode 模式(可选)
*/
reportDownload(pageName: string): void {
reportDownload(pageName: string, mode?: string): void {
try {
const time = formatTime();
const count = getEventCount('download');
const params = {
count,
time,
uuid: this.uuid,
page_name: pageName,
};
const params = this.getCommonEventParams('download', pageName, {
...(mode && { mode }),
});
this.printLog('下载事件', params);
wx.reportEvent('download', params);
@@ -149,7 +149,7 @@ class Tracker {
}
}
// 创建并导出 tracker 实例
const tracker = new Tracker();
// 导出 Tracker 单例实例
const tracker = Tracker.getInstance();
export default tracker;
+78
View File
@@ -0,0 +1,78 @@
const UUID_STORAGE_KEY = 'app_uuid';
/**
* 生成一个随机十六进制字符
*/
function randomHexChar(): string {
return Math.floor(Math.random() * 16).toString(16);
}
/**
* 生成符合 UUID v4 标准的 UUID
* UUID v4 格式:xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
* 其中:
* - x 是任意十六进制数字
* - 第 13 个字符必须是 '4'(表示版本 4
* - 第 17 个字符必须是 8, 9, a, 或 b 中的一个(表示变体)
*
* @returns 符合 UUID v4 标准的字符串
*/
export function generateUUID(): string {
// UUID v4 格式:xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
// 生成随机十六进制数字
const chars: string[] = [];
// 生成 32 个十六进制字符
for (let i = 0; i < 32; i++) {
if (i === 12) {
// 第 13 个字符必须是 '4'(版本号)
chars[i] = '4';
} else if (i === 16) {
// 第 17 个字符必须是 8, 9, a, 或 b 中的一个(变体)
const variant = ['8', '9', 'a', 'b'][Math.floor(Math.random() * 4)];
chars[i] = variant;
} else {
chars[i] = randomHexChar();
}
}
// 按照 UUID 格式组合:xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
return [
chars.slice(0, 8).join(''),
chars.slice(8, 12).join(''),
chars.slice(12, 16).join(''),
chars.slice(16, 20).join(''),
chars.slice(20, 32).join(''),
].join('-');
}
export function setAppUUID(uuid: string): void {
wx.setStorageSync(UUID_STORAGE_KEY, uuid);
}
/**
* 获取应用的 UUID
* 优先从 globalData 获取,如果没有则从 localStorage 获取
*/
export function getAppUUID(): string {
try {
// 尝试从 globalData 获取
const app = getApp<IAppOption>();
if (app && app.globalData && app.globalData.uuid) {
return app.globalData.uuid;
}
// 如果 globalData 中没有,从 localStorage 获取
const uuid = wx.getStorageSync(UUID_STORAGE_KEY);
if (uuid) {
return uuid;
}
// 如果都没有,返回空字符串(这种情况不应该发生,因为 app.ts 会在启动时生成)
console.warn('UUID 未找到,请确保应用已正确启动');
return '';
} catch (error) {
console.error('获取 UUID 失败:', error);
return '';
}
}
+16 -2
View File
@@ -23,12 +23,26 @@
"condition": {
"miniprogram": {
"list": [
{
"name": "pages/shape/index",
"pathName": "pages/shape/index",
"query": "",
"scene": null,
"launchMode": "default"
},
{
"name": "pages/index/index",
"pathName": "pages/index/index",
"query": "",
"launchMode": "default",
"scene": null
},
{
"name": "mathPages/numberDecompose/numberDecompose",
"pathName": "mathPages/numberDecompose/numberDecompose",
"query": "id=number-decompose&mode=with-image",
"scene": null,
"launchMode": "default"
"launchMode": "default",
"scene": null
},
{
"name": "mathPages/countingSelect/countingSelect",
+3 -2
View File
@@ -4,9 +4,10 @@ type WordCard = {
word: string;
};
interface CardList extends Array<WordCard> { }
interface CardList extends Array<WordCard> {}
interface PrintConfig {
header: PrintHeader;
appName: string;
}
appHint: string;
}
+1
View File
@@ -3,6 +3,7 @@
interface IAppOption {
globalData: {
uuid: string;
userInfo?: WechatMiniprogram.UserInfo;
env: string;
printConfig?: PrintConfig;