Files
doodle-mini/miniprogram/englishPages/letterTracing/letterTracing.ts
T
2026-04-22 16:49:29 +08:00

421 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import LetterTracingDraw from './draw/letterTracingDraw';
import { createPage, type CanvasDataState } from '../../base/pageMixin';
import { defaultShareConfig } from '../../config/config';
import { getWorksheetById } from '../../core/data/worksheets';
import {
generateLetterTracing,
mergeLetterTracingConfig,
normalizeLetter,
type LetterTracingGeneratorConfig,
type LetterTracingMode,
} from './generators/letter-tracing-generator';
import { LETTERS_UPPER, LETTERS_PAIRS } from '../shared/data/alphabet';
import {
DEFAULT_LETTER_PROFILE,
getFontProfile,
type FontProfile,
} from '../shared/data/fontProfiles';
import { loadLetterFont } from '../shared/draw/drawTools';
/** 练习模式定义(顺序与产品文档一致),同时用于模式选择器和页面元信息 */
const MODES = [
{
id: 'letter-tracing-single',
icon: 'start-a',
label: '默认字帖',
desc: '配图、例句与描红',
},
{
id: 'letter-tracing-upper-lower',
icon: 'draw-o',
// label: '字母总览',
label: '基础描红',
desc: 'Uppercase / Lowercase 总览',
},
{
id: 'letter-tracing-case-pairing',
icon: 'font-size',
label: '大小写对照',
desc: '半组字母左大写右小写',
},
{
id: 'letter-tracing-two-column',
icon: 'two-columns',
label: '两列描红',
desc: '左 AM、右 NZ 配对描红',
},
{
id: 'letter-tracing-half',
icon: 'square-half',
label: '13字母半表',
desc: '每行一个字母,13 字母半表',
},
{
id: 'letter-tracing-three',
icon: 'ABC-list',
label: '三字母精练',
desc: '每页聚焦 3 个字母深度书写',
},
{
id: 'letter-tracing-daily-checkin',
icon: 'draw-o',
label: '每日打卡',
desc: '四宫格每日字母打卡练习',
},
] as const;
const MODE_MAP = Object.fromEntries(MODES.map((m) => [m.id, m])) as Record<
string,
(typeof MODES)[number]
>;
/** 三字母精练分组:26 字母每 3 个一组 */
const TRIPLE_GROUPS: { label: string; letters: string[] }[] = [];
for (let i = 0; i < LETTERS_UPPER.length; i += 3) {
const group = LETTERS_UPPER.slice(i, i + 3);
const label =
group.length > 1 ? `${group[0]}${group[group.length - 1]}` : group[0];
TRIPLE_GROUPS.push({ label, letters: [...group] });
}
/** 每日打卡分组:4 个字母一组 */
const DAILY_GROUPS: { label: string; letters: string[] }[] = [];
for (let i = 0; i < LETTERS_UPPER.length; i += 4) {
const group = LETTERS_UPPER.slice(i, i + 4);
const label =
group.length > 1 ? `${group[0]}${group[group.length - 1]}` : group[0];
DAILY_GROUPS.push({ label, letters: [...group] });
}
function getDailyGroupIndex(letters?: string[]): number {
const first = normalizeLetter(letters?.[0] ?? 'A');
const idx = DAILY_GROUPS.findIndex((group) => group.letters[0] === first);
return idx >= 0 ? idx : 0;
}
/** 根据 worksheetId 查找页面元信息 */
function pageInfoLookup(id: string) {
const m = MODE_MAP[id];
return m ? { title: m.label, desc: m.desc } : undefined;
}
type PageData = CanvasDataState & {
worksheetId: string;
traceMode: LetterTracingMode;
selectedLetter: string;
selectedLetterPair: string;
letterCaseLower: boolean;
showLetterPicker: boolean;
showCaseToggle: boolean;
showNextLetter: boolean;
showTripleGroupPicker: boolean;
showDailyGroupPicker: boolean;
tripleGroups: { label: string; letters: string[] }[];
selectedTripleGroupIdx: number;
dailyGroups: { label: string; letters: string[] }[];
selectedDailyGroupIdx: number;
letterGrid: string[];
modeOptions: ReadonlyArray<{ id: string; label: string; desc: string }>;
isPreviewFavorite: boolean;
};
createPage(
{
canvas: null as Canvas | null,
ctx: null as RenderingContext | null,
boxHeight: 0,
boxWidth: 0,
drawService: null as LetterTracingDraw | null,
fontProfile: DEFAULT_LETTER_PROFILE as FontProfile,
data: {
pageTitle: '字母描红',
functionId: '',
hasContent: false,
showShareDialog: false,
worksheetId: 'letter-tracing-single',
traceMode: 'letter-tracing-single',
selectedLetter: 'A',
selectedLetterPair: 'Aa',
letterCaseLower: false,
showLetterPicker: true,
showCaseToggle: false,
showNextLetter: true,
showTripleGroupPicker: false,
showDailyGroupPicker: false,
tripleGroups: TRIPLE_GROUPS,
selectedTripleGroupIdx: 0,
dailyGroups: DAILY_GROUPS,
selectedDailyGroupIdx: 0,
letterGrid: LETTERS_PAIRS,
modeOptions: [...MODES],
isPreviewFavorite: false,
} as unknown as PageData,
/** 页面加载:从路由参数获取 worksheetId、letter、case、font 并应用,同时预加载字体 */
onLoad(options: {
id?: string;
letter?: string;
case?: 'upper' | 'lower';
font?: string;
}) {
if (options.font) {
this.fontProfile = getFontProfile(options.font);
}
loadLetterFont(this.fontProfile).catch(() => {});
const worksheetId =
options.id && MODE_MAP[options.id]
? options.id
: 'letter-tracing-single';
this.applyWorksheet(worksheetId);
const updates: Partial<PageData> = {};
if (options.letter) {
const normalized = normalizeLetter(options.letter);
if (normalized) {
updates.selectedLetter = normalized;
updates.selectedLetterPair = `${normalized}${normalized.toLowerCase()}`;
}
}
if (options.case === 'lower') {
updates.letterCaseLower = true;
} else if (options.case === 'upper') {
updates.letterCaseLower = false;
}
if (Object.keys(updates).length > 0) {
this.setData(
updates as Partial<PageData> & WechatMiniprogram.IAnyObject,
);
}
},
/** 用户点击模式选择器时切换 worksheet */
onSelectMode(e: WechatMiniprogram.TouchEvent) {
const id = e.currentTarget.dataset.id as string | undefined;
if (!id || id === this.data.worksheetId) return;
this.applyWorksheet(id, {
preserveLetter: true,
redraw: true,
});
},
/** Canvas 组件就绪回调:创建 DrawService → 首次绘制 */
onCanvasReady(e: WechatMiniprogram.CustomEvent) {
this.initCanvasFromComponent(e.detail, {
createDrawService: (
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, unknown>,
) => new LetterTracingDraw(canvas, ctx, options),
drawServiceOptions: {
title: this.data.pageTitle,
},
onCanvasReady: () => {
this.drawCanvas();
},
});
},
/** 构建运行时生成器配置:合并 worksheet 预设 + 当前页面状态 */
buildRuntimeConfig(): LetterTracingGeneratorConfig {
const def = getWorksheetById(this.data.worksheetId);
const base = (def?.generatorConfig ??
{}) as Partial<LetterTracingGeneratorConfig>;
const letterCase =
this.data.traceMode === 'letter-tracing-single'
? (base.letterCase ?? 'upper')
: this.data.letterCaseLower
? 'lower'
: 'upper';
const needsHalf =
this.data.traceMode === 'letter-tracing-case-pairing' ||
this.data.traceMode === 'letter-tracing-half';
const isTriple = this.data.traceMode === 'letter-tracing-three';
const isDaily =
this.data.traceMode === 'letter-tracing-daily-checkin';
return mergeLetterTracingConfig(base, {
selectedLetter: this.data.selectedLetter,
letterCase,
...(needsHalf
? {
alphabetHalf: this.data.letterCaseLower
? 'N-Z'
: 'A-M',
}
: {}),
...(isTriple
? {
tripleLetters:
TRIPLE_GROUPS[this.data.selectedTripleGroupIdx]
?.letters,
}
: {}),
...(isDaily
? {
dailyLetters:
DAILY_GROUPS[this.data.selectedDailyGroupIdx]
?.letters,
}
: {}),
});
},
/** 执行 Canvas 绘制:等待字体就绪 → 生成数据 → 调用 drawService 渲染 */
async drawCanvas() {
if (!this.ctx || !this.drawService) return;
try {
await loadLetterFont(this.fontProfile);
const config = this.buildRuntimeConfig();
const data = generateLetterTracing(config);
console.log('drawCanvas data', data);
await (this.drawService as LetterTracingDraw).draw(data);
this.setData({ hasContent: true });
} catch (e) {
console.error('letterTracing draw failed', e);
this.setData({ hasContent: false });
}
},
/** 用户在字母网格中选择了一个字母(item 为 "Aa" 格式,取首字母作为 selectedLetter */
onSelectLetter(e: WechatMiniprogram.TouchEvent) {
const raw = e.currentTarget.dataset.letter as string;
if (!raw) return;
const letter = raw.charAt(0).toUpperCase();
this.setData({
selectedLetter: letter,
selectedLetterPair: `${letter}${letter.toLowerCase()}`,
});
this.drawCanvas();
},
/** 切换为大写模式并重绘 */
onSelectCaseUpper() {
this.setData({ letterCaseLower: false });
this.drawCanvas();
},
/** 切换为小写模式并重绘 */
onSelectCaseLower() {
this.setData({ letterCaseLower: true });
this.drawCanvas();
},
/** 跳到下一个字母(循环 A→Z→A)并重绘 */
onNextLetter() {
const list = LETTERS_UPPER;
const i = list.indexOf(this.data.selectedLetter);
const next = list[(i + 1) % list.length];
this.setData({
selectedLetter: next,
selectedLetterPair: `${next}${next.toLowerCase()}`,
});
this.drawCanvas();
},
onPreviewFavorite() {
const next = !this.data.isPreviewFavorite;
this.setData({ isPreviewFavorite: next });
wx.showToast({
title: next ? '收藏成功' : '已取消收藏',
icon: 'none',
});
},
/** 用户在三字母分组选择器中选择了一组 */
onSelectTripleGroup(e: WechatMiniprogram.TouchEvent) {
const idx = Number(e.currentTarget.dataset.idx);
if (isNaN(idx) || idx === this.data.selectedTripleGroupIdx) return;
this.setData({ selectedTripleGroupIdx: idx });
this.drawCanvas();
},
/** 用户在每日打卡分组选择器中选择了一组 */
onSelectDailyGroup(e: WechatMiniprogram.TouchEvent) {
const idx = Number(e.currentTarget.dataset.idx);
if (isNaN(idx) || idx === this.data.selectedDailyGroupIdx) return;
this.setData({ selectedDailyGroupIdx: idx });
this.drawCanvas();
},
/**
* 应用指定的 worksheet 配置到页面状态
* 根据 worksheet 的 generatorConfig 决定 UI 控件的显隐(字母选择器、大小写切换等)
* @param worksheetId - 要应用的 worksheet ID
* @param options.preserveLetter - 是否保留当前选中的字母(模式切换时使用)
* @param options.redraw - 是否立即重绘 Canvas
*/
applyWorksheet(
worksheetId: string,
options?: {
preserveLetter?: boolean;
redraw?: boolean;
},
) {
const def = getWorksheetById(worksheetId);
console.log('def', def);
const base = (def?.generatorConfig ??
{}) as Partial<LetterTracingGeneratorConfig>;
const merged = mergeLetterTracingConfig(base, {});
const traceMode = merged.mode;
const showLetterPicker = traceMode === 'letter-tracing-single';
const showCaseToggle =
traceMode === 'letter-tracing-case-pairing' ||
traceMode === 'letter-tracing-half';
const showNextLetter = traceMode === 'letter-tracing-single';
const showTripleGroupPicker = traceMode === 'letter-tracing-three';
const showDailyGroupPicker =
traceMode === 'letter-tracing-daily-checkin';
const selectedLetter = options?.preserveLetter
? (this.data.selectedLetter ?? 'A')
: normalizeLetter(merged.selectedLetter ?? '');
const selectedDailyGroupIdx =
options?.preserveLetter && showDailyGroupPicker
? (this.data.selectedDailyGroupIdx ?? 0)
: getDailyGroupIndex(merged.dailyLetters);
const letterCaseLower =
traceMode === 'letter-tracing-case-pairing' ||
traceMode === 'letter-tracing-half'
? merged.alphabetHalf === 'N-Z'
: traceMode === 'letter-tracing-single'
? merged.letterCase === 'lower'
: false;
this.setData(
{
worksheetId,
functionId: worksheetId,
traceMode,
selectedLetter,
selectedLetterPair: `${selectedLetter}${selectedLetter.toLowerCase()}`,
letterCaseLower,
showLetterPicker,
showCaseToggle,
showNextLetter,
showTripleGroupPicker,
showDailyGroupPicker,
selectedDailyGroupIdx,
},
() => {
this.initPageInfo(
worksheetId,
MODE_MAP[worksheetId]?.label ?? '字母描红',
);
if (this.drawService) {
this.drawService.options.title = this.data.pageTitle;
}
if (options?.redraw) {
this.drawCanvas();
}
},
);
},
},
{
shareConfig: defaultShareConfig,
pageInfoLookup,
},
);