feat:架构代码优化
This commit is contained in:
+13
-3
@@ -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` 创建页面
|
||||
|
||||
这样可以保持代码的模块化和可维护性。
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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: '数学|专注|练字|涂鸦',
|
||||
};
|
||||
|
||||
@@ -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: '🔍',
|
||||
},
|
||||
];
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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/` 目录**,因为:
|
||||
|
||||
- 语义最清晰
|
||||
- 符合常见实践
|
||||
- 易于理解和维护
|
||||
@@ -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,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();
|
||||
},
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
<!-- 类型选择器和随机生成按钮 -->
|
||||
<math-type-selector
|
||||
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,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';
|
||||
@@ -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
-1
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
import { getImage } from '../../utils/index';
|
||||
import { getImage } from '../../../utils/index';
|
||||
|
||||
interface DrawAdditionContentParams {
|
||||
canvas: WechatMiniprogram.Canvas;
|
||||
+2
-2
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
import { getImage } from '../../utils/index';
|
||||
import { getImage } from '../../../utils/index';
|
||||
|
||||
interface DrawCompareContentParams {
|
||||
canvas: WechatMiniprogram.Canvas;
|
||||
+2
-2
@@ -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;
|
||||
+2
-2
@@ -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;
|
||||
+2
-2
@@ -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[]; // 打乱顺序后的数字数组
|
||||
+2
-2
@@ -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;
|
||||
+2
-2
@@ -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;
|
||||
+2
-2
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
import { getNumberColors } from '../../constants/colors';
|
||||
import { getNumberColors } from '../../../constants/colors';
|
||||
|
||||
interface DrawNumberColorContentParams {
|
||||
canvas: WechatMiniprogram.Canvas;
|
||||
+2
-2
@@ -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;
|
||||
+2
-2
@@ -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;
|
||||
+2
-2
@@ -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;
|
||||
+2
-2
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
import { getImage } from '../../utils/index';
|
||||
import { getImage } from '../../../utils/index';
|
||||
|
||||
/**
|
||||
* 数字到英文单词的映射
|
||||
@@ -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',
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 });
|
||||
},
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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`: 页面配置文件
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"navigationBarTitleText": "汉字选择",
|
||||
"navigationBarBackgroundColor": "#d2e7d8",
|
||||
"backgroundColor": "#f5f5f5",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 });
|
||||
},
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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()` 统一处理 DPR(device 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. 验证打印输出质量
|
||||
@@ -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 {
|
||||
* 绘制迷你Header(逻辑像素,尺寸除以3)
|
||||
*/
|
||||
drawMiniHeader() {
|
||||
drawMathMiniHeader({
|
||||
drawBaseMiniHeader({
|
||||
ctx: this.ctx,
|
||||
canvasWidth: this.canvasWidth,
|
||||
options: {
|
||||
+9
-9
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 为 alphabetic,y 对应字母基线(类似左下角)
|
||||
*
|
||||
* 注意:所有尺寸已转换为逻辑像素(除以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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 '';
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
Vendored
+3
-2
@@ -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;
|
||||
}
|
||||
|
||||
Vendored
+1
@@ -3,6 +3,7 @@
|
||||
|
||||
interface IAppOption {
|
||||
globalData: {
|
||||
uuid: string;
|
||||
userInfo?: WechatMiniprogram.UserInfo;
|
||||
env: string;
|
||||
printConfig?: PrintConfig;
|
||||
|
||||
Reference in New Issue
Block a user