Files
doodle-mini/miniprogram/englishPages/letterTracing/letterTracing.ts
T
2026-05-06 18:32:15 +08:00

409 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 {
generateLetterTracing,
mergeLetterTracingConfig,
normalizeLetter,
type LetterTracingGeneratorConfig,
type LetterTracingMode,
} from './generators/letter-tracing-generator';
import {
getModeInfo,
getPublishMetaByMode,
isValidMode,
LETTER_TRACING_MODE_OPTIONS,
} from './letterTracing.config';
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';
import type { DebugPublishMeta } from '../../utils/debugPublish';
import { addFavorite, removeFavorite, batchCheckFavorited } from '../../utils/favorites';
/** 三字母精练分组:26 字母每 3 个一组 */
const TRIPLE_GROUPS: { title: string; letters: string[] }[] = [];
for (let i = 0; i < LETTERS_UPPER.length; i += 3) {
const group = LETTERS_UPPER.slice(i, i + 3);
const title =
group.length > 1 ? `${group[0]}${group[group.length - 1]}` : group[0];
TRIPLE_GROUPS.push({ title, letters: [...group] });
}
/** 每日打卡分组:4 个字母一组 */
const DAILY_GROUPS: { title: string; letters: string[] }[] = [];
for (let i = 0; i < LETTERS_UPPER.length; i += 4) {
const group = LETTERS_UPPER.slice(i, i + 4);
const title =
group.length > 1 ? `${group[0]}${group[group.length - 1]}` : group[0];
DAILY_GROUPS.push({ title, 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;
}
const pageInfoLookup = getModeInfo;
type PageData = CanvasDataState & {
worksheetId: string;
traceMode: LetterTracingMode;
selectedLetter: string;
selectedLetterPair: string;
letterCaseLower: boolean;
showLetterPicker: boolean;
showCaseToggle: boolean;
showNextLetter: boolean;
showTripleGroupPicker: boolean;
showDailyGroupPicker: boolean;
tripleGroups: { title: string; letters: string[] }[];
selectedTripleGroupIdx: number;
dailyGroups: { title: string; letters: string[] }[];
selectedDailyGroupIdx: number;
letterGrid: string[];
modeOptions: typeof LETTER_TRACING_MODE_OPTIONS;
isPreviewFavorite: boolean;
isDevEnv: boolean;
debugPublishVisible: boolean;
debugPublishLoading: boolean;
debugPublishMeta: DebugPublishMeta | null;
};
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,
_favoritedMap: {} as Record<string, boolean>,
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: LETTER_TRACING_MODE_OPTIONS,
isPreviewFavorite: false,
isDevEnv: false,
debugPublishVisible: false,
debugPublishLoading: false,
debugPublishMeta: null,
} 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 && isValidMode(options.id)
? options.id
: 'letter-tracing-single';
this.syncDebugPublishEnv();
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,
);
}
this.loadFavoritedMap();
},
/** 用户点击模式选择器时切换 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();
},
});
},
/** 构建运行时生成器配置:合并页面模式预设 + 当前页面状态 */
buildRuntimeConfig(): LetterTracingGeneratorConfig {
const base: Partial<LetterTracingGeneratorConfig> = {
mode: this.data.traceMode,
};
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();
},
async onPreviewFavorite() {
const next = !this.data.isPreviewFavorite;
this.setData({ isPreviewFavorite: next });
const id = this.data.worksheetId;
if (id) {
this._favoritedMap[id] = next;
if (next) {
addFavorite(id);
} else {
removeFavorite(id);
}
}
wx.showToast({
title: next ? '收藏成功' : '已取消收藏',
icon: 'none',
});
},
async loadFavoritedMap() {
const ids = LETTER_TRACING_MODE_OPTIONS.map((d) => d.id);
this._favoritedMap = await batchCheckFavorited(ids);
const currentId = this.data.worksheetId;
if (this._favoritedMap[currentId]) {
this.setData({ isPreviewFavorite: true });
}
},
getPublishMeta(): DebugPublishMeta {
const meta = getPublishMetaByMode(this.data.worksheetId);
if (!meta) {
throw new Error('当前题型配置不存在');
}
return meta;
},
/** 用户在三字母分组选择器中选择了一组 */
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();
},
/**
* 应用指定的页面模式配置到页面状态
* 根据当前模式的运行时默认参数决定 UI 控件的显隐(字母选择器、大小写切换等)
* @param worksheetId - 要应用的模式 ID
* @param options.preserveLetter - 是否保留当前选中的字母(模式切换时使用)
* @param options.redraw - 是否立即重绘 Canvas
*/
applyWorksheet(
worksheetId: string,
options?: {
preserveLetter?: boolean;
redraw?: boolean;
},
) {
if (!isValidMode(worksheetId)) return;
const merged = mergeLetterTracingConfig(
{ mode: worksheetId as LetterTracingMode },
{},
);
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,
isPreviewFavorite: !!this._favoritedMap[worksheetId],
selectedLetter,
selectedLetterPair: `${selectedLetter}${selectedLetter.toLowerCase()}`,
letterCaseLower,
showLetterPicker,
showCaseToggle,
showNextLetter,
showTripleGroupPicker,
showDailyGroupPicker,
selectedDailyGroupIdx,
},
() => {
this.initPageInfo(worksheetId, '字母描红');
if (this.drawService) {
this.drawService.options.title = this.data.pageTitle;
}
if (options?.redraw) {
this.drawCanvas();
}
},
);
},
},
{
shareConfig: defaultShareConfig,
pageInfoLookup,
},
);