386 lines
14 KiB
TypeScript
386 lines
14 KiB
TypeScript
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';
|
||
|
||
/** 三字母精练分组: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,
|
||
|
||
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,
|
||
);
|
||
}
|
||
},
|
||
|
||
/** 用户点击模式选择器时切换 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();
|
||
},
|
||
|
||
onPreviewFavorite() {
|
||
const next = !this.data.isPreviewFavorite;
|
||
this.setData({ isPreviewFavorite: next });
|
||
wx.showToast({
|
||
title: next ? '收藏成功' : '已取消收藏',
|
||
icon: 'none',
|
||
});
|
||
},
|
||
|
||
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,
|
||
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,
|
||
},
|
||
);
|