232 lines
7.7 KiB
TypeScript
232 lines
7.7 KiB
TypeScript
import LetterTracingDraw from '../shared/draw/letterTracingDraw';
|
||
import { createPage, type CanvasDataState } from '../../base/pageMixin';
|
||
import { defaultShareConfig } from '../../config/config';
|
||
import { getWorksheetById } from '../../core/data/worksheets';
|
||
import {
|
||
generateLetterTracing,
|
||
mergeLetterTracingConfig,
|
||
type LetterTracingGeneratorConfig,
|
||
type LetterTracingMode,
|
||
} from '../../core/generators/letter-tracing-generator';
|
||
import { LETTERS_UPPER } from '../../core/data/alphabet';
|
||
|
||
const PAGE_META: Record<string, { title: string; desc: string }> = {
|
||
'letter-tracing-single': {
|
||
title: '字母学习页',
|
||
desc: '单字母配图与描红',
|
||
},
|
||
'letter-tracing-full': {
|
||
title: '26 字母描红',
|
||
desc: '整页字母书写练习',
|
||
},
|
||
'letter-tracing-upper-lower': {
|
||
title: '大小写字母对照',
|
||
desc: '一页认识 A-Z 与 a-z',
|
||
},
|
||
'letter-tracing-two-column': {
|
||
title: '字母描红(两列)',
|
||
desc: 'A–M / N–Z 两列练习',
|
||
},
|
||
};
|
||
|
||
function pageInfoLookup(id: string) {
|
||
return PAGE_META[id];
|
||
}
|
||
|
||
const MODE_OPTIONS = [
|
||
{ id: 'letter-tracing-single', label: '单字母学习页' },
|
||
{ id: 'letter-tracing-full', label: '整页描红' },
|
||
{ id: 'letter-tracing-upper-lower', label: '大小写对照' },
|
||
{ id: 'letter-tracing-two-column', label: '两列描红' },
|
||
] as const;
|
||
|
||
type PageData = CanvasDataState & {
|
||
worksheetId: string;
|
||
traceMode: LetterTracingMode;
|
||
selectedLetter: string;
|
||
letterCaseLower: boolean;
|
||
showLetterPicker: boolean;
|
||
showCaseToggle: boolean;
|
||
showNextLetter: boolean;
|
||
letterGrid: string[];
|
||
modeOptions: ReadonlyArray<{ id: string; label: string }>;
|
||
};
|
||
|
||
createPage(
|
||
{
|
||
canvas: null as Canvas | null,
|
||
ctx: null as RenderingContext | null,
|
||
boxHeight: 0,
|
||
boxWidth: 0,
|
||
drawService: null as LetterTracingDraw | null,
|
||
|
||
data: {
|
||
pageTitle: '字母描红',
|
||
functionId: '',
|
||
hasContent: false,
|
||
showShareDialog: false,
|
||
worksheetId: 'letter-tracing-single',
|
||
traceMode: 'single-letter',
|
||
selectedLetter: 'A',
|
||
letterCaseLower: false,
|
||
showLetterPicker: true,
|
||
showCaseToggle: false,
|
||
showNextLetter: true,
|
||
letterGrid: LETTERS_UPPER,
|
||
modeOptions: [...MODE_OPTIONS],
|
||
} as unknown as PageData,
|
||
|
||
onLoad(options: { id?: string }) {
|
||
const worksheetId =
|
||
options.id && PAGE_META[options.id]
|
||
? options.id
|
||
: 'letter-tracing-single';
|
||
this.applyWorksheet(worksheetId);
|
||
},
|
||
|
||
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,
|
||
});
|
||
},
|
||
|
||
onReady() {
|
||
this.initCanvas({
|
||
createDrawService: (
|
||
canvas: Canvas,
|
||
ctx: RenderingContext,
|
||
options?: Record<string, unknown>,
|
||
) => new LetterTracingDraw(canvas, ctx, options),
|
||
drawServiceOptions: {
|
||
title: this.data.pageTitle,
|
||
subTitle: this.data.subTitle || '',
|
||
},
|
||
onCanvasReady: () => {
|
||
this.drawCanvas();
|
||
},
|
||
});
|
||
},
|
||
|
||
buildRuntimeConfig(): LetterTracingGeneratorConfig {
|
||
const def = getWorksheetById(this.data.worksheetId);
|
||
const base = (def?.generatorConfig ??
|
||
{}) as Partial<LetterTracingGeneratorConfig>;
|
||
const letterCase =
|
||
this.data.traceMode === 'single-letter'
|
||
? (base.letterCase ?? 'upper')
|
||
: this.data.letterCaseLower
|
||
? 'lower'
|
||
: 'upper';
|
||
return mergeLetterTracingConfig(base, {
|
||
selectedLetter: this.data.selectedLetter,
|
||
letterCase,
|
||
});
|
||
},
|
||
|
||
async drawCanvas() {
|
||
if (!this.ctx || !this.drawService) return;
|
||
try {
|
||
const config = this.buildRuntimeConfig();
|
||
const data = generateLetterTracing(config);
|
||
await (this.drawService as LetterTracingDraw).draw(data);
|
||
this.setData({ hasContent: true });
|
||
} catch (e) {
|
||
console.error('letterTracing draw failed', e);
|
||
this.setData({ hasContent: false });
|
||
}
|
||
},
|
||
|
||
onSelectLetter(e: WechatMiniprogram.TouchEvent) {
|
||
const letter = e.currentTarget.dataset.letter as string;
|
||
if (!letter) return;
|
||
this.setData({ selectedLetter: letter });
|
||
this.drawCanvas();
|
||
},
|
||
|
||
onSelectCaseUpper() {
|
||
this.setData({ letterCaseLower: false });
|
||
this.drawCanvas();
|
||
},
|
||
|
||
onSelectCaseLower() {
|
||
this.setData({ letterCaseLower: true });
|
||
this.drawCanvas();
|
||
},
|
||
|
||
onNextLetter() {
|
||
const list = LETTERS_UPPER;
|
||
const i = list.indexOf(this.data.selectedLetter);
|
||
const next = list[(i + 1) % list.length];
|
||
this.setData({ selectedLetter: next });
|
||
this.drawCanvas();
|
||
},
|
||
|
||
applyWorksheet(
|
||
worksheetId: string,
|
||
options?: {
|
||
preserveLetter?: boolean;
|
||
redraw?: boolean;
|
||
},
|
||
) {
|
||
const def = getWorksheetById(worksheetId);
|
||
const base = (def?.generatorConfig ??
|
||
{}) as Partial<LetterTracingGeneratorConfig>;
|
||
const merged = mergeLetterTracingConfig(base, {});
|
||
|
||
const traceMode = merged.mode;
|
||
const showLetterPicker = traceMode === 'single-letter';
|
||
const showCaseToggle =
|
||
traceMode === 'full-alphabet' || traceMode === 'two-column';
|
||
const showNextLetter = traceMode === 'single-letter';
|
||
const selectedLetter = options?.preserveLetter
|
||
? this.data.selectedLetter
|
||
: normalizePick(merged.selectedLetter);
|
||
const letterCaseLower =
|
||
traceMode === 'single-letter'
|
||
? merged.letterCase === 'lower'
|
||
: this.data.letterCaseLower;
|
||
|
||
this.setData(
|
||
{
|
||
worksheetId,
|
||
functionId: worksheetId,
|
||
traceMode,
|
||
selectedLetter,
|
||
letterCaseLower,
|
||
showLetterPicker,
|
||
showCaseToggle,
|
||
showNextLetter,
|
||
},
|
||
() => {
|
||
this.initPageInfo(
|
||
worksheetId,
|
||
PAGE_META[worksheetId]?.title ?? '字母描红',
|
||
);
|
||
|
||
if (this.drawService) {
|
||
this.drawService.options.title = this.data.pageTitle;
|
||
this.drawService.options.subTitle =
|
||
this.data.subTitle || '';
|
||
}
|
||
|
||
if (options?.redraw) {
|
||
this.drawCanvas();
|
||
}
|
||
},
|
||
);
|
||
},
|
||
},
|
||
{
|
||
shareConfig: defaultShareConfig,
|
||
pageInfoLookup,
|
||
},
|
||
);
|
||
|
||
function normalizePick(c?: string): string {
|
||
if (!c || c.length === 0) return 'A';
|
||
return c.trim().toUpperCase().charAt(0);
|
||
}
|