import WordDrawService from '../../service/wordDrawService'; import { checkAndSaveImage } from '../../utils/saveImage'; import { shouldShowShareGuide } from '../../utils/shareGuide'; 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, ctx: null as RenderingContext | null, wordDrawService: null as WordDrawService | null, svgWords: {} as Record, maxRow: 0 as number, maxCol: 0 as number, data: { // 选中的汉字列表 words: [] as string[], showSelectWordPopup: false, showColorPopup: false, currentKey: 0, currentColor: '', boxWidth: 0, boxHeight: 0, showShareDialog: false, // 显示分享引导弹窗 }, async onLoad() { await this.loadSvgWords(); const hasShowPracticeDemo = wx.getStorageSync('hasShowPracticeDemo') || false; if (!hasShowPracticeDemo) { this.setData( { words: ['好', '好', '学', '习', '天', '天', '向', '上'], }, () => { wx.setStorageSync('hasShowPracticeDemo', true); }, ); } await this.setCanvasBoxSize(); await this.initCanvas(); }, async loadSvgWords() { try { wx.showToast({ title: '加载字体中...', icon: 'loading' }); // 使用带缓存的SVG汉字数据获取函数 const svgWordsData = await getWordsSvgData(); this.svgWords = svgWordsData; wx.hideToast(); console.log( 'SVG汉字数据加载成功,共', Object.keys(this.svgWords).length, '个汉字', ); const { words } = this.data as any; if (words && words.length > 0) { this.renderPracticeContent().catch(console.error); } } catch (error) { console.error('加载SVG汉字数据失败:', error); wx.hideToast(); // 显示错误提示 wx.showModal({ title: '加载失败', content: '无法加载汉字数据,请检查网络连接后重试', showCancel: true, cancelText: '取消', confirmText: '重试', success: (res) => { if (res.confirm) { // 用户点击重试,重新加载 this.loadSvgWords(); } }, }); } }, // 输入组件回调 onConfirmInput(e: any) { const value: string = (e && e.detail && e.detail.value) || ''; const text = (value || '').trim(); if (!text) return; if (!this.isChineseText(text)) { wx.showToast({ title: '请输入汉字', icon: 'none' }); return; } const { words } = this.data as any; // 1. 解析新输入的汉字 const newChars = this.splitToSingleChars(text); if (newChars.length === 0) { wx.showToast({ title: '请输入有效汉字', icon: 'none' }); return; } // 2. 检查是否超过限制 if (words.length + newChars.length > 11) { wx.showToast({ title: `最多只能添加 11 个字,当前已有 ${words.length} 个,输入了 ${newChars.length} 个`, icon: 'none', duration: 3000, }); return; } // 3. 直接追加新汉字 const updatedWords = [...words, ...newChars]; // 4. 更新数据并提示 this.setData({ words: updatedWords }, () => { this.renderPracticeContent().catch(console.error); }); // 5. 显示添加结果提示 wx.showToast({ title: `成功添加 ${newChars.length} 个汉字`, icon: 'none', duration: 2000, }); }, // 选择面板 openSelectWordPopup() { this.setData({ showSelectWordPopup: true }); }, closeSelectWordPopup() { this.setData({ showSelectWordPopup: false }); }, onChangeWord(e: any) { const selectedWords = e.detail.selectedWords as string[]; const newWords = [...this.data.words, ...selectedWords]; this.setData({ words: newWords, showSelectWordPopup: false }, () => { if (newWords && newWords.length > 0) { this.renderPracticeContent().catch(console.error); } }); }, // 删除 deleteWordCard(e: any) { const { key } = e.detail; const { words } = this.data as any; const next = words.filter((_: string, index: number) => index !== key); this.setData({ words: next }, () => this.renderPracticeContent().catch(console.error), ); }, // 清空 clearWords() { this.setData({ words: [] }, () => this.renderPracticeContent().catch(console.error), ); }, // 随机生成(取基础分类,过滤为可用SVG字,最多11个) refreshWords() { // 选取较简单的几个分类(如 0:基础独体字,1:生活高频字,2:自然与动作) const candidate = WORDS.slice(0, 3).reduce( (acc: string[], c: any) => acc.concat(c.words), [] as string[], ); // 去重 const unique = Array.from(new Set(candidate)); // 打乱 const shuffled = unique.sort(() => Math.random() - 0.5); // 先取前 11 个 const picked = shuffled.slice(0, 10); // 过滤为支持的SVG字 const supported = this.getSupportedWords(picked); if (supported.length === 0) { wx.showToast({ title: '随机到的字暂不支持,重试一下', icon: 'none', }); return; } this.setData({ words: supported }, () => this.renderPracticeContent().catch(console.error), ); }, // 校验 & 分割 isChineseText(text: string): boolean { const chineseRegex = /^[\u4e00-\u9fff]+$/; return chineseRegex.test(text); }, splitToSingleChars(text: string): string[] { const chineseRegex = /[\u4e00-\u9fff]/g; const matches = text.match(chineseRegex); return matches || []; }, setCanvasBoxSize() { const query = wx.createSelectorQuery(); query .select('#canvasWrapper') .boundingClientRect((rect) => { if (rect) { const { width, height } = PAPER_SIZE['A4']; const boxWidth = rect.width; const boxHeight = boxWidth / (width / height); this.setData({ boxWidth, boxHeight }); } }) .exec(); }, async initCanvas() { return new Promise((resolve) => { // 然后初始化canvas wx.createSelectorQuery() .select('#canvasContent') .fields({ node: true, size: true, }) .exec(async (res) => { if (res[0] && res[0].node) { const canvas = res[0].node; const ctx = canvas.getContext('2d'); if (ctx) { this.canvas = canvas; this.ctx = ctx; this.wordDrawService = new WordDrawService( canvas, ctx, ); // 初始化时只绘制页眉和布局 await this.wordDrawService.drawLayout(); // 绘制完成后计算并缓存最大行列数(与实际绘制一致) const { maxRow, maxCol } = this.wordDrawService.getMaxGridLayout(); this.maxRow = maxRow; this.maxCol = maxCol; // 如果有汉字数据,绘制练字内容 const { words } = this.data as any; if (words && words.length > 0) { await this.renderPracticeContent(); } resolve(); } } }); }); }, // 绘制练字内容(只绘制田字格和汉字内容) async renderPracticeContent() { if (!this.canvas || !this.wordDrawService) { await this.initCanvas(); return; } const { words } = this.data as any; if (!words || words.length === 0) { this.wordDrawService.drawContentEmpty(); return; } // 检查汉字是否支持 const supportedWords = this.getSupportedWords(words); if (supportedWords.length === 0) { wx.showToast({ title: '暂不支持这些汉字', icon: 'none' }); this.wordDrawService.drawContentEmpty(); return; } // 使用初始化阶段缓存的最大行列数;若未初始化则计算一次并缓存 let maxRow = this.maxRow; let maxCol = this.maxCol; if (!maxRow || !maxCol) { const layout = this.wordDrawService!.getMaxGridLayout(); this.maxRow = layout.maxRow; this.maxCol = layout.maxCol; maxRow = layout.maxRow; maxCol = layout.maxCol; } // 处理布局和生成练字数据(基于实际最大行列数) const characterData = this.processCharacterLayout( supportedWords, maxRow, maxCol, ); this.wordDrawService.drawPracticeContent(characterData); }, /** 检查汉字是否支持,返回支持的汉字列表 */ getSupportedWords(words: string[]): string[] { return words.filter((word) => this.svgWords[word]); }, /** 处理汉字布局,生成练字数据 */ processCharacterLayout( words: string[], maxRow: number, maxCol: number, ): CharacterItem[] { let rowIndex = 0; const characterData: CharacterItem[] = []; const boundaryWords: string[] = []; // 按顺序处理每个汉字 words.forEach((word: string) => { const strokes = this.svgWords[word]; const strokeCount = strokes.length; const totalCells = 1 + strokeCount + 0; // 预览格 + 练习格 + 空白格 const totalRows = Math.ceil(totalCells / maxCol); // 这里的意思是:把这一个汉字所占的行数(totalRows)累加到 rowIndex 上。 // 这样 rowIndex 就记录了当前已经排布了多少行,用于判断是否超出了最大允许行数(maxRow)。 rowIndex = rowIndex + totalRows; if (rowIndex <= maxRow) { characterData.push({ character: word, strokes }); } else { boundaryWords.push(word); } }); // // 提示超出页面的汉字 // if (showToast && boundaryWords.length > 0) { // wx.showToast({ // title: `练字贴已超过一页,部分汉字未包含在本次生成结果中`, // icon: 'none' // }); // } return characterData; }, /** 记录待下载状态(当需要分享引导时) */ pendingDownload: false, // 下载打印练字贴 exportToPrint() { if (!this.canvas) { wx.showToast({ title: '请先生成练字贴', icon: 'none' }); return; } // 检查是否需要显示分享引导 if (shouldShowShareGuide()) { this.setData({ showShareDialog: true }); return; } // 上报下载埋点 tracker.reportDownload('练字贴'); // 直接下载 checkAndSaveImage(this.canvas); }, /** 关闭分享引导弹窗 */ onCloseShareDialog() { this.setData({ showShareDialog: false }); }, // 分享功能 onShareAppMessage() { // 上报分享埋点 tracker.reportShare('练字贴'); return { title: '涂鸦丫-涂色|练字|识字|打印', path: '/pages/copyBook/copyBook', imageUrl: 'https://cdn.joeyone.cn/doodle/share-img/index-share-v2.png', }; }, onShareTimeline() { // 上报分享埋点 tracker.reportShare('练字贴'); return { title: '涂鸦丫-涂色|练字|识字|打印', query: '/pages/copyBook/copyBook', imageUrl: 'https://cdn.joeyone.cn/doodle/share-img/index-share-v2.png', }; }, /** 分享成功回调(由组件触发) */ onShareSuccess() { // 只关闭弹窗,不触发下载 this.setData({ showShareDialog: false }); }, });