feat:架构代码优化
This commit is contained in:
+13
-3
@@ -1,17 +1,27 @@
|
|||||||
import config from './config/config';
|
import { defaultPrintConfig } from './config/config';
|
||||||
|
import { generateUUID, getAppUUID, setAppUUID } from './utils/uuid';
|
||||||
const defaultPrintConfig: PrintConfig = config.printHeader as PrintConfig;
|
|
||||||
|
|
||||||
// app.ts
|
// app.ts
|
||||||
App<IAppOption>({
|
App<IAppOption>({
|
||||||
globalData: {
|
globalData: {
|
||||||
env: 'release',
|
env: 'release',
|
||||||
|
uuid: '',
|
||||||
printConfig: defaultPrintConfig,
|
printConfig: defaultPrintConfig,
|
||||||
},
|
},
|
||||||
onLaunch() {
|
onLaunch() {
|
||||||
const accountInfo = wx.getAccountInfoSync();
|
const accountInfo = wx.getAccountInfoSync();
|
||||||
const env = accountInfo.miniProgram.envVersion || 'release';
|
const env = accountInfo.miniProgram.envVersion || 'release';
|
||||||
this.globalData.env = env;
|
this.globalData.env = env;
|
||||||
|
|
||||||
|
// 从 localStorage 读取 UUID,如果没有则生成并存储
|
||||||
|
let uuid = getAppUUID();
|
||||||
|
if (!uuid) {
|
||||||
|
uuid = generateUUID();
|
||||||
|
setAppUUID(uuid);
|
||||||
|
console.log('生成并存储 UUID', uuid);
|
||||||
|
}
|
||||||
|
this.globalData.uuid = uuid;
|
||||||
|
|
||||||
if (env !== 'release') {
|
if (env !== 'release') {
|
||||||
const printConfig =
|
const printConfig =
|
||||||
wx.getStorageSync('printConfig') || defaultPrintConfig;
|
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 { PAPER_SIZE } from '../constants/colors';
|
||||||
import { checkAndSaveImage } from '../../utils/saveImage';
|
import { checkAndSaveImage } from '../utils/saveImage';
|
||||||
import { shouldShowShareGuide } from '../../utils/shareGuide';
|
import { shouldShowShareGuide } from '../utils/shareGuide';
|
||||||
import { BaseMathDrawService } from '../service/baseMathDraw';
|
import { BaseDrawService } from '../service/baseDraw';
|
||||||
import {
|
import tracker from '../utils/tracker';
|
||||||
MATH_FUNCTION_TYPES,
|
|
||||||
MathFunctionType,
|
|
||||||
} from '../../constants/mathFunctions';
|
|
||||||
import tracker from '../../utils/tracker';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Canvas 相关的页面实例属性
|
* Canvas 相关的页面实例属性
|
||||||
*/
|
*/
|
||||||
export interface MathPageCanvasInstance {
|
export interface PageCanvasInstance {
|
||||||
|
data: CanvasDataState;
|
||||||
canvas: Canvas | null;
|
canvas: Canvas | null;
|
||||||
ctx: RenderingContext | null;
|
ctx: RenderingContext | null;
|
||||||
boxHeight: number;
|
boxHeight: number;
|
||||||
boxWidth: number;
|
boxWidth: number;
|
||||||
drawService: BaseMathDrawService | null;
|
drawService: BaseDrawService | null;
|
||||||
setData(data: any, callback?: () => void): void;
|
setData(data: any, callback?: () => void): void;
|
||||||
|
route: string;
|
||||||
|
getShareOptions(): ShareOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,8 +28,12 @@ export interface CanvasDataState {
|
|||||||
boxWidth: number;
|
boxWidth: number;
|
||||||
boxHeight: number;
|
boxHeight: number;
|
||||||
functionId: string;
|
functionId: string;
|
||||||
|
currentMode?: string;
|
||||||
pageTitle: string;
|
pageTitle: string;
|
||||||
subTitle?: string;
|
subTitle?: string;
|
||||||
|
data: any;
|
||||||
|
setData(data: any, callback?: () => void): void;
|
||||||
|
route: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,7 +47,7 @@ export interface InitCanvasOptions {
|
|||||||
canvas: Canvas,
|
canvas: Canvas,
|
||||||
ctx: RenderingContext,
|
ctx: RenderingContext,
|
||||||
options?: Record<string, any>,
|
options?: Record<string, any>,
|
||||||
) => BaseMathDrawService;
|
) => BaseDrawService;
|
||||||
/**
|
/**
|
||||||
* 绘制服务的选项
|
* 绘制服务的选项
|
||||||
*/
|
*/
|
||||||
@@ -62,28 +65,60 @@ export interface ShareOptions {
|
|||||||
/**
|
/**
|
||||||
* 页面路径(相对于 mathPages 目录,例如:'missingNumber/missingNumber')
|
* 页面路径(相对于 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',
|
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 {
|
return {
|
||||||
/**
|
/**
|
||||||
* 初始化 Canvas
|
* 初始化 Canvas
|
||||||
*/
|
*/
|
||||||
initCanvas(
|
initCanvas(this: PageCanvasInstance, options: InitCanvasOptions) {
|
||||||
this: MathPageCanvasInstance & { data: CanvasDataState },
|
|
||||||
options: InitCanvasOptions,
|
|
||||||
) {
|
|
||||||
console.log('initCanvas this', this);
|
|
||||||
const query = wx.createSelectorQuery();
|
const query = wx.createSelectorQuery();
|
||||||
query
|
query
|
||||||
.select('#canvasWrapper')
|
.select('#canvasWrapper')
|
||||||
@@ -141,9 +176,7 @@ export function getMathPageCommonMethods() {
|
|||||||
/**
|
/**
|
||||||
* 导出打印
|
* 导出打印
|
||||||
*/
|
*/
|
||||||
exportToPrint(
|
exportToPrint(this: PageCanvasInstance) {
|
||||||
this: MathPageCanvasInstance & { data: CanvasDataState },
|
|
||||||
) {
|
|
||||||
if (!this.canvas || !this.data.hasContent) {
|
if (!this.canvas || !this.data.hasContent) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -154,58 +187,55 @@ export function getMathPageCommonMethods() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 上报下载埋点
|
// 上报下载埋点
|
||||||
tracker.reportDownload(this.data.pageTitle);
|
tracker.reportDownload(this.data.pageTitle, this.data.currentMode);
|
||||||
|
|
||||||
checkAndSaveImage(this.canvas);
|
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(
|
onShareAppMessage(this: PageCanvasInstance) {
|
||||||
this: MathPageCanvasInstance & { data: CanvasDataState },
|
|
||||||
) {
|
|
||||||
console.log('onShareAppMessage');
|
|
||||||
// 上报分享埋点
|
// 上报分享埋点
|
||||||
tracker.reportShare(this.data.pageTitle);
|
tracker.reportShare(this.data.pageTitle);
|
||||||
|
|
||||||
return {
|
return this.getShareOptions();
|
||||||
...shareConfig,
|
|
||||||
query: `id=${this.data.functionId}`,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分享到朋友圈
|
* 分享到朋友圈
|
||||||
*/
|
*/
|
||||||
onShareTimeline(
|
onShareTimeline(this: PageCanvasInstance) {
|
||||||
this: MathPageCanvasInstance & { data: CanvasDataState },
|
|
||||||
) {
|
|
||||||
console.log('onShareTimeline');
|
console.log('onShareTimeline');
|
||||||
// 上报分享埋点
|
// 上报分享埋点
|
||||||
tracker.reportShare(this.data.pageTitle);
|
tracker.reportShare(this.data.pageTitle);
|
||||||
|
return this.getShareOptions();
|
||||||
return {
|
|
||||||
...shareConfig,
|
|
||||||
query: `id=${this.data.functionId}`,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 关闭分享引导弹窗
|
* 关闭分享引导弹窗
|
||||||
*/
|
*/
|
||||||
onCloseShareDialog(
|
onCloseShareDialog(this: PageCanvasInstance) {
|
||||||
this: MathPageCanvasInstance & { data: CanvasDataState },
|
|
||||||
) {
|
|
||||||
this.setData({ showShareDialog: false });
|
this.setData({ showShareDialog: false });
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分享成功回调
|
* 分享成功回调
|
||||||
*/
|
*/
|
||||||
onShareSuccess(
|
onShareSuccess(this: PageCanvasInstance) {
|
||||||
this: MathPageCanvasInstance & { data: CanvasDataState },
|
|
||||||
) {
|
|
||||||
this.setData({ showShareDialog: false });
|
this.setData({ showShareDialog: false });
|
||||||
if (this.canvas) {
|
if (this.canvas) {
|
||||||
// 上报下载埋点(分享成功后下载)
|
// 上报下载埋点(分享成功后下载)
|
||||||
@@ -215,19 +245,43 @@ export function getMathPageCommonMethods() {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化页面信息(从 functionId 获取标题等信息)
|
* 初始化页面信息
|
||||||
|
*
|
||||||
|
* 支持两种调用方式:
|
||||||
|
* 1. initPageInfo(functionId, defaultTitle) - 如果有 pageInfoLookup,会尝试查找;否则使用 defaultTitle
|
||||||
|
* 2. initPageInfo({ title, desc, functionId }) - 直接提供页面信息
|
||||||
*/
|
*/
|
||||||
initPageInfo(
|
initPageInfo(
|
||||||
this: MathPageCanvasInstance & { data: CanvasDataState },
|
this: PageCanvasInstance,
|
||||||
functionId: string,
|
functionIdOrOptions:
|
||||||
|
| string
|
||||||
|
| { title: string; desc?: string; functionId: string },
|
||||||
defaultTitle?: string,
|
defaultTitle?: string,
|
||||||
) {
|
) {
|
||||||
const functionItem = MATH_FUNCTION_TYPES.find(
|
let functionId: string;
|
||||||
(item: MathFunctionType) => item.id === functionId,
|
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({
|
this.setData({
|
||||||
pageTitle: title,
|
pageTitle: title,
|
||||||
@@ -241,14 +295,17 @@ export function getMathPageCommonMethods() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 应用数学页面公共方法的辅助函数
|
* 应用页面公共方法的辅助函数
|
||||||
* 自动混入公共方法,简化页面代码
|
* 自动混入公共方法,简化页面代码
|
||||||
* @param pageOptions 页面选项
|
* @param pageOptions 页面选项
|
||||||
* @param shareOptions 分享配置选项
|
* @param config 公共方法配置
|
||||||
* @returns 合并后的页面选项
|
* @returns 合并后的页面选项
|
||||||
*/
|
*/
|
||||||
export function applyMathPageMixin(pageOptions: any) {
|
export function applyPageMixin(
|
||||||
const commonMethods = getMathPageCommonMethods();
|
pageOptions: any,
|
||||||
|
config: PageCommonMethodsConfig = {},
|
||||||
|
) {
|
||||||
|
const commonMethods = getPageCommonMethods(config);
|
||||||
|
|
||||||
// 提取 pageOptions 中的 data(如果有)
|
// 提取 pageOptions 中的 data(如果有)
|
||||||
const pageData = pageOptions.data || {};
|
const pageData = pageOptions.data || {};
|
||||||
@@ -268,11 +325,14 @@ export function applyMathPageMixin(pageOptions: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建数学页面的便捷函数
|
* 创建页面的便捷函数
|
||||||
* 自动应用公共方法并注册为页面
|
* 自动应用公共方法并注册为页面
|
||||||
* 页面路径从函数调用栈自动推断(从调用文件路径提取)
|
|
||||||
* @param pageOptions 页面选项
|
* @param pageOptions 页面选项
|
||||||
|
* @param config 公共方法配置
|
||||||
*/
|
*/
|
||||||
export function createMathPage(pageOptions: any) {
|
export function createPage(
|
||||||
Page(applyMathPageMixin(pageOptions));
|
pageOptions: any,
|
||||||
|
config: PageCommonMethodsConfig = {},
|
||||||
|
) {
|
||||||
|
Page(applyPageMixin(pageOptions, config));
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,13 @@
|
|||||||
export default {
|
// export default {
|
||||||
printHeader: {
|
// printHeader: {
|
||||||
// header: 'wechat',
|
// // header: 'wechat',
|
||||||
header: 'LogoImage',
|
// header: 'LogoImage',
|
||||||
appName: '涂鸦丫小程序',
|
// 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以内的加法和减法混合运算',
|
desc: '练习10以内的加法和减法混合运算',
|
||||||
icon: '±',
|
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,
|
"enablePullDownRefresh": false,
|
||||||
"usingComponents": {
|
"usingComponents": {
|
||||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
||||||
"math-type-selector": "../components/math-type-selector/math-type-selector",
|
"math-type-selector": "../shared/components/math-type-selector/math-type-selector",
|
||||||
"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,230 +1,219 @@
|
|||||||
import AdditionDraw from '../service/additionDraw';
|
import AdditionDraw from '../shared/service/additionDraw';
|
||||||
import { applyMathPageMixin, CanvasDataState } from '../common/mathPageMixin';
|
import {
|
||||||
|
createMathPage,
|
||||||
|
CanvasDataState,
|
||||||
|
} from '../shared/common/mathPageMixin';
|
||||||
|
|
||||||
Page(
|
createMathPage({
|
||||||
applyMathPageMixin(
|
canvas: null as Canvas | null,
|
||||||
{
|
ctx: null as RenderingContext | null,
|
||||||
canvas: null as Canvas | null,
|
boxHeight: 0,
|
||||||
ctx: null as RenderingContext | null,
|
boxWidth: 0,
|
||||||
boxHeight: 0,
|
drawService: null as AdditionDraw | null,
|
||||||
boxWidth: 0,
|
calculationData: null as {
|
||||||
drawService: null as AdditionDraw | null,
|
problems: Array<{
|
||||||
calculationData: null as {
|
type: 'addition' | 'subtraction';
|
||||||
problems: Array<{
|
left: number;
|
||||||
type: 'addition' | 'subtraction';
|
right: number;
|
||||||
left: number;
|
result: number;
|
||||||
right: number;
|
}>;
|
||||||
result: number;
|
} | null,
|
||||||
}>;
|
|
||||||
} | null,
|
|
||||||
|
|
||||||
data: {
|
data: {
|
||||||
pageTitle: '加减法计算',
|
pageTitle: '加减法计算',
|
||||||
subTitle: '通过图形化方式学习加减法运算',
|
subTitle: '通过图形化方式学习加减法运算',
|
||||||
functionId: '',
|
functionId: '',
|
||||||
hasContent: false,
|
hasContent: false,
|
||||||
showShareDialog: false,
|
showShareDialog: false,
|
||||||
currentType: 'addition-5', // 'addition-5', 'addition-10', 'subtraction-10', 'addition-subtraction-10'
|
currentMode: 'addition-5', // 'addition-5', 'addition-10', 'subtraction-10', 'addition-subtraction-10'
|
||||||
currentTypeName: '5以内加法',
|
currentModeName: '5以内加法',
|
||||||
typeActions: [
|
typeActions: [
|
||||||
{ name: '5以内加法', value: 'addition-5' },
|
{ name: '5以内加法', value: 'addition-5' },
|
||||||
{ name: '10以内加法', value: 'addition-10' },
|
{ name: '10以内加法', value: 'addition-10' },
|
||||||
{ name: '10以内减法', value: 'subtraction-10' },
|
{ name: '10以内减法', value: 'subtraction-10' },
|
||||||
{ name: '10以内加减法', value: 'addition-subtraction-10' },
|
{ name: '10以内加减法', value: 'addition-subtraction-10' },
|
||||||
],
|
],
|
||||||
imageType: 'twelve-animals', // 图片类型:twelve-animals 或 fruits
|
imageType: 'twelve-animals', // 图片类型:twelve-animals 或 fruits
|
||||||
} as CanvasDataState & {
|
} as CanvasDataState & {
|
||||||
currentType: string;
|
currentMode: string;
|
||||||
currentTypeName: string;
|
currentModeName: string;
|
||||||
typeActions: Array<{ name: string; value: string }>;
|
typeActions: Array<{ name: string; value: string }>;
|
||||||
imageType: 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);
|
||||||
},
|
},
|
||||||
|
drawServiceOptions: {
|
||||||
onLoad(options: { id?: string }) {
|
subTitle: this.data.subTitle,
|
||||||
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以内加减法',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
onCanvasReady: () => {
|
||||||
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,
|
|
||||||
});
|
|
||||||
// 重新生成数据
|
|
||||||
this.onRandom();
|
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
|
<math-type-selector
|
||||||
current-type-name="{{currentTypeName}}"
|
current-type-name="{{currentModeName}}"
|
||||||
type-actions="{{typeActions}}"
|
type-actions="{{typeActions}}"
|
||||||
bind:select="onSelectType"
|
bind:select="onSelectType"
|
||||||
bind:random="onRandom" />
|
bind:random="onRandom" />
|
||||||
|
|||||||
@@ -7,6 +7,6 @@
|
|||||||
"usingComponents": {
|
"usingComponents": {
|
||||||
"toy-button": "../../ui/button/button",
|
"toy-button": "../../ui/button/button",
|
||||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
"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 {
|
import {
|
||||||
getMathPageCommonMethods,
|
createMathPage,
|
||||||
CanvasDataState,
|
CanvasDataState,
|
||||||
} from '../common/mathPageMixin';
|
} from '../shared/common/mathPageMixin';
|
||||||
|
|
||||||
// 获取公共方法
|
// 获取公共方
|
||||||
const commonMethods = getMathPageCommonMethods({
|
|
||||||
pagePath: 'compare/compare',
|
|
||||||
});
|
|
||||||
|
|
||||||
Page({
|
createMathPage({
|
||||||
canvas: null as Canvas | null,
|
canvas: null as Canvas | null,
|
||||||
ctx: null as RenderingContext | null,
|
ctx: null as RenderingContext | null,
|
||||||
boxHeight: 0,
|
boxHeight: 0,
|
||||||
@@ -40,7 +37,11 @@ Page({
|
|||||||
|
|
||||||
onReady() {
|
onReady() {
|
||||||
this.initCanvas({
|
this.initCanvas({
|
||||||
createDrawService: (canvas, ctx, options) => {
|
createDrawService: (
|
||||||
|
canvas: Canvas,
|
||||||
|
ctx: RenderingContext,
|
||||||
|
options?: Record<string, any>,
|
||||||
|
) => {
|
||||||
return new CompareDraw(canvas, ctx, options);
|
return new CompareDraw(canvas, ctx, options);
|
||||||
},
|
},
|
||||||
drawServiceOptions: {
|
drawServiceOptions: {
|
||||||
@@ -120,13 +121,4 @@ Page({
|
|||||||
|
|
||||||
this.compareData = { problems };
|
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,
|
"enablePullDownRefresh": false,
|
||||||
"usingComponents": {
|
"usingComponents": {
|
||||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
||||||
"math-type-selector": "../components/math-type-selector/math-type-selector",
|
"math-type-selector": "../shared/components/math-type-selector/math-type-selector",
|
||||||
"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,200 +1,194 @@
|
|||||||
import CountMatchDraw from '../service/countMatchDraw';
|
import CountMatchDraw from '../shared/service/countMatchDraw';
|
||||||
import NumberColorDraw from '../service/numberColorDraw';
|
import NumberColorDraw from '../shared/service/numberColorDraw';
|
||||||
import { applyMathPageMixin, CanvasDataState } from '../common/mathPageMixin';
|
import {
|
||||||
|
createMathPage,
|
||||||
|
CanvasDataState,
|
||||||
|
} from '../shared/common/mathPageMixin';
|
||||||
|
|
||||||
Page(
|
createMathPage({
|
||||||
applyMathPageMixin(
|
canvas: null as Canvas | null,
|
||||||
{
|
ctx: null as RenderingContext | null,
|
||||||
canvas: null as Canvas | null,
|
boxHeight: 0,
|
||||||
ctx: null as RenderingContext | null,
|
boxWidth: 0,
|
||||||
boxHeight: 0,
|
drawService: null as CountMatchDraw | NumberColorDraw | null,
|
||||||
boxWidth: 0,
|
matchData: null as {
|
||||||
drawService: null as CountMatchDraw | NumberColorDraw | null,
|
leftNumbers: number[];
|
||||||
matchData: null as {
|
rightNumbers: number[];
|
||||||
leftNumbers: number[];
|
} | null,
|
||||||
rightNumbers: number[];
|
colorData: null as {
|
||||||
} | null,
|
numbers: number[];
|
||||||
colorData: null as {
|
} | null,
|
||||||
numbers: number[];
|
|
||||||
} | null,
|
|
||||||
|
|
||||||
data: {
|
data: {
|
||||||
pageTitle: '数一数,连一连',
|
pageTitle: '数一数,连一连',
|
||||||
functionId: '',
|
functionId: '',
|
||||||
hasContent: false,
|
hasContent: false,
|
||||||
showShareDialog: false,
|
showShareDialog: false,
|
||||||
showTypeSelector: true, // 控制是否显示类型选择器
|
showTypeSelector: true, // 控制是否显示类型选择器
|
||||||
currentType: 'twelve-animals', // 'twelve-animals' 或 'fruits' 或 'circle' 或 'caterpillar'
|
currentMode: 'twelve-animals', // 'twelve-animals' 或 'fruits' 或 'circle' 或 'caterpillar'
|
||||||
currentTypeName: '十二生肖',
|
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: [
|
typeActions: [
|
||||||
{ name: '十二生肖', value: 'twelve-animals' },
|
{ name: '十二生肖', value: 'twelve-animals' },
|
||||||
{ name: '水果', value: 'fruits' },
|
{ name: '水果', value: 'fruits' },
|
||||||
],
|
],
|
||||||
} as CanvasDataState & {
|
});
|
||||||
showTypeSelector: boolean;
|
}
|
||||||
currentType: string;
|
},
|
||||||
currentTypeName: string;
|
|
||||||
typeActions: Array<{ name: string; value: string }>;
|
|
||||||
},
|
|
||||||
|
|
||||||
onLoad(options: { id?: string }) {
|
onReady() {
|
||||||
const functionId = options.id || 'counting-matching';
|
this.initCanvas({
|
||||||
this.setData({ functionId });
|
createDrawService: (
|
||||||
|
canvas: Canvas,
|
||||||
const functionItem =
|
ctx: RenderingContext,
|
||||||
require('../../constants/mathFunctions').MATH_FUNCTION_TYPES.find(
|
options?: Record<string, any>,
|
||||||
(item: any) => item.id === functionId,
|
) => {
|
||||||
);
|
// 根据 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() {
|
|
||||||
if (this.data.functionId === 'number-coloring') {
|
if (this.data.functionId === 'number-coloring') {
|
||||||
// 按数字涂颜色模式:生成6个随机数字(1-10)
|
return new NumberColorDraw(canvas, ctx, options);
|
||||||
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 {
|
} else {
|
||||||
// 数一数连一连模式:生成5个不同的数字(1-10)
|
return new CountMatchDraw(canvas, ctx, options);
|
||||||
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();
|
|
||||||
},
|
},
|
||||||
|
drawServiceOptions: {
|
||||||
/** 选择类型 */
|
subTitle:
|
||||||
onSelectType(event: any) {
|
this.data.functionId === 'number-coloring'
|
||||||
const { name, value } = event.detail;
|
? '按数字给相应的圆圈涂上颜色'
|
||||||
this.setData({
|
: '通过连线配对数字和对应的数量图形',
|
||||||
currentType: value,
|
},
|
||||||
currentTypeName: name,
|
onCanvasReady: () => {
|
||||||
});
|
// 初始随机生成
|
||||||
// 重新生成数据
|
|
||||||
this.onRandom();
|
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
|
<math-type-selector
|
||||||
wx:if="{{showTypeSelector}}"
|
wx:if="{{showTypeSelector}}"
|
||||||
current-type-name="{{currentTypeName}}"
|
current-type-name="{{currentModeName}}"
|
||||||
type-actions="{{typeActions}}"
|
type-actions="{{typeActions}}"
|
||||||
bind:select="onSelectType"
|
bind:select="onSelectType"
|
||||||
bind:random="onRandom" />
|
bind:random="onRandom" />
|
||||||
|
|||||||
@@ -7,6 +7,6 @@
|
|||||||
"usingComponents": {
|
"usingComponents": {
|
||||||
"toy-button": "../../ui/button/button",
|
"toy-button": "../../ui/button/button",
|
||||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
"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 CountingSelectDraw from '../shared/service/countingSelectDraw';
|
||||||
import { applyMathPageMixin, CanvasDataState } from '../common/mathPageMixin';
|
import {
|
||||||
|
createMathPage,
|
||||||
|
CanvasDataState,
|
||||||
|
} from '../shared/common/mathPageMixin';
|
||||||
|
|
||||||
Page(
|
createMathPage({
|
||||||
applyMathPageMixin(
|
canvas: null as Canvas | null,
|
||||||
{
|
ctx: null as RenderingContext | null,
|
||||||
canvas: null as Canvas | null,
|
boxHeight: 0,
|
||||||
ctx: null as RenderingContext | null,
|
boxWidth: 0,
|
||||||
boxHeight: 0,
|
drawService: null as CountingSelectDraw | null,
|
||||||
boxWidth: 0,
|
countingSelectData: null as {
|
||||||
drawService: null as CountingSelectDraw | null,
|
problems: Array<{
|
||||||
countingSelectData: null as {
|
count: number; // 图片数量(正确答案)
|
||||||
problems: Array<{
|
imageIndex: number; // 图片索引
|
||||||
count: number; // 图片数量(正确答案)
|
imageType: 'fruits' | 'twelve-animals'; // 图片类型
|
||||||
imageIndex: number; // 图片索引
|
options?: number[]; // 三个数字选项(选一选模式需要)
|
||||||
imageType: 'fruits' | 'twelve-animals'; // 图片类型
|
correctIndex?: number; // 正确答案在options中的索引(选一选模式需要)
|
||||||
options?: number[]; // 三个数字选项(选一选模式需要)
|
}>;
|
||||||
correctIndex?: number; // 正确答案在options中的索引(选一选模式需要)
|
} | null,
|
||||||
}>;
|
|
||||||
} | null,
|
|
||||||
|
|
||||||
data: {
|
data: {
|
||||||
pageTitle: '数一数,选一选',
|
pageTitle: '数一数,选一选',
|
||||||
subTitle: '数出物品数量,从多个选项中选择正确答案',
|
subTitle: '数出物品数量,从多个选项中选择正确答案',
|
||||||
functionId: '',
|
functionId: '',
|
||||||
hasContent: false,
|
hasContent: false,
|
||||||
showShareDialog: false,
|
showShareDialog: false,
|
||||||
} as CanvasDataState & {
|
} as CanvasDataState,
|
||||||
showTypeSelector?: boolean;
|
|
||||||
currentType?: string;
|
onLoad(options: { id?: string }) {
|
||||||
currentTypeName?: string;
|
const functionId = options.id || 'counting-select';
|
||||||
typeActions?: Array<{ name: string; value: any }>;
|
// 初始化页面信息(会从 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);
|
||||||
},
|
},
|
||||||
|
drawServiceOptions: {
|
||||||
onLoad(options: { id?: string }) {
|
subTitle: this.data.subTitle,
|
||||||
const functionId = options.id || 'counting-select';
|
|
||||||
// 初始化页面信息(会从 mathFunctions.ts 读取 title 和 desc)
|
|
||||||
this.initPageInfo(functionId, '数一数,选一选');
|
|
||||||
},
|
},
|
||||||
|
onCanvasReady: () => {
|
||||||
onReady() {
|
// Canvas 初始化完成后,生成初始数据
|
||||||
this.initCanvas({
|
this.onRandom();
|
||||||
createDrawService: (
|
|
||||||
canvas: Canvas,
|
|
||||||
ctx: RenderingContext,
|
|
||||||
options?: Record<string, any>,
|
|
||||||
) => {
|
|
||||||
return new CountingSelectDraw(canvas, ctx, options);
|
|
||||||
},
|
|
||||||
drawServiceOptions: {
|
|
||||||
subTitle: this.data.subTitle,
|
|
||||||
},
|
|
||||||
onCanvasReady: () => {
|
|
||||||
// Canvas 初始化完成后,生成初始数据
|
|
||||||
this.onRandom();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 绘制Canvas内容
|
* 绘制Canvas内容
|
||||||
*/
|
*/
|
||||||
async drawCanvas() {
|
async drawCanvas() {
|
||||||
if (
|
if (!this.ctx || !this.drawService || !this.countingSelectData) {
|
||||||
!this.ctx ||
|
return;
|
||||||
!this.drawService ||
|
}
|
||||||
!this.countingSelectData
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 判断是选一选还是填一填模式
|
// 判断是选一选还是填一填模式
|
||||||
const mode =
|
const mode =
|
||||||
this.data.functionId === 'counting-fill'
|
this.data.functionId === 'counting-fill' ? 'fill' : 'select';
|
||||||
? 'fill'
|
await this.drawService.draw(this.countingSelectData, mode);
|
||||||
: 'select';
|
this.setData({ hasContent: true });
|
||||||
await this.drawService.draw(this.countingSelectData, mode);
|
} catch (error) {
|
||||||
this.setData({ hasContent: true });
|
console.error('绘制失败:', error);
|
||||||
} catch (error) {
|
this.setData({ hasContent: false });
|
||||||
console.error('绘制失败:', error);
|
}
|
||||||
this.setData({ hasContent: false });
|
},
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 随机生成
|
* 随机生成
|
||||||
*/
|
*/
|
||||||
onRandom() {
|
onRandom() {
|
||||||
this.generateCountingSelectData();
|
this.generateCountingSelectData();
|
||||||
this.drawCanvas();
|
this.drawCanvas();
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成数一数选一选/填一填数据
|
* 生成数一数选一选/填一填数据
|
||||||
*/
|
*/
|
||||||
generateCountingSelectData() {
|
generateCountingSelectData() {
|
||||||
const isFillMode = this.data.functionId === 'counting-fill';
|
const isFillMode = this.data.functionId === 'counting-fill';
|
||||||
const problems: Array<{
|
const problems: Array<{
|
||||||
count: number;
|
count: number;
|
||||||
imageIndex: number;
|
imageIndex: number;
|
||||||
imageType: 'fruits' | 'twelve-animals';
|
imageType: 'fruits' | 'twelve-animals';
|
||||||
options?: number[];
|
options?: number[];
|
||||||
correctIndex?: number;
|
correctIndex?: number;
|
||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
// 生成9道题目
|
// 生成9道题目
|
||||||
for (let i = 0; i < 9; i++) {
|
for (let i = 0; i < 9; i++) {
|
||||||
// 随机选择图片类型
|
// 随机选择图片类型
|
||||||
const imageType: 'fruits' | 'twelve-animals' =
|
const imageType: 'fruits' | 'twelve-animals' =
|
||||||
Math.random() < 0.5 ? 'fruits' : 'twelve-animals';
|
Math.random() < 0.5 ? 'fruits' : 'twelve-animals';
|
||||||
|
|
||||||
// 根据图片类型确定最大索引
|
// 根据图片类型确定最大索引
|
||||||
const maxImageIndex = imageType === 'fruits' ? 22 : 12;
|
const maxImageIndex = imageType === 'fruits' ? 22 : 12;
|
||||||
|
|
||||||
// 生成图片数量(1-10)
|
// 生成图片数量(1-10)
|
||||||
const count = Math.floor(Math.random() * 10) + 1;
|
const count = Math.floor(Math.random() * 10) + 1;
|
||||||
|
|
||||||
// 随机选择图片索引
|
// 随机选择图片索引
|
||||||
const imageIndex =
|
const imageIndex = Math.floor(Math.random() * maxImageIndex) + 1;
|
||||||
Math.floor(Math.random() * maxImageIndex) + 1;
|
|
||||||
|
|
||||||
const problem: {
|
const problem: {
|
||||||
count: number;
|
count: number;
|
||||||
imageIndex: number;
|
imageIndex: number;
|
||||||
imageType: 'fruits' | 'twelve-animals';
|
imageType: 'fruits' | 'twelve-animals';
|
||||||
options?: number[];
|
options?: number[];
|
||||||
correctIndex?: number;
|
correctIndex?: number;
|
||||||
} = {
|
} = {
|
||||||
count,
|
count,
|
||||||
imageIndex,
|
imageIndex,
|
||||||
imageType,
|
imageType,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 选一选模式:生成三个选项
|
// 选一选模式:生成三个选项
|
||||||
if (!isFillMode) {
|
if (!isFillMode) {
|
||||||
const options: number[] = [];
|
const options: number[] = [];
|
||||||
const correctIndex = Math.floor(Math.random() * 3);
|
const correctIndex = Math.floor(Math.random() * 3);
|
||||||
|
|
||||||
for (let j = 0; j < 3; j++) {
|
for (let j = 0; j < 3; j++) {
|
||||||
if (j === correctIndex) {
|
if (j === correctIndex) {
|
||||||
options.push(count); // 正确答案
|
options.push(count); // 正确答案
|
||||||
} else {
|
} else {
|
||||||
// 生成错误答案(与正确答案不同)
|
// 生成错误答案(与正确答案不同)
|
||||||
let wrongAnswer: number;
|
let wrongAnswer: number;
|
||||||
do {
|
do {
|
||||||
wrongAnswer =
|
wrongAnswer = Math.floor(Math.random() * 10) + 1;
|
||||||
Math.floor(Math.random() * 10) + 1;
|
} while (wrongAnswer === count);
|
||||||
} while (wrongAnswer === count);
|
options.push(wrongAnswer);
|
||||||
options.push(wrongAnswer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
problem.options = options;
|
|
||||||
problem.correctIndex = correctIndex;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
problems.push(problem);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.countingSelectData = { problems };
|
problem.options = options;
|
||||||
},
|
problem.correctIndex = correctIndex;
|
||||||
},
|
}
|
||||||
{
|
|
||||||
pagePath: 'countingSelect/countingSelect',
|
problems.push(problem);
|
||||||
},
|
}
|
||||||
),
|
|
||||||
);
|
this.countingSelectData = { problems };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"enablePullDownRefresh": false,
|
"enablePullDownRefresh": false,
|
||||||
"usingComponents": {
|
"usingComponents": {
|
||||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
||||||
"math-type-selector": "../components/math-type-selector/math-type-selector",
|
"math-type-selector": "../shared/components/math-type-selector/math-type-selector",
|
||||||
"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,198 +1,198 @@
|
|||||||
import { getRandomNumberColor } from '../../constants/colors';
|
import { getRandomNumberColor } from '../../constants/colors';
|
||||||
import MissingNumberDraw from '../service/missingNumberDraw';
|
import MissingNumberDraw from '../shared/service/missingNumberDraw';
|
||||||
import { applyMathPageMixin, CanvasDataState } from '../common/mathPageMixin';
|
import {
|
||||||
|
createMathPage,
|
||||||
|
CanvasDataState,
|
||||||
|
} from '../shared/common/mathPageMixin';
|
||||||
|
|
||||||
Page(
|
createMathPage({
|
||||||
applyMathPageMixin(
|
canvas: null as Canvas | null,
|
||||||
{
|
ctx: null as RenderingContext | null,
|
||||||
canvas: null as Canvas | null,
|
boxHeight: 0,
|
||||||
ctx: null as RenderingContext | null,
|
boxWidth: 0,
|
||||||
boxHeight: 0,
|
drawService: null as MissingNumberDraw | null,
|
||||||
boxWidth: 0,
|
missingNumberData: null as {
|
||||||
drawService: null as MissingNumberDraw | null,
|
grids: Array<{
|
||||||
missingNumberData: null as {
|
numbers: (number | null)[];
|
||||||
grids: Array<{
|
colors: (string | null)[];
|
||||||
numbers: (number | null)[];
|
}>;
|
||||||
colors: (string | null)[];
|
maxNumber: number;
|
||||||
}>;
|
} | null,
|
||||||
maxNumber: number;
|
|
||||||
} | null,
|
|
||||||
|
|
||||||
data: {
|
data: {
|
||||||
pageTitle: '填上缺少的数字',
|
pageTitle: '填上缺少的数字',
|
||||||
subTitle: '在数字序列中找出并填写缺失的数字',
|
subTitle: '在数字序列中找出并填写缺失的数字',
|
||||||
functionId: '',
|
functionId: '',
|
||||||
hasContent: false,
|
hasContent: false,
|
||||||
showShareDialog: false,
|
showShareDialog: false,
|
||||||
showTypeSelector: true,
|
showTypeSelector: true,
|
||||||
currentType: 10,
|
currentMode: 10,
|
||||||
currentTypeName: '10以内',
|
currentModeName: '10以内',
|
||||||
typeActions: [
|
typeActions: [
|
||||||
{ name: '10以内', value: 10 },
|
{ name: '10以内', value: 10 },
|
||||||
{ name: '20以内', value: 20 },
|
{ name: '20以内', value: 20 },
|
||||||
{ name: '40以内', value: 40 },
|
{ name: '40以内', value: 40 },
|
||||||
{ name: '50以内', value: 50 },
|
{ name: '50以内', value: 50 },
|
||||||
{ name: '80以内', value: 80 },
|
{ name: '80以内', value: 80 },
|
||||||
{ name: '100以内', value: 100 },
|
{ name: '100以内', value: 100 },
|
||||||
{ name: '120以内', value: 120 },
|
{ name: '120以内', value: 120 },
|
||||||
],
|
],
|
||||||
} as CanvasDataState & {
|
} as CanvasDataState & {
|
||||||
showTypeSelector: boolean;
|
showTypeSelector: boolean;
|
||||||
currentType: number;
|
currentMode: number;
|
||||||
currentTypeName: string;
|
currentModeName: string;
|
||||||
typeActions: Array<{ name: string; value: number }>;
|
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);
|
||||||
},
|
},
|
||||||
|
drawServiceOptions: {
|
||||||
onLoad(options: { id?: string }) {
|
subTitle: this.data.subTitle,
|
||||||
const functionId = options.id || 'missing-number';
|
|
||||||
this.initPageInfo(functionId, '填上缺少的数字');
|
|
||||||
},
|
},
|
||||||
|
onCanvasReady: () => {
|
||||||
onReady() {
|
// Canvas 初始化完成后,生成初始数据
|
||||||
this.initCanvas({
|
|
||||||
createDrawService: (
|
|
||||||
canvas: Canvas,
|
|
||||||
ctx: RenderingContext,
|
|
||||||
options?: Record<string, any>,
|
|
||||||
) => {
|
|
||||||
return new MissingNumberDraw(canvas, ctx, options);
|
|
||||||
},
|
|
||||||
drawServiceOptions: {
|
|
||||||
subTitle: this.data.subTitle,
|
|
||||||
},
|
|
||||||
onCanvasReady: () => {
|
|
||||||
// Canvas 初始化完成后,生成初始数据
|
|
||||||
this.onRandom();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 绘制Canvas内容
|
|
||||||
*/
|
|
||||||
async drawCanvas() {
|
|
||||||
if (!this.ctx || !this.drawService || !this.missingNumberData) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.drawService.draw(
|
|
||||||
this.missingNumberData,
|
|
||||||
String(this.data.currentType),
|
|
||||||
);
|
|
||||||
this.setData({ hasContent: true });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('绘制失败:', error);
|
|
||||||
this.setData({ hasContent: false });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 随机生成
|
|
||||||
*/
|
|
||||||
onRandom() {
|
|
||||||
const maxNumber = this.data.currentType;
|
|
||||||
this.generateMissingNumberData(maxNumber);
|
|
||||||
this.drawCanvas();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成缺失数字数据
|
|
||||||
*/
|
|
||||||
generateMissingNumberData(maxNumber: number) {
|
|
||||||
const grids: Array<{
|
|
||||||
numbers: (number | null)[];
|
|
||||||
colors: (string | null)[];
|
|
||||||
}> = [];
|
|
||||||
const gridMap = {
|
|
||||||
10: {
|
|
||||||
gridCount: 3,
|
|
||||||
},
|
|
||||||
20: {
|
|
||||||
gridCount: 2,
|
|
||||||
},
|
|
||||||
40: {
|
|
||||||
gridCount: 2,
|
|
||||||
},
|
|
||||||
50: {
|
|
||||||
gridCount: 2,
|
|
||||||
},
|
|
||||||
80: {
|
|
||||||
gridCount: 1,
|
|
||||||
},
|
|
||||||
100: {
|
|
||||||
gridCount: 1,
|
|
||||||
},
|
|
||||||
120: {
|
|
||||||
gridCount: 1,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const { gridCount } =
|
|
||||||
gridMap[maxNumber as keyof typeof gridMap];
|
|
||||||
|
|
||||||
// 生成多个网格
|
|
||||||
for (let gridIndex = 0; gridIndex < gridCount; gridIndex++) {
|
|
||||||
let startNumber = 1;
|
|
||||||
let endNumber = maxNumber;
|
|
||||||
|
|
||||||
const actualNumbersPerGrid = endNumber - startNumber + 1;
|
|
||||||
|
|
||||||
const numbers: number[] = Array.from(
|
|
||||||
{ length: actualNumbersPerGrid },
|
|
||||||
(_, i) => startNumber + i,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 随机隐藏一部分数字(隐藏40-60%)
|
|
||||||
const hideCount = Math.floor(
|
|
||||||
actualNumbersPerGrid * (0.4 + Math.random() * 0.2),
|
|
||||||
);
|
|
||||||
const hiddenIndices = new Set<number>();
|
|
||||||
|
|
||||||
while (hiddenIndices.size < hideCount) {
|
|
||||||
const randomIndex = Math.floor(
|
|
||||||
Math.random() * actualNumbersPerGrid,
|
|
||||||
);
|
|
||||||
hiddenIndices.add(randomIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
const gridNumbers: (number | null)[] = numbers.map(
|
|
||||||
(num, index) => (hiddenIndices.has(index) ? null : num),
|
|
||||||
);
|
|
||||||
|
|
||||||
// 为每个数字分配颜色(包括null位置)
|
|
||||||
const gridColors: (string | null)[] = gridNumbers.map(
|
|
||||||
(num) => (num ? this.getRandomNumberColor() : null),
|
|
||||||
);
|
|
||||||
|
|
||||||
grids.push({ numbers: gridNumbers, colors: gridColors });
|
|
||||||
}
|
|
||||||
|
|
||||||
this.missingNumberData = {
|
|
||||||
grids,
|
|
||||||
maxNumber,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取随机数字颜色
|
|
||||||
*/
|
|
||||||
getRandomNumberColor(): string {
|
|
||||||
return getRandomNumberColor();
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 选择类型 */
|
|
||||||
onSelectType(event: any) {
|
|
||||||
const { name, value } = event.detail;
|
|
||||||
this.setData({
|
|
||||||
currentType: value,
|
|
||||||
currentTypeName: name,
|
|
||||||
});
|
|
||||||
// 重新生成数据
|
|
||||||
this.onRandom();
|
this.onRandom();
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
{
|
},
|
||||||
pagePath: 'missingNumber/missingNumber',
|
|
||||||
},
|
/**
|
||||||
),
|
* 绘制Canvas内容
|
||||||
);
|
*/
|
||||||
|
async drawCanvas() {
|
||||||
|
if (!this.ctx || !this.drawService || !this.missingNumberData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.drawService.draw(
|
||||||
|
this.missingNumberData,
|
||||||
|
String(this.data.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
|
<math-type-selector
|
||||||
wx:if="{{showTypeSelector}}"
|
wx:if="{{showTypeSelector}}"
|
||||||
current-type-name="{{currentTypeName}}"
|
current-type-name="{{currentModeName}}"
|
||||||
type-actions="{{typeActions}}"
|
type-actions="{{typeActions}}"
|
||||||
bind:select="onSelectType"
|
bind:select="onSelectType"
|
||||||
bind:random="onRandom" />
|
bind:random="onRandom" />
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"enablePullDownRefresh": false,
|
"enablePullDownRefresh": false,
|
||||||
"usingComponents": {
|
"usingComponents": {
|
||||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
"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",
|
||||||
"math-type-selector": "../components/math-type-selector/math-type-selector"
|
"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 NumberDecomposeDraw from '../shared/service/numberDecomposeDraw';
|
||||||
import { applyMathPageMixin, CanvasDataState } from '../common/mathPageMixin';
|
import {
|
||||||
|
createMathPage,
|
||||||
|
CanvasDataState,
|
||||||
|
} from '../shared/common/mathPageMixin';
|
||||||
|
|
||||||
type DecomposeMode = 'with-image' | 'decompose' | 'compose';
|
type DecomposeMode = 'with-image' | 'decompose' | 'compose';
|
||||||
|
|
||||||
Page(
|
createMathPage({
|
||||||
applyMathPageMixin(
|
canvas: null as Canvas | null,
|
||||||
{
|
ctx: null as RenderingContext | null,
|
||||||
canvas: null as Canvas | null,
|
boxHeight: 0,
|
||||||
ctx: null as RenderingContext | null,
|
boxWidth: 0,
|
||||||
boxHeight: 0,
|
drawService: null as NumberDecomposeDraw | null,
|
||||||
boxWidth: 0,
|
decomposeData: null as {
|
||||||
drawService: null as NumberDecomposeDraw | null,
|
problems: Array<{
|
||||||
decomposeData: null as {
|
whole: number | null; // 总数(null表示组合模式需要填写)
|
||||||
problems: Array<{
|
part1: number | null; // 第一个部分(null表示需要填写)
|
||||||
whole: number | null; // 总数(null表示组合模式需要填写)
|
part2: number | null; // 第二个部分(null表示需要填写)
|
||||||
part1: number | null; // 第一个部分(null表示需要填写)
|
imageIndex?: number; // 图片索引(有图片模式需要)
|
||||||
part2: number | null; // 第二个部分(null表示需要填写)
|
imageType?: 'fruits' | 'twelve-animals'; // 图片类型
|
||||||
imageIndex?: number; // 图片索引(有图片模式需要)
|
}>;
|
||||||
imageType?: 'fruits' | 'twelve-animals'; // 图片类型
|
mode: DecomposeMode;
|
||||||
}>;
|
} | null,
|
||||||
mode: DecomposeMode;
|
|
||||||
} | null,
|
|
||||||
|
|
||||||
data: {
|
data: {
|
||||||
pageTitle: '10以内数的分与合',
|
pageTitle: '10以内数的分与合',
|
||||||
subTitle: '学习数的分解与组合',
|
subTitle: '学习数的分解与组合',
|
||||||
functionId: '',
|
functionId: '',
|
||||||
hasContent: false,
|
hasContent: false,
|
||||||
showShareDialog: false,
|
showShareDialog: false,
|
||||||
showTypeSelector: true,
|
showTypeSelector: true,
|
||||||
currentType: 'with-image',
|
currentMode: 'with-image',
|
||||||
currentTypeName: '有图片模式',
|
currentModeName: '有图片模式',
|
||||||
typeActions: [
|
typeActions: [
|
||||||
{ name: '有图片模式', value: 'with-image' },
|
{ name: '有图片模式', value: 'with-image' },
|
||||||
{ name: '分模式', value: 'decompose' },
|
{ name: '分模式', value: 'decompose' },
|
||||||
{ name: '组合模式', value: 'compose' },
|
{ name: '组合模式', value: 'compose' },
|
||||||
],
|
],
|
||||||
} as CanvasDataState & {
|
maxNumber: 10, // 最大数字:10 或 20
|
||||||
showTypeSelector: boolean;
|
} as CanvasDataState & {
|
||||||
currentType: DecomposeMode;
|
showTypeSelector: boolean;
|
||||||
currentTypeName: string;
|
currentMode: DecomposeMode;
|
||||||
typeActions: Array<{ name: string; value: 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);
|
||||||
},
|
},
|
||||||
|
drawServiceOptions: {
|
||||||
onLoad(options: { id?: string; mode?: DecomposeMode }) {
|
subTitle: this.data.subTitle,
|
||||||
const functionId = options.id || 'number-decompose';
|
|
||||||
const mode = options.mode || 'with-image';
|
|
||||||
const currentTypeName =
|
|
||||||
mode === 'with-image'
|
|
||||||
? '有图片模式'
|
|
||||||
: mode === 'decompose'
|
|
||||||
? '分模式'
|
|
||||||
: '组合模式';
|
|
||||||
this.setData({
|
|
||||||
currentType: mode,
|
|
||||||
currentTypeName,
|
|
||||||
});
|
|
||||||
this.initPageInfo(functionId, '10以内数的分与合');
|
|
||||||
},
|
},
|
||||||
|
onCanvasReady: () => {
|
||||||
onReady() {
|
// Canvas 初始化完成后,生成初始数据
|
||||||
this.initCanvas({
|
|
||||||
createDrawService: (
|
|
||||||
canvas: Canvas,
|
|
||||||
ctx: RenderingContext,
|
|
||||||
options?: Record<string, any>,
|
|
||||||
) => {
|
|
||||||
return new NumberDecomposeDraw(canvas, ctx, options);
|
|
||||||
},
|
|
||||||
drawServiceOptions: {
|
|
||||||
subTitle: this.data.subTitle,
|
|
||||||
},
|
|
||||||
onCanvasReady: () => {
|
|
||||||
// Canvas 初始化完成后,生成初始数据
|
|
||||||
this.onRandom();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 绘制Canvas内容
|
|
||||||
*/
|
|
||||||
async drawCanvas() {
|
|
||||||
if (!this.ctx || !this.drawService || !this.decomposeData) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.drawService.draw(this.decomposeData);
|
|
||||||
this.setData({ hasContent: true });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('绘制失败:', error);
|
|
||||||
this.setData({ hasContent: false });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 随机生成
|
|
||||||
*/
|
|
||||||
onRandom() {
|
|
||||||
this.generateDecomposeData();
|
|
||||||
this.drawCanvas();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成分解数据(确保不重复)
|
|
||||||
*/
|
|
||||||
generateDecomposeData() {
|
|
||||||
const mode = this.data.currentType;
|
|
||||||
const problems: Array<{
|
|
||||||
whole: number | null;
|
|
||||||
part1: number | null;
|
|
||||||
part2: number | null;
|
|
||||||
imageIndex?: number;
|
|
||||||
imageType?: 'fruits' | 'twelve-animals';
|
|
||||||
}> = [];
|
|
||||||
|
|
||||||
// 用于去重的 Set,存储题目唯一标识
|
|
||||||
// 格式:对于分解模式 "whole:part1:part2:showPart1",对于组合模式 "part1:part2"
|
|
||||||
const usedKeys = new Set<string>();
|
|
||||||
|
|
||||||
if (mode === 'with-image') {
|
|
||||||
// 有图片模式:9个题目,一行三列,总共三行
|
|
||||||
const problemCount = 9;
|
|
||||||
let attempts = 0;
|
|
||||||
const maxAttempts = problemCount * 50; // 最大尝试次数
|
|
||||||
|
|
||||||
while (
|
|
||||||
problems.length < problemCount &&
|
|
||||||
attempts < maxAttempts
|
|
||||||
) {
|
|
||||||
attempts++;
|
|
||||||
|
|
||||||
// 生成总数(2-10)
|
|
||||||
const whole = Math.floor(Math.random() * 9) + 2;
|
|
||||||
// 随机选择一个部分(1 到 whole-1)
|
|
||||||
const part1 =
|
|
||||||
Math.floor(Math.random() * (whole - 1)) + 1;
|
|
||||||
const part2 = whole - part1;
|
|
||||||
|
|
||||||
// 随机决定显示 part1 还是 part2(另一个为 null)
|
|
||||||
const showPart1 = Math.random() < 0.5;
|
|
||||||
|
|
||||||
// 生成唯一标识(统一格式:part1 <= part2)
|
|
||||||
const minPart = Math.min(part1, part2);
|
|
||||||
const maxPart = Math.max(part1, part2);
|
|
||||||
const key = `${whole}:${minPart}:${maxPart}:${showPart1}`;
|
|
||||||
|
|
||||||
// 检查是否已存在
|
|
||||||
if (usedKeys.has(key)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
usedKeys.add(key);
|
|
||||||
|
|
||||||
// 随机选择图片类型和索引
|
|
||||||
const imageType: 'fruits' | 'twelve-animals' =
|
|
||||||
Math.random() < 0.5 ? 'fruits' : 'twelve-animals';
|
|
||||||
const maxImageIndex = imageType === 'fruits' ? 22 : 12;
|
|
||||||
const imageIndex =
|
|
||||||
Math.floor(Math.random() * maxImageIndex) + 1;
|
|
||||||
|
|
||||||
problems.push({
|
|
||||||
whole,
|
|
||||||
part1: showPart1 ? part1 : null,
|
|
||||||
part2: showPart1 ? null : part2,
|
|
||||||
imageIndex,
|
|
||||||
imageType,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (mode === 'decompose') {
|
|
||||||
// 分模式:15个题目,一行3个,总共5行
|
|
||||||
const problemCount = 15;
|
|
||||||
let attempts = 0;
|
|
||||||
const maxAttempts = problemCount * 50; // 最大尝试次数
|
|
||||||
|
|
||||||
while (
|
|
||||||
problems.length < problemCount &&
|
|
||||||
attempts < maxAttempts
|
|
||||||
) {
|
|
||||||
attempts++;
|
|
||||||
|
|
||||||
// 生成总数(2-10)
|
|
||||||
const whole = Math.floor(Math.random() * 9) + 2;
|
|
||||||
// 随机选择一个部分(1 到 whole-1)
|
|
||||||
const part1 =
|
|
||||||
Math.floor(Math.random() * (whole - 1)) + 1;
|
|
||||||
const part2 = whole - part1;
|
|
||||||
|
|
||||||
// 随机决定显示 part1 还是 part2(另一个为 null)
|
|
||||||
const showPart1 = Math.random() < 0.5;
|
|
||||||
|
|
||||||
// 生成唯一标识(统一格式:part1 <= part2)
|
|
||||||
const minPart = Math.min(part1, part2);
|
|
||||||
const maxPart = Math.max(part1, part2);
|
|
||||||
const key = `${whole}:${minPart}:${maxPart}:${showPart1}`;
|
|
||||||
|
|
||||||
// 检查是否已存在
|
|
||||||
if (usedKeys.has(key)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
usedKeys.add(key);
|
|
||||||
|
|
||||||
problems.push({
|
|
||||||
whole,
|
|
||||||
part1: showPart1 ? part1 : null,
|
|
||||||
part2: showPart1 ? null : part2,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (mode === 'compose') {
|
|
||||||
// 组合模式:15个题目,一行3个,总共5行
|
|
||||||
const problemCount = 15;
|
|
||||||
let attempts = 0;
|
|
||||||
const maxAttempts = problemCount * 50; // 最大尝试次数
|
|
||||||
|
|
||||||
while (
|
|
||||||
problems.length < problemCount &&
|
|
||||||
attempts < maxAttempts
|
|
||||||
) {
|
|
||||||
attempts++;
|
|
||||||
|
|
||||||
// 生成总数(2-10)
|
|
||||||
const whole = Math.floor(Math.random() * 9) + 2;
|
|
||||||
// 随机选择一个部分(1 到 whole-1)
|
|
||||||
const part1 =
|
|
||||||
Math.floor(Math.random() * (whole - 1)) + 1;
|
|
||||||
const part2 = whole - part1;
|
|
||||||
|
|
||||||
// 生成唯一标识(统一格式:part1 <= part2)
|
|
||||||
const minPart = Math.min(part1, part2);
|
|
||||||
const maxPart = Math.max(part1, part2);
|
|
||||||
const key = `${minPart}:${maxPart}`;
|
|
||||||
|
|
||||||
// 检查是否已存在
|
|
||||||
if (usedKeys.has(key)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
usedKeys.add(key);
|
|
||||||
|
|
||||||
// 组合模式:两个部分都显示,根节点为 null
|
|
||||||
problems.push({
|
|
||||||
whole: null, // 根节点需要填写
|
|
||||||
part1,
|
|
||||||
part2,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.decomposeData = { problems, mode };
|
|
||||||
},
|
|
||||||
|
|
||||||
/** 选择类型 */
|
|
||||||
onSelectType(event: any) {
|
|
||||||
const { name, value } = event.detail;
|
|
||||||
this.setData({
|
|
||||||
currentType: value,
|
|
||||||
currentTypeName: name,
|
|
||||||
});
|
|
||||||
// 重新生成数据
|
|
||||||
this.onRandom();
|
this.onRandom();
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
{
|
},
|
||||||
pagePath: 'numberDecompose/numberDecompose',
|
|
||||||
},
|
/**
|
||||||
),
|
* 绘制Canvas内容
|
||||||
);
|
*/
|
||||||
|
async drawCanvas() {
|
||||||
|
if (!this.ctx || !this.drawService || !this.decomposeData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.drawService.draw(this.decomposeData);
|
||||||
|
this.setData({ hasContent: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('绘制失败:', error);
|
||||||
|
this.setData({ hasContent: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 随机生成
|
||||||
|
*/
|
||||||
|
onRandom() {
|
||||||
|
this.generateDecomposeData();
|
||||||
|
this.drawCanvas();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成分解数据(确保不重复)
|
||||||
|
*/
|
||||||
|
generateDecomposeData() {
|
||||||
|
const mode = this.data.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
|
<math-type-selector
|
||||||
wx:if="{{showTypeSelector}}"
|
wx:if="{{showTypeSelector}}"
|
||||||
current-type-name="{{currentTypeName}}"
|
current-type-name="{{currentModeName}}"
|
||||||
type-actions="{{typeActions}}"
|
type-actions="{{typeActions}}"
|
||||||
bind:select="onSelectType"
|
bind:select="onSelectType"
|
||||||
bind:random="onRandom" />
|
bind:random="onRandom" />
|
||||||
|
|||||||
@@ -7,6 +7,6 @@
|
|||||||
"usingComponents": {
|
"usingComponents": {
|
||||||
"toy-button": "../../ui/button/button",
|
"toy-button": "../../ui/button/button",
|
||||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
"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 {
|
.number-selection-area {
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import NumberFindDraw from '../service/numberFindDraw';
|
import NumberFindDraw from '../shared/service/numberFindDraw';
|
||||||
import { createMathPage, CanvasDataState } from '../common/mathPageMixin';
|
import {
|
||||||
|
createMathPage,
|
||||||
|
CanvasDataState,
|
||||||
|
} from '../shared/common/mathPageMixin';
|
||||||
|
|
||||||
createMathPage({
|
createMathPage({
|
||||||
canvas: null as Canvas | null,
|
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,
|
"component": true,
|
||||||
"usingComponents": {
|
"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 {
|
interface DrawAdditionContentParams {
|
||||||
canvas: WechatMiniprogram.Canvas;
|
canvas: WechatMiniprogram.Canvas;
|
||||||
+2
-2
@@ -1,11 +1,11 @@
|
|||||||
import { BaseMathDrawService } from './baseMathDraw';
|
import { BaseDrawService } from '../../../service/baseDraw';
|
||||||
import { drawAdditionContent } from './additionContentDraw';
|
import { drawAdditionContent } from './additionContentDraw';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 加减法计算绘制服务
|
* 加减法计算绘制服务
|
||||||
* 组合使用基础绘制服务和内容区域绘制服务
|
* 组合使用基础绘制服务和内容区域绘制服务
|
||||||
*/
|
*/
|
||||||
class AdditionDraw extends BaseMathDrawService {
|
class AdditionDraw extends BaseDrawService {
|
||||||
calculationData: {
|
calculationData: {
|
||||||
problems: Array<{
|
problems: Array<{
|
||||||
type: 'addition' | 'subtraction';
|
type: 'addition' | 'subtraction';
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import { getImage } from '../../utils/index';
|
import { getImage } from '../../../utils/index';
|
||||||
|
|
||||||
interface DrawCompareContentParams {
|
interface DrawCompareContentParams {
|
||||||
canvas: WechatMiniprogram.Canvas;
|
canvas: WechatMiniprogram.Canvas;
|
||||||
+2
-2
@@ -1,11 +1,11 @@
|
|||||||
import { BaseMathDrawService } from './baseMathDraw';
|
import { BaseDrawService } from '../../../service/baseDraw';
|
||||||
import { drawCompareContent } from './compareContentDraw';
|
import { drawCompareContent } from './compareContentDraw';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 数一数比大小绘制服务
|
* 数一数比大小绘制服务
|
||||||
* 组合使用基础绘制服务和内容区域绘制服务
|
* 组合使用基础绘制服务和内容区域绘制服务
|
||||||
*/
|
*/
|
||||||
class CompareDraw extends BaseMathDrawService {
|
class CompareDraw extends BaseDrawService {
|
||||||
compareData: {
|
compareData: {
|
||||||
problems: Array<{
|
problems: Array<{
|
||||||
leftCount: number;
|
leftCount: number;
|
||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
import { getImage } from '../../utils/index';
|
import { getImage } from '../../../utils/index';
|
||||||
import { getRandomNumberColor } from '../../constants/colors';
|
import { getRandomNumberColor } from '../../../constants/colors';
|
||||||
|
|
||||||
interface DrawCountMatchContentParams {
|
interface DrawCountMatchContentParams {
|
||||||
canvas: WechatMiniprogram.Canvas;
|
canvas: WechatMiniprogram.Canvas;
|
||||||
+2
-2
@@ -1,11 +1,11 @@
|
|||||||
import { BaseMathDrawService } from './baseMathDraw';
|
import { BaseDrawService } from '../../../service/baseDraw';
|
||||||
import { drawCountMatchContent } from './countMatchContentDraw';
|
import { drawCountMatchContent } from './countMatchContentDraw';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 数一数连一连绘制服务
|
* 数一数连一连绘制服务
|
||||||
* 组合使用基础绘制服务和内容区域绘制服务
|
* 组合使用基础绘制服务和内容区域绘制服务
|
||||||
*/
|
*/
|
||||||
class CountMatchDraw extends BaseMathDrawService {
|
class CountMatchDraw extends BaseDrawService {
|
||||||
matchData: {
|
matchData: {
|
||||||
leftNumbers: number[];
|
leftNumbers: number[];
|
||||||
rightNumbers: number[]; // 打乱顺序后的数字数组
|
rightNumbers: number[]; // 打乱顺序后的数字数组
|
||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
import { getImage } from '../../utils/index';
|
import { getImage } from '../../../utils/index';
|
||||||
import { getRandomNumberColor } from '../../constants/colors';
|
import { getRandomNumberColor } from '../../../constants/colors';
|
||||||
|
|
||||||
interface DrawCountingSelectContentParams {
|
interface DrawCountingSelectContentParams {
|
||||||
canvas: WechatMiniprogram.Canvas;
|
canvas: WechatMiniprogram.Canvas;
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
import { BaseMathDrawService } from './baseMathDraw';
|
import { BaseDrawService } from '../../../service/baseDraw';
|
||||||
import { drawCountingSelectContent } from './countingSelectContentDraw';
|
import { drawCountingSelectContent } from './countingSelectContentDraw';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -6,7 +6,7 @@ import { drawCountingSelectContent } from './countingSelectContentDraw';
|
|||||||
* 组合使用基础绘制服务和内容区域绘制服务
|
* 组合使用基础绘制服务和内容区域绘制服务
|
||||||
* 支持两种模式:'select'(选一选)和 'fill'(填一填)
|
* 支持两种模式:'select'(选一选)和 'fill'(填一填)
|
||||||
*/
|
*/
|
||||||
class CountingSelectDraw extends BaseMathDrawService {
|
class CountingSelectDraw extends BaseDrawService {
|
||||||
countingData: {
|
countingData: {
|
||||||
problems: Array<{
|
problems: Array<{
|
||||||
count: number;
|
count: number;
|
||||||
+2
-2
@@ -1,11 +1,11 @@
|
|||||||
import { BaseMathDrawService } from './baseMathDraw';
|
import { BaseDrawService } from '../../../service/baseDraw';
|
||||||
import { drawMissingNumberContent } from './missingNumberContentDraw';
|
import { drawMissingNumberContent } from './missingNumberContentDraw';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 填上缺少的数字绘制服务
|
* 填上缺少的数字绘制服务
|
||||||
* 组合使用基础绘制服务和内容区域绘制服务
|
* 组合使用基础绘制服务和内容区域绘制服务
|
||||||
*/
|
*/
|
||||||
class MissingNumberDraw extends BaseMathDrawService {
|
class MissingNumberDraw extends BaseDrawService {
|
||||||
missingNumberData: {
|
missingNumberData: {
|
||||||
grids: Array<{
|
grids: Array<{
|
||||||
numbers: (number | null)[];
|
numbers: (number | null)[];
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import { getNumberColors } from '../../constants/colors';
|
import { getNumberColors } from '../../../constants/colors';
|
||||||
|
|
||||||
interface DrawNumberColorContentParams {
|
interface DrawNumberColorContentParams {
|
||||||
canvas: WechatMiniprogram.Canvas;
|
canvas: WechatMiniprogram.Canvas;
|
||||||
+2
-2
@@ -1,11 +1,11 @@
|
|||||||
import { BaseMathDrawService } from './baseMathDraw';
|
import { BaseDrawService } from '../../../service/baseDraw';
|
||||||
import { drawNumberColorContent } from './numberColorContentDraw';
|
import { drawNumberColorContent } from './numberColorContentDraw';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按数字涂颜色绘制服务
|
* 按数字涂颜色绘制服务
|
||||||
* 组合使用基础绘制服务和内容区域绘制服务
|
* 组合使用基础绘制服务和内容区域绘制服务
|
||||||
*/
|
*/
|
||||||
class NumberColorDraw extends BaseMathDrawService {
|
class NumberColorDraw extends BaseDrawService {
|
||||||
colorData: {
|
colorData: {
|
||||||
numbers: number[];
|
numbers: number[];
|
||||||
} | null;
|
} | null;
|
||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
import { getImage } from '../../utils/index';
|
import { getImage } from '../../../utils/index';
|
||||||
import { getRandomNumberColor } from '../../constants/colors';
|
import { getRandomNumberColor } from '../../../constants/colors';
|
||||||
|
|
||||||
interface DrawNumberDecomposeContentParams {
|
interface DrawNumberDecomposeContentParams {
|
||||||
canvas: WechatMiniprogram.Canvas;
|
canvas: WechatMiniprogram.Canvas;
|
||||||
+2
-2
@@ -1,11 +1,11 @@
|
|||||||
import { BaseMathDrawService } from './baseMathDraw';
|
import { BaseDrawService } from '../../../service/baseDraw';
|
||||||
import { drawNumberDecomposeContent } from './numberDecomposeContentDraw';
|
import { drawNumberDecomposeContent } from './numberDecomposeContentDraw';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 10以内数的分与合绘制服务
|
* 10以内数的分与合绘制服务
|
||||||
* 组合使用基础绘制服务和内容区域绘制服务
|
* 组合使用基础绘制服务和内容区域绘制服务
|
||||||
*/
|
*/
|
||||||
class NumberDecomposeDraw extends BaseMathDrawService {
|
class NumberDecomposeDraw extends BaseDrawService {
|
||||||
decomposeData: {
|
decomposeData: {
|
||||||
problems: Array<{
|
problems: Array<{
|
||||||
whole: number | null;
|
whole: number | null;
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
import { BaseMathDrawService } from './baseMathDraw';
|
import { BaseDrawService } from '../../../service/baseDraw';
|
||||||
import { drawNumberPreview } from './numberPreviewDraw';
|
import { drawNumberPreview } from './numberPreviewDraw';
|
||||||
import { drawNumberContent } from './numberContentDraw';
|
import { drawNumberContent } from './numberContentDraw';
|
||||||
import { drawNumberWriteContent } from './numberWriteDraw';
|
import { drawNumberWriteContent } from './numberWriteDraw';
|
||||||
@@ -7,7 +7,7 @@ import { drawNumberWriteContent } from './numberWriteDraw';
|
|||||||
* 数字涂色绘制服务
|
* 数字涂色绘制服务
|
||||||
* 组合使用基础绘制服务、预览区域绘制服务和内容区域绘制服务
|
* 组合使用基础绘制服务、预览区域绘制服务和内容区域绘制服务
|
||||||
*/
|
*/
|
||||||
class NumberFindDraw extends BaseMathDrawService {
|
class NumberFindDraw extends BaseDrawService {
|
||||||
selectedNumber: number;
|
selectedNumber: number;
|
||||||
functionId: string; // 功能ID,用于判断绘制类型
|
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 { getWordsSvgData } from '../../utils/getWordsSvgJson';
|
||||||
import { WORDS } from '../../constants/words';
|
import { WORDS } from '../../constants/words';
|
||||||
import { CharacterItem } from '../../types/characterType';
|
import { CharacterItem } from '../../types/characterType';
|
||||||
|
import tracker from '../../utils/tracker';
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
canvas: null as Canvas | null,
|
canvas: null as Canvas | null,
|
||||||
@@ -360,6 +361,9 @@ Page({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 上报下载埋点
|
||||||
|
tracker.reportDownload('练字贴');
|
||||||
|
|
||||||
// 直接下载
|
// 直接下载
|
||||||
checkAndSaveImage(this.canvas);
|
checkAndSaveImage(this.canvas);
|
||||||
},
|
},
|
||||||
@@ -371,6 +375,9 @@ Page({
|
|||||||
|
|
||||||
// 分享功能
|
// 分享功能
|
||||||
onShareAppMessage() {
|
onShareAppMessage() {
|
||||||
|
// 上报分享埋点
|
||||||
|
tracker.reportShare('练字贴');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: '涂鸦丫-涂色|练字|识字|打印',
|
title: '涂鸦丫-涂色|练字|识字|打印',
|
||||||
path: '/pages/copyBook/copyBook',
|
path: '/pages/copyBook/copyBook',
|
||||||
@@ -379,6 +386,9 @@ Page({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
onShareTimeline() {
|
onShareTimeline() {
|
||||||
|
// 上报分享埋点
|
||||||
|
tracker.reportShare('练字贴');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: '涂鸦丫-涂色|练字|识字|打印',
|
title: '涂鸦丫-涂色|练字|识字|打印',
|
||||||
query: '/pages/copyBook/copyBook',
|
query: '/pages/copyBook/copyBook',
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import config from '../../config/config';
|
import { defaultPrintConfig } from '../../config/config';
|
||||||
|
|
||||||
const defaultPrintConfig: PrintConfig = config.printHeader as PrintConfig;
|
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
data: {
|
data: {
|
||||||
printConfig: defaultPrintConfig,
|
printConfig: defaultPrintConfig,
|
||||||
enableDebug: false
|
enableDebug: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
onLoad() {
|
onLoad() {
|
||||||
@@ -41,5 +39,5 @@ Page({
|
|||||||
toggleDebug() {
|
toggleDebug() {
|
||||||
const enableDebug = !this.data.enableDebug;
|
const enableDebug = !this.data.enableDebug;
|
||||||
this.setData({ 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';
|
} from '../../service/drawServiceFactory';
|
||||||
import { checkAndSaveImage } from '../../utils/saveImage';
|
import { checkAndSaveImage } from '../../utils/saveImage';
|
||||||
import { shouldShowShareGuide } from '../../utils/shareGuide';
|
import { shouldShowShareGuide } from '../../utils/shareGuide';
|
||||||
|
import tracker from '../../utils/tracker';
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
canvas: null as Canvas | null,
|
canvas: null as Canvas | null,
|
||||||
@@ -29,7 +30,12 @@ Page({
|
|||||||
showShareDialog: false, // 显示分享引导弹窗
|
showShareDialog: false, // 显示分享引导弹窗
|
||||||
},
|
},
|
||||||
|
|
||||||
onLoad() {
|
onLoad(options: { template?: string }) {
|
||||||
|
const { template } = options;
|
||||||
|
this.setData({
|
||||||
|
selectedTemplate: template || 'grid',
|
||||||
|
});
|
||||||
|
|
||||||
const hasShowIntroduction =
|
const hasShowIntroduction =
|
||||||
wx.getStorageSync('hasShowIntroduction') || false;
|
wx.getStorageSync('hasShowIntroduction') || false;
|
||||||
|
|
||||||
@@ -292,6 +298,9 @@ Page({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 上报下载埋点
|
||||||
|
tracker.reportDownload('涂色识字', this.data.selectedTemplate);
|
||||||
|
|
||||||
// 直接下载
|
// 直接下载
|
||||||
checkAndSaveImage(this.canvas);
|
checkAndSaveImage(this.canvas);
|
||||||
},
|
},
|
||||||
@@ -344,17 +353,23 @@ Page({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onShareAppMessage() {
|
onShareAppMessage() {
|
||||||
|
// 上报分享埋点
|
||||||
|
tracker.reportShare('涂色识字');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: '涂鸦丫-涂色|识字|画画|打印',
|
title: '涂鸦丫-涂色|识字|画画|打印',
|
||||||
path: '/pages/index/index',
|
path: `/pages/index/index?template=${this.data.selectedTemplate}`,
|
||||||
imageUrl:
|
imageUrl:
|
||||||
'https://cdn.joeyone.cn/doodle/share-img/index-share-v2.png',
|
'https://cdn.joeyone.cn/doodle/share-img/index-share-v2.png',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
onShareTimeline() {
|
onShareTimeline() {
|
||||||
|
// 上报分享埋点
|
||||||
|
tracker.reportShare('涂色识字');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: '涂鸦丫-涂色|识字|画画|打印',
|
title: '涂鸦丫-涂色|识字|画画|打印',
|
||||||
query: '/pages/index/index',
|
query: `/pages/index/index?template=${this.data.selectedTemplate}`,
|
||||||
imageUrl:
|
imageUrl:
|
||||||
'https://cdn.joeyone.cn/doodle/share-img/index-share-v2.png',
|
'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 ShapeDrawService from '../../service/shapeDrawService';
|
||||||
import { checkAndSaveImage } from '../../utils/saveImage';
|
import { checkAndSaveImage } from '../../utils/saveImage';
|
||||||
import { shouldShowShareGuide } from '../../utils/shareGuide';
|
import { shouldShowShareGuide } from '../../utils/shareGuide';
|
||||||
|
import tracker from '../../utils/tracker';
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
canvas: null as Canvas | null,
|
canvas: null as Canvas | null,
|
||||||
@@ -296,6 +297,9 @@ Page({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 上报下载埋点
|
||||||
|
tracker.reportDownload('图形涂色');
|
||||||
|
|
||||||
// 直接下载
|
// 直接下载
|
||||||
checkAndSaveImage(this.canvas);
|
checkAndSaveImage(this.canvas);
|
||||||
},
|
},
|
||||||
@@ -306,6 +310,9 @@ Page({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onShareAppMessage() {
|
onShareAppMessage() {
|
||||||
|
// 上报分享埋点
|
||||||
|
tracker.reportShare('图形涂色');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: '涂鸦丫-涂色|识字|图形|打印',
|
title: '涂鸦丫-涂色|识字|图形|打印',
|
||||||
path: '/pages/shape/index',
|
path: '/pages/shape/index',
|
||||||
@@ -314,6 +321,9 @@ Page({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
onShareTimeline() {
|
onShareTimeline() {
|
||||||
|
// 上报分享埋点
|
||||||
|
tracker.reportShare('图形涂色');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: '涂鸦丫-涂色|识字|图形|打印',
|
title: '涂鸦丫-涂色|识字|图形|打印',
|
||||||
query: '/pages/shape/index',
|
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 { PAPER_SIZE } from '../constants/colors';
|
||||||
import { drawMathHeader, drawMathMiniHeader } from './mathHeaderDraw';
|
import { drawBaseHeader, drawBaseMiniHeader } from './baseHeaderDraw';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 基础数学绘制服务
|
* 基础绘制服务
|
||||||
* 包含Paper设置和Header绘制功能,可被其他绘制服务复用
|
* 包含Paper设置和Header绘制功能,可被所有绘制服务复用
|
||||||
|
*
|
||||||
|
* 提供功能:
|
||||||
|
* - Canvas 初始化和配置
|
||||||
|
* - Paper 尺寸设置(支持 A4 等标准尺寸)
|
||||||
|
* - Header 绘制(支持完整 Header 和迷你 Header)
|
||||||
|
* - 分割线绘制
|
||||||
|
* - 打印配置管理
|
||||||
*/
|
*/
|
||||||
export class BaseMathDrawService {
|
export class BaseDrawService {
|
||||||
headerType: PrintHeader = 'wechat';
|
headerType: PrintHeader = 'wechat';
|
||||||
canvas: WechatMiniprogram.Canvas;
|
canvas: WechatMiniprogram.Canvas;
|
||||||
ctx: RenderingContext;
|
ctx: RenderingContext;
|
||||||
@@ -22,14 +29,16 @@ export class BaseMathDrawService {
|
|||||||
options?: Record<string, any>,
|
options?: Record<string, any>,
|
||||||
) {
|
) {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
|
const { appName, appHint } = getApp().getPrintConfig();
|
||||||
this.canvas = canvas;
|
this.canvas = canvas;
|
||||||
this.ctx = ctx;
|
this.ctx = ctx;
|
||||||
this.paperSize = 'A4';
|
this.paperSize = 'A4';
|
||||||
this.options = {
|
this.options = {
|
||||||
appName: '涂鸦丫小程序',
|
appName,
|
||||||
appHint: '在玩耍中学习数学',
|
appHint,
|
||||||
title: '看数字,涂一涂',
|
title: '看数字,涂一涂',
|
||||||
subTitle: '找一找下面相同的数字,涂上颜色',
|
subTitle: '找一找下面相同的数字,涂上颜色',
|
||||||
|
|
||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
this.currentX = 0;
|
this.currentX = 0;
|
||||||
@@ -86,7 +95,7 @@ export class BaseMathDrawService {
|
|||||||
async drawHeader() {
|
async drawHeader() {
|
||||||
this.currentX = 25;
|
this.currentX = 25;
|
||||||
this.currentY = 25;
|
this.currentY = 25;
|
||||||
await drawMathHeader({
|
await drawBaseHeader({
|
||||||
canvas: this.canvas,
|
canvas: this.canvas,
|
||||||
ctx: this.ctx,
|
ctx: this.ctx,
|
||||||
headerType: this.headerType,
|
headerType: this.headerType,
|
||||||
@@ -107,7 +116,7 @@ export class BaseMathDrawService {
|
|||||||
* 绘制迷你Header(逻辑像素,尺寸除以3)
|
* 绘制迷你Header(逻辑像素,尺寸除以3)
|
||||||
*/
|
*/
|
||||||
drawMiniHeader() {
|
drawMiniHeader() {
|
||||||
drawMathMiniHeader({
|
drawBaseMiniHeader({
|
||||||
ctx: this.ctx,
|
ctx: this.ctx,
|
||||||
canvasWidth: this.canvasWidth,
|
canvasWidth: this.canvasWidth,
|
||||||
options: {
|
options: {
|
||||||
+9
-9
@@ -1,9 +1,9 @@
|
|||||||
import { getMiniCodeImage, getImage } from '../../utils/index';
|
import { getMiniCodeImage, getImage } from '../utils/index';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 绘制数学模块页眉的参数接口(尺寸除以3)
|
* 绘制数学模块页眉的参数接口(尺寸除以3)
|
||||||
*/
|
*/
|
||||||
interface DrawMathHeaderParams {
|
interface drawBaseHeaderParams {
|
||||||
canvas: WechatMiniprogram.Canvas;
|
canvas: WechatMiniprogram.Canvas;
|
||||||
ctx: RenderingContext;
|
ctx: RenderingContext;
|
||||||
headerType: PrintHeader;
|
headerType: PrintHeader;
|
||||||
@@ -19,13 +19,13 @@ interface DrawMathHeaderParams {
|
|||||||
/**
|
/**
|
||||||
* 绘制数学模块完整页眉(尺寸除以3)
|
* 绘制数学模块完整页眉(尺寸除以3)
|
||||||
*/
|
*/
|
||||||
export async function drawMathHeader({
|
export async function drawBaseHeader({
|
||||||
canvas,
|
canvas,
|
||||||
ctx,
|
ctx,
|
||||||
headerType,
|
headerType,
|
||||||
options,
|
options,
|
||||||
onHeaderDrawn,
|
onHeaderDrawn,
|
||||||
}: DrawMathHeaderParams): Promise<void> {
|
}: drawBaseHeaderParams): Promise<void> {
|
||||||
const { appName, appHint, title, subTitle } = options;
|
const { appName, appHint, title, subTitle } = options;
|
||||||
|
|
||||||
let titleX = 108; // 约109.33
|
let titleX = 108; // 约109.33
|
||||||
@@ -85,14 +85,14 @@ export async function drawMathHeader({
|
|||||||
|
|
||||||
// 调用回调函数
|
// 调用回调函数
|
||||||
if (onHeaderDrawn) {
|
if (onHeaderDrawn) {
|
||||||
onHeaderDrawn(110);
|
onHeaderDrawn(104);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 绘制数学模块迷你页眉的参数接口(尺寸除以3)
|
* 绘制数学模块迷你页眉的参数接口(尺寸除以3)
|
||||||
*/
|
*/
|
||||||
interface DrawMathMiniHeaderParams {
|
interface drawBaseMiniHeaderParams {
|
||||||
// canvas: WechatMiniprogram.Canvas;
|
// canvas: WechatMiniprogram.Canvas;
|
||||||
ctx: RenderingContext;
|
ctx: RenderingContext;
|
||||||
options: {
|
options: {
|
||||||
@@ -106,12 +106,12 @@ interface DrawMathMiniHeaderParams {
|
|||||||
/**
|
/**
|
||||||
* 绘制数学模块迷你页眉(尺寸除以3)
|
* 绘制数学模块迷你页眉(尺寸除以3)
|
||||||
*/
|
*/
|
||||||
export function drawMathMiniHeader({
|
export function drawBaseMiniHeader({
|
||||||
ctx,
|
ctx,
|
||||||
options,
|
options,
|
||||||
canvasWidth,
|
canvasWidth,
|
||||||
onHeaderDrawn,
|
onHeaderDrawn,
|
||||||
}: DrawMathMiniHeaderParams): void {
|
}: drawBaseMiniHeaderParams): void {
|
||||||
const { appName, title } = options;
|
const { appName, title } = options;
|
||||||
const titleY = 46;
|
const titleY = 46;
|
||||||
const centerX = canvasWidth / 2;
|
const centerX = canvasWidth / 2;
|
||||||
@@ -124,6 +124,6 @@ export function drawMathMiniHeader({
|
|||||||
|
|
||||||
// 调用回调函数,传递除以3后的currentY
|
// 调用回调函数,传递除以3后的currentY
|
||||||
if (onHeaderDrawn) {
|
if (onHeaderDrawn) {
|
||||||
onHeaderDrawn(66); // 约66.67
|
onHeaderDrawn(60); // 约66.67
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,4 @@
|
|||||||
import { PAPER_SIZE } from '../constants/colors';
|
import { BaseDrawService } from './baseDraw';
|
||||||
import {
|
|
||||||
drawHeader as drawHeaderCommon,
|
|
||||||
drawMiniHeader as drawMiniHeaderCommon,
|
|
||||||
} from './headerDrawService';
|
|
||||||
import { POSITION_TEMPLATES } from './findWordTemplate';
|
import { POSITION_TEMPLATES } from './findWordTemplate';
|
||||||
|
|
||||||
// ==================== Debug 开关 ====================
|
// ==================== Debug 开关 ====================
|
||||||
@@ -35,13 +31,11 @@ function selectTemplate(): { templateIndex: number } {
|
|||||||
/**
|
/**
|
||||||
* 从指定模板中获取位置
|
* 从指定模板中获取位置
|
||||||
* @param templateIndex 模板索引
|
* @param templateIndex 模板索引
|
||||||
* @param centerX 中心X坐标
|
* @param centerX 中心X坐标(逻辑像素)
|
||||||
* @param centerY 中心Y坐标
|
* @param centerY 中心Y坐标(逻辑像素)
|
||||||
* @param padding 边距
|
* @returns 位置数组(逻辑像素)
|
||||||
* @param canvasWidth 画布宽度
|
*
|
||||||
* @param canvasHeight 画布高度
|
* 注意:POSITION_TEMPLATES 中的坐标是基于原始像素的,需要转换为逻辑像素(除以3)
|
||||||
* @param radius 字符圆半径
|
|
||||||
* @returns 位置数组
|
|
||||||
*/
|
*/
|
||||||
function getPositionsFromTemplate(
|
function getPositionsFromTemplate(
|
||||||
templateIndex: number,
|
templateIndex: number,
|
||||||
@@ -50,25 +44,18 @@ function getPositionsFromTemplate(
|
|||||||
): Array<{ x: number; y: number }> {
|
): Array<{ x: number; y: number }> {
|
||||||
const template = POSITION_TEMPLATES[templateIndex];
|
const template = POSITION_TEMPLATES[templateIndex];
|
||||||
|
|
||||||
// 转换为绝对坐标并检查边界
|
// 转换为绝对坐标(模板坐标除以3转换为逻辑像素)
|
||||||
const positions: Array<{ x: number; y: number }> = [];
|
const positions: Array<{ x: number; y: number }> = [];
|
||||||
for (const pos of template) {
|
for (const pos of template) {
|
||||||
const x = centerX + pos.x;
|
const x = centerX + pos.x / 3; // 转换为逻辑像素
|
||||||
const y = centerY + pos.y;
|
const y = centerY + pos.y / 3; // 转换为逻辑像素
|
||||||
|
|
||||||
positions.push({ x, y });
|
positions.push({ x, y });
|
||||||
}
|
}
|
||||||
return positions;
|
return positions;
|
||||||
}
|
}
|
||||||
|
|
||||||
class FindWordDrawService {
|
class FindWordDrawService extends BaseDrawService {
|
||||||
headerType: PrintHeader = 'wechat';
|
|
||||||
canvas: WechatMiniprogram.Canvas;
|
|
||||||
ctx: RenderingContext;
|
|
||||||
options: Record<string, any>;
|
|
||||||
paperSize: PaperSize;
|
|
||||||
currentX: number;
|
|
||||||
currentY: number;
|
|
||||||
colors: string[];
|
colors: string[];
|
||||||
characters: string[];
|
characters: string[];
|
||||||
debug: boolean = false; // Debug模式开关
|
debug: boolean = false; // Debug模式开关
|
||||||
@@ -79,29 +66,15 @@ class FindWordDrawService {
|
|||||||
options?: Record<string, any>,
|
options?: Record<string, any>,
|
||||||
) {
|
) {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
this.canvas = canvas;
|
super(canvas, ctx, {
|
||||||
this.ctx = ctx;
|
|
||||||
this.paperSize = 'A4';
|
|
||||||
this.options = {
|
|
||||||
appName: '涂鸦丫小程序',
|
|
||||||
appHint: '识字|识图|练字|打印',
|
|
||||||
title: '找一找 涂 色',
|
title: '找一找 涂 色',
|
||||||
subTitle: '找出相同的文字涂色',
|
subTitle: '找出相同的文字涂色',
|
||||||
...options,
|
...options,
|
||||||
};
|
});
|
||||||
// 从options中读取debug参数,如果没有则使用全局DEBUG常量
|
// 从options中读取debug参数,如果没有则使用全局DEBUG常量
|
||||||
this.debug = options.debug === true || DEBUG;
|
this.debug = options.debug === true || DEBUG;
|
||||||
this.currentX = 0;
|
|
||||||
this.currentY = 0;
|
|
||||||
this.colors = ['#000'];
|
this.colors = ['#000'];
|
||||||
this.characters = ['王'];
|
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 }>) {
|
async draw(list: Array<{ color: string; word: string }>) {
|
||||||
@@ -111,6 +84,8 @@ class FindWordDrawService {
|
|||||||
this.characters = list.map((item) => item.word || '日');
|
this.characters = list.map((item) => item.word || '日');
|
||||||
this.clear();
|
this.clear();
|
||||||
this.setPaper();
|
this.setPaper();
|
||||||
|
|
||||||
|
// 绘制Header
|
||||||
if (this.headerType !== 'minimal') {
|
if (this.headerType !== 'minimal') {
|
||||||
await this.drawHeader();
|
await this.drawHeader();
|
||||||
} else {
|
} else {
|
||||||
@@ -118,64 +93,30 @@ class FindWordDrawService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 找字模板没有 drawLegend 部分
|
// 找字模板没有 drawLegend 部分
|
||||||
|
this.drawDivider();
|
||||||
this.drawContent();
|
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() {
|
drawContent() {
|
||||||
const { canvas, ctx, characters, colors } = this;
|
const { ctx, characters, colors } = this;
|
||||||
if (characters.length <= 0) return;
|
if (characters.length <= 0) return;
|
||||||
|
|
||||||
// 第一个字作为中心大字
|
// 第一个字作为中心大字
|
||||||
const firstChar = characters[0];
|
const firstChar = characters[0];
|
||||||
const firstColor = colors[0];
|
const firstColor = colors[0];
|
||||||
|
|
||||||
// 计算内容区域
|
// 计算内容区域(逻辑像素)
|
||||||
const contentTop = this.currentY + 50;
|
const contentTop = this.currentY + 17; // 50/3≈17
|
||||||
const contentBottom = canvas.height - 80;
|
const contentBottom = this.canvasHeight - 27; // 80/3≈27
|
||||||
const contentHeight = contentBottom - contentTop;
|
const contentHeight = contentBottom - contentTop;
|
||||||
|
|
||||||
// 中心大字的参数
|
// 中心大字的参数
|
||||||
const centerX = canvas.width / 2;
|
const centerX = this.canvasWidth / 2;
|
||||||
const centerY = contentTop + contentHeight / 2; // 内容区域垂直居中
|
const centerY = contentTop + contentHeight / 2;
|
||||||
|
|
||||||
// 普通圆的参数(和textDrawService一致)
|
// 普通圆的参数(逻辑像素)
|
||||||
const radius = 80;
|
const radius = 27; // 80/3≈27
|
||||||
const fontSize = 72;
|
const fontSize = 24; // 72/3=24
|
||||||
|
|
||||||
// 先选择模板,获取模板的实际位置数量
|
// 先选择模板,获取模板的实际位置数量
|
||||||
const { templateIndex } = selectTemplate();
|
const { templateIndex } = selectTemplate();
|
||||||
@@ -242,12 +183,12 @@ class FindWordDrawService {
|
|||||||
// 绘制四周的字符
|
// 绘制四周的字符
|
||||||
positions.forEach((pos, index) => {
|
positions.forEach((pos, index) => {
|
||||||
const item = charsToDraw[index];
|
const item = charsToDraw[index];
|
||||||
const y = pos.y; // 已经是绝对坐标,不需要再加contentTop
|
const y = pos.y; // 已经是绝对坐标
|
||||||
|
|
||||||
// 绘制圆
|
// 绘制圆
|
||||||
ctx.fillStyle = '#fff';
|
ctx.fillStyle = '#fff';
|
||||||
ctx.strokeStyle = '#000';
|
ctx.strokeStyle = '#000';
|
||||||
ctx.lineWidth = 4;
|
ctx.lineWidth = 1; // 4/3≈1
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(pos.x, y, radius, 0, Math.PI * 2);
|
ctx.arc(pos.x, y, radius, 0, Math.PI * 2);
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
@@ -265,35 +206,9 @@ class FindWordDrawService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// drawLine 已由基类的 drawDivider 替代,保留此方法以保持兼容
|
||||||
drawLine(linY: number) {
|
drawLine(linY: number) {
|
||||||
const { canvas, ctx } = this;
|
this.drawDivider();
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,69 +1,8 @@
|
|||||||
import { PAPER_SIZE } from '../constants/colors';
|
import { BaseDrawService } from './baseDraw';
|
||||||
import { ShapeCard } from '../constants/shapes';
|
import { ShapeCard } from '../constants/shapes';
|
||||||
import { drawShape } from './drawShape';
|
import { drawShape } from './drawShape';
|
||||||
import {
|
|
||||||
drawHeader as drawHeaderCommon,
|
|
||||||
drawMiniHeader as drawMiniHeaderCommon,
|
|
||||||
} from './headerDrawService';
|
|
||||||
|
|
||||||
/**
|
class ShapeDrawService extends BaseDrawService {
|
||||||
* 计算图形在画布上的位置,避免重叠
|
|
||||||
* @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;
|
|
||||||
shapes: ShapeCard[];
|
shapes: ShapeCard[];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -72,116 +11,61 @@ class ShapeDrawService {
|
|||||||
options?: Record<string, any>,
|
options?: Record<string, any>,
|
||||||
) {
|
) {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
this.canvas = canvas;
|
super(canvas, ctx, {
|
||||||
this.ctx = ctx;
|
|
||||||
this.paperSize = 'A4';
|
|
||||||
this.options = {
|
|
||||||
appName: '涂鸦丫小程序',
|
|
||||||
appHint: '涂色|识字|画画|打印',
|
|
||||||
title: '找一找 涂 色',
|
title: '找一找 涂 色',
|
||||||
subTitle: '给图形涂上相同的颜色',
|
subTitle: '给图形涂上相同的颜色',
|
||||||
...options,
|
...options,
|
||||||
};
|
});
|
||||||
this.currentX = 0;
|
|
||||||
this.currentY = 0;
|
|
||||||
this.shapes = [];
|
this.shapes = [];
|
||||||
this.headerType = 'wechat'; // 默认值
|
|
||||||
this.setPrintConfig();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setPrintConfig() {
|
async draw(shapes: ShapeCard[]) {
|
||||||
const printConfig = getApp().getPrintConfig();
|
|
||||||
this.headerType = printConfig.header;
|
|
||||||
this.options.appName = printConfig.appName;
|
|
||||||
}
|
|
||||||
|
|
||||||
draw(shapes: ShapeCard[]) {
|
|
||||||
this.setPrintConfig();
|
this.setPrintConfig();
|
||||||
this.shapes = shapes.slice(0, 6); // 最多6个图形
|
this.shapes = shapes.slice(0, 6); // 最多6个图形
|
||||||
this.clear();
|
this.clear();
|
||||||
this.setPaper();
|
this.setPaper();
|
||||||
|
|
||||||
|
// 绘制Header
|
||||||
if (this.headerType !== 'minimal') {
|
if (this.headerType !== 'minimal') {
|
||||||
this.drawHeader();
|
await this.drawHeader();
|
||||||
} else {
|
} else {
|
||||||
this.drawMiniHeader();
|
await this.drawMiniHeader();
|
||||||
}
|
}
|
||||||
|
this.drawDivider();
|
||||||
this.drawLegend();
|
this.drawLegend();
|
||||||
this.drawContent();
|
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() {
|
drawLegend() {
|
||||||
const { canvas, ctx, shapes } = this;
|
const { ctx, shapes } = this;
|
||||||
if (shapes.length <= 0) return;
|
if (shapes.length <= 0) return;
|
||||||
|
|
||||||
this.currentY = this.headerType === 'minimal' ? 200 : 304;
|
// 逻辑像素尺寸(原始尺寸除以3)
|
||||||
const shapeSize = 200;
|
this.currentY = this.headerType === 'minimal' ? 75 : 110; // 200/3≈67, 304/3≈101
|
||||||
const rectWidth = 180;
|
const shapeSize = 67; // 200/3≈67
|
||||||
const rectHeight = 80;
|
const rectWidth = 60; // 180/3=60
|
||||||
// 固定图例的Y位置,不依赖shapeSize
|
const rectHeight = 27; // 80/3≈27
|
||||||
const startY = this.currentY + 125; // 固定距离,不依赖shapeSize
|
const startY = this.currentY + 42; // 125/3≈42
|
||||||
const len = shapes.length;
|
const len = shapes.length;
|
||||||
const canvasWidth = canvas.width;
|
|
||||||
|
|
||||||
// 计算示例图形的间距
|
// 计算示例图形的间距
|
||||||
const totalWidth = len * shapeSize + (len - 1) * 40;
|
const totalWidth = len * shapeSize + (len - 1) * 13; // 40/3≈13
|
||||||
const startX = (canvasWidth - totalWidth) / 2 + shapeSize / 2;
|
const startX = (this.canvasWidth - totalWidth) / 2 + shapeSize / 2;
|
||||||
|
|
||||||
// 绘制所有图形
|
// 绘制所有图形
|
||||||
shapes.forEach((shape: ShapeCard, index: number) => {
|
shapes.forEach((shape: ShapeCard, index: number) => {
|
||||||
const x = startX + index * (shapeSize + 40);
|
const x = startX + index * (shapeSize + 13); // 40/3≈13
|
||||||
const y = startY;
|
const y = startY;
|
||||||
|
|
||||||
// 绘制示例图形
|
// 绘制示例图形
|
||||||
drawShape(ctx, shape, x, y, shapeSize, shape.fillColor);
|
drawShape(ctx, shape, x, y, shapeSize, shape.fillColor);
|
||||||
|
|
||||||
// // 绘制长方形
|
|
||||||
// ctx.fillStyle = '#fff';
|
|
||||||
// ctx.strokeStyle = '#000';
|
|
||||||
// ctx.lineWidth = 4;
|
|
||||||
const rectangleX = x - rectWidth / 2;
|
const rectangleX = x - rectWidth / 2;
|
||||||
const rectangleY = y + shapeSize / 2 + 10;
|
const rectangleY = y + shapeSize / 2 + 3; // 10/3≈3
|
||||||
// ctx.strokeRect(rectangleX, rectangleY, rectWidth, rectHeight);
|
|
||||||
|
|
||||||
// 绘制图形名称
|
// 绘制图形名称
|
||||||
ctx.fillStyle = '#333';
|
ctx.fillStyle = '#333';
|
||||||
ctx.font = 'bold 36px "Microsoft Yahei"';
|
ctx.font = 'bold 12px "Microsoft Yahei"'; // 36/3=12
|
||||||
ctx.textBaseline = 'middle';
|
ctx.textBaseline = 'middle';
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.fillText(
|
ctx.fillText(
|
||||||
@@ -192,33 +76,29 @@ class ShapeDrawService {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.currentY = this.headerType === 'minimal' ? 532 : 622;
|
this.currentY = this.headerType === 'minimal' ? 180 : 220; // 532/3≈177, 622/3≈207
|
||||||
this.drawLine(this.currentY);
|
this.drawDivider();
|
||||||
}
|
}
|
||||||
|
|
||||||
drawContent() {
|
drawContent() {
|
||||||
const { canvas, ctx, shapes } = this;
|
const { ctx, shapes } = this;
|
||||||
if (shapes.length <= 0) return;
|
if (shapes.length <= 0) return;
|
||||||
|
|
||||||
// 固定可渲染的总行数
|
// 逻辑像素尺寸(原始尺寸除以3)
|
||||||
const ROW_COUNT = 6;
|
const ROW_COUNT = 6;
|
||||||
// 每行之间的垂直间距
|
const VERTICAL_GAP = 40; // 120/3=40
|
||||||
const VERTICAL_GAP = 120;
|
const shapeSize = 60; // 180/3=60
|
||||||
// 图形大小
|
const leftMargin = 53; // 160/3≈53
|
||||||
const shapeSize = 180;
|
const rightMargin = 53; // 160/3≈53
|
||||||
// 左右边距
|
|
||||||
const leftMargin = 160;
|
|
||||||
const rightMargin = 160;
|
|
||||||
// 以 drawLegend 画完后的分割线作为基准,内容区顶部间距 50
|
|
||||||
const legendBottomY = this.currentY;
|
const legendBottomY = this.currentY;
|
||||||
const topGap = 60;
|
const topGap = 20; // 60/3=20
|
||||||
const contentTop = legendBottomY + topGap;
|
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(
|
const maxPerRow = Math.floor(
|
||||||
(contentWidth + minGap) / (shapeSize + minGap),
|
(contentWidth + minGap) / (shapeSize + minGap),
|
||||||
);
|
);
|
||||||
@@ -300,35 +180,9 @@ class ShapeDrawService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// drawLine 已由基类的 drawDivider 替代,保留此方法以保持兼容
|
||||||
drawLine(linY: number) {
|
drawLine(linY: number) {
|
||||||
const { canvas, ctx } = this;
|
this.drawDivider();
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,10 @@
|
|||||||
// import { PAPER_SIZE } from './constant';
|
import { BaseDrawService } from './baseDraw';
|
||||||
import { PAPER_SIZE } from '../constants/colors';
|
|
||||||
import {
|
|
||||||
drawHeader as drawHeaderCommon,
|
|
||||||
drawMiniHeader as drawMiniHeaderCommon,
|
|
||||||
} from './headerDrawService';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 计算示例区域圆的中心点
|
* 计算示例区域圆的中心点
|
||||||
* @param canvasWidth 画布宽度
|
* @param canvasWidth 画布宽度(逻辑像素)
|
||||||
* @param circleCount 圆的数量
|
* @param circleCount 圆的数量
|
||||||
* @param padding 圆的间距
|
* @param padding 圆的间距(逻辑像素)
|
||||||
* @param radius 圆的半径
|
* @param radius 圆的半径(逻辑像素)
|
||||||
* @returns 圆的中心点
|
* @returns 圆的中心点
|
||||||
*/
|
*/
|
||||||
function calculateCircleCenters(
|
function calculateCircleCenters(
|
||||||
@@ -24,7 +18,6 @@ function calculateCircleCenters(
|
|||||||
const centers = [];
|
const centers = [];
|
||||||
|
|
||||||
if (circleCount === 1) {
|
if (circleCount === 1) {
|
||||||
// 单个圆圈直接居中
|
|
||||||
centers.push(canvasWidth / 2);
|
centers.push(canvasWidth / 2);
|
||||||
} else {
|
} else {
|
||||||
const requiredSpace = 2 * radius * circleCount;
|
const requiredSpace = 2 * radius * circleCount;
|
||||||
@@ -32,7 +25,6 @@ function calculateCircleCenters(
|
|||||||
(availableWidth - requiredSpace) / (circleCount - 1);
|
(availableWidth - requiredSpace) / (circleCount - 1);
|
||||||
|
|
||||||
if (spaceBetween > maxSpacing) {
|
if (spaceBetween > maxSpacing) {
|
||||||
// 超过最大间距时,固定间距并居中对齐
|
|
||||||
const totalWidth = 2 * radius + (circleCount - 1) * maxSpacing;
|
const totalWidth = 2 * radius + (circleCount - 1) * maxSpacing;
|
||||||
const startX = (canvasWidth - totalWidth) / 2 + radius;
|
const startX = (canvasWidth - totalWidth) / 2 + radius;
|
||||||
|
|
||||||
@@ -40,7 +32,6 @@ function calculateCircleCenters(
|
|||||||
centers.push(startX + i * maxSpacing);
|
centers.push(startX + i * maxSpacing);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 正常均匀分布
|
|
||||||
const startX = padding + radius;
|
const startX = padding + radius;
|
||||||
|
|
||||||
for (let i = 0; i < circleCount; i++) {
|
for (let i = 0; i < circleCount; i++) {
|
||||||
@@ -52,14 +43,7 @@ function calculateCircleCenters(
|
|||||||
return centers;
|
return centers;
|
||||||
}
|
}
|
||||||
|
|
||||||
class TextDrawService {
|
class TextDrawService extends BaseDrawService {
|
||||||
headerType: PrintHeader = 'wechat';
|
|
||||||
canvas: WechatMiniprogram.Canvas;
|
|
||||||
ctx: RenderingContext;
|
|
||||||
options: Record<string, any>;
|
|
||||||
paperSize: PaperSize;
|
|
||||||
currentX: number;
|
|
||||||
currentY: number;
|
|
||||||
colors: string[];
|
colors: string[];
|
||||||
characters: string[];
|
characters: string[];
|
||||||
|
|
||||||
@@ -69,27 +53,13 @@ class TextDrawService {
|
|||||||
options?: Record<string, any>,
|
options?: Record<string, any>,
|
||||||
) {
|
) {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
this.canvas = canvas;
|
super(canvas, ctx, {
|
||||||
this.ctx = ctx;
|
|
||||||
this.paperSize = 'A4';
|
|
||||||
this.options = {
|
|
||||||
appName: '涂鸦丫小程序',
|
|
||||||
appHint: '识字|识图|练字|打印',
|
|
||||||
title: '找一找 涂 色',
|
title: '找一找 涂 色',
|
||||||
subTitle: '给文字涂上相同的颜色',
|
subTitle: '给文字涂上相同的颜色',
|
||||||
...options,
|
...options,
|
||||||
};
|
});
|
||||||
this.currentX = 0;
|
|
||||||
this.currentY = 0;
|
|
||||||
this.colors = ['#000'];
|
this.colors = ['#000'];
|
||||||
this.characters = ['王'];
|
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 }>) {
|
async draw(list: Array<{ color: string; word: string }>) {
|
||||||
@@ -99,82 +69,58 @@ class TextDrawService {
|
|||||||
this.characters = list.map((item) => item.word || '日');
|
this.characters = list.map((item) => item.word || '日');
|
||||||
this.clear();
|
this.clear();
|
||||||
this.setPaper();
|
this.setPaper();
|
||||||
|
|
||||||
|
// 绘制Header
|
||||||
if (this.headerType !== 'minimal') {
|
if (this.headerType !== 'minimal') {
|
||||||
await this.drawHeader();
|
await this.drawHeader();
|
||||||
} else {
|
} else {
|
||||||
this.drawMiniHeader();
|
await this.drawMiniHeader();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.drawDivider();
|
||||||
this.drawLegend();
|
this.drawLegend();
|
||||||
this.drawContent();
|
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、y为圆心
|
* 圆形:x、y为圆心
|
||||||
* 文字:x textAlign 为 start,文本左边缘对齐
|
* 文字:x textAlign 为 start,文本左边缘对齐
|
||||||
* y textBaseline 为 alphabetic,y 对应字母基线(类似左下角)
|
* y textBaseline 为 alphabetic,y 对应字母基线(类似左下角)
|
||||||
*
|
*
|
||||||
|
* 注意:所有尺寸已转换为逻辑像素(除以3)
|
||||||
* */
|
* */
|
||||||
drawLegend() {
|
drawLegend() {
|
||||||
const { canvas, ctx, colors, characters } = this;
|
const { ctx, colors, characters } = this;
|
||||||
if (characters.length <= 0) return;
|
if (characters.length <= 0) return;
|
||||||
// const { colors, characters } = options;
|
|
||||||
this.currentY = this.headerType === 'minimal' ? 200 : 304;
|
// 逻辑像素尺寸(原始尺寸除以3)
|
||||||
const radius = 65;
|
this.currentY = this.headerType === 'minimal' ? 68 : 110; // 200/3≈67, 304/3≈101
|
||||||
const rectWidth = 180;
|
const radius = 22; // 65/3≈22
|
||||||
const rectHeight = 80;
|
const rectWidth = 60; // 180/3=60
|
||||||
const startX = 260 + radius; // 圆形的X坐标是圆心
|
const rectHeight = 27; // 80/3≈27
|
||||||
const startY = this.currentY + 30 + radius; // 圆形的Y坐标是圆心
|
const startX = 87 + radius; // (260+65)/3≈108
|
||||||
|
const startY = this.currentY + 10 + radius; // (30+65)/3≈32
|
||||||
const len = characters.length || 4;
|
const len = characters.length || 4;
|
||||||
const canvasWidth = canvas.width;
|
const centers = calculateCircleCenters(
|
||||||
const centers = calculateCircleCenters(canvasWidth, len, 220, 65);
|
this.canvasWidth,
|
||||||
/** 计算每个圆之间的间距 俩个60,一个是页面右侧边距,另一个是圆离右边距地边距*/
|
len,
|
||||||
const spaceWidth = len > 3 ? (canvasWidth - startX * 2) / (len - 1) : 0;
|
73,
|
||||||
|
radius,
|
||||||
|
); // 220/3≈73
|
||||||
|
|
||||||
|
/** 计算每个圆之间的间距 */
|
||||||
|
const spaceWidth =
|
||||||
|
len > 3 ? (this.canvasWidth - startX * 2) / (len - 1) : 0;
|
||||||
|
|
||||||
ctx.moveTo(startX, startY);
|
ctx.moveTo(startX, startY);
|
||||||
colors.forEach((color: string, index: number) => {
|
colors.forEach((color: string, index: number) => {
|
||||||
const x = centers[index] || startX + index * spaceWidth; // 计算圆的X坐标
|
const x = centers[index] || startX + index * spaceWidth;
|
||||||
const y = startY; // 计算圆的Y坐标
|
const y = startY;
|
||||||
// 绘制圆
|
// 绘制圆
|
||||||
ctx.strokeStyle = '#000';
|
ctx.strokeStyle = '#000';
|
||||||
ctx.fillStyle = color;
|
ctx.fillStyle = color;
|
||||||
ctx.lineWidth = 4;
|
ctx.lineWidth = 1; // 4/3≈1
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
@@ -182,16 +128,16 @@ class TextDrawService {
|
|||||||
ctx.closePath();
|
ctx.closePath();
|
||||||
|
|
||||||
// 绘制长方形
|
// 绘制长方形
|
||||||
ctx.fillStyle = '#fff'; // 长方形背景色
|
ctx.fillStyle = '#fff';
|
||||||
ctx.strokeStyle = '#000';
|
ctx.strokeStyle = '#000';
|
||||||
ctx.lineWidth = 4;
|
ctx.lineWidth = 1; // 4/3≈1
|
||||||
const rectangleX = x - rectWidth / 2; // 从圆形移动一半的长方形的长
|
const rectangleX = x - rectWidth / 2;
|
||||||
const rectangleY = y + radius + 43; // 从圆形下移一个半径,再加上43的间距
|
const rectangleY = y + radius + 14; // 43/3≈14
|
||||||
ctx.strokeRect(rectangleX, rectangleY, rectWidth, rectHeight); // 绘制长方形
|
ctx.strokeRect(rectangleX, rectangleY, rectWidth, rectHeight);
|
||||||
|
|
||||||
// 绘制字符
|
// 绘制字符
|
||||||
ctx.fillStyle = '#333';
|
ctx.fillStyle = '#333';
|
||||||
ctx.font = 'bold 48px "Microsoft Yahei"';
|
ctx.font = 'bold 16px "Microsoft Yahei"'; // 48/3=16
|
||||||
ctx.textBaseline = 'middle';
|
ctx.textBaseline = 'middle';
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.fillText(
|
ctx.fillText(
|
||||||
@@ -202,26 +148,25 @@ class TextDrawService {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.currentY = this.headerType === 'minimal' ? 522 : 612; // 计算分割线的Y坐标,距离示例图20px
|
this.currentY = this.headerType === 'minimal' ? 178 : 230; // 522/3=174, 612/3=204
|
||||||
this.drawLine(this.currentY);
|
this.drawDivider();
|
||||||
}
|
}
|
||||||
|
|
||||||
drawContent() {
|
drawContent() {
|
||||||
const { canvas, ctx, characters } = this;
|
const { ctx, characters } = this;
|
||||||
if (characters.length <= 0) return;
|
if (characters.length <= 0) return;
|
||||||
|
|
||||||
const len = characters.length;
|
const len = characters.length;
|
||||||
const rows = this.headerType === 'minimal' ? 9 : 8;
|
const rows = this.headerType === 'minimal' ? 9 : 8;
|
||||||
const radius = 80;
|
const radius = 27; // 80/3≈27
|
||||||
const fontSize = 72;
|
const fontSize = 24; // 72/3=24
|
||||||
|
|
||||||
const startY = this.currentY + 62 + radius;
|
const startY = this.currentY + 10 + radius; // 62/3≈21
|
||||||
const startX1 = 310 + radius;
|
const startX1 = 103 + radius; // 310/3≈103
|
||||||
const startX2 = 200 + radius;
|
const startX2 = 67 + radius; // 200/3≈67
|
||||||
const canvasWidth = canvas.width;
|
const spaceWidthFrist = (this.canvasWidth - startX1 * 2) / (5 - 1);
|
||||||
const spaceWidthFrist = (canvasWidth - startX1 * 2) / (5 - 1);
|
const spaceWidthSecond = (this.canvasWidth - startX2 * 2) / (6 - 1);
|
||||||
const spaceWidthSecond = (canvasWidth - startX2 * 2) / (6 - 1);
|
const spaceHeight = 18; // 54/3=18
|
||||||
const spaceHeight = 54;
|
|
||||||
|
|
||||||
ctx.moveTo(startX1, startY);
|
ctx.moveTo(startX1, startY);
|
||||||
for (let i = 0; i < rows; i++) {
|
for (let i = 0; i < rows; i++) {
|
||||||
@@ -238,7 +183,7 @@ class TextDrawService {
|
|||||||
|
|
||||||
ctx.fillStyle = '#fff';
|
ctx.fillStyle = '#fff';
|
||||||
ctx.strokeStyle = '#000';
|
ctx.strokeStyle = '#000';
|
||||||
ctx.lineWidth = 4;
|
ctx.lineWidth = 1; // 4/3≈1
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
@@ -254,36 +199,9 @@ class TextDrawService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// drawLine 已由基类的 drawDivider 替代,保留此方法以保持兼容
|
||||||
drawLine(linY: number) {
|
drawLine(linY: number) {
|
||||||
const { canvas, ctx } = this;
|
this.drawDivider();
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import { PAPER_SIZE } from '../constants/colors';
|
import { BaseDrawService } from './baseDraw';
|
||||||
import { CharacterItem } from '../types/characterType';
|
import { CharacterItem } from '../types/characterType';
|
||||||
import {
|
|
||||||
drawHeader as drawHeaderCommon,
|
|
||||||
drawMiniHeader as drawMiniHeaderCommon,
|
|
||||||
} from './headerDrawService';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 仅支持 M/L/Z 的简单 SVG 路径解析与绘制
|
* 仅支持 M/L/Z 的简单 SVG 路径解析与绘制
|
||||||
@@ -197,14 +193,14 @@ function drawTianZiGrid({
|
|||||||
lineColor = '#e0e0e0',
|
lineColor = '#e0e0e0',
|
||||||
boldColor = '#cccccc',
|
boldColor = '#cccccc',
|
||||||
}: DrawTianZiGridParams) {
|
}: DrawTianZiGridParams) {
|
||||||
// 外框
|
// 外框(逻辑像素)
|
||||||
ctx.strokeStyle = boldColor;
|
ctx.strokeStyle = boldColor;
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 1; // 2/3≈1,逻辑像素
|
||||||
ctx.strokeRect(x - size / 2, y - size / 2, size, size);
|
ctx.strokeRect(x - size / 2, y - size / 2, size, size);
|
||||||
|
|
||||||
// 中线
|
// 中线(逻辑像素)
|
||||||
ctx.strokeStyle = lineColor;
|
ctx.strokeStyle = lineColor;
|
||||||
ctx.lineWidth = 1;
|
ctx.lineWidth = 1; // 逻辑像素
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
// 竖线
|
// 竖线
|
||||||
ctx.moveTo(x, y - size / 2);
|
ctx.moveTo(x, y - size / 2);
|
||||||
@@ -215,7 +211,7 @@ function drawTianZiGrid({
|
|||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
ctx.closePath();
|
ctx.closePath();
|
||||||
|
|
||||||
// 对角线(淡)
|
// 对角线(淡,逻辑像素)
|
||||||
ctx.strokeStyle = '#eeeeee';
|
ctx.strokeStyle = '#eeeeee';
|
||||||
ctx.lineWidth = 1;
|
ctx.lineWidth = 1;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
@@ -227,40 +223,18 @@ function drawTianZiGrid({
|
|||||||
ctx.closePath();
|
ctx.closePath();
|
||||||
}
|
}
|
||||||
|
|
||||||
class WordDrawService {
|
class WordDrawService extends BaseDrawService {
|
||||||
headerType: PrintHeader = 'wechat';
|
|
||||||
canvas: WechatMiniprogram.Canvas;
|
|
||||||
ctx: RenderingContext;
|
|
||||||
options: Record<string, any>;
|
|
||||||
paperSize: PaperSize;
|
|
||||||
currentX: number;
|
|
||||||
currentY: number;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
canvas: Canvas,
|
canvas: Canvas,
|
||||||
ctx: RenderingContext,
|
ctx: RenderingContext,
|
||||||
options?: Record<string, any>,
|
options?: Record<string, any>,
|
||||||
) {
|
) {
|
||||||
options = options || {};
|
options = options || {};
|
||||||
this.canvas = canvas;
|
super(canvas, ctx, {
|
||||||
this.ctx = ctx;
|
|
||||||
this.paperSize = 'A4';
|
|
||||||
this.options = {
|
|
||||||
appName: '涂鸦丫小程序',
|
|
||||||
appHint: '识字|识图|练字|打印',
|
|
||||||
title: '田字格 练 字 贴',
|
title: '田字格 练 字 贴',
|
||||||
subTitle: '按笔画临摹练习',
|
subTitle: '按笔画临摹练习',
|
||||||
...options,
|
...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.clear();
|
||||||
this.setPaper();
|
this.setPaper();
|
||||||
|
|
||||||
// 等待页眉绘制完成,确保 this.currentY 被正确设置
|
// 绘制Header
|
||||||
if (this.headerType !== 'minimal') {
|
if (this.headerType !== 'minimal') {
|
||||||
await this.drawHeader();
|
await this.drawHeader();
|
||||||
} else {
|
} else {
|
||||||
await this.drawMiniHeader();
|
this.drawMiniHeader();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.drawDivider();
|
||||||
this.drawContentEmpty();
|
this.drawContentEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,60 +277,19 @@ class WordDrawService {
|
|||||||
* 清空内容区域(页眉以下的部分)
|
* 清空内容区域(页眉以下的部分)
|
||||||
*/
|
*/
|
||||||
private clearContentArea() {
|
private clearContentArea() {
|
||||||
const { ctx, canvas } = this;
|
const { ctx } = this;
|
||||||
const contentStartY = this.currentY;
|
const contentStartY = this.currentY;
|
||||||
|
|
||||||
// 清空页眉以下的所有内容
|
// 清空页眉以下的所有内容(使用逻辑像素)
|
||||||
ctx.fillStyle = '#fff';
|
ctx.fillStyle = '#fff';
|
||||||
ctx.fillRect(
|
ctx.fillRect(
|
||||||
0,
|
0,
|
||||||
contentStartY,
|
contentStartY,
|
||||||
canvas.width,
|
this.canvasWidth,
|
||||||
canvas.height - contentStartY,
|
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。
|
* rowGap(行间距)用于田字格竖直方向(每一行田字格之间的间隔)的初始最小值。它防止行与行的田字格上下贴得太紧,增加一定的可视呼吸空间。之后同样会结合剩余空间动态调整为实际间距 actualRowGap。
|
||||||
*/
|
*/
|
||||||
drawContent(characterData: CharacterItem[] | null) {
|
drawContent(characterData: CharacterItem[] | null) {
|
||||||
const { canvas, ctx } = this;
|
const { ctx } = this;
|
||||||
|
|
||||||
// 布局参数
|
// 布局参数(逻辑像素,原始尺寸除以3)
|
||||||
const topGap = 50; // 与页眉分割线的距离
|
const topGap = 17; // 50/3≈17
|
||||||
const leftMargin = 120;
|
const leftMargin = 40; // 120/3=40
|
||||||
const rightMargin = 120;
|
const rightMargin = 40; // 120/3=40
|
||||||
const bottomMargin = 120;
|
const bottomMargin = 40; // 120/3=40
|
||||||
const contentTop = this.currentY + topGap;
|
const contentTop = this.currentY + topGap;
|
||||||
const contentWidth = canvas.width - leftMargin - rightMargin;
|
const contentWidth = this.canvasWidth - leftMargin - rightMargin;
|
||||||
const contentHeight = canvas.height - contentTop - bottomMargin;
|
const contentHeight = this.canvasHeight - contentTop - bottomMargin;
|
||||||
|
|
||||||
const cellSize = 140;
|
const cellSize = 47; // 140/3≈47
|
||||||
// 统一通过 getMaxGridLayout 获取最大行列数
|
// 统一通过 getMaxGridLayout 获取最大行列数
|
||||||
const { maxRow, maxCol } = this.getMaxGridLayout();
|
const { maxRow, maxCol } = this.getMaxGridLayout();
|
||||||
|
|
||||||
@@ -423,20 +358,18 @@ class WordDrawService {
|
|||||||
* 获取当前页面可绘制田字格的最大行数与列数(与绘制使用同一套计算规则)
|
* 获取当前页面可绘制田字格的最大行数与列数(与绘制使用同一套计算规则)
|
||||||
*/
|
*/
|
||||||
getMaxGridLayout(): { maxRow: number; maxCol: number } {
|
getMaxGridLayout(): { maxRow: number; maxCol: number } {
|
||||||
const { canvas } = this;
|
// 布局参数需与 drawContent 保持一致(逻辑像素)
|
||||||
|
const topGap = 17; // 50/3≈17
|
||||||
// 布局参数需与 drawContent 保持一致
|
const leftMargin = 40; // 120/3=40
|
||||||
const topGap = 50;
|
const rightMargin = 40; // 120/3=40
|
||||||
const leftMargin = 120;
|
const bottomMargin = 40; // 120/3=40
|
||||||
const rightMargin = 120;
|
|
||||||
const bottomMargin = 120;
|
|
||||||
const contentTop = this.currentY + topGap;
|
const contentTop = this.currentY + topGap;
|
||||||
const contentWidth = canvas.width - leftMargin - rightMargin;
|
const contentWidth = this.canvasWidth - leftMargin - rightMargin;
|
||||||
const contentHeight = canvas.height - contentTop - bottomMargin;
|
const contentHeight = this.canvasHeight - contentTop - bottomMargin;
|
||||||
|
|
||||||
const cellSize = 140;
|
const cellSize = 47; // 140/3≈47
|
||||||
const minGap = 24;
|
const minGap = 8; // 24/3=8
|
||||||
const rowGap = 36;
|
const rowGap = 12; // 36/3=12
|
||||||
|
|
||||||
const maxCol = Math.max(
|
const maxCol = Math.max(
|
||||||
1,
|
1,
|
||||||
@@ -596,7 +529,7 @@ class WordDrawService {
|
|||||||
uptoInclusive: strokes.length - 1,
|
uptoInclusive: strokes.length - 1,
|
||||||
fillStyle: 'rgb(0,0,0)', // 黑色填充
|
fillStyle: 'rgb(0,0,0)', // 黑色填充
|
||||||
strokeStyle: '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}`);
|
// console.log(`练习格 [${rowIndex}, ${columnIndex}] 坐标: (${x}, ${y}), 笔画: ${strokeIndex + 1}/${strokes.length}`);
|
||||||
|
|
||||||
// 绘制到指定笔画的汉字(红色,中等粗细)
|
// 绘制到指定笔画的汉字(灰色,中等粗细)
|
||||||
drawStrokes({
|
drawStrokes({
|
||||||
ctx,
|
ctx,
|
||||||
strokes,
|
strokes,
|
||||||
@@ -641,38 +574,13 @@ class WordDrawService {
|
|||||||
uptoInclusive: strokeIndex,
|
uptoInclusive: strokeIndex,
|
||||||
fillStyle: '#ccc',
|
fillStyle: '#ccc',
|
||||||
strokeStyle: '#ccc',
|
strokeStyle: '#ccc',
|
||||||
lineWidth: 3, // 中等粗细
|
lineWidth: 1, // 中等粗细(3/3=1,逻辑像素)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
drawDivider(linY: number) {
|
// drawDivider 已在基类中实现,此方法保留以保持兼容
|
||||||
const { canvas, ctx } = this;
|
drawDivider(linY?: number) {
|
||||||
ctx.strokeStyle = '#000';
|
super.drawDivider();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,7 @@
|
|||||||
* 提供分享和下载打印事件的上报功能
|
* 提供分享和下载打印事件的上报功能
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// 生成唯一 UUID
|
import { getAppUUID } from './uuid';
|
||||||
function generateUUID(): string {
|
|
||||||
const timestamp = Date.now();
|
|
||||||
const random = Math.floor(Math.random() * 1000000);
|
|
||||||
return `${timestamp}-${random}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化时间为 yyyy-MM-dd HH:mm
|
// 格式化时间为 yyyy-MM-dd HH:mm
|
||||||
function formatTime(date: Date = new Date()): string {
|
function formatTime(date: Date = new Date()): string {
|
||||||
@@ -65,29 +60,24 @@ function getEventCount(eventName: string): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 埋点追踪器
|
* 埋点追踪器(单例模式)
|
||||||
*/
|
*/
|
||||||
class Tracker {
|
class Tracker {
|
||||||
private uuid: string;
|
private static instance: Tracker | null = null;
|
||||||
private openLog: boolean;
|
private openLog: boolean;
|
||||||
|
|
||||||
constructor() {
|
private constructor() {
|
||||||
// 初始化时获取或生成 UUID(持久化存储)
|
|
||||||
this.uuid = this.getOrCreateUUID();
|
|
||||||
this.openLog = true;
|
this.openLog = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取或创建 UUID
|
* 获取 Tracker 单例实例
|
||||||
*/
|
*/
|
||||||
private getOrCreateUUID(): string {
|
public static getInstance(): Tracker {
|
||||||
const uuidKey = 'tracker_uuid';
|
if (!Tracker.instance) {
|
||||||
let storedUUID = wx.getStorageSync(uuidKey);
|
Tracker.instance = new Tracker();
|
||||||
if (!storedUUID) {
|
|
||||||
storedUUID = generateUUID();
|
|
||||||
wx.setStorageSync(uuidKey, storedUUID);
|
|
||||||
}
|
}
|
||||||
return storedUUID;
|
return Tracker.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
printLog(tip: string, message?: string | object): void {
|
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 页面名称
|
* @param pageName 页面名称
|
||||||
*/
|
*/
|
||||||
reportShare(pageName: string): void {
|
reportShare(pageName: string): void {
|
||||||
try {
|
try {
|
||||||
const time = formatTime();
|
const params = this.getCommonEventParams('share_click', pageName);
|
||||||
const count = getEventCount('share_click');
|
|
||||||
const params = {
|
|
||||||
count,
|
|
||||||
time,
|
|
||||||
uuid: this.uuid,
|
|
||||||
page_name: pageName,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.printLog('分享事件', params);
|
this.printLog('分享事件', params);
|
||||||
wx.reportEvent('share_click', params);
|
wx.reportEvent('share_click', params);
|
||||||
@@ -129,17 +133,13 @@ class Tracker {
|
|||||||
/**
|
/**
|
||||||
* 上报下载打印事件
|
* 上报下载打印事件
|
||||||
* @param pageName 页面名称
|
* @param pageName 页面名称
|
||||||
|
* @param mode 模式(可选)
|
||||||
*/
|
*/
|
||||||
reportDownload(pageName: string): void {
|
reportDownload(pageName: string, mode?: string): void {
|
||||||
try {
|
try {
|
||||||
const time = formatTime();
|
const params = this.getCommonEventParams('download', pageName, {
|
||||||
const count = getEventCount('download');
|
...(mode && { mode }),
|
||||||
const params = {
|
});
|
||||||
count,
|
|
||||||
time,
|
|
||||||
uuid: this.uuid,
|
|
||||||
page_name: pageName,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.printLog('下载事件', params);
|
this.printLog('下载事件', params);
|
||||||
wx.reportEvent('download', params);
|
wx.reportEvent('download', params);
|
||||||
@@ -149,7 +149,7 @@ class Tracker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建并导出 tracker 实例
|
// 导出 Tracker 单例实例
|
||||||
const tracker = new Tracker();
|
const tracker = Tracker.getInstance();
|
||||||
|
|
||||||
export default tracker;
|
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": {
|
"condition": {
|
||||||
"miniprogram": {
|
"miniprogram": {
|
||||||
"list": [
|
"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",
|
"name": "mathPages/numberDecompose/numberDecompose",
|
||||||
"pathName": "mathPages/numberDecompose/numberDecompose",
|
"pathName": "mathPages/numberDecompose/numberDecompose",
|
||||||
"query": "id=number-decompose&mode=with-image",
|
"query": "id=number-decompose&mode=with-image",
|
||||||
"scene": null,
|
"launchMode": "default",
|
||||||
"launchMode": "default"
|
"scene": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "mathPages/countingSelect/countingSelect",
|
"name": "mathPages/countingSelect/countingSelect",
|
||||||
|
|||||||
Vendored
+3
-2
@@ -4,9 +4,10 @@ type WordCard = {
|
|||||||
word: string;
|
word: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface CardList extends Array<WordCard> { }
|
interface CardList extends Array<WordCard> {}
|
||||||
|
|
||||||
interface PrintConfig {
|
interface PrintConfig {
|
||||||
header: PrintHeader;
|
header: PrintHeader;
|
||||||
appName: string;
|
appName: string;
|
||||||
}
|
appHint: string;
|
||||||
|
}
|
||||||
|
|||||||
Vendored
+1
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
interface IAppOption {
|
interface IAppOption {
|
||||||
globalData: {
|
globalData: {
|
||||||
|
uuid: string;
|
||||||
userInfo?: WechatMiniprogram.UserInfo;
|
userInfo?: WechatMiniprogram.UserInfo;
|
||||||
env: string;
|
env: string;
|
||||||
printConfig?: PrintConfig;
|
printConfig?: PrintConfig;
|
||||||
|
|||||||
Reference in New Issue
Block a user