diff --git a/miniprogram/app.ts b/miniprogram/app.ts index dbd58fe..818bc87 100644 --- a/miniprogram/app.ts +++ b/miniprogram/app.ts @@ -1,17 +1,27 @@ -import config from './config/config'; - -const defaultPrintConfig: PrintConfig = config.printHeader as PrintConfig; +import { defaultPrintConfig } from './config/config'; +import { generateUUID, getAppUUID, setAppUUID } from './utils/uuid'; // app.ts App({ globalData: { env: 'release', + uuid: '', printConfig: defaultPrintConfig, }, onLaunch() { const accountInfo = wx.getAccountInfoSync(); const env = accountInfo.miniProgram.envVersion || 'release'; this.globalData.env = env; + + // 从 localStorage 读取 UUID,如果没有则生成并存储 + let uuid = getAppUUID(); + if (!uuid) { + uuid = generateUUID(); + setAppUUID(uuid); + console.log('生成并存储 UUID', uuid); + } + this.globalData.uuid = uuid; + if (env !== 'release') { const printConfig = wx.getStorageSync('printConfig') || defaultPrintConfig; diff --git a/miniprogram/base/README_PAGE_MIXIN_REFACTOR.md b/miniprogram/base/README_PAGE_MIXIN_REFACTOR.md new file mode 100644 index 0000000..e712e2e --- /dev/null +++ b/miniprogram/base/README_PAGE_MIXIN_REFACTOR.md @@ -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` 创建页面 + +这样可以保持代码的模块化和可维护性。 diff --git a/miniprogram/base/pageMixin.README.md b/miniprogram/base/pageMixin.README.md new file mode 100644 index 0000000..a1c0af0 --- /dev/null +++ b/miniprogram/base/pageMixin.README.md @@ -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` 保持了相同的接口。 diff --git a/miniprogram/mathPages/common/mathPageMixin.ts b/miniprogram/base/pageMixin.ts similarity index 56% rename from miniprogram/mathPages/common/mathPageMixin.ts rename to miniprogram/base/pageMixin.ts index d268d93..f6bdd59 100644 --- a/miniprogram/mathPages/common/mathPageMixin.ts +++ b/miniprogram/base/pageMixin.ts @@ -1,23 +1,22 @@ -import { PAPER_SIZE } from '../../constants/colors'; -import { checkAndSaveImage } from '../../utils/saveImage'; -import { shouldShowShareGuide } from '../../utils/shareGuide'; -import { BaseMathDrawService } from '../service/baseMathDraw'; -import { - MATH_FUNCTION_TYPES, - MathFunctionType, -} from '../../constants/mathFunctions'; -import tracker from '../../utils/tracker'; +import { PAPER_SIZE } from '../constants/colors'; +import { checkAndSaveImage } from '../utils/saveImage'; +import { shouldShowShareGuide } from '../utils/shareGuide'; +import { BaseDrawService } from '../service/baseDraw'; +import tracker from '../utils/tracker'; /** * Canvas 相关的页面实例属性 */ -export interface MathPageCanvasInstance { +export interface PageCanvasInstance { + data: CanvasDataState; canvas: Canvas | null; ctx: RenderingContext | null; boxHeight: number; boxWidth: number; - drawService: BaseMathDrawService | null; + drawService: BaseDrawService | null; setData(data: any, callback?: () => void): void; + route: string; + getShareOptions(): ShareOptions; } /** @@ -29,8 +28,12 @@ export interface CanvasDataState { boxWidth: number; boxHeight: number; functionId: string; + currentMode?: string; pageTitle: string; subTitle?: string; + data: any; + setData(data: any, callback?: () => void): void; + route: string; } /** @@ -44,7 +47,7 @@ export interface InitCanvasOptions { canvas: Canvas, ctx: RenderingContext, options?: Record, - ) => BaseMathDrawService; + ) => BaseDrawService; /** * 绘制服务的选项 */ @@ -62,28 +65,60 @@ export interface ShareOptions { /** * 页面路径(相对于 mathPages 目录,例如:'missingNumber/missingNumber') */ - pagePath: string; + path: string; + title: string; + imageUrl: string; } -const shareConfig = { - title: '涂鸦丫-数学学习涂鸦卡', +/** + * 页面信息接口 + */ +export interface PageInfo { + title: string; + desc?: string; +} + +/** + * 页面信息查找函数类型 + */ +export type PageInfoLookup = (functionId: string) => PageInfo | undefined; + +/** + * 页面公共方法配置 + */ +export interface PageCommonMethodsConfig { + /** + * 分享配置 + */ + shareConfig?: { + title: string; + imageUrl: string; + }; + /** + * 页面信息查找函数(可选) + * 如果提供,initPageInfo 方法会使用它来查找页面信息 + */ + pageInfoLookup?: PageInfoLookup; +} + +const defaultShareConfig = { + title: '涂鸦丫', imageUrl: 'https://cdn.joeyone.cn/doodle/share-img/index-share-v2.png', }; /** - * 获取数学页面的公共方法 - * 这些方法可以在所有数学页面中复用 + * 获取页面公共方法 + * 这些方法可以在所有Canvas绘制页面中复用 + * @param config 配置选项 */ -export function getMathPageCommonMethods() { +export function getPageCommonMethods(config: PageCommonMethodsConfig = {}) { + const { shareConfig = defaultShareConfig, pageInfoLookup } = config; + return { /** * 初始化 Canvas */ - initCanvas( - this: MathPageCanvasInstance & { data: CanvasDataState }, - options: InitCanvasOptions, - ) { - console.log('initCanvas this', this); + initCanvas(this: PageCanvasInstance, options: InitCanvasOptions) { const query = wx.createSelectorQuery(); query .select('#canvasWrapper') @@ -141,9 +176,7 @@ export function getMathPageCommonMethods() { /** * 导出打印 */ - exportToPrint( - this: MathPageCanvasInstance & { data: CanvasDataState }, - ) { + exportToPrint(this: PageCanvasInstance) { if (!this.canvas || !this.data.hasContent) { return; } @@ -154,58 +187,55 @@ export function getMathPageCommonMethods() { } // 上报下载埋点 - tracker.reportDownload(this.data.pageTitle); + tracker.reportDownload(this.data.pageTitle, this.data.currentMode); checkAndSaveImage(this.canvas); }, + getShareOptions(this: PageCanvasInstance): ShareOptions { + const route = this.route || ''; + const { functionId, currentMode } = this.data; + const currentModeQuery = currentMode ? `&mode=${currentMode}` : ''; + const path = `${route}?id=${functionId}${currentModeQuery}`; + + console.log('getShareOptions path', path); + return { + ...shareConfig, + path, + }; + }, + /** * 分享小程序 */ - onShareAppMessage( - this: MathPageCanvasInstance & { data: CanvasDataState }, - ) { - console.log('onShareAppMessage'); + onShareAppMessage(this: PageCanvasInstance) { // 上报分享埋点 tracker.reportShare(this.data.pageTitle); - return { - ...shareConfig, - query: `id=${this.data.functionId}`, - }; + return this.getShareOptions(); }, /** * 分享到朋友圈 */ - onShareTimeline( - this: MathPageCanvasInstance & { data: CanvasDataState }, - ) { + onShareTimeline(this: PageCanvasInstance) { console.log('onShareTimeline'); // 上报分享埋点 tracker.reportShare(this.data.pageTitle); - - return { - ...shareConfig, - query: `id=${this.data.functionId}`, - }; + return this.getShareOptions(); }, /** * 关闭分享引导弹窗 */ - onCloseShareDialog( - this: MathPageCanvasInstance & { data: CanvasDataState }, - ) { + onCloseShareDialog(this: PageCanvasInstance) { this.setData({ showShareDialog: false }); }, /** * 分享成功回调 */ - onShareSuccess( - this: MathPageCanvasInstance & { data: CanvasDataState }, - ) { + onShareSuccess(this: PageCanvasInstance) { this.setData({ showShareDialog: false }); if (this.canvas) { // 上报下载埋点(分享成功后下载) @@ -215,19 +245,43 @@ export function getMathPageCommonMethods() { }, /** - * 初始化页面信息(从 functionId 获取标题等信息) + * 初始化页面信息 + * + * 支持两种调用方式: + * 1. initPageInfo(functionId, defaultTitle) - 如果有 pageInfoLookup,会尝试查找;否则使用 defaultTitle + * 2. initPageInfo({ title, desc, functionId }) - 直接提供页面信息 */ initPageInfo( - this: MathPageCanvasInstance & { data: CanvasDataState }, - functionId: string, + this: PageCanvasInstance, + functionIdOrOptions: + | string + | { title: string; desc?: string; functionId: string }, defaultTitle?: string, ) { - const functionItem = MATH_FUNCTION_TYPES.find( - (item: MathFunctionType) => item.id === functionId, - ); + let functionId: string; + let title: string; + let desc: string = ''; - const title = functionItem?.title || defaultTitle || ''; - const desc = functionItem?.desc || ''; + // 判断调用方式 + if (typeof functionIdOrOptions === 'string') { + // 方式1: initPageInfo(functionId, defaultTitle) + functionId = functionIdOrOptions; + title = defaultTitle || ''; + + // 如果有 pageInfoLookup,尝试查找 + if (pageInfoLookup) { + const pageInfo = pageInfoLookup(functionId); + if (pageInfo) { + title = pageInfo.title; + desc = pageInfo.desc || ''; + } + } + } else { + // 方式2: initPageInfo({ title, desc, functionId }) + functionId = functionIdOrOptions.functionId; + title = functionIdOrOptions.title; + desc = functionIdOrOptions.desc || ''; + } this.setData({ pageTitle: title, @@ -241,14 +295,17 @@ export function getMathPageCommonMethods() { } /** - * 应用数学页面公共方法的辅助函数 + * 应用页面公共方法的辅助函数 * 自动混入公共方法,简化页面代码 * @param pageOptions 页面选项 - * @param shareOptions 分享配置选项 + * @param config 公共方法配置 * @returns 合并后的页面选项 */ -export function applyMathPageMixin(pageOptions: any) { - const commonMethods = getMathPageCommonMethods(); +export function applyPageMixin( + pageOptions: any, + config: PageCommonMethodsConfig = {}, +) { + const commonMethods = getPageCommonMethods(config); // 提取 pageOptions 中的 data(如果有) const pageData = pageOptions.data || {}; @@ -268,11 +325,14 @@ export function applyMathPageMixin(pageOptions: any) { } /** - * 创建数学页面的便捷函数 + * 创建页面的便捷函数 * 自动应用公共方法并注册为页面 - * 页面路径从函数调用栈自动推断(从调用文件路径提取) * @param pageOptions 页面选项 + * @param config 公共方法配置 */ -export function createMathPage(pageOptions: any) { - Page(applyMathPageMixin(pageOptions)); +export function createPage( + pageOptions: any, + config: PageCommonMethodsConfig = {}, +) { + Page(applyPageMixin(pageOptions, config)); } diff --git a/miniprogram/config/config.ts b/miniprogram/config/config.ts index 34c8487..130b922 100644 --- a/miniprogram/config/config.ts +++ b/miniprogram/config/config.ts @@ -1,7 +1,13 @@ -export default { - printHeader: { - // header: 'wechat', - header: 'LogoImage', - appName: '涂鸦丫小程序', - }, +// export default { +// printHeader: { +// // header: 'wechat', +// header: 'LogoImage', +// appName: '涂鸦丫小程序', +// }, +// }; + +export const defaultPrintConfig: PrintConfig = { + header: 'LogoImage', + appName: '涂鸦丫小程序', + appHint: '数学|专注|练字|涂鸦', }; diff --git a/miniprogram/constants/focusFunctions.ts b/miniprogram/constants/focusFunctions.ts new file mode 100644 index 0000000..2fe8429 --- /dev/null +++ b/miniprogram/constants/focusFunctions.ts @@ -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: '🔍', + }, +]; diff --git a/miniprogram/constants/mathFunctions.ts b/miniprogram/constants/mathFunctions.ts index d060dcc..d586c6f 100644 --- a/miniprogram/constants/mathFunctions.ts +++ b/miniprogram/constants/mathFunctions.ts @@ -102,4 +102,11 @@ export const MATH_FUNCTION_TYPES: MathFunctionType[] = [ desc: '练习10以内的加法和减法混合运算', icon: '±', }, + { + id: 'number-decompose-20', + page: 'numberDecompose', + title: '20以内数的分与合', + desc: '把数字分一分,合一合', + icon: '🔢', + }, ]; diff --git a/miniprogram/focusPages/common/focusPageMixin.ts b/miniprogram/focusPages/common/focusPageMixin.ts new file mode 100644 index 0000000..033dadd --- /dev/null +++ b/miniprogram/focusPages/common/focusPageMixin.ts @@ -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); +} diff --git a/miniprogram/mathPages/REFACTOR_PLAN.md b/miniprogram/mathPages/REFACTOR_PLAN.md new file mode 100644 index 0000000..a20f3d0 --- /dev/null +++ b/miniprogram/mathPages/REFACTOR_PLAN.md @@ -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/` 目录**,因为: + +- 语义最清晰 +- 符合常见实践 +- 易于理解和维护 diff --git a/miniprogram/mathPages/addition/addition.json b/miniprogram/mathPages/addition/addition.json index ca15e45..6827d7b 100644 --- a/miniprogram/mathPages/addition/addition.json +++ b/miniprogram/mathPages/addition/addition.json @@ -6,7 +6,7 @@ "enablePullDownRefresh": false, "usingComponents": { "share-guide-popup": "../../components/share-guide-popup/share-guide-popup", - "math-type-selector": "../components/math-type-selector/math-type-selector", - "math-bottom-buttons": "../components/math-bottom-buttons/math-bottom-buttons" + "math-type-selector": "../shared/components/math-type-selector/math-type-selector", + "math-bottom-buttons": "../shared/components/math-bottom-buttons/math-bottom-buttons" } } diff --git a/miniprogram/mathPages/addition/addition.less b/miniprogram/mathPages/addition/addition.less index 12602d2..65d3fba 100644 --- a/miniprogram/mathPages/addition/addition.less +++ b/miniprogram/mathPages/addition/addition.less @@ -1 +1 @@ -@import '../common/mathPage.less'; \ No newline at end of file +@import '../shared/common/mathPage.less'; \ No newline at end of file diff --git a/miniprogram/mathPages/addition/addition.ts b/miniprogram/mathPages/addition/addition.ts index 7f37acc..8339a07 100644 --- a/miniprogram/mathPages/addition/addition.ts +++ b/miniprogram/mathPages/addition/addition.ts @@ -1,230 +1,219 @@ -import AdditionDraw from '../service/additionDraw'; -import { applyMathPageMixin, CanvasDataState } from '../common/mathPageMixin'; +import AdditionDraw from '../shared/service/additionDraw'; +import { + createMathPage, + CanvasDataState, +} from '../shared/common/mathPageMixin'; -Page( - applyMathPageMixin( - { - canvas: null as Canvas | null, - ctx: null as RenderingContext | null, - boxHeight: 0, - boxWidth: 0, - drawService: null as AdditionDraw | null, - calculationData: null as { - problems: Array<{ - type: 'addition' | 'subtraction'; - left: number; - right: number; - result: number; - }>; - } | null, +createMathPage({ + canvas: null as Canvas | null, + ctx: null as RenderingContext | null, + boxHeight: 0, + boxWidth: 0, + drawService: null as AdditionDraw | null, + calculationData: null as { + problems: Array<{ + type: 'addition' | 'subtraction'; + left: number; + right: number; + result: number; + }>; + } | null, - data: { - pageTitle: '加减法计算', - subTitle: '通过图形化方式学习加减法运算', - functionId: '', - hasContent: false, - showShareDialog: false, - currentType: 'addition-5', // 'addition-5', 'addition-10', 'subtraction-10', 'addition-subtraction-10' - currentTypeName: '5以内加法', - typeActions: [ - { name: '5以内加法', value: 'addition-5' }, - { name: '10以内加法', value: 'addition-10' }, - { name: '10以内减法', value: 'subtraction-10' }, - { name: '10以内加减法', value: 'addition-subtraction-10' }, - ], - imageType: 'twelve-animals', // 图片类型:twelve-animals 或 fruits - } as CanvasDataState & { - currentType: string; - currentTypeName: string; - typeActions: Array<{ name: string; value: string }>; - imageType: string; + data: { + pageTitle: '加减法计算', + subTitle: '通过图形化方式学习加减法运算', + functionId: '', + hasContent: false, + showShareDialog: false, + currentMode: 'addition-5', // 'addition-5', 'addition-10', 'subtraction-10', 'addition-subtraction-10' + currentModeName: '5以内加法', + typeActions: [ + { name: '5以内加法', value: 'addition-5' }, + { name: '10以内加法', value: 'addition-10' }, + { name: '10以内减法', value: 'subtraction-10' }, + { name: '10以内加减法', value: 'addition-subtraction-10' }, + ], + imageType: 'twelve-animals', // 图片类型:twelve-animals 或 fruits + } as CanvasDataState & { + currentMode: string; + currentModeName: string; + typeActions: Array<{ name: string; value: string }>; + imageType: string; + }, + + onLoad(options: { id?: string }) { + const functionId = options.id || 'addition-5'; + this.initPageInfo(functionId, '加减法计算'); + + // 根据 functionId 设置默认类型 + if (functionId === 'addition-5') { + this.setData({ + currentMode: 'addition-5', + currentModeName: '5以内加法', + }); + } else if (functionId === 'addition-10') { + this.setData({ + currentMode: 'addition-10', + currentModeName: '10以内加法', + }); + } else if (functionId === 'subtraction-10') { + this.setData({ + currentMode: 'subtraction-10', + currentModeName: '10以内减法', + }); + } else if (functionId === 'addition-subtraction-10') { + this.setData({ + currentMode: 'addition-subtraction-10', + currentModeName: '10以内加减法', + }); + } + }, + + onReady() { + this.initCanvas({ + createDrawService: ( + canvas: Canvas, + ctx: RenderingContext, + options?: Record, + ) => { + return new AdditionDraw(canvas, ctx, options); }, - - onLoad(options: { id?: string }) { - const functionId = options.id || 'addition-5'; - this.initPageInfo(functionId, '加减法计算'); - - // 根据 functionId 设置默认类型 - if (functionId === 'addition-5') { - this.setData({ - currentType: 'addition-5', - currentTypeName: '5以内加法', - }); - } else if (functionId === 'addition-10') { - this.setData({ - currentType: 'addition-10', - currentTypeName: '10以内加法', - }); - } else if (functionId === 'subtraction-10') { - this.setData({ - currentType: 'subtraction-10', - currentTypeName: '10以内减法', - }); - } else if (functionId === 'addition-subtraction-10') { - this.setData({ - currentType: 'addition-subtraction-10', - currentTypeName: '10以内加减法', - }); - } + drawServiceOptions: { + subTitle: this.data.subTitle, }, - - onReady() { - this.initCanvas({ - createDrawService: ( - canvas: Canvas, - ctx: RenderingContext, - options?: Record, - ) => { - return new AdditionDraw(canvas, ctx, options); - }, - drawServiceOptions: { - subTitle: this.data.subTitle, - }, - onCanvasReady: () => { - // 初始随机生成 - this.onRandom(); - }, - }); - }, - - /** - * 绘制Canvas内容 - */ - async drawCanvas() { - if (!this.ctx || !this.drawService || !this.calculationData) { - return; - } - - try { - // 更新 Header 的 Title - if (this.drawService) { - this.drawService.options.title = - this.data.currentTypeName; - } - - await this.drawService.draw( - this.calculationData, - this.data.currentType, - ); - this.setData({ hasContent: true }); - } catch (error) { - console.error('绘制失败:', error); - this.setData({ hasContent: false }); - } - }, - - /** - * 随机生成 - */ - onRandom() { - const problems: Array<{ - type: 'addition' | 'subtraction'; - left: number; - right: number; - result: number; - }> = []; - - const type = this.data.currentType; - - // 生成5道题目 - for (let i = 0; i < 5; i++) { - if (type === 'addition-5') { - // 5以内加法:和 ≤ 5 - // left >= 1, right >= 1, left + right <= 5 - const maxSum = 5; - const left = - Math.floor(Math.random() * (maxSum - 1)) + 1; // 1 到 4 - const maxRight = maxSum - left; // 确保 left + right <= 5 - const right = Math.floor(Math.random() * maxRight) + 1; // 1 到 maxRight - const result = left + right; - problems.push({ - type: 'addition', - left, - right, - result, - }); - } else if (type === 'addition-10') { - // 10以内加法:和 ≤ 10 - // left >= 1, right >= 1, left + right <= 10 - const maxSum = 10; - const left = - Math.floor(Math.random() * (maxSum - 1)) + 1; // 1 到 9 - const maxRight = maxSum - left; // 确保 left + right <= 10 - const right = Math.floor(Math.random() * maxRight) + 1; // 1 到 maxRight - const result = left + right; - problems.push({ - type: 'addition', - left, - right, - result, - }); - } else if (type === 'subtraction-10') { - // 10以内减法:被减数 ≤ 10 - // left <= 10, left - right = result, result >= 1 - const maxLeft = 10; - const left = Math.floor(Math.random() * maxLeft) + 1; // 1 到 10 - const maxRight = left - 1; // 确保 result >= 1 - const right = Math.floor(Math.random() * maxRight) + 1; // 1 到 maxRight - const result = left - right; - problems.push({ - type: 'subtraction', - left, - right, - result, - }); - } else if (type === 'addition-subtraction-10') { - // 加减法混合 - if (Math.random() < 0.5) { - // 加法:和 ≤ 10 - const maxSum = 10; - const left = - Math.floor(Math.random() * (maxSum - 1)) + 1; // 1 到 9 - const maxRight = maxSum - left; // 确保 left + right <= 10 - const right = - Math.floor(Math.random() * maxRight) + 1; // 1 到 maxRight - const result = left + right; - problems.push({ - type: 'addition', - left, - right, - result, - }); - } else { - // 减法:被减数 ≤ 10 - const maxLeft = 10; - const left = - Math.floor(Math.random() * maxLeft) + 1; // 1 到 10 - const maxRight = left - 1; // 确保 result >= 1 - const right = - Math.floor(Math.random() * maxRight) + 1; // 1 到 maxRight - const result = left - right; - problems.push({ - type: 'subtraction', - left, - right, - result, - }); - } - } - } - - this.calculationData = { problems }; - this.drawCanvas(); - }, - - /** 选择类型 */ - onSelectType(event: any) { - const { name, value } = event.detail; - this.setData({ - currentType: value, - currentTypeName: name, - }); - // 重新生成数据 + onCanvasReady: () => { + // 初始随机生成 this.onRandom(); }, - }, - { - pagePath: 'addition/addition', - }, - ), -); + }); + }, + + /** + * 绘制Canvas内容 + */ + async drawCanvas() { + if (!this.ctx || !this.drawService || !this.calculationData) { + return; + } + + try { + // 更新 Header 的 Title + if (this.drawService) { + this.drawService.options.title = this.data.currentModeName; + } + + await this.drawService.draw( + this.calculationData, + this.data.currentMode, + ); + this.setData({ hasContent: true }); + } catch (error) { + console.error('绘制失败:', error); + this.setData({ hasContent: false }); + } + }, + + /** + * 随机生成 + */ + onRandom() { + const problems: Array<{ + type: 'addition' | 'subtraction'; + left: number; + right: number; + result: number; + }> = []; + + const type = this.data.currentMode; + + // 生成5道题目 + for (let i = 0; i < 5; i++) { + if (type === 'addition-5') { + // 5以内加法:和 ≤ 5 + // left >= 1, right >= 1, left + right <= 5 + const maxSum = 5; + const left = Math.floor(Math.random() * (maxSum - 1)) + 1; // 1 到 4 + const maxRight = maxSum - left; // 确保 left + right <= 5 + const right = Math.floor(Math.random() * maxRight) + 1; // 1 到 maxRight + const result = left + right; + problems.push({ + type: 'addition', + left, + right, + result, + }); + } else if (type === 'addition-10') { + // 10以内加法:和 ≤ 10 + // left >= 1, right >= 1, left + right <= 10 + const maxSum = 10; + const left = Math.floor(Math.random() * (maxSum - 1)) + 1; // 1 到 9 + const maxRight = maxSum - left; // 确保 left + right <= 10 + const right = Math.floor(Math.random() * maxRight) + 1; // 1 到 maxRight + const result = left + right; + problems.push({ + type: 'addition', + left, + right, + result, + }); + } else if (type === 'subtraction-10') { + // 10以内减法:被减数 ≤ 10 + // left <= 10, left - right = result, result >= 1 + const maxLeft = 10; + const left = Math.floor(Math.random() * maxLeft) + 1; // 1 到 10 + const maxRight = left - 1; // 确保 result >= 1 + const right = Math.floor(Math.random() * maxRight) + 1; // 1 到 maxRight + const result = left - right; + problems.push({ + type: 'subtraction', + left, + right, + result, + }); + } else if (type === 'addition-subtraction-10') { + // 加减法混合 + if (Math.random() < 0.5) { + // 加法:和 ≤ 10 + const maxSum = 10; + const left = Math.floor(Math.random() * (maxSum - 1)) + 1; // 1 到 9 + const maxRight = maxSum - left; // 确保 left + right <= 10 + const right = Math.floor(Math.random() * maxRight) + 1; // 1 到 maxRight + const result = left + right; + problems.push({ + type: 'addition', + left, + right, + result, + }); + } else { + // 减法:被减数 ≤ 10 + const maxLeft = 10; + const left = Math.floor(Math.random() * maxLeft) + 1; // 1 到 10 + const maxRight = left - 1; // 确保 result >= 1 + const right = Math.floor(Math.random() * maxRight) + 1; // 1 到 maxRight + const result = left - right; + problems.push({ + type: 'subtraction', + left, + right, + result, + }); + } + } + } + + this.calculationData = { problems }; + this.drawCanvas(); + }, + + /** 选择类型 */ + onSelectType(event: any) { + const { name, value } = event.detail; + this.setData({ + currentMode: value, + currentModeName: name, + }); + // 重新生成数据 + this.onRandom(); + }, +}); diff --git a/miniprogram/mathPages/addition/addition.wxml b/miniprogram/mathPages/addition/addition.wxml index 3dcfa5d..d8594f4 100644 --- a/miniprogram/mathPages/addition/addition.wxml +++ b/miniprogram/mathPages/addition/addition.wxml @@ -13,7 +13,7 @@ diff --git a/miniprogram/mathPages/compare/compare.json b/miniprogram/mathPages/compare/compare.json index 90335ca..987054d 100644 --- a/miniprogram/mathPages/compare/compare.json +++ b/miniprogram/mathPages/compare/compare.json @@ -7,6 +7,6 @@ "usingComponents": { "toy-button": "../../ui/button/button", "share-guide-popup": "../../components/share-guide-popup/share-guide-popup", - "math-bottom-buttons": "../components/math-bottom-buttons/math-bottom-buttons" + "math-bottom-buttons": "../shared/components/math-bottom-buttons/math-bottom-buttons" } } diff --git a/miniprogram/mathPages/compare/compare.less b/miniprogram/mathPages/compare/compare.less index 12602d2..65d3fba 100644 --- a/miniprogram/mathPages/compare/compare.less +++ b/miniprogram/mathPages/compare/compare.less @@ -1 +1 @@ -@import '../common/mathPage.less'; \ No newline at end of file +@import '../shared/common/mathPage.less'; \ No newline at end of file diff --git a/miniprogram/mathPages/compare/compare.ts b/miniprogram/mathPages/compare/compare.ts index 293327f..5bfe10b 100644 --- a/miniprogram/mathPages/compare/compare.ts +++ b/miniprogram/mathPages/compare/compare.ts @@ -1,15 +1,12 @@ -import CompareDraw from '../service/compareDraw'; +import CompareDraw from '../shared/service/compareDraw'; import { - getMathPageCommonMethods, + createMathPage, CanvasDataState, -} from '../common/mathPageMixin'; +} from '../shared/common/mathPageMixin'; -// 获取公共方法 -const commonMethods = getMathPageCommonMethods({ - pagePath: 'compare/compare', -}); +// 获取公共方 -Page({ +createMathPage({ canvas: null as Canvas | null, ctx: null as RenderingContext | null, boxHeight: 0, @@ -40,7 +37,11 @@ Page({ onReady() { this.initCanvas({ - createDrawService: (canvas, ctx, options) => { + createDrawService: ( + canvas: Canvas, + ctx: RenderingContext, + options?: Record, + ) => { return new CompareDraw(canvas, ctx, options); }, drawServiceOptions: { @@ -120,13 +121,4 @@ Page({ this.compareData = { problems }; }, - - // ========== 使用公共方法 ========== - initCanvas: commonMethods.initCanvas, - exportToPrint: commonMethods.exportToPrint, - onShareAppMessage: commonMethods.onShareAppMessage, - onShareTimeline: commonMethods.onShareTimeline, - onCloseShareDialog: commonMethods.onCloseShareDialog, - onShareSuccess: commonMethods.onShareSuccess, - initPageInfo: commonMethods.initPageInfo, }); diff --git a/miniprogram/mathPages/components/math-type-selector/math-type-selector.json b/miniprogram/mathPages/components/math-type-selector/math-type-selector.json deleted file mode 100644 index 31c75f2..0000000 --- a/miniprogram/mathPages/components/math-type-selector/math-type-selector.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "component": true, - "usingComponents": { - "toy-button": "../../../ui/button/button", - "van-action-sheet": "../../../miniprogram_npm/@vant/weapp/action-sheet/index" - } -} diff --git a/miniprogram/mathPages/countMatch/countMatch.json b/miniprogram/mathPages/countMatch/countMatch.json index b1bead2..9eda340 100644 --- a/miniprogram/mathPages/countMatch/countMatch.json +++ b/miniprogram/mathPages/countMatch/countMatch.json @@ -6,7 +6,7 @@ "enablePullDownRefresh": false, "usingComponents": { "share-guide-popup": "../../components/share-guide-popup/share-guide-popup", - "math-type-selector": "../components/math-type-selector/math-type-selector", - "math-bottom-buttons": "../components/math-bottom-buttons/math-bottom-buttons" + "math-type-selector": "../shared/components/math-type-selector/math-type-selector", + "math-bottom-buttons": "../shared/components/math-bottom-buttons/math-bottom-buttons" } } diff --git a/miniprogram/mathPages/countMatch/countMatch.less b/miniprogram/mathPages/countMatch/countMatch.less index 12602d2..65d3fba 100644 --- a/miniprogram/mathPages/countMatch/countMatch.less +++ b/miniprogram/mathPages/countMatch/countMatch.less @@ -1 +1 @@ -@import '../common/mathPage.less'; \ No newline at end of file +@import '../shared/common/mathPage.less'; \ No newline at end of file diff --git a/miniprogram/mathPages/countMatch/countMatch.ts b/miniprogram/mathPages/countMatch/countMatch.ts index 106c369..1432183 100644 --- a/miniprogram/mathPages/countMatch/countMatch.ts +++ b/miniprogram/mathPages/countMatch/countMatch.ts @@ -1,200 +1,194 @@ -import CountMatchDraw from '../service/countMatchDraw'; -import NumberColorDraw from '../service/numberColorDraw'; -import { applyMathPageMixin, CanvasDataState } from '../common/mathPageMixin'; +import CountMatchDraw from '../shared/service/countMatchDraw'; +import NumberColorDraw from '../shared/service/numberColorDraw'; +import { + createMathPage, + CanvasDataState, +} from '../shared/common/mathPageMixin'; -Page( - applyMathPageMixin( - { - canvas: null as Canvas | null, - ctx: null as RenderingContext | null, - boxHeight: 0, - boxWidth: 0, - drawService: null as CountMatchDraw | NumberColorDraw | null, - matchData: null as { - leftNumbers: number[]; - rightNumbers: number[]; - } | null, - colorData: null as { - numbers: number[]; - } | null, +createMathPage({ + canvas: null as Canvas | null, + ctx: null as RenderingContext | null, + boxHeight: 0, + boxWidth: 0, + drawService: null as CountMatchDraw | NumberColorDraw | null, + matchData: null as { + leftNumbers: number[]; + rightNumbers: number[]; + } | null, + colorData: null as { + numbers: number[]; + } | null, - data: { - pageTitle: '数一数,连一连', - functionId: '', - hasContent: false, - showShareDialog: false, - showTypeSelector: true, // 控制是否显示类型选择器 - currentType: 'twelve-animals', // 'twelve-animals' 或 'fruits' 或 'circle' 或 'caterpillar' - currentTypeName: '十二生肖', + data: { + pageTitle: '数一数,连一连', + functionId: '', + hasContent: false, + showShareDialog: false, + showTypeSelector: true, // 控制是否显示类型选择器 + currentMode: 'twelve-animals', // 'twelve-animals' 或 'fruits' 或 'circle' 或 'caterpillar' + currentModeName: '十二生肖', + typeActions: [ + { name: '十二生肖', value: 'twelve-animals' }, + { name: '水果', value: 'fruits' }, + ], + } as CanvasDataState & { + showTypeSelector: boolean; + currentMode: string; + currentModeName: string; + typeActions: Array<{ name: string; value: string }>; + }, + + onLoad(options: { id?: string; mode?: string }) { + const functionId = options.id || 'counting-matching'; + this.initPageInfo(functionId, '数一数,连一连'); + + // 根据 functionId 设置不同的类型选择器 + if (functionId === 'number-coloring') { + const currentMode = options.mode || 'caterpillar'; + this.setData({ + currentMode: currentMode, + currentModeName: + currentMode === 'caterpillar' ? '毛毛虫' : '圆圈', + typeActions: [ + { name: '毛毛虫', value: 'caterpillar' }, + { name: '圆圈', value: 'circle' }, + ], + }); + } else { + const currentMode = options.mode || 'twelve-animals'; + this.setData({ + currentMode, + currentModeName: + currentMode === 'twelve-animals' ? '十二生肖' : '水果', typeActions: [ { name: '十二生肖', value: 'twelve-animals' }, { name: '水果', value: 'fruits' }, ], - } as CanvasDataState & { - showTypeSelector: boolean; - currentType: string; - currentTypeName: string; - typeActions: Array<{ name: string; value: string }>; - }, + }); + } + }, - onLoad(options: { id?: string }) { - const functionId = options.id || 'counting-matching'; - this.setData({ functionId }); - - const functionItem = - require('../../constants/mathFunctions').MATH_FUNCTION_TYPES.find( - (item: any) => item.id === functionId, - ); - - const pageTitle = functionItem?.title || '数一数,连一连'; - this.setData({ pageTitle }); - this.initPageInfo(functionId, pageTitle); - - // 根据 functionId 设置不同的类型选择器 - if (functionId === 'number-coloring') { - this.setData({ - currentType: 'caterpillar', - currentTypeName: '毛毛虫', - typeActions: [ - { name: '毛毛虫', value: 'caterpillar' }, - { name: '圆圈', value: 'circle' }, - ], - }); - } - }, - - onReady() { - this.initCanvas({ - createDrawService: ( - canvas: Canvas, - ctx: RenderingContext, - options?: Record, - ) => { - // 根据 functionId 创建不同的绘制服务 - if (this.data.functionId === 'number-coloring') { - return new NumberColorDraw(canvas, ctx, options); - } else { - return new CountMatchDraw(canvas, ctx, options); - } - }, - drawServiceOptions: { - subTitle: - this.data.functionId === 'number-coloring' - ? '按数字给相应的圆圈涂上颜色' - : '通过连线配对数字和对应的数量图形', - }, - onCanvasReady: () => { - // 初始随机生成 - this.onRandom(); - }, - }); - }, - - /** - * 绘制Canvas内容 - */ - async drawCanvas() { - if (!this.ctx || !this.drawService) { - return; - } - - try { - if (this.data.functionId === 'number-coloring') { - if (!this.colorData) { - return; - } - await (this.drawService as NumberColorDraw).draw( - this.colorData, - this.data.currentType, - ); - } else { - if (!this.matchData) { - return; - } - await (this.drawService as CountMatchDraw).draw( - this.matchData, - this.data.currentType, - ); - } - this.setData({ hasContent: true }); - } catch (error) { - console.error('绘制失败:', error); - this.setData({ hasContent: false }); - } - }, - - /** - * 随机生成 - */ - onRandom() { + onReady() { + this.initCanvas({ + createDrawService: ( + canvas: Canvas, + ctx: RenderingContext, + options?: Record, + ) => { + // 根据 functionId 创建不同的绘制服务 if (this.data.functionId === 'number-coloring') { - // 按数字涂颜色模式:生成6个随机数字(1-10) - const availableNumbers = Array.from( - { length: 10 }, - (_, i) => i + 1, - ); - const numbers: number[] = []; - - for (let i = 0; i < 6; i++) { - const randomIndex = Math.floor( - Math.random() * availableNumbers.length, - ); - const number = availableNumbers.splice( - randomIndex, - 1, - )[0]; - numbers.push(number); - } - - this.colorData = { numbers }; + return new NumberColorDraw(canvas, ctx, options); } else { - // 数一数连一连模式:生成5个不同的数字(1-10) - const availableNumbers = Array.from( - { length: 10 }, - (_, i) => i + 1, - ); - const leftNumbers: number[] = []; - - for (let i = 0; i < 5; i++) { - const randomIndex = Math.floor( - Math.random() * availableNumbers.length, - ); - const number = availableNumbers.splice( - randomIndex, - 1, - )[0]; - leftNumbers.push(number); - } - - // 复制数字数组并打乱顺序,作为右侧显示的数字 - const rightNumbers = [...leftNumbers]; - for (let i = rightNumbers.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [rightNumbers[i], rightNumbers[j]] = [ - rightNumbers[j], - rightNumbers[i], - ]; - } - - this.matchData = { leftNumbers, rightNumbers }; + return new CountMatchDraw(canvas, ctx, options); } - - this.drawCanvas(); }, - - /** 选择类型 */ - onSelectType(event: any) { - const { name, value } = event.detail; - this.setData({ - currentType: value, - currentTypeName: name, - }); - // 重新生成数据 + drawServiceOptions: { + subTitle: + this.data.functionId === 'number-coloring' + ? '按数字给相应的圆圈涂上颜色' + : '通过连线配对数字和对应的数量图形', + }, + onCanvasReady: () => { + // 初始随机生成 this.onRandom(); }, - }, - { - pagePath: 'countMatch/countMatch', - }, - ), -); + }); + }, + + /** + * 绘制Canvas内容 + */ + async drawCanvas() { + if (!this.ctx || !this.drawService) { + return; + } + + try { + if (this.data.functionId === 'number-coloring') { + if (!this.colorData) { + return; + } + await (this.drawService as NumberColorDraw).draw( + this.colorData, + this.data.currentMode, + ); + } else { + if (!this.matchData) { + return; + } + await (this.drawService as CountMatchDraw).draw( + this.matchData, + this.data.currentMode, + ); + } + this.setData({ hasContent: true }); + } catch (error) { + console.error('绘制失败:', error); + this.setData({ hasContent: false }); + } + }, + + /** + * 随机生成 + */ + onRandom() { + if (this.data.functionId === 'number-coloring') { + // 按数字涂颜色模式:生成6个随机数字(1-10) + const availableNumbers = Array.from( + { length: 10 }, + (_, i) => i + 1, + ); + const numbers: number[] = []; + + for (let i = 0; i < 6; i++) { + const randomIndex = Math.floor( + Math.random() * availableNumbers.length, + ); + const number = availableNumbers.splice(randomIndex, 1)[0]; + numbers.push(number); + } + + this.colorData = { numbers }; + } else { + // 数一数连一连模式:生成5个不同的数字(1-10) + const availableNumbers = Array.from( + { length: 10 }, + (_, i) => i + 1, + ); + const leftNumbers: number[] = []; + + for (let i = 0; i < 5; i++) { + const randomIndex = Math.floor( + Math.random() * availableNumbers.length, + ); + const number = availableNumbers.splice(randomIndex, 1)[0]; + leftNumbers.push(number); + } + + // 复制数字数组并打乱顺序,作为右侧显示的数字 + const rightNumbers = [...leftNumbers]; + for (let i = rightNumbers.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [rightNumbers[i], rightNumbers[j]] = [ + rightNumbers[j], + rightNumbers[i], + ]; + } + + this.matchData = { leftNumbers, rightNumbers }; + } + + this.drawCanvas(); + }, + + /** 选择类型 */ + onSelectType(event: any) { + const { name, value } = event.detail; + this.setData({ + currentMode: value, + currentModeName: name, + }); + // 重新生成数据 + this.onRandom(); + }, +}); diff --git a/miniprogram/mathPages/countMatch/countMatch.wxml b/miniprogram/mathPages/countMatch/countMatch.wxml index 15a8a9c..d3bc2ea 100644 --- a/miniprogram/mathPages/countMatch/countMatch.wxml +++ b/miniprogram/mathPages/countMatch/countMatch.wxml @@ -14,7 +14,7 @@ diff --git a/miniprogram/mathPages/countingSelect/countingSelect.json b/miniprogram/mathPages/countingSelect/countingSelect.json index 0746d06..a49ce52 100644 --- a/miniprogram/mathPages/countingSelect/countingSelect.json +++ b/miniprogram/mathPages/countingSelect/countingSelect.json @@ -7,6 +7,6 @@ "usingComponents": { "toy-button": "../../ui/button/button", "share-guide-popup": "../../components/share-guide-popup/share-guide-popup", - "math-bottom-buttons": "../components/math-bottom-buttons/math-bottom-buttons" + "math-bottom-buttons": "../shared/components/math-bottom-buttons/math-bottom-buttons" } } diff --git a/miniprogram/mathPages/countingSelect/countingSelect.less b/miniprogram/mathPages/countingSelect/countingSelect.less index 12602d2..65d3fba 100644 --- a/miniprogram/mathPages/countingSelect/countingSelect.less +++ b/miniprogram/mathPages/countingSelect/countingSelect.less @@ -1 +1 @@ -@import '../common/mathPage.less'; \ No newline at end of file +@import '../shared/common/mathPage.less'; \ No newline at end of file diff --git a/miniprogram/mathPages/countingSelect/countingSelect.ts b/miniprogram/mathPages/countingSelect/countingSelect.ts index 5194aa2..0ed2dbe 100644 --- a/miniprogram/mathPages/countingSelect/countingSelect.ts +++ b/miniprogram/mathPages/countingSelect/countingSelect.ts @@ -1,168 +1,151 @@ -import CountingSelectDraw from '../service/countingSelectDraw'; -import { applyMathPageMixin, CanvasDataState } from '../common/mathPageMixin'; +import CountingSelectDraw from '../shared/service/countingSelectDraw'; +import { + createMathPage, + CanvasDataState, +} from '../shared/common/mathPageMixin'; -Page( - applyMathPageMixin( - { - canvas: null as Canvas | null, - ctx: null as RenderingContext | null, - boxHeight: 0, - boxWidth: 0, - drawService: null as CountingSelectDraw | null, - countingSelectData: null as { - problems: Array<{ - count: number; // 图片数量(正确答案) - imageIndex: number; // 图片索引 - imageType: 'fruits' | 'twelve-animals'; // 图片类型 - options?: number[]; // 三个数字选项(选一选模式需要) - correctIndex?: number; // 正确答案在options中的索引(选一选模式需要) - }>; - } | null, +createMathPage({ + canvas: null as Canvas | null, + ctx: null as RenderingContext | null, + boxHeight: 0, + boxWidth: 0, + drawService: null as CountingSelectDraw | null, + countingSelectData: null as { + problems: Array<{ + count: number; // 图片数量(正确答案) + imageIndex: number; // 图片索引 + imageType: 'fruits' | 'twelve-animals'; // 图片类型 + options?: number[]; // 三个数字选项(选一选模式需要) + correctIndex?: number; // 正确答案在options中的索引(选一选模式需要) + }>; + } | null, - data: { - pageTitle: '数一数,选一选', - subTitle: '数出物品数量,从多个选项中选择正确答案', - functionId: '', - hasContent: false, - showShareDialog: false, - } as CanvasDataState & { - showTypeSelector?: boolean; - currentType?: string; - currentTypeName?: string; - typeActions?: Array<{ name: string; value: any }>; + data: { + pageTitle: '数一数,选一选', + subTitle: '数出物品数量,从多个选项中选择正确答案', + functionId: '', + hasContent: false, + showShareDialog: false, + } as CanvasDataState, + + onLoad(options: { id?: string }) { + const functionId = options.id || 'counting-select'; + // 初始化页面信息(会从 mathFunctions.ts 读取 title 和 desc) + this.initPageInfo(functionId, '数一数,选一选'); + }, + + onReady() { + this.initCanvas({ + createDrawService: ( + canvas: Canvas, + ctx: RenderingContext, + options?: Record, + ) => { + return new CountingSelectDraw(canvas, ctx, options); }, - - onLoad(options: { id?: string }) { - const functionId = options.id || 'counting-select'; - // 初始化页面信息(会从 mathFunctions.ts 读取 title 和 desc) - this.initPageInfo(functionId, '数一数,选一选'); + drawServiceOptions: { + subTitle: this.data.subTitle, }, - - onReady() { - this.initCanvas({ - createDrawService: ( - canvas: Canvas, - ctx: RenderingContext, - options?: Record, - ) => { - return new CountingSelectDraw(canvas, ctx, options); - }, - drawServiceOptions: { - subTitle: this.data.subTitle, - }, - onCanvasReady: () => { - // Canvas 初始化完成后,生成初始数据 - this.onRandom(); - }, - }); + onCanvasReady: () => { + // Canvas 初始化完成后,生成初始数据 + this.onRandom(); }, + }); + }, - /** - * 绘制Canvas内容 - */ - async drawCanvas() { - if ( - !this.ctx || - !this.drawService || - !this.countingSelectData - ) { - return; - } + /** + * 绘制Canvas内容 + */ + async drawCanvas() { + if (!this.ctx || !this.drawService || !this.countingSelectData) { + return; + } - try { - // 判断是选一选还是填一填模式 - const mode = - this.data.functionId === 'counting-fill' - ? 'fill' - : 'select'; - await this.drawService.draw(this.countingSelectData, mode); - this.setData({ hasContent: true }); - } catch (error) { - console.error('绘制失败:', error); - this.setData({ hasContent: false }); - } - }, + try { + // 判断是选一选还是填一填模式 + const mode = + this.data.functionId === 'counting-fill' ? 'fill' : 'select'; + await this.drawService.draw(this.countingSelectData, mode); + this.setData({ hasContent: true }); + } catch (error) { + console.error('绘制失败:', error); + this.setData({ hasContent: false }); + } + }, - /** - * 随机生成 - */ - onRandom() { - this.generateCountingSelectData(); - this.drawCanvas(); - }, + /** + * 随机生成 + */ + onRandom() { + this.generateCountingSelectData(); + this.drawCanvas(); + }, - /** - * 生成数一数选一选/填一填数据 - */ - generateCountingSelectData() { - const isFillMode = this.data.functionId === 'counting-fill'; - const problems: Array<{ - count: number; - imageIndex: number; - imageType: 'fruits' | 'twelve-animals'; - options?: number[]; - correctIndex?: number; - }> = []; + /** + * 生成数一数选一选/填一填数据 + */ + generateCountingSelectData() { + const isFillMode = this.data.functionId === 'counting-fill'; + const problems: Array<{ + count: number; + imageIndex: number; + imageType: 'fruits' | 'twelve-animals'; + options?: number[]; + correctIndex?: number; + }> = []; - // 生成9道题目 - for (let i = 0; i < 9; i++) { - // 随机选择图片类型 - const imageType: 'fruits' | 'twelve-animals' = - Math.random() < 0.5 ? 'fruits' : 'twelve-animals'; + // 生成9道题目 + for (let i = 0; i < 9; i++) { + // 随机选择图片类型 + const imageType: 'fruits' | 'twelve-animals' = + Math.random() < 0.5 ? 'fruits' : 'twelve-animals'; - // 根据图片类型确定最大索引 - const maxImageIndex = imageType === 'fruits' ? 22 : 12; + // 根据图片类型确定最大索引 + const maxImageIndex = imageType === 'fruits' ? 22 : 12; - // 生成图片数量(1-10) - const count = Math.floor(Math.random() * 10) + 1; + // 生成图片数量(1-10) + const count = Math.floor(Math.random() * 10) + 1; - // 随机选择图片索引 - const imageIndex = - Math.floor(Math.random() * maxImageIndex) + 1; + // 随机选择图片索引 + const imageIndex = Math.floor(Math.random() * maxImageIndex) + 1; - const problem: { - count: number; - imageIndex: number; - imageType: 'fruits' | 'twelve-animals'; - options?: number[]; - correctIndex?: number; - } = { - count, - imageIndex, - imageType, - }; + const problem: { + count: number; + imageIndex: number; + imageType: 'fruits' | 'twelve-animals'; + options?: number[]; + correctIndex?: number; + } = { + count, + imageIndex, + imageType, + }; - // 选一选模式:生成三个选项 - if (!isFillMode) { - const options: number[] = []; - const correctIndex = Math.floor(Math.random() * 3); + // 选一选模式:生成三个选项 + if (!isFillMode) { + const options: number[] = []; + const correctIndex = Math.floor(Math.random() * 3); - for (let j = 0; j < 3; j++) { - if (j === correctIndex) { - options.push(count); // 正确答案 - } else { - // 生成错误答案(与正确答案不同) - let wrongAnswer: number; - do { - wrongAnswer = - Math.floor(Math.random() * 10) + 1; - } while (wrongAnswer === count); - options.push(wrongAnswer); - } - } - - problem.options = options; - problem.correctIndex = correctIndex; + for (let j = 0; j < 3; j++) { + if (j === correctIndex) { + options.push(count); // 正确答案 + } else { + // 生成错误答案(与正确答案不同) + let wrongAnswer: number; + do { + wrongAnswer = Math.floor(Math.random() * 10) + 1; + } while (wrongAnswer === count); + options.push(wrongAnswer); } - - problems.push(problem); } - this.countingSelectData = { problems }; - }, - }, - { - pagePath: 'countingSelect/countingSelect', - }, - ), -); + problem.options = options; + problem.correctIndex = correctIndex; + } + + problems.push(problem); + } + + this.countingSelectData = { problems }; + }, +}); diff --git a/miniprogram/mathPages/missingNumber/missingNumber.json b/miniprogram/mathPages/missingNumber/missingNumber.json index 79bdc26..42774ef 100644 --- a/miniprogram/mathPages/missingNumber/missingNumber.json +++ b/miniprogram/mathPages/missingNumber/missingNumber.json @@ -6,7 +6,7 @@ "enablePullDownRefresh": false, "usingComponents": { "share-guide-popup": "../../components/share-guide-popup/share-guide-popup", - "math-type-selector": "../components/math-type-selector/math-type-selector", - "math-bottom-buttons": "../components/math-bottom-buttons/math-bottom-buttons" + "math-type-selector": "../shared/components/math-type-selector/math-type-selector", + "math-bottom-buttons": "../shared/components/math-bottom-buttons/math-bottom-buttons" } } diff --git a/miniprogram/mathPages/missingNumber/missingNumber.less b/miniprogram/mathPages/missingNumber/missingNumber.less index 12602d2..65d3fba 100644 --- a/miniprogram/mathPages/missingNumber/missingNumber.less +++ b/miniprogram/mathPages/missingNumber/missingNumber.less @@ -1 +1 @@ -@import '../common/mathPage.less'; \ No newline at end of file +@import '../shared/common/mathPage.less'; \ No newline at end of file diff --git a/miniprogram/mathPages/missingNumber/missingNumber.ts b/miniprogram/mathPages/missingNumber/missingNumber.ts index e00593f..c4bf62f 100644 --- a/miniprogram/mathPages/missingNumber/missingNumber.ts +++ b/miniprogram/mathPages/missingNumber/missingNumber.ts @@ -1,198 +1,198 @@ import { getRandomNumberColor } from '../../constants/colors'; -import MissingNumberDraw from '../service/missingNumberDraw'; -import { applyMathPageMixin, CanvasDataState } from '../common/mathPageMixin'; +import MissingNumberDraw from '../shared/service/missingNumberDraw'; +import { + createMathPage, + CanvasDataState, +} from '../shared/common/mathPageMixin'; -Page( - applyMathPageMixin( - { - canvas: null as Canvas | null, - ctx: null as RenderingContext | null, - boxHeight: 0, - boxWidth: 0, - drawService: null as MissingNumberDraw | null, - missingNumberData: null as { - grids: Array<{ - numbers: (number | null)[]; - colors: (string | null)[]; - }>; - maxNumber: number; - } | null, +createMathPage({ + canvas: null as Canvas | null, + ctx: null as RenderingContext | null, + boxHeight: 0, + boxWidth: 0, + drawService: null as MissingNumberDraw | null, + missingNumberData: null as { + grids: Array<{ + numbers: (number | null)[]; + colors: (string | null)[]; + }>; + maxNumber: number; + } | null, - data: { - pageTitle: '填上缺少的数字', - subTitle: '在数字序列中找出并填写缺失的数字', - functionId: '', - hasContent: false, - showShareDialog: false, - showTypeSelector: true, - currentType: 10, - currentTypeName: '10以内', - typeActions: [ - { name: '10以内', value: 10 }, - { name: '20以内', value: 20 }, - { name: '40以内', value: 40 }, - { name: '50以内', value: 50 }, - { name: '80以内', value: 80 }, - { name: '100以内', value: 100 }, - { name: '120以内', value: 120 }, - ], - } as CanvasDataState & { - showTypeSelector: boolean; - currentType: number; - currentTypeName: string; - typeActions: Array<{ name: string; value: number }>; + data: { + pageTitle: '填上缺少的数字', + subTitle: '在数字序列中找出并填写缺失的数字', + functionId: '', + hasContent: false, + showShareDialog: false, + showTypeSelector: true, + currentMode: 10, + currentModeName: '10以内', + typeActions: [ + { name: '10以内', value: 10 }, + { name: '20以内', value: 20 }, + { name: '40以内', value: 40 }, + { name: '50以内', value: 50 }, + { name: '80以内', value: 80 }, + { name: '100以内', value: 100 }, + { name: '120以内', value: 120 }, + ], + } as CanvasDataState & { + showTypeSelector: boolean; + currentMode: number; + currentModeName: string; + typeActions: Array<{ name: string; value: number }>; + }, + + onLoad(options: { id?: string; mode?: number }) { + const functionId = options.id || 'missing-number'; + this.initPageInfo(functionId, '填上缺少的数字'); + + this.setData({ + currentMode: options.mode || 10, + currentModeName: options.mode ? `${options.mode}以内` : '10以内', + }); + }, + + onReady() { + this.initCanvas({ + createDrawService: ( + canvas: Canvas, + ctx: RenderingContext, + options?: Record, + ) => { + return new MissingNumberDraw(canvas, ctx, options); }, - - onLoad(options: { id?: string }) { - const functionId = options.id || 'missing-number'; - this.initPageInfo(functionId, '填上缺少的数字'); + drawServiceOptions: { + subTitle: this.data.subTitle, }, - - onReady() { - this.initCanvas({ - createDrawService: ( - canvas: Canvas, - ctx: RenderingContext, - options?: Record, - ) => { - 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(); - - while (hiddenIndices.size < hideCount) { - const randomIndex = Math.floor( - Math.random() * actualNumbersPerGrid, - ); - hiddenIndices.add(randomIndex); - } - - const gridNumbers: (number | null)[] = numbers.map( - (num, index) => (hiddenIndices.has(index) ? null : num), - ); - - // 为每个数字分配颜色(包括null位置) - const gridColors: (string | null)[] = gridNumbers.map( - (num) => (num ? this.getRandomNumberColor() : null), - ); - - grids.push({ numbers: gridNumbers, colors: gridColors }); - } - - this.missingNumberData = { - grids, - maxNumber, - }; - }, - - /** - * 获取随机数字颜色 - */ - getRandomNumberColor(): string { - return getRandomNumberColor(); - }, - - /** 选择类型 */ - onSelectType(event: any) { - const { name, value } = event.detail; - this.setData({ - currentType: value, - currentTypeName: name, - }); - // 重新生成数据 + onCanvasReady: () => { + // Canvas 初始化完成后,生成初始数据 this.onRandom(); }, - }, - { - pagePath: 'missingNumber/missingNumber', - }, - ), -); + }); + }, + + /** + * 绘制Canvas内容 + */ + async drawCanvas() { + if (!this.ctx || !this.drawService || !this.missingNumberData) { + return; + } + + try { + await this.drawService.draw( + this.missingNumberData, + String(this.data.currentMode), + ); + this.setData({ hasContent: true }); + } catch (error) { + console.error('绘制失败:', error); + this.setData({ hasContent: false }); + } + }, + + /** + * 随机生成 + */ + onRandom() { + const maxNumber = this.data.currentMode; + this.generateMissingNumberData(maxNumber); + this.drawCanvas(); + }, + + /** + * 生成缺失数字数据 + */ + generateMissingNumberData(maxNumber: number) { + const grids: Array<{ + numbers: (number | null)[]; + colors: (string | null)[]; + }> = []; + const gridMap = { + 10: { + gridCount: 3, + }, + 20: { + gridCount: 2, + }, + 40: { + gridCount: 2, + }, + 50: { + gridCount: 2, + }, + 80: { + gridCount: 1, + }, + 100: { + gridCount: 1, + }, + 120: { + gridCount: 1, + }, + }; + const { gridCount } = gridMap[maxNumber as keyof typeof gridMap]; + + // 生成多个网格 + for (let gridIndex = 0; gridIndex < gridCount; gridIndex++) { + let startNumber = 1; + let endNumber = maxNumber; + + const actualNumbersPerGrid = endNumber - startNumber + 1; + + const numbers: number[] = Array.from( + { length: actualNumbersPerGrid }, + (_, i) => startNumber + i, + ); + + // 随机隐藏一部分数字(隐藏40-60%) + const hideCount = Math.floor( + actualNumbersPerGrid * (0.4 + Math.random() * 0.2), + ); + const hiddenIndices = new Set(); + + 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(); + }, +}); diff --git a/miniprogram/mathPages/missingNumber/missingNumber.wxml b/miniprogram/mathPages/missingNumber/missingNumber.wxml index 15a8a9c..d3bc2ea 100644 --- a/miniprogram/mathPages/missingNumber/missingNumber.wxml +++ b/miniprogram/mathPages/missingNumber/missingNumber.wxml @@ -14,7 +14,7 @@ diff --git a/miniprogram/mathPages/numberDecompose/numberDecompose.json b/miniprogram/mathPages/numberDecompose/numberDecompose.json index bd1bf92..7eaffeb 100644 --- a/miniprogram/mathPages/numberDecompose/numberDecompose.json +++ b/miniprogram/mathPages/numberDecompose/numberDecompose.json @@ -6,7 +6,7 @@ "enablePullDownRefresh": false, "usingComponents": { "share-guide-popup": "../../components/share-guide-popup/share-guide-popup", - "math-bottom-buttons": "../components/math-bottom-buttons/math-bottom-buttons", - "math-type-selector": "../components/math-type-selector/math-type-selector" + "math-bottom-buttons": "../shared/components/math-bottom-buttons/math-bottom-buttons", + "math-type-selector": "../shared/components/math-type-selector/math-type-selector" } } diff --git a/miniprogram/mathPages/numberDecompose/numberDecompose.less b/miniprogram/mathPages/numberDecompose/numberDecompose.less index 12602d2..65d3fba 100644 --- a/miniprogram/mathPages/numberDecompose/numberDecompose.less +++ b/miniprogram/mathPages/numberDecompose/numberDecompose.less @@ -1 +1 @@ -@import '../common/mathPage.less'; \ No newline at end of file +@import '../shared/common/mathPage.less'; \ No newline at end of file diff --git a/miniprogram/mathPages/numberDecompose/numberDecompose.ts b/miniprogram/mathPages/numberDecompose/numberDecompose.ts index 2d9d8a5..a9d81e3 100644 --- a/miniprogram/mathPages/numberDecompose/numberDecompose.ts +++ b/miniprogram/mathPages/numberDecompose/numberDecompose.ts @@ -1,270 +1,300 @@ -import NumberDecomposeDraw from '../service/numberDecomposeDraw'; -import { applyMathPageMixin, CanvasDataState } from '../common/mathPageMixin'; +import NumberDecomposeDraw from '../shared/service/numberDecomposeDraw'; +import { + createMathPage, + CanvasDataState, +} from '../shared/common/mathPageMixin'; type DecomposeMode = 'with-image' | 'decompose' | 'compose'; -Page( - applyMathPageMixin( - { - canvas: null as Canvas | null, - ctx: null as RenderingContext | null, - boxHeight: 0, - boxWidth: 0, - drawService: null as NumberDecomposeDraw | null, - decomposeData: null as { - problems: Array<{ - whole: number | null; // 总数(null表示组合模式需要填写) - part1: number | null; // 第一个部分(null表示需要填写) - part2: number | null; // 第二个部分(null表示需要填写) - imageIndex?: number; // 图片索引(有图片模式需要) - imageType?: 'fruits' | 'twelve-animals'; // 图片类型 - }>; - mode: DecomposeMode; - } | null, +createMathPage({ + canvas: null as Canvas | null, + ctx: null as RenderingContext | null, + boxHeight: 0, + boxWidth: 0, + drawService: null as NumberDecomposeDraw | null, + decomposeData: null as { + problems: Array<{ + whole: number | null; // 总数(null表示组合模式需要填写) + part1: number | null; // 第一个部分(null表示需要填写) + part2: number | null; // 第二个部分(null表示需要填写) + imageIndex?: number; // 图片索引(有图片模式需要) + imageType?: 'fruits' | 'twelve-animals'; // 图片类型 + }>; + mode: DecomposeMode; + } | null, - data: { - pageTitle: '10以内数的分与合', - subTitle: '学习数的分解与组合', - functionId: '', - hasContent: false, - showShareDialog: false, - showTypeSelector: true, - currentType: 'with-image', - currentTypeName: '有图片模式', - typeActions: [ - { name: '有图片模式', value: 'with-image' }, - { name: '分模式', value: 'decompose' }, - { name: '组合模式', value: 'compose' }, - ], - } as CanvasDataState & { - showTypeSelector: boolean; - currentType: DecomposeMode; - currentTypeName: string; - typeActions: Array<{ name: string; value: DecomposeMode }>; + data: { + pageTitle: '10以内数的分与合', + subTitle: '学习数的分解与组合', + functionId: '', + hasContent: false, + showShareDialog: false, + showTypeSelector: true, + currentMode: 'with-image', + currentModeName: '有图片模式', + typeActions: [ + { name: '有图片模式', value: 'with-image' }, + { name: '分模式', value: 'decompose' }, + { name: '组合模式', value: 'compose' }, + ], + maxNumber: 10, // 最大数字:10 或 20 + } as CanvasDataState & { + showTypeSelector: boolean; + currentMode: DecomposeMode; + currentModeName: string; + typeActions: Array<{ name: string; value: DecomposeMode }>; + maxNumber: number; + }, + + onLoad(options: { id?: string; mode?: DecomposeMode }) { + const functionId = options.id || 'number-decompose'; + const is20Within = functionId === 'number-decompose-20'; + const maxNumber = is20Within ? 20 : 10; + + // 20以内只有两种模式,10以内有三种模式 + let defaultMode: DecomposeMode; + let typeActions: Array<{ name: string; value: DecomposeMode }>; + let defaultTypeName: string; + + if (is20Within) { + // 20以内:只有分模式和组合模式 + defaultMode = options.mode || 'decompose'; + typeActions = [ + { name: '20以内的分解', value: 'decompose' }, + { name: '20以内的组合', value: 'compose' }, + ]; + defaultTypeName = + defaultMode === 'decompose' ? '20以内的分解' : '20以内的组合'; + } else { + // 10以内:有图片模式、分模式、组合模式 + defaultMode = options.mode || 'with-image'; + typeActions = [ + { name: '有图片模式', value: 'with-image' }, + { name: '分模式', value: 'decompose' }, + { name: '组合模式', value: 'compose' }, + ]; + defaultTypeName = + defaultMode === 'with-image' + ? '有图片模式' + : defaultMode === 'decompose' + ? '分模式' + : '组合模式'; + } + + this.setData({ + currentMode: defaultMode, + currentModeName: defaultTypeName, + typeActions, + maxNumber, + }); + this.initPageInfo( + functionId, + is20Within ? '20以内数的分与合' : '10以内数的分与合', + ); + }, + + onReady() { + this.initCanvas({ + createDrawService: ( + canvas: Canvas, + ctx: RenderingContext, + options?: Record, + ) => { + return new NumberDecomposeDraw(canvas, ctx, options); }, - - onLoad(options: { id?: string; mode?: DecomposeMode }) { - const functionId = options.id || 'number-decompose'; - const mode = options.mode || 'with-image'; - const currentTypeName = - mode === 'with-image' - ? '有图片模式' - : mode === 'decompose' - ? '分模式' - : '组合模式'; - this.setData({ - currentType: mode, - currentTypeName, - }); - this.initPageInfo(functionId, '10以内数的分与合'); + drawServiceOptions: { + subTitle: this.data.subTitle, }, - - onReady() { - this.initCanvas({ - createDrawService: ( - canvas: Canvas, - ctx: RenderingContext, - options?: Record, - ) => { - 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(); - - if (mode === 'with-image') { - // 有图片模式:9个题目,一行三列,总共三行 - const problemCount = 9; - let attempts = 0; - const maxAttempts = problemCount * 50; // 最大尝试次数 - - while ( - problems.length < problemCount && - attempts < maxAttempts - ) { - attempts++; - - // 生成总数(2-10) - const whole = Math.floor(Math.random() * 9) + 2; - // 随机选择一个部分(1 到 whole-1) - const part1 = - Math.floor(Math.random() * (whole - 1)) + 1; - const part2 = whole - part1; - - // 随机决定显示 part1 还是 part2(另一个为 null) - const showPart1 = Math.random() < 0.5; - - // 生成唯一标识(统一格式:part1 <= part2) - const minPart = Math.min(part1, part2); - const maxPart = Math.max(part1, part2); - const key = `${whole}:${minPart}:${maxPart}:${showPart1}`; - - // 检查是否已存在 - if (usedKeys.has(key)) { - continue; - } - - usedKeys.add(key); - - // 随机选择图片类型和索引 - const imageType: 'fruits' | 'twelve-animals' = - Math.random() < 0.5 ? 'fruits' : 'twelve-animals'; - const maxImageIndex = imageType === 'fruits' ? 22 : 12; - const imageIndex = - Math.floor(Math.random() * maxImageIndex) + 1; - - problems.push({ - whole, - part1: showPart1 ? part1 : null, - part2: showPart1 ? null : part2, - imageIndex, - imageType, - }); - } - } else if (mode === 'decompose') { - // 分模式:15个题目,一行3个,总共5行 - const problemCount = 15; - let attempts = 0; - const maxAttempts = problemCount * 50; // 最大尝试次数 - - while ( - problems.length < problemCount && - attempts < maxAttempts - ) { - attempts++; - - // 生成总数(2-10) - const whole = Math.floor(Math.random() * 9) + 2; - // 随机选择一个部分(1 到 whole-1) - const part1 = - Math.floor(Math.random() * (whole - 1)) + 1; - const part2 = whole - part1; - - // 随机决定显示 part1 还是 part2(另一个为 null) - const showPart1 = Math.random() < 0.5; - - // 生成唯一标识(统一格式:part1 <= part2) - const minPart = Math.min(part1, part2); - const maxPart = Math.max(part1, part2); - const key = `${whole}:${minPart}:${maxPart}:${showPart1}`; - - // 检查是否已存在 - if (usedKeys.has(key)) { - continue; - } - - usedKeys.add(key); - - problems.push({ - whole, - part1: showPart1 ? part1 : null, - part2: showPart1 ? null : part2, - }); - } - } else if (mode === 'compose') { - // 组合模式:15个题目,一行3个,总共5行 - const problemCount = 15; - let attempts = 0; - const maxAttempts = problemCount * 50; // 最大尝试次数 - - while ( - problems.length < problemCount && - attempts < maxAttempts - ) { - attempts++; - - // 生成总数(2-10) - const whole = Math.floor(Math.random() * 9) + 2; - // 随机选择一个部分(1 到 whole-1) - const part1 = - Math.floor(Math.random() * (whole - 1)) + 1; - const part2 = whole - part1; - - // 生成唯一标识(统一格式:part1 <= part2) - const minPart = Math.min(part1, part2); - const maxPart = Math.max(part1, part2); - const key = `${minPart}:${maxPart}`; - - // 检查是否已存在 - if (usedKeys.has(key)) { - continue; - } - - usedKeys.add(key); - - // 组合模式:两个部分都显示,根节点为 null - problems.push({ - whole: null, // 根节点需要填写 - part1, - part2, - }); - } - } - - this.decomposeData = { problems, mode }; - }, - - /** 选择类型 */ - onSelectType(event: any) { - const { name, value } = event.detail; - this.setData({ - currentType: value, - currentTypeName: name, - }); - // 重新生成数据 + onCanvasReady: () => { + // Canvas 初始化完成后,生成初始数据 this.onRandom(); }, - }, - { - pagePath: 'numberDecompose/numberDecompose', - }, - ), -); + }); + }, + + /** + * 绘制Canvas内容 + */ + async drawCanvas() { + if (!this.ctx || !this.drawService || !this.decomposeData) { + return; + } + + try { + await this.drawService.draw(this.decomposeData); + this.setData({ hasContent: true }); + } catch (error) { + console.error('绘制失败:', error); + this.setData({ hasContent: false }); + } + }, + + /** + * 随机生成 + */ + onRandom() { + this.generateDecomposeData(); + this.drawCanvas(); + }, + + /** + * 生成分解数据(确保不重复) + */ + generateDecomposeData() { + const mode = this.data.currentMode; + const maxNumber = this.data.maxNumber; + const is20Within = maxNumber === 20; + + const problems: Array<{ + whole: number | null; + part1: number | null; + part2: number | null; + imageIndex?: number; + imageType?: 'fruits' | 'twelve-animals'; + }> = []; + + // 用于去重的 Set,存储题目唯一标识 + // 格式:对于分解模式 "whole:part1:part2:showPart1",对于组合模式 "part1:part2" + const usedKeys = new Set(); + + // 确定根节点的数字范围 + 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(); + }, +}); diff --git a/miniprogram/mathPages/numberDecompose/numberDecompose.wxml b/miniprogram/mathPages/numberDecompose/numberDecompose.wxml index 15a8a9c..d3bc2ea 100644 --- a/miniprogram/mathPages/numberDecompose/numberDecompose.wxml +++ b/miniprogram/mathPages/numberDecompose/numberDecompose.wxml @@ -14,7 +14,7 @@ diff --git a/miniprogram/mathPages/numberFind/numberFind.json b/miniprogram/mathPages/numberFind/numberFind.json index 0110410..716541c 100644 --- a/miniprogram/mathPages/numberFind/numberFind.json +++ b/miniprogram/mathPages/numberFind/numberFind.json @@ -7,6 +7,6 @@ "usingComponents": { "toy-button": "../../ui/button/button", "share-guide-popup": "../../components/share-guide-popup/share-guide-popup", - "math-bottom-buttons": "../components/math-bottom-buttons/math-bottom-buttons" + "math-bottom-buttons": "../shared/components/math-bottom-buttons/math-bottom-buttons" } } diff --git a/miniprogram/mathPages/numberFind/numberFind.less b/miniprogram/mathPages/numberFind/numberFind.less index 055c89d..2d3aac0 100644 --- a/miniprogram/mathPages/numberFind/numberFind.less +++ b/miniprogram/mathPages/numberFind/numberFind.less @@ -1,4 +1,4 @@ -@import '../common/mathPage.less'; +@import '../shared/common/mathPage.less'; /* 数字选择区域 */ .number-selection-area { diff --git a/miniprogram/mathPages/numberFind/numberFind.ts b/miniprogram/mathPages/numberFind/numberFind.ts index 8c7e92f..d5806d8 100644 --- a/miniprogram/mathPages/numberFind/numberFind.ts +++ b/miniprogram/mathPages/numberFind/numberFind.ts @@ -1,5 +1,8 @@ -import NumberFindDraw from '../service/numberFindDraw'; -import { createMathPage, CanvasDataState } from '../common/mathPageMixin'; +import NumberFindDraw from '../shared/service/numberFindDraw'; +import { + createMathPage, + CanvasDataState, +} from '../shared/common/mathPageMixin'; createMathPage({ canvas: null as Canvas | null, diff --git a/miniprogram/mathPages/common/README.md b/miniprogram/mathPages/shared/common/README.md similarity index 100% rename from miniprogram/mathPages/common/README.md rename to miniprogram/mathPages/shared/common/README.md diff --git a/miniprogram/mathPages/common/mathPage.less b/miniprogram/mathPages/shared/common/mathPage.less similarity index 100% rename from miniprogram/mathPages/common/mathPage.less rename to miniprogram/mathPages/shared/common/mathPage.less diff --git a/miniprogram/mathPages/shared/common/mathPageMixin.ts b/miniprogram/mathPages/shared/common/mathPageMixin.ts new file mode 100644 index 0000000..61634c3 --- /dev/null +++ b/miniprogram/mathPages/shared/common/mathPageMixin.ts @@ -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 }; diff --git a/miniprogram/mathPages/components/math-bottom-buttons/math-bottom-buttons.json b/miniprogram/mathPages/shared/components/math-bottom-buttons/math-bottom-buttons.json similarity index 52% rename from miniprogram/mathPages/components/math-bottom-buttons/math-bottom-buttons.json rename to miniprogram/mathPages/shared/components/math-bottom-buttons/math-bottom-buttons.json index 806eb6d..d00f5a4 100644 --- a/miniprogram/mathPages/components/math-bottom-buttons/math-bottom-buttons.json +++ b/miniprogram/mathPages/shared/components/math-bottom-buttons/math-bottom-buttons.json @@ -1,6 +1,6 @@ { "component": true, "usingComponents": { - "toy-button": "../../../ui/button/button" + "toy-button": "../../../../ui/button/button" } } diff --git a/miniprogram/mathPages/components/math-bottom-buttons/math-bottom-buttons.less b/miniprogram/mathPages/shared/components/math-bottom-buttons/math-bottom-buttons.less similarity index 100% rename from miniprogram/mathPages/components/math-bottom-buttons/math-bottom-buttons.less rename to miniprogram/mathPages/shared/components/math-bottom-buttons/math-bottom-buttons.less diff --git a/miniprogram/mathPages/components/math-bottom-buttons/math-bottom-buttons.ts b/miniprogram/mathPages/shared/components/math-bottom-buttons/math-bottom-buttons.ts similarity index 100% rename from miniprogram/mathPages/components/math-bottom-buttons/math-bottom-buttons.ts rename to miniprogram/mathPages/shared/components/math-bottom-buttons/math-bottom-buttons.ts diff --git a/miniprogram/mathPages/components/math-bottom-buttons/math-bottom-buttons.wxml b/miniprogram/mathPages/shared/components/math-bottom-buttons/math-bottom-buttons.wxml similarity index 100% rename from miniprogram/mathPages/components/math-bottom-buttons/math-bottom-buttons.wxml rename to miniprogram/mathPages/shared/components/math-bottom-buttons/math-bottom-buttons.wxml diff --git a/miniprogram/mathPages/shared/components/math-type-selector/math-type-selector.json b/miniprogram/mathPages/shared/components/math-type-selector/math-type-selector.json new file mode 100644 index 0000000..25326c0 --- /dev/null +++ b/miniprogram/mathPages/shared/components/math-type-selector/math-type-selector.json @@ -0,0 +1,7 @@ +{ + "component": true, + "usingComponents": { + "toy-button": "../../../../ui/button/button", + "van-action-sheet": "../../../../miniprogram_npm/@vant/weapp/action-sheet/index" + } +} diff --git a/miniprogram/mathPages/components/math-type-selector/math-type-selector.less b/miniprogram/mathPages/shared/components/math-type-selector/math-type-selector.less similarity index 100% rename from miniprogram/mathPages/components/math-type-selector/math-type-selector.less rename to miniprogram/mathPages/shared/components/math-type-selector/math-type-selector.less diff --git a/miniprogram/mathPages/components/math-type-selector/math-type-selector.ts b/miniprogram/mathPages/shared/components/math-type-selector/math-type-selector.ts similarity index 100% rename from miniprogram/mathPages/components/math-type-selector/math-type-selector.ts rename to miniprogram/mathPages/shared/components/math-type-selector/math-type-selector.ts diff --git a/miniprogram/mathPages/components/math-type-selector/math-type-selector.wxml b/miniprogram/mathPages/shared/components/math-type-selector/math-type-selector.wxml similarity index 100% rename from miniprogram/mathPages/components/math-type-selector/math-type-selector.wxml rename to miniprogram/mathPages/shared/components/math-type-selector/math-type-selector.wxml diff --git a/miniprogram/mathPages/service/additionContentDraw.ts b/miniprogram/mathPages/shared/service/additionContentDraw.ts similarity index 99% rename from miniprogram/mathPages/service/additionContentDraw.ts rename to miniprogram/mathPages/shared/service/additionContentDraw.ts index a8464ef..1a298d7 100644 --- a/miniprogram/mathPages/service/additionContentDraw.ts +++ b/miniprogram/mathPages/shared/service/additionContentDraw.ts @@ -1,4 +1,4 @@ -import { getImage } from '../../utils/index'; +import { getImage } from '../../../utils/index'; interface DrawAdditionContentParams { canvas: WechatMiniprogram.Canvas; diff --git a/miniprogram/mathPages/service/additionDraw.ts b/miniprogram/mathPages/shared/service/additionDraw.ts similarity index 94% rename from miniprogram/mathPages/service/additionDraw.ts rename to miniprogram/mathPages/shared/service/additionDraw.ts index a362274..41feefd 100644 --- a/miniprogram/mathPages/service/additionDraw.ts +++ b/miniprogram/mathPages/shared/service/additionDraw.ts @@ -1,11 +1,11 @@ -import { BaseMathDrawService } from './baseMathDraw'; +import { BaseDrawService } from '../../../service/baseDraw'; import { drawAdditionContent } from './additionContentDraw'; /** * 加减法计算绘制服务 * 组合使用基础绘制服务和内容区域绘制服务 */ -class AdditionDraw extends BaseMathDrawService { +class AdditionDraw extends BaseDrawService { calculationData: { problems: Array<{ type: 'addition' | 'subtraction'; diff --git a/miniprogram/mathPages/service/compareContentDraw.ts b/miniprogram/mathPages/shared/service/compareContentDraw.ts similarity index 99% rename from miniprogram/mathPages/service/compareContentDraw.ts rename to miniprogram/mathPages/shared/service/compareContentDraw.ts index e2b6ef7..80883b4 100644 --- a/miniprogram/mathPages/service/compareContentDraw.ts +++ b/miniprogram/mathPages/shared/service/compareContentDraw.ts @@ -1,4 +1,4 @@ -import { getImage } from '../../utils/index'; +import { getImage } from '../../../utils/index'; interface DrawCompareContentParams { canvas: WechatMiniprogram.Canvas; diff --git a/miniprogram/mathPages/service/compareDraw.ts b/miniprogram/mathPages/shared/service/compareDraw.ts similarity index 93% rename from miniprogram/mathPages/service/compareDraw.ts rename to miniprogram/mathPages/shared/service/compareDraw.ts index db2e5ef..f1a1869 100644 --- a/miniprogram/mathPages/service/compareDraw.ts +++ b/miniprogram/mathPages/shared/service/compareDraw.ts @@ -1,11 +1,11 @@ -import { BaseMathDrawService } from './baseMathDraw'; +import { BaseDrawService } from '../../../service/baseDraw'; import { drawCompareContent } from './compareContentDraw'; /** * 数一数比大小绘制服务 * 组合使用基础绘制服务和内容区域绘制服务 */ -class CompareDraw extends BaseMathDrawService { +class CompareDraw extends BaseDrawService { compareData: { problems: Array<{ leftCount: number; diff --git a/miniprogram/mathPages/service/countMatchContentDraw.ts b/miniprogram/mathPages/shared/service/countMatchContentDraw.ts similarity index 98% rename from miniprogram/mathPages/service/countMatchContentDraw.ts rename to miniprogram/mathPages/shared/service/countMatchContentDraw.ts index 2c30625..10546c5 100644 --- a/miniprogram/mathPages/service/countMatchContentDraw.ts +++ b/miniprogram/mathPages/shared/service/countMatchContentDraw.ts @@ -1,5 +1,5 @@ -import { getImage } from '../../utils/index'; -import { getRandomNumberColor } from '../../constants/colors'; +import { getImage } from '../../../utils/index'; +import { getRandomNumberColor } from '../../../constants/colors'; interface DrawCountMatchContentParams { canvas: WechatMiniprogram.Canvas; diff --git a/miniprogram/mathPages/service/countMatchDraw.ts b/miniprogram/mathPages/shared/service/countMatchDraw.ts similarity index 92% rename from miniprogram/mathPages/service/countMatchDraw.ts rename to miniprogram/mathPages/shared/service/countMatchDraw.ts index 6089c46..eee2f7e 100644 --- a/miniprogram/mathPages/service/countMatchDraw.ts +++ b/miniprogram/mathPages/shared/service/countMatchDraw.ts @@ -1,11 +1,11 @@ -import { BaseMathDrawService } from './baseMathDraw'; +import { BaseDrawService } from '../../../service/baseDraw'; import { drawCountMatchContent } from './countMatchContentDraw'; /** * 数一数连一连绘制服务 * 组合使用基础绘制服务和内容区域绘制服务 */ -class CountMatchDraw extends BaseMathDrawService { +class CountMatchDraw extends BaseDrawService { matchData: { leftNumbers: number[]; rightNumbers: number[]; // 打乱顺序后的数字数组 diff --git a/miniprogram/mathPages/service/countingSelectContentDraw.ts b/miniprogram/mathPages/shared/service/countingSelectContentDraw.ts similarity index 98% rename from miniprogram/mathPages/service/countingSelectContentDraw.ts rename to miniprogram/mathPages/shared/service/countingSelectContentDraw.ts index 1654e31..448050a 100644 --- a/miniprogram/mathPages/service/countingSelectContentDraw.ts +++ b/miniprogram/mathPages/shared/service/countingSelectContentDraw.ts @@ -1,5 +1,5 @@ -import { getImage } from '../../utils/index'; -import { getRandomNumberColor } from '../../constants/colors'; +import { getImage } from '../../../utils/index'; +import { getRandomNumberColor } from '../../../constants/colors'; interface DrawCountingSelectContentParams { canvas: WechatMiniprogram.Canvas; diff --git a/miniprogram/mathPages/service/countingSelectDraw.ts b/miniprogram/mathPages/shared/service/countingSelectDraw.ts similarity index 94% rename from miniprogram/mathPages/service/countingSelectDraw.ts rename to miniprogram/mathPages/shared/service/countingSelectDraw.ts index f1ce25f..d6844aa 100644 --- a/miniprogram/mathPages/service/countingSelectDraw.ts +++ b/miniprogram/mathPages/shared/service/countingSelectDraw.ts @@ -1,4 +1,4 @@ -import { BaseMathDrawService } from './baseMathDraw'; +import { BaseDrawService } from '../../../service/baseDraw'; import { drawCountingSelectContent } from './countingSelectContentDraw'; /** @@ -6,7 +6,7 @@ import { drawCountingSelectContent } from './countingSelectContentDraw'; * 组合使用基础绘制服务和内容区域绘制服务 * 支持两种模式:'select'(选一选)和 'fill'(填一填) */ -class CountingSelectDraw extends BaseMathDrawService { +class CountingSelectDraw extends BaseDrawService { countingData: { problems: Array<{ count: number; diff --git a/miniprogram/mathPages/service/missingNumberContentDraw.ts b/miniprogram/mathPages/shared/service/missingNumberContentDraw.ts similarity index 100% rename from miniprogram/mathPages/service/missingNumberContentDraw.ts rename to miniprogram/mathPages/shared/service/missingNumberContentDraw.ts diff --git a/miniprogram/mathPages/service/missingNumberDraw.ts b/miniprogram/mathPages/shared/service/missingNumberDraw.ts similarity index 93% rename from miniprogram/mathPages/service/missingNumberDraw.ts rename to miniprogram/mathPages/shared/service/missingNumberDraw.ts index c32ad02..bfb8051 100644 --- a/miniprogram/mathPages/service/missingNumberDraw.ts +++ b/miniprogram/mathPages/shared/service/missingNumberDraw.ts @@ -1,11 +1,11 @@ -import { BaseMathDrawService } from './baseMathDraw'; +import { BaseDrawService } from '../../../service/baseDraw'; import { drawMissingNumberContent } from './missingNumberContentDraw'; /** * 填上缺少的数字绘制服务 * 组合使用基础绘制服务和内容区域绘制服务 */ -class MissingNumberDraw extends BaseMathDrawService { +class MissingNumberDraw extends BaseDrawService { missingNumberData: { grids: Array<{ numbers: (number | null)[]; diff --git a/miniprogram/mathPages/service/numberColorContentDraw.ts b/miniprogram/mathPages/shared/service/numberColorContentDraw.ts similarity index 99% rename from miniprogram/mathPages/service/numberColorContentDraw.ts rename to miniprogram/mathPages/shared/service/numberColorContentDraw.ts index 3e0a1ec..4718c46 100644 --- a/miniprogram/mathPages/service/numberColorContentDraw.ts +++ b/miniprogram/mathPages/shared/service/numberColorContentDraw.ts @@ -1,4 +1,4 @@ -import { getNumberColors } from '../../constants/colors'; +import { getNumberColors } from '../../../constants/colors'; interface DrawNumberColorContentParams { canvas: WechatMiniprogram.Canvas; diff --git a/miniprogram/mathPages/service/numberColorDraw.ts b/miniprogram/mathPages/shared/service/numberColorDraw.ts similarity index 92% rename from miniprogram/mathPages/service/numberColorDraw.ts rename to miniprogram/mathPages/shared/service/numberColorDraw.ts index b8af2cc..01d0021 100644 --- a/miniprogram/mathPages/service/numberColorDraw.ts +++ b/miniprogram/mathPages/shared/service/numberColorDraw.ts @@ -1,11 +1,11 @@ -import { BaseMathDrawService } from './baseMathDraw'; +import { BaseDrawService } from '../../../service/baseDraw'; import { drawNumberColorContent } from './numberColorContentDraw'; /** * 按数字涂颜色绘制服务 * 组合使用基础绘制服务和内容区域绘制服务 */ -class NumberColorDraw extends BaseMathDrawService { +class NumberColorDraw extends BaseDrawService { colorData: { numbers: number[]; } | null; diff --git a/miniprogram/mathPages/service/numberContentDraw.ts b/miniprogram/mathPages/shared/service/numberContentDraw.ts similarity index 100% rename from miniprogram/mathPages/service/numberContentDraw.ts rename to miniprogram/mathPages/shared/service/numberContentDraw.ts diff --git a/miniprogram/mathPages/service/numberDecomposeContentDraw.ts b/miniprogram/mathPages/shared/service/numberDecomposeContentDraw.ts similarity index 99% rename from miniprogram/mathPages/service/numberDecomposeContentDraw.ts rename to miniprogram/mathPages/shared/service/numberDecomposeContentDraw.ts index 8e407dd..157fac1 100644 --- a/miniprogram/mathPages/service/numberDecomposeContentDraw.ts +++ b/miniprogram/mathPages/shared/service/numberDecomposeContentDraw.ts @@ -1,5 +1,5 @@ -import { getImage } from '../../utils/index'; -import { getRandomNumberColor } from '../../constants/colors'; +import { getImage } from '../../../utils/index'; +import { getRandomNumberColor } from '../../../constants/colors'; interface DrawNumberDecomposeContentParams { canvas: WechatMiniprogram.Canvas; diff --git a/miniprogram/mathPages/service/numberDecomposeDraw.ts b/miniprogram/mathPages/shared/service/numberDecomposeDraw.ts similarity index 93% rename from miniprogram/mathPages/service/numberDecomposeDraw.ts rename to miniprogram/mathPages/shared/service/numberDecomposeDraw.ts index 9afe799..6fc86dc 100644 --- a/miniprogram/mathPages/service/numberDecomposeDraw.ts +++ b/miniprogram/mathPages/shared/service/numberDecomposeDraw.ts @@ -1,11 +1,11 @@ -import { BaseMathDrawService } from './baseMathDraw'; +import { BaseDrawService } from '../../../service/baseDraw'; import { drawNumberDecomposeContent } from './numberDecomposeContentDraw'; /** * 10以内数的分与合绘制服务 * 组合使用基础绘制服务和内容区域绘制服务 */ -class NumberDecomposeDraw extends BaseMathDrawService { +class NumberDecomposeDraw extends BaseDrawService { decomposeData: { problems: Array<{ whole: number | null; diff --git a/miniprogram/mathPages/service/numberFindDraw.ts b/miniprogram/mathPages/shared/service/numberFindDraw.ts similarity index 95% rename from miniprogram/mathPages/service/numberFindDraw.ts rename to miniprogram/mathPages/shared/service/numberFindDraw.ts index 0d9cd0c..cffb7b0 100644 --- a/miniprogram/mathPages/service/numberFindDraw.ts +++ b/miniprogram/mathPages/shared/service/numberFindDraw.ts @@ -1,4 +1,4 @@ -import { BaseMathDrawService } from './baseMathDraw'; +import { BaseDrawService } from '../../../service/baseDraw'; import { drawNumberPreview } from './numberPreviewDraw'; import { drawNumberContent } from './numberContentDraw'; import { drawNumberWriteContent } from './numberWriteDraw'; @@ -7,7 +7,7 @@ import { drawNumberWriteContent } from './numberWriteDraw'; * 数字涂色绘制服务 * 组合使用基础绘制服务、预览区域绘制服务和内容区域绘制服务 */ -class NumberFindDraw extends BaseMathDrawService { +class NumberFindDraw extends BaseDrawService { selectedNumber: number; functionId: string; // 功能ID,用于判断绘制类型 diff --git a/miniprogram/mathPages/service/numberPreviewDraw.ts b/miniprogram/mathPages/shared/service/numberPreviewDraw.ts similarity index 98% rename from miniprogram/mathPages/service/numberPreviewDraw.ts rename to miniprogram/mathPages/shared/service/numberPreviewDraw.ts index db5c4c9..cd45348 100644 --- a/miniprogram/mathPages/service/numberPreviewDraw.ts +++ b/miniprogram/mathPages/shared/service/numberPreviewDraw.ts @@ -1,4 +1,4 @@ -import { getImage } from '../../utils/index'; +import { getImage } from '../../../utils/index'; /** * 数字到英文单词的映射 diff --git a/miniprogram/mathPages/service/numberWriteDraw.ts b/miniprogram/mathPages/shared/service/numberWriteDraw.ts similarity index 100% rename from miniprogram/mathPages/service/numberWriteDraw.ts rename to miniprogram/mathPages/shared/service/numberWriteDraw.ts diff --git a/miniprogram/pages/copyBook/copyBook.ts b/miniprogram/pages/copyBook/copyBook.ts index 4996115..1b8d15e 100644 --- a/miniprogram/pages/copyBook/copyBook.ts +++ b/miniprogram/pages/copyBook/copyBook.ts @@ -5,6 +5,7 @@ import { PAPER_SIZE } from '../../constants/colors'; import { getWordsSvgData } from '../../utils/getWordsSvgJson'; import { WORDS } from '../../constants/words'; import { CharacterItem } from '../../types/characterType'; +import tracker from '../../utils/tracker'; Page({ canvas: null as Canvas | null, @@ -360,6 +361,9 @@ Page({ return; } + // 上报下载埋点 + tracker.reportDownload('练字贴'); + // 直接下载 checkAndSaveImage(this.canvas); }, @@ -371,6 +375,9 @@ Page({ // 分享功能 onShareAppMessage() { + // 上报分享埋点 + tracker.reportShare('练字贴'); + return { title: '涂鸦丫-涂色|练字|识字|打印', path: '/pages/copyBook/copyBook', @@ -379,6 +386,9 @@ Page({ }; }, onShareTimeline() { + // 上报分享埋点 + tracker.reportShare('练字贴'); + return { title: '涂鸦丫-涂色|练字|识字|打印', query: '/pages/copyBook/copyBook', diff --git a/miniprogram/pages/debug/debug.ts b/miniprogram/pages/debug/debug.ts index 2df77b3..2259583 100644 --- a/miniprogram/pages/debug/debug.ts +++ b/miniprogram/pages/debug/debug.ts @@ -1,11 +1,9 @@ -import config from '../../config/config'; - -const defaultPrintConfig: PrintConfig = config.printHeader as PrintConfig; +import { defaultPrintConfig } from '../../config/config'; Page({ data: { printConfig: defaultPrintConfig, - enableDebug: false + enableDebug: false, }, onLoad() { @@ -41,5 +39,5 @@ Page({ toggleDebug() { const enableDebug = !this.data.enableDebug; this.setData({ enableDebug }); - } -}); \ No newline at end of file + }, +}); diff --git a/miniprogram/pages/demoIndex/index.less b/miniprogram/pages/demoIndex/index.less deleted file mode 100644 index 8592e6b..0000000 --- a/miniprogram/pages/demoIndex/index.less +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/miniprogram/pages/demoIndex/index.ts b/miniprogram/pages/demoIndex/index.ts deleted file mode 100644 index 67c642b..0000000 --- a/miniprogram/pages/demoIndex/index.ts +++ /dev/null @@ -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 }); - }, -}); diff --git a/miniprogram/pages/demoIndex/index.wxml b/miniprogram/pages/demoIndex/index.wxml deleted file mode 100644 index 90c4ebd..0000000 --- a/miniprogram/pages/demoIndex/index.wxml +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - - - - - - - - - 已选择的汉字 ({{wordList.length}}) - - - - - {{word}} - × - - - - - - - - 可选汉字 ({{availableWords.length}}) - - - - {{word}} - - - - - - - - 功能模块 - - - - {{card.title}} - - - - diff --git a/miniprogram/pages/index/index.ts b/miniprogram/pages/index/index.ts index 1ec36aa..c2de993 100644 --- a/miniprogram/pages/index/index.ts +++ b/miniprogram/pages/index/index.ts @@ -7,6 +7,7 @@ import { } from '../../service/drawServiceFactory'; import { checkAndSaveImage } from '../../utils/saveImage'; import { shouldShowShareGuide } from '../../utils/shareGuide'; +import tracker from '../../utils/tracker'; Page({ canvas: null as Canvas | null, @@ -29,7 +30,12 @@ Page({ showShareDialog: false, // 显示分享引导弹窗 }, - onLoad() { + onLoad(options: { template?: string }) { + const { template } = options; + this.setData({ + selectedTemplate: template || 'grid', + }); + const hasShowIntroduction = wx.getStorageSync('hasShowIntroduction') || false; @@ -292,6 +298,9 @@ Page({ return; } + // 上报下载埋点 + tracker.reportDownload('涂色识字', this.data.selectedTemplate); + // 直接下载 checkAndSaveImage(this.canvas); }, @@ -344,17 +353,23 @@ Page({ }, onShareAppMessage() { + // 上报分享埋点 + tracker.reportShare('涂色识字'); + return { title: '涂鸦丫-涂色|识字|画画|打印', - path: '/pages/index/index', + path: `/pages/index/index?template=${this.data.selectedTemplate}`, imageUrl: 'https://cdn.joeyone.cn/doodle/share-img/index-share-v2.png', }; }, onShareTimeline() { + // 上报分享埋点 + tracker.reportShare('涂色识字'); + return { title: '涂鸦丫-涂色|识字|画画|打印', - query: '/pages/index/index', + query: `/pages/index/index?template=${this.data.selectedTemplate}`, imageUrl: 'https://cdn.joeyone.cn/doodle/share-img/index-share-v2.png', }; diff --git a/miniprogram/pages/shape/index.ts b/miniprogram/pages/shape/index.ts index 2d71faa..cbf2107 100644 --- a/miniprogram/pages/shape/index.ts +++ b/miniprogram/pages/shape/index.ts @@ -3,6 +3,7 @@ import { WATER_COLORS, PAPER_SIZE } from '../../constants/colors'; import ShapeDrawService from '../../service/shapeDrawService'; import { checkAndSaveImage } from '../../utils/saveImage'; import { shouldShowShareGuide } from '../../utils/shareGuide'; +import tracker from '../../utils/tracker'; Page({ canvas: null as Canvas | null, @@ -296,6 +297,9 @@ Page({ return; } + // 上报下载埋点 + tracker.reportDownload('图形涂色'); + // 直接下载 checkAndSaveImage(this.canvas); }, @@ -306,6 +310,9 @@ Page({ }, onShareAppMessage() { + // 上报分享埋点 + tracker.reportShare('图形涂色'); + return { title: '涂鸦丫-涂色|识字|图形|打印', path: '/pages/shape/index', @@ -314,6 +321,9 @@ Page({ }; }, onShareTimeline() { + // 上报分享埋点 + tracker.reportShare('图形涂色'); + return { title: '涂鸦丫-涂色|识字|图形|打印', query: '/pages/shape/index', diff --git a/miniprogram/pages/wordDemo/README.md b/miniprogram/pages/wordDemo/README.md deleted file mode 100644 index 412193d..0000000 --- a/miniprogram/pages/wordDemo/README.md +++ /dev/null @@ -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`: 页面配置文件 diff --git a/miniprogram/pages/wordDemo/index.json b/miniprogram/pages/wordDemo/index.json deleted file mode 100644 index a206a84..0000000 --- a/miniprogram/pages/wordDemo/index.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "navigationBarTitleText": "汉字选择", - "navigationBarBackgroundColor": "#d2e7d8", - "backgroundColor": "#f5f5f5", - "enablePullDownRefresh": false -} diff --git a/miniprogram/pages/wordDemo/index.less b/miniprogram/pages/wordDemo/index.less deleted file mode 100644 index 1e30e29..0000000 --- a/miniprogram/pages/wordDemo/index.less +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/miniprogram/pages/wordDemo/index.ts b/miniprogram/pages/wordDemo/index.ts deleted file mode 100644 index c6f53b7..0000000 --- a/miniprogram/pages/wordDemo/index.ts +++ /dev/null @@ -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 }); - }, -}); diff --git a/miniprogram/pages/wordDemo/index.wxml b/miniprogram/pages/wordDemo/index.wxml deleted file mode 100644 index 2bf6a65..0000000 --- a/miniprogram/pages/wordDemo/index.wxml +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - - - - - - - - - 已选择的汉字 ({{wordList.length}}) - - - - - {{word}} - × - - - - - - - - 可选汉字 ({{availableWords.length}}) - - - - {{word}} - - - - - - - - 预览练字田字格 - - - - {{rowHeaderWords[rowIndex]}} - - - - - - 请选择或输入汉字后生成田字格 - diff --git a/miniprogram/service/REFACTOR_COMPLETE.md b/miniprogram/service/REFACTOR_COMPLETE.md new file mode 100644 index 0000000..37c8345 --- /dev/null +++ b/miniprogram/service/REFACTOR_COMPLETE.md @@ -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. 验证打印输出质量 diff --git a/miniprogram/service/REFACTOR_SUMMARY.md b/miniprogram/service/REFACTOR_SUMMARY.md new file mode 100644 index 0000000..5d352aa --- /dev/null +++ b/miniprogram/service/REFACTOR_SUMMARY.md @@ -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; + 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, + ) { + 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) diff --git a/miniprogram/mathPages/service/baseMathDraw.ts b/miniprogram/service/baseDraw.ts similarity index 86% rename from miniprogram/mathPages/service/baseMathDraw.ts rename to miniprogram/service/baseDraw.ts index 62d958e..ecde550 100644 --- a/miniprogram/mathPages/service/baseMathDraw.ts +++ b/miniprogram/service/baseDraw.ts @@ -1,11 +1,18 @@ -import { PAPER_SIZE } from '../../constants/colors'; -import { drawMathHeader, drawMathMiniHeader } from './mathHeaderDraw'; +import { PAPER_SIZE } from '../constants/colors'; +import { drawBaseHeader, drawBaseMiniHeader } from './baseHeaderDraw'; /** - * 基础数学绘制服务 - * 包含Paper设置和Header绘制功能,可被其他绘制服务复用 + * 基础绘制服务 + * 包含Paper设置和Header绘制功能,可被所有绘制服务复用 + * + * 提供功能: + * - Canvas 初始化和配置 + * - Paper 尺寸设置(支持 A4 等标准尺寸) + * - Header 绘制(支持完整 Header 和迷你 Header) + * - 分割线绘制 + * - 打印配置管理 */ -export class BaseMathDrawService { +export class BaseDrawService { headerType: PrintHeader = 'wechat'; canvas: WechatMiniprogram.Canvas; ctx: RenderingContext; @@ -22,14 +29,16 @@ export class BaseMathDrawService { options?: Record, ) { options = options || {}; + const { appName, appHint } = getApp().getPrintConfig(); this.canvas = canvas; this.ctx = ctx; this.paperSize = 'A4'; this.options = { - appName: '涂鸦丫小程序', - appHint: '在玩耍中学习数学', + appName, + appHint, title: '看数字,涂一涂', subTitle: '找一找下面相同的数字,涂上颜色', + ...options, }; this.currentX = 0; @@ -86,7 +95,7 @@ export class BaseMathDrawService { async drawHeader() { this.currentX = 25; this.currentY = 25; - await drawMathHeader({ + await drawBaseHeader({ canvas: this.canvas, ctx: this.ctx, headerType: this.headerType, @@ -107,7 +116,7 @@ export class BaseMathDrawService { * 绘制迷你Header(逻辑像素,尺寸除以3) */ drawMiniHeader() { - drawMathMiniHeader({ + drawBaseMiniHeader({ ctx: this.ctx, canvasWidth: this.canvasWidth, options: { diff --git a/miniprogram/mathPages/service/mathHeaderDraw.ts b/miniprogram/service/baseHeaderDraw.ts similarity index 89% rename from miniprogram/mathPages/service/mathHeaderDraw.ts rename to miniprogram/service/baseHeaderDraw.ts index 9a0b900..4dfc164 100644 --- a/miniprogram/mathPages/service/mathHeaderDraw.ts +++ b/miniprogram/service/baseHeaderDraw.ts @@ -1,9 +1,9 @@ -import { getMiniCodeImage, getImage } from '../../utils/index'; +import { getMiniCodeImage, getImage } from '../utils/index'; /** * 绘制数学模块页眉的参数接口(尺寸除以3) */ -interface DrawMathHeaderParams { +interface drawBaseHeaderParams { canvas: WechatMiniprogram.Canvas; ctx: RenderingContext; headerType: PrintHeader; @@ -19,13 +19,13 @@ interface DrawMathHeaderParams { /** * 绘制数学模块完整页眉(尺寸除以3) */ -export async function drawMathHeader({ +export async function drawBaseHeader({ canvas, ctx, headerType, options, onHeaderDrawn, -}: DrawMathHeaderParams): Promise { +}: drawBaseHeaderParams): Promise { const { appName, appHint, title, subTitle } = options; let titleX = 108; // 约109.33 @@ -85,14 +85,14 @@ export async function drawMathHeader({ // 调用回调函数 if (onHeaderDrawn) { - onHeaderDrawn(110); + onHeaderDrawn(104); } } /** * 绘制数学模块迷你页眉的参数接口(尺寸除以3) */ -interface DrawMathMiniHeaderParams { +interface drawBaseMiniHeaderParams { // canvas: WechatMiniprogram.Canvas; ctx: RenderingContext; options: { @@ -106,12 +106,12 @@ interface DrawMathMiniHeaderParams { /** * 绘制数学模块迷你页眉(尺寸除以3) */ -export function drawMathMiniHeader({ +export function drawBaseMiniHeader({ ctx, options, canvasWidth, onHeaderDrawn, -}: DrawMathMiniHeaderParams): void { +}: drawBaseMiniHeaderParams): void { const { appName, title } = options; const titleY = 46; const centerX = canvasWidth / 2; @@ -124,6 +124,6 @@ export function drawMathMiniHeader({ // 调用回调函数,传递除以3后的currentY if (onHeaderDrawn) { - onHeaderDrawn(66); // 约66.67 + onHeaderDrawn(60); // 约66.67 } } diff --git a/miniprogram/service/findWordDrawService.ts b/miniprogram/service/findWordDrawService.ts index b83d163..c2385a9 100644 --- a/miniprogram/service/findWordDrawService.ts +++ b/miniprogram/service/findWordDrawService.ts @@ -1,8 +1,4 @@ -import { PAPER_SIZE } from '../constants/colors'; -import { - drawHeader as drawHeaderCommon, - drawMiniHeader as drawMiniHeaderCommon, -} from './headerDrawService'; +import { BaseDrawService } from './baseDraw'; import { POSITION_TEMPLATES } from './findWordTemplate'; // ==================== Debug 开关 ==================== @@ -35,13 +31,11 @@ function selectTemplate(): { templateIndex: number } { /** * 从指定模板中获取位置 * @param templateIndex 模板索引 - * @param centerX 中心X坐标 - * @param centerY 中心Y坐标 - * @param padding 边距 - * @param canvasWidth 画布宽度 - * @param canvasHeight 画布高度 - * @param radius 字符圆半径 - * @returns 位置数组 + * @param centerX 中心X坐标(逻辑像素) + * @param centerY 中心Y坐标(逻辑像素) + * @returns 位置数组(逻辑像素) + * + * 注意:POSITION_TEMPLATES 中的坐标是基于原始像素的,需要转换为逻辑像素(除以3) */ function getPositionsFromTemplate( templateIndex: number, @@ -50,25 +44,18 @@ function getPositionsFromTemplate( ): Array<{ x: number; y: number }> { const template = POSITION_TEMPLATES[templateIndex]; - // 转换为绝对坐标并检查边界 + // 转换为绝对坐标(模板坐标除以3转换为逻辑像素) const positions: Array<{ x: number; y: number }> = []; for (const pos of template) { - const x = centerX + pos.x; - const y = centerY + pos.y; + const x = centerX + pos.x / 3; // 转换为逻辑像素 + const y = centerY + pos.y / 3; // 转换为逻辑像素 positions.push({ x, y }); } return positions; } -class FindWordDrawService { - headerType: PrintHeader = 'wechat'; - canvas: WechatMiniprogram.Canvas; - ctx: RenderingContext; - options: Record; - paperSize: PaperSize; - currentX: number; - currentY: number; +class FindWordDrawService extends BaseDrawService { colors: string[]; characters: string[]; debug: boolean = false; // Debug模式开关 @@ -79,29 +66,15 @@ class FindWordDrawService { options?: Record, ) { options = options || {}; - this.canvas = canvas; - this.ctx = ctx; - this.paperSize = 'A4'; - this.options = { - appName: '涂鸦丫小程序', - appHint: '识字|识图|练字|打印', + super(canvas, ctx, { title: '找一找 涂 色', subTitle: '找出相同的文字涂色', ...options, - }; + }); // 从options中读取debug参数,如果没有则使用全局DEBUG常量 this.debug = options.debug === true || DEBUG; - this.currentX = 0; - this.currentY = 0; this.colors = ['#000']; this.characters = ['王']; - this.setPrintConfig(); - } - - setPrintConfig() { - const printConfig = getApp().getPrintConfig(); - this.headerType = printConfig.header; - this.options.appName = printConfig.appName; } async draw(list: Array<{ color: string; word: string }>) { @@ -111,6 +84,8 @@ class FindWordDrawService { this.characters = list.map((item) => item.word || '日'); this.clear(); this.setPaper(); + + // 绘制Header if (this.headerType !== 'minimal') { await this.drawHeader(); } else { @@ -118,64 +93,30 @@ class FindWordDrawService { } // 找字模板没有 drawLegend 部分 + this.drawDivider(); this.drawContent(); } - async drawHeader() { - this.currentX = 80; - this.currentY = 80; - await drawHeaderCommon({ - canvas: this.canvas, - ctx: this.ctx, - headerType: this.headerType, - options: { - appName: this.options.appName || '涂鸦丫小程序', - appHint: this.options.appHint || '识字|识图|练字|打印', - title: this.options.title || '找一找 涂 色', - subTitle: this.options.subTitle || '找出相同的文字涂色', - }, - onHeaderDrawn: (currentY) => { - this.currentY = currentY; - this.drawLine(this.currentY); - }, - }); - } - - drawMiniHeader() { - drawMiniHeaderCommon({ - canvas: this.canvas, - ctx: this.ctx, - options: { - appName: this.options.appName || '涂鸦丫小程序', - title: this.options.title || '找一找 涂 色', - }, - onHeaderDrawn: (currentY) => { - this.currentY = currentY; - this.drawLine(this.currentY); - }, - }); - } - drawContent() { - const { canvas, ctx, characters, colors } = this; + const { ctx, characters, colors } = this; if (characters.length <= 0) return; // 第一个字作为中心大字 const firstChar = characters[0]; const firstColor = colors[0]; - // 计算内容区域 - const contentTop = this.currentY + 50; - const contentBottom = canvas.height - 80; + // 计算内容区域(逻辑像素) + const contentTop = this.currentY + 17; // 50/3≈17 + const contentBottom = this.canvasHeight - 27; // 80/3≈27 const contentHeight = contentBottom - contentTop; // 中心大字的参数 - const centerX = canvas.width / 2; - const centerY = contentTop + contentHeight / 2; // 内容区域垂直居中 + const centerX = this.canvasWidth / 2; + const centerY = contentTop + contentHeight / 2; - // 普通圆的参数(和textDrawService一致) - const radius = 80; - const fontSize = 72; + // 普通圆的参数(逻辑像素) + const radius = 27; // 80/3≈27 + const fontSize = 24; // 72/3=24 // 先选择模板,获取模板的实际位置数量 const { templateIndex } = selectTemplate(); @@ -242,12 +183,12 @@ class FindWordDrawService { // 绘制四周的字符 positions.forEach((pos, index) => { const item = charsToDraw[index]; - const y = pos.y; // 已经是绝对坐标,不需要再加contentTop + const y = pos.y; // 已经是绝对坐标 // 绘制圆 ctx.fillStyle = '#fff'; ctx.strokeStyle = '#000'; - ctx.lineWidth = 4; + ctx.lineWidth = 1; // 4/3≈1 ctx.beginPath(); ctx.arc(pos.x, y, radius, 0, Math.PI * 2); ctx.fill(); @@ -265,35 +206,9 @@ class FindWordDrawService { }); } + // drawLine 已由基类的 drawDivider 替代,保留此方法以保持兼容 drawLine(linY: number) { - const { canvas, ctx } = this; - - ctx.strokeStyle = '#000'; - ctx.lineWidth = 2; - ctx.beginPath(); - ctx.moveTo(80, linY); - ctx.lineTo(canvas.width - 80, linY); - ctx.stroke(); - } - - setPaper() { - const { ctx, canvas } = this; - const { pixelRatio: dpr } = wx.getWindowInfo(); - let { width, height } = PAPER_SIZE[this.paperSize]; - width = width * dpr; - height = height * dpr; - - canvas.width = width; - canvas.height = height; - - this.clear(); - ctx.fillStyle = '#fff'; - ctx.fillRect(0, 0, width, height); - } - - clear() { - const canvas = this.canvas; - this.ctx.clearRect(0, 0, canvas.width, canvas.height); + this.drawDivider(); } } diff --git a/miniprogram/service/shapeDrawService.ts b/miniprogram/service/shapeDrawService.ts index dd51a72..834148f 100644 --- a/miniprogram/service/shapeDrawService.ts +++ b/miniprogram/service/shapeDrawService.ts @@ -1,69 +1,8 @@ -import { PAPER_SIZE } from '../constants/colors'; +import { BaseDrawService } from './baseDraw'; import { ShapeCard } from '../constants/shapes'; import { drawShape } from './drawShape'; -import { - drawHeader as drawHeaderCommon, - drawMiniHeader as drawMiniHeaderCommon, -} from './headerDrawService'; -/** - * 计算图形在画布上的位置,避免重叠 - * @param canvasWidth 画布宽度 - * @param canvasHeight 画布高度 - * @param shapeCount 图形数量 - * @param shapeSize 图形大小 - * @returns 图形位置数组 - */ -function calculateShapePositions( - canvasWidth: number, - canvasHeight: number, - shapeCount: number, - shapeSize: number, -) { - const positions = []; - const padding = 80; - const minSpacing = shapeSize * 1.5; // 最小间距为图形大小的1.5倍 - - const availableWidth = canvasWidth - 2 * padding; - const availableHeight = canvasHeight - 2 * padding; - - // 计算网格布局 - const cols = Math.ceil(Math.sqrt(shapeCount)); - const rows = Math.ceil(shapeCount / cols); - - const cellWidth = availableWidth / cols; - const cellHeight = availableHeight / rows; - - for (let i = 0; i < shapeCount; i++) { - const row = Math.floor(i / cols); - const col = i % cols; - - // 在单元格内随机位置 - const x = - padding + - col * cellWidth + - (cellWidth - shapeSize) / 2 + - (Math.random() - 0.5) * (cellWidth - shapeSize) * 0.3; - const y = - padding + - row * cellHeight + - (cellHeight - shapeSize) / 2 + - (Math.random() - 0.5) * (cellHeight - shapeSize) * 0.3; - - positions.push({ x, y }); - } - - return positions; -} - -class ShapeDrawService { - headerType: PrintHeader; - canvas: WechatMiniprogram.Canvas; - ctx: RenderingContext; - options: Record; - paperSize: PaperSize; - currentX: number; - currentY: number; +class ShapeDrawService extends BaseDrawService { shapes: ShapeCard[]; constructor( @@ -72,116 +11,61 @@ class ShapeDrawService { options?: Record, ) { options = options || {}; - this.canvas = canvas; - this.ctx = ctx; - this.paperSize = 'A4'; - this.options = { - appName: '涂鸦丫小程序', - appHint: '涂色|识字|画画|打印', + super(canvas, ctx, { title: '找一找 涂 色', subTitle: '给图形涂上相同的颜色', ...options, - }; - this.currentX = 0; - this.currentY = 0; + }); this.shapes = []; - this.headerType = 'wechat'; // 默认值 - this.setPrintConfig(); } - setPrintConfig() { - const printConfig = getApp().getPrintConfig(); - this.headerType = printConfig.header; - this.options.appName = printConfig.appName; - } - - draw(shapes: ShapeCard[]) { + async draw(shapes: ShapeCard[]) { this.setPrintConfig(); this.shapes = shapes.slice(0, 6); // 最多6个图形 this.clear(); this.setPaper(); + // 绘制Header if (this.headerType !== 'minimal') { - this.drawHeader(); + await this.drawHeader(); } else { - this.drawMiniHeader(); + await this.drawMiniHeader(); } - + this.drawDivider(); this.drawLegend(); this.drawContent(); } - async drawHeader() { - this.currentX = 80; - this.currentY = 80; - await drawHeaderCommon({ - canvas: this.canvas, - ctx: this.ctx, - headerType: this.headerType, - options: { - appName: this.options.appName || '涂鸦丫小程序', - appHint: this.options.appHint || '涂色|识字|画画|打印', - title: this.options.title || '找一找 涂 色', - subTitle: this.options.subTitle || '给图形涂上相同的颜色', - }, - onHeaderDrawn: (currentY) => { - this.currentY = currentY; - this.drawLine(this.currentY); - }, - }); - } - - drawMiniHeader() { - drawMiniHeaderCommon({ - canvas: this.canvas, - ctx: this.ctx, - options: { - appName: this.options.appName || '涂鸦丫小程序', - title: this.options.title || '找一找 涂 色', - }, - onHeaderDrawn: (currentY) => { - this.currentY = currentY; - this.drawLine(this.currentY); - }, - }); - } - drawLegend() { - const { canvas, ctx, shapes } = this; + const { ctx, shapes } = this; if (shapes.length <= 0) return; - this.currentY = this.headerType === 'minimal' ? 200 : 304; - const shapeSize = 200; - const rectWidth = 180; - const rectHeight = 80; - // 固定图例的Y位置,不依赖shapeSize - const startY = this.currentY + 125; // 固定距离,不依赖shapeSize + // 逻辑像素尺寸(原始尺寸除以3) + this.currentY = this.headerType === 'minimal' ? 75 : 110; // 200/3≈67, 304/3≈101 + const shapeSize = 67; // 200/3≈67 + const rectWidth = 60; // 180/3=60 + const rectHeight = 27; // 80/3≈27 + const startY = this.currentY + 42; // 125/3≈42 const len = shapes.length; - const canvasWidth = canvas.width; // 计算示例图形的间距 - const totalWidth = len * shapeSize + (len - 1) * 40; - const startX = (canvasWidth - totalWidth) / 2 + shapeSize / 2; + const totalWidth = len * shapeSize + (len - 1) * 13; // 40/3≈13 + const startX = (this.canvasWidth - totalWidth) / 2 + shapeSize / 2; // 绘制所有图形 shapes.forEach((shape: ShapeCard, index: number) => { - const x = startX + index * (shapeSize + 40); + const x = startX + index * (shapeSize + 13); // 40/3≈13 const y = startY; // 绘制示例图形 drawShape(ctx, shape, x, y, shapeSize, shape.fillColor); - // // 绘制长方形 - // ctx.fillStyle = '#fff'; - // ctx.strokeStyle = '#000'; - // ctx.lineWidth = 4; const rectangleX = x - rectWidth / 2; - const rectangleY = y + shapeSize / 2 + 10; - // ctx.strokeRect(rectangleX, rectangleY, rectWidth, rectHeight); + const rectangleY = y + shapeSize / 2 + 3; // 10/3≈3 // 绘制图形名称 ctx.fillStyle = '#333'; - ctx.font = 'bold 36px "Microsoft Yahei"'; + ctx.font = 'bold 12px "Microsoft Yahei"'; // 36/3=12 ctx.textBaseline = 'middle'; ctx.textAlign = 'center'; ctx.fillText( @@ -192,33 +76,29 @@ class ShapeDrawService { ); }); - this.currentY = this.headerType === 'minimal' ? 532 : 622; - this.drawLine(this.currentY); + this.currentY = this.headerType === 'minimal' ? 180 : 220; // 532/3≈177, 622/3≈207 + this.drawDivider(); } drawContent() { - const { canvas, ctx, shapes } = this; + const { ctx, shapes } = this; if (shapes.length <= 0) return; - // 固定可渲染的总行数 + // 逻辑像素尺寸(原始尺寸除以3) const ROW_COUNT = 6; - // 每行之间的垂直间距 - const VERTICAL_GAP = 120; - // 图形大小 - const shapeSize = 180; - // 左右边距 - const leftMargin = 160; - const rightMargin = 160; - // 以 drawLegend 画完后的分割线作为基准,内容区顶部间距 50 + const VERTICAL_GAP = 40; // 120/3=40 + const shapeSize = 60; // 180/3=60 + const leftMargin = 53; // 160/3≈53 + const rightMargin = 53; // 160/3≈53 const legendBottomY = this.currentY; - const topGap = 60; + const topGap = 20; // 60/3=20 const contentTop = legendBottomY + topGap; // 可用宽度 - const contentWidth = canvas.width - leftMargin - rightMargin; + const contentWidth = this.canvasWidth - leftMargin - rightMargin; - // 每行最多能放多少个图形(考虑最小间距40) - const minGap = 40; + // 每行最多能放多少个图形(考虑最小间距,逻辑像素) + const minGap = 13; // 40/3≈13 const maxPerRow = Math.floor( (contentWidth + minGap) / (shapeSize + minGap), ); @@ -300,35 +180,9 @@ class ShapeDrawService { }); } + // drawLine 已由基类的 drawDivider 替代,保留此方法以保持兼容 drawLine(linY: number) { - const { canvas, ctx } = this; - - ctx.strokeStyle = '#000'; - ctx.lineWidth = 2; - ctx.beginPath(); - ctx.moveTo(80, linY); - ctx.lineTo(canvas.width - 80, linY); - ctx.stroke(); - } - - setPaper() { - const { ctx, canvas } = this; - const { pixelRatio: dpr } = wx.getWindowInfo(); - let { width, height } = PAPER_SIZE[this.paperSize]; - width = width * dpr; - height = height * dpr; - - canvas.width = width; - canvas.height = height; - - this.clear(); - ctx.fillStyle = '#fff'; - ctx.fillRect(0, 0, width, height); - } - - clear() { - const canvas = this.canvas; - this.ctx.clearRect(0, 0, canvas.width, canvas.height); + this.drawDivider(); } } diff --git a/miniprogram/service/textDrawService.ts b/miniprogram/service/textDrawService.ts index 5308350..e124d9f 100644 --- a/miniprogram/service/textDrawService.ts +++ b/miniprogram/service/textDrawService.ts @@ -1,16 +1,10 @@ -// import { PAPER_SIZE } from './constant'; -import { PAPER_SIZE } from '../constants/colors'; -import { - drawHeader as drawHeaderCommon, - drawMiniHeader as drawMiniHeaderCommon, -} from './headerDrawService'; - +import { BaseDrawService } from './baseDraw'; /** * 计算示例区域圆的中心点 - * @param canvasWidth 画布宽度 + * @param canvasWidth 画布宽度(逻辑像素) * @param circleCount 圆的数量 - * @param padding 圆的间距 - * @param radius 圆的半径 + * @param padding 圆的间距(逻辑像素) + * @param radius 圆的半径(逻辑像素) * @returns 圆的中心点 */ function calculateCircleCenters( @@ -24,7 +18,6 @@ function calculateCircleCenters( const centers = []; if (circleCount === 1) { - // 单个圆圈直接居中 centers.push(canvasWidth / 2); } else { const requiredSpace = 2 * radius * circleCount; @@ -32,7 +25,6 @@ function calculateCircleCenters( (availableWidth - requiredSpace) / (circleCount - 1); if (spaceBetween > maxSpacing) { - // 超过最大间距时,固定间距并居中对齐 const totalWidth = 2 * radius + (circleCount - 1) * maxSpacing; const startX = (canvasWidth - totalWidth) / 2 + radius; @@ -40,7 +32,6 @@ function calculateCircleCenters( centers.push(startX + i * maxSpacing); } } else { - // 正常均匀分布 const startX = padding + radius; for (let i = 0; i < circleCount; i++) { @@ -52,14 +43,7 @@ function calculateCircleCenters( return centers; } -class TextDrawService { - headerType: PrintHeader = 'wechat'; - canvas: WechatMiniprogram.Canvas; - ctx: RenderingContext; - options: Record; - paperSize: PaperSize; - currentX: number; - currentY: number; +class TextDrawService extends BaseDrawService { colors: string[]; characters: string[]; @@ -69,27 +53,13 @@ class TextDrawService { options?: Record, ) { options = options || {}; - this.canvas = canvas; - this.ctx = ctx; - this.paperSize = 'A4'; - this.options = { - appName: '涂鸦丫小程序', - appHint: '识字|识图|练字|打印', + super(canvas, ctx, { title: '找一找 涂 色', subTitle: '给文字涂上相同的颜色', ...options, - }; - this.currentX = 0; - this.currentY = 0; + }); this.colors = ['#000']; this.characters = ['王']; - this.setPrintConfig(); - } - - setPrintConfig() { - const printConfig = getApp().getPrintConfig(); - this.headerType = printConfig.header; - this.options.appName = printConfig.appName; } async draw(list: Array<{ color: string; word: string }>) { @@ -99,82 +69,58 @@ class TextDrawService { this.characters = list.map((item) => item.word || '日'); this.clear(); this.setPaper(); + + // 绘制Header if (this.headerType !== 'minimal') { await this.drawHeader(); } else { - this.drawMiniHeader(); + await this.drawMiniHeader(); } + this.drawDivider(); this.drawLegend(); this.drawContent(); } - async drawHeader() { - this.currentX = 80; - this.currentY = 80; - await drawHeaderCommon({ - canvas: this.canvas, - ctx: this.ctx, - headerType: this.headerType, - options: { - appName: this.options.appName || '涂鸦丫小程序', - appHint: this.options.appHint || '识字|识图|练字|打印', - title: this.options.title || '找一找 涂 色', - subTitle: this.options.subTitle || '给文字涂上相同的颜色', - }, - onHeaderDrawn: (currentY) => { - this.currentY = currentY; - this.drawLine(this.currentY); - }, - }); - } - - drawMiniHeader() { - drawMiniHeaderCommon({ - canvas: this.canvas, - ctx: this.ctx, - options: { - appName: this.options.appName || '涂鸦丫小程序', - title: this.options.title || '找一找 涂 色', - }, - onHeaderDrawn: (currentY) => { - this.currentY = currentY; - this.drawLine(this.currentY); - }, - }); - } - /** 绘制示例 * 矩形: x、y为左上角 * 圆形:x、y为圆心 * 文字:x textAlign 为 start,文本左边缘对齐 * y textBaseline 为 alphabetic,y 对应字母基线(类似左下角) * + * 注意:所有尺寸已转换为逻辑像素(除以3) * */ drawLegend() { - const { canvas, ctx, colors, characters } = this; + const { ctx, colors, characters } = this; if (characters.length <= 0) return; - // const { colors, characters } = options; - this.currentY = this.headerType === 'minimal' ? 200 : 304; - const radius = 65; - const rectWidth = 180; - const rectHeight = 80; - const startX = 260 + radius; // 圆形的X坐标是圆心 - const startY = this.currentY + 30 + radius; // 圆形的Y坐标是圆心 + + // 逻辑像素尺寸(原始尺寸除以3) + this.currentY = this.headerType === 'minimal' ? 68 : 110; // 200/3≈67, 304/3≈101 + const radius = 22; // 65/3≈22 + const rectWidth = 60; // 180/3=60 + const rectHeight = 27; // 80/3≈27 + const startX = 87 + radius; // (260+65)/3≈108 + const startY = this.currentY + 10 + radius; // (30+65)/3≈32 const len = characters.length || 4; - const canvasWidth = canvas.width; - const centers = calculateCircleCenters(canvasWidth, len, 220, 65); - /** 计算每个圆之间的间距 俩个60,一个是页面右侧边距,另一个是圆离右边距地边距*/ - const spaceWidth = len > 3 ? (canvasWidth - startX * 2) / (len - 1) : 0; + const centers = calculateCircleCenters( + this.canvasWidth, + len, + 73, + radius, + ); // 220/3≈73 + + /** 计算每个圆之间的间距 */ + const spaceWidth = + len > 3 ? (this.canvasWidth - startX * 2) / (len - 1) : 0; ctx.moveTo(startX, startY); colors.forEach((color: string, index: number) => { - const x = centers[index] || startX + index * spaceWidth; // 计算圆的X坐标 - const y = startY; // 计算圆的Y坐标 + const x = centers[index] || startX + index * spaceWidth; + const y = startY; // 绘制圆 ctx.strokeStyle = '#000'; ctx.fillStyle = color; - ctx.lineWidth = 4; + ctx.lineWidth = 1; // 4/3≈1 ctx.beginPath(); ctx.arc(x, y, radius, 0, Math.PI * 2); ctx.fill(); @@ -182,16 +128,16 @@ class TextDrawService { ctx.closePath(); // 绘制长方形 - ctx.fillStyle = '#fff'; // 长方形背景色 + ctx.fillStyle = '#fff'; ctx.strokeStyle = '#000'; - ctx.lineWidth = 4; - const rectangleX = x - rectWidth / 2; // 从圆形移动一半的长方形的长 - const rectangleY = y + radius + 43; // 从圆形下移一个半径,再加上43的间距 - ctx.strokeRect(rectangleX, rectangleY, rectWidth, rectHeight); // 绘制长方形 + ctx.lineWidth = 1; // 4/3≈1 + const rectangleX = x - rectWidth / 2; + const rectangleY = y + radius + 14; // 43/3≈14 + ctx.strokeRect(rectangleX, rectangleY, rectWidth, rectHeight); // 绘制字符 ctx.fillStyle = '#333'; - ctx.font = 'bold 48px "Microsoft Yahei"'; + ctx.font = 'bold 16px "Microsoft Yahei"'; // 48/3=16 ctx.textBaseline = 'middle'; ctx.textAlign = 'center'; ctx.fillText( @@ -202,26 +148,25 @@ class TextDrawService { ); }); - this.currentY = this.headerType === 'minimal' ? 522 : 612; // 计算分割线的Y坐标,距离示例图20px - this.drawLine(this.currentY); + this.currentY = this.headerType === 'minimal' ? 178 : 230; // 522/3=174, 612/3=204 + this.drawDivider(); } drawContent() { - const { canvas, ctx, characters } = this; + const { ctx, characters } = this; if (characters.length <= 0) return; const len = characters.length; const rows = this.headerType === 'minimal' ? 9 : 8; - const radius = 80; - const fontSize = 72; + const radius = 27; // 80/3≈27 + const fontSize = 24; // 72/3=24 - const startY = this.currentY + 62 + radius; - const startX1 = 310 + radius; - const startX2 = 200 + radius; - const canvasWidth = canvas.width; - const spaceWidthFrist = (canvasWidth - startX1 * 2) / (5 - 1); - const spaceWidthSecond = (canvasWidth - startX2 * 2) / (6 - 1); - const spaceHeight = 54; + const startY = this.currentY + 10 + radius; // 62/3≈21 + const startX1 = 103 + radius; // 310/3≈103 + const startX2 = 67 + radius; // 200/3≈67 + const spaceWidthFrist = (this.canvasWidth - startX1 * 2) / (5 - 1); + const spaceWidthSecond = (this.canvasWidth - startX2 * 2) / (6 - 1); + const spaceHeight = 18; // 54/3=18 ctx.moveTo(startX1, startY); for (let i = 0; i < rows; i++) { @@ -238,7 +183,7 @@ class TextDrawService { ctx.fillStyle = '#fff'; ctx.strokeStyle = '#000'; - ctx.lineWidth = 4; + ctx.lineWidth = 1; // 4/3≈1 ctx.beginPath(); ctx.arc(x, y, radius, 0, Math.PI * 2); ctx.fill(); @@ -254,36 +199,9 @@ class TextDrawService { } } + // drawLine 已由基类的 drawDivider 替代,保留此方法以保持兼容 drawLine(linY: number) { - const { canvas, ctx } = this; - - ctx.strokeStyle = '#000'; - ctx.lineWidth = 2; - ctx.beginPath(); - ctx.moveTo(80, linY); - ctx.lineTo(canvas.width - 80, linY); - ctx.stroke(); - } - - setPaper() { - const { ctx, canvas } = this; - const { pixelRatio: dpr } = wx.getWindowInfo(); - // const { width, height } = { width: 595 * dpr, height: 842 * dpr }; - let { width, height } = PAPER_SIZE[this.paperSize]; - width = width * dpr; - height = height * dpr; - - canvas.width = width; - canvas.height = height; - - this.clear(); - ctx.fillStyle = '#fff'; // 设置背景色为白色 - ctx.fillRect(0, 0, width, height); - } - - clear() { - const canvas = this.canvas; - this.ctx.clearRect(0, 0, canvas.width, canvas.height); + this.drawDivider(); } } diff --git a/miniprogram/service/wordDrawService.ts b/miniprogram/service/wordDrawService.ts index f3a62a1..0987235 100644 --- a/miniprogram/service/wordDrawService.ts +++ b/miniprogram/service/wordDrawService.ts @@ -1,9 +1,5 @@ -import { PAPER_SIZE } from '../constants/colors'; +import { BaseDrawService } from './baseDraw'; import { CharacterItem } from '../types/characterType'; -import { - drawHeader as drawHeaderCommon, - drawMiniHeader as drawMiniHeaderCommon, -} from './headerDrawService'; /** * 仅支持 M/L/Z 的简单 SVG 路径解析与绘制 @@ -197,14 +193,14 @@ function drawTianZiGrid({ lineColor = '#e0e0e0', boldColor = '#cccccc', }: DrawTianZiGridParams) { - // 外框 + // 外框(逻辑像素) ctx.strokeStyle = boldColor; - ctx.lineWidth = 2; + ctx.lineWidth = 1; // 2/3≈1,逻辑像素 ctx.strokeRect(x - size / 2, y - size / 2, size, size); - // 中线 + // 中线(逻辑像素) ctx.strokeStyle = lineColor; - ctx.lineWidth = 1; + ctx.lineWidth = 1; // 逻辑像素 ctx.beginPath(); // 竖线 ctx.moveTo(x, y - size / 2); @@ -215,7 +211,7 @@ function drawTianZiGrid({ ctx.stroke(); ctx.closePath(); - // 对角线(淡) + // 对角线(淡,逻辑像素) ctx.strokeStyle = '#eeeeee'; ctx.lineWidth = 1; ctx.beginPath(); @@ -227,40 +223,18 @@ function drawTianZiGrid({ ctx.closePath(); } -class WordDrawService { - headerType: PrintHeader = 'wechat'; - canvas: WechatMiniprogram.Canvas; - ctx: RenderingContext; - options: Record; - paperSize: PaperSize; - currentX: number; - currentY: number; - +class WordDrawService extends BaseDrawService { constructor( canvas: Canvas, ctx: RenderingContext, options?: Record, ) { options = options || {}; - this.canvas = canvas; - this.ctx = ctx; - this.paperSize = 'A4'; - this.options = { - appName: '涂鸦丫小程序', - appHint: '识字|识图|练字|打印', + super(canvas, ctx, { title: '田字格 练 字 贴', subTitle: '按笔画临摹练习', ...options, - }; - this.currentX = 0; - this.currentY = 0; - this.setPrintConfig(); - } - - setPrintConfig() { - const printConfig = getApp().getPrintConfig(); - this.headerType = printConfig.header; - this.options.appName = printConfig.appName; + }); } /** @@ -271,12 +245,14 @@ class WordDrawService { this.clear(); this.setPaper(); - // 等待页眉绘制完成,确保 this.currentY 被正确设置 + // 绘制Header if (this.headerType !== 'minimal') { await this.drawHeader(); } else { - await this.drawMiniHeader(); + this.drawMiniHeader(); } + + this.drawDivider(); this.drawContentEmpty(); } @@ -301,60 +277,19 @@ class WordDrawService { * 清空内容区域(页眉以下的部分) */ private clearContentArea() { - const { ctx, canvas } = this; + const { ctx } = this; const contentStartY = this.currentY; - // 清空页眉以下的所有内容 + // 清空页眉以下的所有内容(使用逻辑像素) ctx.fillStyle = '#fff'; ctx.fillRect( 0, contentStartY, - canvas.width, - canvas.height - contentStartY, + this.canvasWidth, + this.canvasHeight - contentStartY, ); } - async drawHeader() { - this.currentX = 80; - this.currentY = 80; - await drawHeaderCommon({ - canvas: this.canvas, - ctx: this.ctx, - headerType: this.headerType, - options: { - appName: this.options.appName || '涂鸦丫小程序', - appHint: this.options.appHint || '识字|识图|练字|打印', - title: this.options.title || '田字格 练 字 贴', - subTitle: this.options.subTitle || '按笔画临摹练习', - }, - onHeaderDrawn: () => { - // wordDrawService 使用不同的计算方式 - const titleY = 80; - const headerHeight = Math.max(titleY + 64 + 48 + 48) + 0; // 64px字体 + 48px间距 + 48px字体 + 20px边距 - this.currentY = 80 + headerHeight; - this.drawDivider(this.currentY); - }, - }); - } - - drawMiniHeader() { - drawMiniHeaderCommon({ - canvas: this.canvas, - ctx: this.ctx, - options: { - appName: this.options.appName || '涂鸦丫小程序', - title: this.options.title || '田字格 练 字 贴', - }, - onHeaderDrawn: () => { - // wordDrawService 使用不同的计算方式 - const titleY = 120; - const miniHeaderHeight = titleY + 64 + 20; // 64px字体 + 20px边距 - this.currentY = miniHeaderHeight; - this.drawDivider(this.currentY); - }, - }); - } - /** * 绘制正文内容:两阶段绘制 - 先绘制空田字格,再绘制练字内容 */ @@ -368,18 +303,18 @@ class WordDrawService { * rowGap(行间距)用于田字格竖直方向(每一行田字格之间的间隔)的初始最小值。它防止行与行的田字格上下贴得太紧,增加一定的可视呼吸空间。之后同样会结合剩余空间动态调整为实际间距 actualRowGap。 */ drawContent(characterData: CharacterItem[] | null) { - const { canvas, ctx } = this; + const { ctx } = this; - // 布局参数 - const topGap = 50; // 与页眉分割线的距离 - const leftMargin = 120; - const rightMargin = 120; - const bottomMargin = 120; + // 布局参数(逻辑像素,原始尺寸除以3) + const topGap = 17; // 50/3≈17 + const leftMargin = 40; // 120/3=40 + const rightMargin = 40; // 120/3=40 + const bottomMargin = 40; // 120/3=40 const contentTop = this.currentY + topGap; - const contentWidth = canvas.width - leftMargin - rightMargin; - const contentHeight = canvas.height - contentTop - bottomMargin; + const contentWidth = this.canvasWidth - leftMargin - rightMargin; + const contentHeight = this.canvasHeight - contentTop - bottomMargin; - const cellSize = 140; + const cellSize = 47; // 140/3≈47 // 统一通过 getMaxGridLayout 获取最大行列数 const { maxRow, maxCol } = this.getMaxGridLayout(); @@ -423,20 +358,18 @@ class WordDrawService { * 获取当前页面可绘制田字格的最大行数与列数(与绘制使用同一套计算规则) */ getMaxGridLayout(): { maxRow: number; maxCol: number } { - const { canvas } = this; - - // 布局参数需与 drawContent 保持一致 - const topGap = 50; - const leftMargin = 120; - const rightMargin = 120; - const bottomMargin = 120; + // 布局参数需与 drawContent 保持一致(逻辑像素) + const topGap = 17; // 50/3≈17 + const leftMargin = 40; // 120/3=40 + const rightMargin = 40; // 120/3=40 + const bottomMargin = 40; // 120/3=40 const contentTop = this.currentY + topGap; - const contentWidth = canvas.width - leftMargin - rightMargin; - const contentHeight = canvas.height - contentTop - bottomMargin; + const contentWidth = this.canvasWidth - leftMargin - rightMargin; + const contentHeight = this.canvasHeight - contentTop - bottomMargin; - const cellSize = 140; - const minGap = 24; - const rowGap = 36; + const cellSize = 47; // 140/3≈47 + const minGap = 8; // 24/3=8 + const rowGap = 12; // 36/3=12 const maxCol = Math.max( 1, @@ -596,7 +529,7 @@ class WordDrawService { uptoInclusive: strokes.length - 1, fillStyle: 'rgb(0,0,0)', // 黑色填充 strokeStyle: 'rgb(0,0,0)', // 黑色描边 - lineWidth: 4, // 较粗的线条 + lineWidth: 1, // 较粗的线条(4/3≈1,逻辑像素) }); } @@ -631,7 +564,7 @@ class WordDrawService { // console.log(`练习格 [${rowIndex}, ${columnIndex}] 坐标: (${x}, ${y}), 笔画: ${strokeIndex + 1}/${strokes.length}`); - // 绘制到指定笔画的汉字(红色,中等粗细) + // 绘制到指定笔画的汉字(灰色,中等粗细) drawStrokes({ ctx, strokes, @@ -641,38 +574,13 @@ class WordDrawService { uptoInclusive: strokeIndex, fillStyle: '#ccc', strokeStyle: '#ccc', - lineWidth: 3, // 中等粗细 + lineWidth: 1, // 中等粗细(3/3=1,逻辑像素) }); } - drawDivider(linY: number) { - const { canvas, ctx } = this; - ctx.strokeStyle = '#000'; - ctx.lineWidth = 2; - ctx.beginPath(); - ctx.moveTo(80, linY); - ctx.lineTo(canvas.width - 80, linY); - ctx.stroke(); - } - - setPaper() { - const { ctx, canvas } = this; - const { pixelRatio: dpr } = wx.getWindowInfo(); - let { width, height } = PAPER_SIZE[this.paperSize]; - width = width * dpr; - height = height * dpr; - - canvas.width = width; - canvas.height = height; - - this.clear(); - ctx.fillStyle = '#fff'; - ctx.fillRect(0, 0, width, height); - } - - clear() { - const canvas = this.canvas; - this.ctx.clearRect(0, 0, canvas.width, canvas.height); + // drawDivider 已在基类中实现,此方法保留以保持兼容 + drawDivider(linY?: number) { + super.drawDivider(); } } diff --git a/miniprogram/utils/tracker.ts b/miniprogram/utils/tracker.ts index 21820b1..bcdd7b4 100644 --- a/miniprogram/utils/tracker.ts +++ b/miniprogram/utils/tracker.ts @@ -3,12 +3,7 @@ * 提供分享和下载打印事件的上报功能 */ -// 生成唯一 UUID -function generateUUID(): string { - const timestamp = Date.now(); - const random = Math.floor(Math.random() * 1000000); - return `${timestamp}-${random}`; -} +import { getAppUUID } from './uuid'; // 格式化时间为 yyyy-MM-dd HH:mm function formatTime(date: Date = new Date()): string { @@ -65,29 +60,24 @@ function getEventCount(eventName: string): number { } /** - * 埋点追踪器 + * 埋点追踪器(单例模式) */ class Tracker { - private uuid: string; + private static instance: Tracker | null = null; private openLog: boolean; - constructor() { - // 初始化时获取或生成 UUID(持久化存储) - this.uuid = this.getOrCreateUUID(); + private constructor() { this.openLog = true; } /** - * 获取或创建 UUID + * 获取 Tracker 单例实例 */ - private getOrCreateUUID(): string { - const uuidKey = 'tracker_uuid'; - let storedUUID = wx.getStorageSync(uuidKey); - if (!storedUUID) { - storedUUID = generateUUID(); - wx.setStorageSync(uuidKey, storedUUID); + public static getInstance(): Tracker { + if (!Tracker.instance) { + Tracker.instance = new Tracker(); } - return storedUUID; + return Tracker.instance; } printLog(tip: string, message?: string | object): void { @@ -104,20 +94,34 @@ class Tracker { } } + /** + * 生成公共埋点属性 + * @param eventName 事件名称 + * @param pageName 页面名称 + * @param extraParams 额外的参数对象 + * @returns 包含公共属性和额外参数的埋点参数对象 + */ + private getCommonEventParams( + eventName: string, + pageName: string, + extraParams?: Record, + ): Record { + return { + count: getEventCount(eventName), + date_time: formatTime(), + uuid: getAppUUID(), + page_name: pageName, + ...extraParams, + }; + } + /** * 上报分享点击事件 * @param pageName 页面名称 */ reportShare(pageName: string): void { try { - const time = formatTime(); - const count = getEventCount('share_click'); - const params = { - count, - time, - uuid: this.uuid, - page_name: pageName, - }; + const params = this.getCommonEventParams('share_click', pageName); this.printLog('分享事件', params); wx.reportEvent('share_click', params); @@ -129,17 +133,13 @@ class Tracker { /** * 上报下载打印事件 * @param pageName 页面名称 + * @param mode 模式(可选) */ - reportDownload(pageName: string): void { + reportDownload(pageName: string, mode?: string): void { try { - const time = formatTime(); - const count = getEventCount('download'); - const params = { - count, - time, - uuid: this.uuid, - page_name: pageName, - }; + const params = this.getCommonEventParams('download', pageName, { + ...(mode && { mode }), + }); this.printLog('下载事件', params); wx.reportEvent('download', params); @@ -149,7 +149,7 @@ class Tracker { } } -// 创建并导出 tracker 实例 -const tracker = new Tracker(); +// 导出 Tracker 单例实例 +const tracker = Tracker.getInstance(); export default tracker; diff --git a/miniprogram/utils/uuid.ts b/miniprogram/utils/uuid.ts new file mode 100644 index 0000000..b368a5b --- /dev/null +++ b/miniprogram/utils/uuid.ts @@ -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(); + 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 ''; + } +} diff --git a/project.private.config.json b/project.private.config.json index 4443c26..596629d 100644 --- a/project.private.config.json +++ b/project.private.config.json @@ -23,12 +23,26 @@ "condition": { "miniprogram": { "list": [ + { + "name": "pages/shape/index", + "pathName": "pages/shape/index", + "query": "", + "scene": null, + "launchMode": "default" + }, + { + "name": "pages/index/index", + "pathName": "pages/index/index", + "query": "", + "launchMode": "default", + "scene": null + }, { "name": "mathPages/numberDecompose/numberDecompose", "pathName": "mathPages/numberDecompose/numberDecompose", "query": "id=number-decompose&mode=with-image", - "scene": null, - "launchMode": "default" + "launchMode": "default", + "scene": null }, { "name": "mathPages/countingSelect/countingSelect", diff --git a/typings/common.d.ts b/typings/common.d.ts index 94339e6..74b0ddc 100644 --- a/typings/common.d.ts +++ b/typings/common.d.ts @@ -4,9 +4,10 @@ type WordCard = { word: string; }; -interface CardList extends Array { } +interface CardList extends Array {} interface PrintConfig { header: PrintHeader; appName: string; -} \ No newline at end of file + appHint: string; +} diff --git a/typings/index.d.ts b/typings/index.d.ts index d673813..20462ba 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -3,6 +3,7 @@ interface IAppOption { globalData: { + uuid: string; userInfo?: WechatMiniprogram.UserInfo; env: string; printConfig?: PrintConfig;