feat: 每日打卡
This commit is contained in:
@@ -453,6 +453,39 @@ export const ALL_WORKSHEETS: WorksheetDefinition[] = [
|
||||
},
|
||||
},
|
||||
),
|
||||
sheet(
|
||||
{
|
||||
id: 'letter-tracing-daily-checkin',
|
||||
title: '每日打卡',
|
||||
desc: '四宫格每日字母打卡,闭合四线三格练习',
|
||||
category: 'english',
|
||||
page: 'letterTracing',
|
||||
subpackage: 'englishPages',
|
||||
icon: '🗓️',
|
||||
img: '',
|
||||
ageRange: [4, 7],
|
||||
difficulty: 'basic',
|
||||
previewBg: '#e8f4ff',
|
||||
previewText: 'ABCD',
|
||||
},
|
||||
{
|
||||
subcategory: 'letter-tracing',
|
||||
template: 'tracing-writing',
|
||||
generator: 'letter-tracing',
|
||||
sortOrder: 48,
|
||||
layoutConfig: {
|
||||
...L.trace,
|
||||
instructionText: '四宫格每日打卡字母描红',
|
||||
},
|
||||
generatorConfig: {
|
||||
mode: 'letter-tracing-daily-checkin',
|
||||
letterCase: 'both',
|
||||
repetitions: 5,
|
||||
fadePattern: 'first-only',
|
||||
dailyLetters: ['A', 'B', 'C', 'D'],
|
||||
},
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
export const MATH_FUNCTION_TYPES: MathFunctionType[] = ALL_WORKSHEETS.filter(
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
import { BaseDrawService } from '../../../core/draw/baseDraw';
|
||||
import type {
|
||||
DailyCheckinRow,
|
||||
DailyCheckinSection,
|
||||
LetterTracingData,
|
||||
} from '../generators/letter-tracing-generator';
|
||||
import {
|
||||
PRINT_CLEARLY_DASHED,
|
||||
PRINT_CLEARLY_REGULAR,
|
||||
fontFamilyOf,
|
||||
} from '../../shared/data/fontProfiles';
|
||||
import {
|
||||
calcLetterFontSizes,
|
||||
drawClosedFourLineGrid,
|
||||
drawLetterInFourLineGrid,
|
||||
loadLetterFont,
|
||||
type LetterFontSizes,
|
||||
} from '../../shared/draw/drawTools';
|
||||
|
||||
type DailyCheckinData = Extract<
|
||||
LetterTracingData,
|
||||
{ mode: 'letter-tracing-daily-checkin' }
|
||||
>;
|
||||
|
||||
// 页面左右边距(px)
|
||||
const PAGE_MARGIN_X = 14;
|
||||
// 页面上下边距(px)
|
||||
const PAGE_MARGIN_Y = 14;
|
||||
// 每个分区内容区左右内边距(px)
|
||||
const SECTION_PADDING_X = 18;
|
||||
// 每个分区内容区顶部内边距(px)
|
||||
const SECTION_PADDING_TOP = 12;
|
||||
// 每个分区内容区底部内边距(px)
|
||||
const SECTION_PADDING_BOTTOM = 10;
|
||||
|
||||
// 标题与上一元素的垂直间距(px)
|
||||
const HEADER_TITLE_GAP = 24;
|
||||
// 标题与元信息(如日期、学号等)的间距(px)
|
||||
const HEADER_META_GAP = 22;
|
||||
// 正文内容距标题/元信息顶部的间距(px)
|
||||
const CONTENT_TOP_GAP = 14;
|
||||
|
||||
// 页脚短语与内容之间的间距(px)
|
||||
const FOOTER_PHRASE_GAP = 26;
|
||||
// 页脚索引与短语之间的间距(px)
|
||||
const FOOTER_INDEX_GAP = 22;
|
||||
|
||||
// 单个格子区列数
|
||||
const GRID_COLS = 5;
|
||||
// 单个格子区行数
|
||||
const GRID_ROWS = 7;
|
||||
// 格子列间距(px)
|
||||
const GRID_COL_GAP = 6;
|
||||
// 格子行间距(px)
|
||||
const GRID_ROW_GAP = 8;
|
||||
|
||||
// 网格描边主色
|
||||
const GRID_BORDER = '#9AA9A2';
|
||||
// 网格中间线虚线颜色
|
||||
const GRID_MIDDLE = 'rgba(154, 169, 162, 0.65)';
|
||||
|
||||
// 黑色文本主色
|
||||
const COLOR_BLACK = '#1a1a1a';
|
||||
// 数据红色(例:未签到状态)
|
||||
const COLOR_LIGHT_RED = '#FF8E8E';
|
||||
|
||||
export default class DailyCheckinDraw extends BaseDrawService {
|
||||
async draw(data: DailyCheckinData) {
|
||||
this.prepareDraw();
|
||||
await Promise.all([
|
||||
loadLetterFont(PRINT_CLEARLY_REGULAR),
|
||||
loadLetterFont(PRINT_CLEARLY_DASHED),
|
||||
]);
|
||||
|
||||
this.drawPageDividers();
|
||||
|
||||
const sections = this.getSectionRects();
|
||||
for (let i = 0; i < sections.length; i++) {
|
||||
this.drawSection(sections[i], data.sections[i] ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
private drawPageDividers() {
|
||||
const verticalX = this.canvasWidth / 2;
|
||||
const horizontalY = this.canvasHeight / 2;
|
||||
|
||||
this.drawLine(
|
||||
verticalX,
|
||||
PAGE_MARGIN_Y,
|
||||
verticalX,
|
||||
this.canvasHeight - PAGE_MARGIN_Y,
|
||||
{
|
||||
isDashed: true,
|
||||
dashPattern: [5, 5],
|
||||
color: '#D6D2CC',
|
||||
lineWidth: 1,
|
||||
},
|
||||
);
|
||||
|
||||
this.drawLine(
|
||||
PAGE_MARGIN_X,
|
||||
horizontalY,
|
||||
this.canvasWidth - PAGE_MARGIN_X,
|
||||
horizontalY,
|
||||
{
|
||||
isDashed: true,
|
||||
dashPattern: [5, 5],
|
||||
color: '#D6D2CC',
|
||||
lineWidth: 1,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private getSectionRects() {
|
||||
const halfW = this.canvasWidth / 2;
|
||||
const halfH = this.canvasHeight / 2;
|
||||
|
||||
return [
|
||||
{
|
||||
x: PAGE_MARGIN_X,
|
||||
y: PAGE_MARGIN_Y,
|
||||
w: halfW - PAGE_MARGIN_X,
|
||||
h: halfH - PAGE_MARGIN_Y,
|
||||
},
|
||||
{
|
||||
x: halfW,
|
||||
y: PAGE_MARGIN_Y,
|
||||
w: this.canvasWidth - PAGE_MARGIN_X - halfW,
|
||||
h: halfH - PAGE_MARGIN_Y,
|
||||
},
|
||||
{
|
||||
x: PAGE_MARGIN_X,
|
||||
y: halfH,
|
||||
w: halfW - PAGE_MARGIN_X,
|
||||
h: this.canvasHeight - PAGE_MARGIN_Y - halfH,
|
||||
},
|
||||
{
|
||||
x: halfW,
|
||||
y: halfH,
|
||||
w: this.canvasWidth - PAGE_MARGIN_X - halfW,
|
||||
h: this.canvasHeight - PAGE_MARGIN_Y - halfH,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
private drawSection(
|
||||
rect: { x: number; y: number; w: number; h: number },
|
||||
section: DailyCheckinSection | null,
|
||||
) {
|
||||
if (!section) return;
|
||||
|
||||
this.drawSectionHeader(rect);
|
||||
this.drawSectionFooter(rect, section.pageNumber);
|
||||
this.drawSectionGrid(rect, section.rows);
|
||||
}
|
||||
|
||||
private drawSectionHeader(rect: {
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
}) {
|
||||
const cx = rect.x + rect.w / 2;
|
||||
const titleY = rect.y + SECTION_PADDING_TOP + 12;
|
||||
const metaY = titleY + HEADER_TITLE_GAP;
|
||||
|
||||
this.ctx.save();
|
||||
this.ctx.fillStyle = '#322E25';
|
||||
this.ctx.textAlign = 'center';
|
||||
this.ctx.textBaseline = 'middle';
|
||||
|
||||
this.ctx.font = '20px serif';
|
||||
this.ctx.fillText('每日打卡', cx, titleY);
|
||||
|
||||
this.ctx.font = '14px serif';
|
||||
this.ctx.fillText('月', rect.x + rect.w * 0.34, metaY);
|
||||
this.ctx.fillText('日', rect.x + rect.w * 0.48, metaY);
|
||||
this.ctx.fillText('姓名:', rect.x + rect.w * 0.7, metaY);
|
||||
|
||||
this.ctx.strokeStyle = '#8E897E';
|
||||
this.ctx.lineWidth = 1;
|
||||
this.ctx.setLineDash([]);
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(rect.x + rect.w * 0.14, metaY + 4);
|
||||
this.ctx.lineTo(rect.x + rect.w * 0.3, metaY + 4);
|
||||
this.ctx.moveTo(rect.x + rect.w * 0.38, metaY + 4);
|
||||
this.ctx.lineTo(rect.x + rect.w * 0.44, metaY + 4);
|
||||
this.ctx.moveTo(rect.x + rect.w * 0.52, metaY + 4);
|
||||
this.ctx.lineTo(rect.x + rect.w * 0.58, metaY + 4);
|
||||
this.ctx.moveTo(rect.x + rect.w * 0.78, metaY + 4);
|
||||
this.ctx.lineTo(rect.x + rect.w * 0.94, metaY + 4);
|
||||
this.ctx.stroke();
|
||||
|
||||
this.ctx.restore();
|
||||
}
|
||||
|
||||
private drawSectionFooter(
|
||||
rect: { x: number; y: number; w: number; h: number },
|
||||
pageNumber: number,
|
||||
) {
|
||||
const cx = rect.x + rect.w / 2;
|
||||
const phraseY =
|
||||
rect.y + rect.h - SECTION_PADDING_BOTTOM - FOOTER_PHRASE_GAP;
|
||||
const indexY = rect.y + rect.h - SECTION_PADDING_BOTTOM - 6;
|
||||
|
||||
this.ctx.save();
|
||||
this.ctx.fillStyle = '#3E3A33';
|
||||
this.ctx.textAlign = 'center';
|
||||
this.ctx.textBaseline = 'middle';
|
||||
this.ctx.font = '14px serif';
|
||||
this.ctx.fillText('日拱一卒 不期而至', cx, phraseY);
|
||||
this.ctx.font = '12px serif';
|
||||
this.ctx.fillText(`- ${pageNumber} -`, cx, indexY);
|
||||
this.ctx.restore();
|
||||
}
|
||||
|
||||
private drawSectionGrid(
|
||||
rect: { x: number; y: number; w: number; h: number },
|
||||
rows: DailyCheckinRow[],
|
||||
) {
|
||||
const contentTop =
|
||||
rect.y +
|
||||
SECTION_PADDING_TOP +
|
||||
HEADER_TITLE_GAP +
|
||||
HEADER_META_GAP +
|
||||
CONTENT_TOP_GAP;
|
||||
const contentBottom =
|
||||
rect.y +
|
||||
rect.h -
|
||||
SECTION_PADDING_BOTTOM -
|
||||
FOOTER_PHRASE_GAP -
|
||||
FOOTER_INDEX_GAP;
|
||||
const contentHeight = contentBottom - contentTop;
|
||||
const contentWidth = rect.w - SECTION_PADDING_X * 2;
|
||||
|
||||
const cellW =
|
||||
(contentWidth - GRID_COL_GAP * (GRID_COLS - 1)) / GRID_COLS;
|
||||
const cellH =
|
||||
(contentHeight - GRID_ROW_GAP * (GRID_ROWS - 1)) / GRID_ROWS;
|
||||
const pairGap = Math.min(16, cellW * 0.28);
|
||||
const regularSizes = calcLetterFontSizes(
|
||||
cellH * 0.86,
|
||||
PRINT_CLEARLY_REGULAR,
|
||||
);
|
||||
const dashedSizes = calcLetterFontSizes(
|
||||
cellH * 0.86,
|
||||
PRINT_CLEARLY_DASHED,
|
||||
);
|
||||
|
||||
for (let rowIdx = 0; rowIdx < rows.length; rowIdx++) {
|
||||
const row = rows[rowIdx];
|
||||
const y = contentTop + rowIdx * (cellH + GRID_ROW_GAP);
|
||||
|
||||
for (let colIdx = 0; colIdx < GRID_COLS; colIdx++) {
|
||||
const x =
|
||||
rect.x +
|
||||
SECTION_PADDING_X +
|
||||
colIdx * (cellW + GRID_COL_GAP);
|
||||
drawClosedFourLineGrid(this.ctx, x, y, cellW, cellH, {
|
||||
ink: GRID_BORDER,
|
||||
middleInk: GRID_MIDDLE,
|
||||
lineWidth: 1,
|
||||
dash: [3, 3],
|
||||
});
|
||||
|
||||
const cell = row.cells[colIdx];
|
||||
if (!cell) continue;
|
||||
|
||||
const sizes =
|
||||
row.style === 'dashed' ? dashedSizes : regularSizes;
|
||||
const fontFamily = fontFamilyOf(
|
||||
row.style === 'dashed'
|
||||
? PRINT_CLEARLY_DASHED
|
||||
: PRINT_CLEARLY_REGULAR,
|
||||
);
|
||||
const centerX = x + cellW / 2;
|
||||
|
||||
this.ctx.save();
|
||||
this.ctx.globalAlpha = cell.alpha;
|
||||
|
||||
drawLetterInFourLineGrid(
|
||||
this.ctx,
|
||||
cell.upper,
|
||||
centerX - pairGap / 2,
|
||||
y,
|
||||
cellH,
|
||||
sizes,
|
||||
{
|
||||
fontFamily,
|
||||
color:
|
||||
cell.color === COLOR_BLACK
|
||||
? COLOR_BLACK
|
||||
: COLOR_LIGHT_RED,
|
||||
},
|
||||
);
|
||||
|
||||
drawLetterInFourLineGrid(
|
||||
this.ctx,
|
||||
cell.lower,
|
||||
centerX + pairGap / 2,
|
||||
y,
|
||||
cellH,
|
||||
sizes,
|
||||
{
|
||||
fontFamily,
|
||||
color:
|
||||
cell.color === COLOR_BLACK
|
||||
? COLOR_BLACK
|
||||
: COLOR_LIGHT_RED,
|
||||
},
|
||||
);
|
||||
|
||||
this.ctx.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import TwoColumnDraw from './twoColumnDraw';
|
||||
import UpperLowerDraw from './upperLowerDraw';
|
||||
import SingleLineDraw from './singleLineDraw';
|
||||
import TripleDraw from './tripleDraw';
|
||||
import DailyCheckinDraw from './dailyCheckinDraw';
|
||||
|
||||
/**
|
||||
* 字母描红绘制分发层。
|
||||
@@ -20,6 +21,7 @@ export default class LetterTracingDraw extends BaseDrawService {
|
||||
'letter-tracing-two-column': TwoColumnDraw,
|
||||
'letter-tracing-half': SingleLineDraw,
|
||||
'letter-tracing-three': TripleDraw,
|
||||
'letter-tracing-daily-checkin': DailyCheckinDraw,
|
||||
}[data.mode];
|
||||
|
||||
const delegate = new DrawClass(this.canvas, this.ctx, this.options);
|
||||
|
||||
@@ -13,7 +13,9 @@ export type LetterTracingMode =
|
||||
/** 5 三字母精练:每页聚焦 3 个字母,每字母 4 行(2 行示范 + 2 行简练) */
|
||||
| 'letter-tracing-three'
|
||||
/** 6 单字母逐行:每行一个字母,13 字母半表(A–M 或 N–Z) */
|
||||
| 'letter-tracing-half';
|
||||
| 'letter-tracing-half'
|
||||
/** 7 每日打卡:整页四宫格,每宫一个字母,7 行 5 列闭合四线三格 */
|
||||
| 'letter-tracing-daily-checkin';
|
||||
|
||||
/** 字母大小写模式(部分模式不使用) */
|
||||
export type LetterCaseMode = 'upper' | 'lower' | 'both';
|
||||
@@ -32,6 +34,8 @@ export interface LetterTracingGeneratorConfig {
|
||||
alphabetHalf?: AlphabetHalf;
|
||||
/** 三字母精练:当前页聚焦的 3 个字母(如 ['A','B','C']) */
|
||||
tripleLetters?: string[];
|
||||
/** 每日打卡:当前页聚焦的 4 个字母(如 ['A','B','C','D']) */
|
||||
dailyLetters?: string[];
|
||||
}
|
||||
|
||||
/** 单个描红格子的数据 */
|
||||
@@ -71,6 +75,19 @@ export interface TripleSection {
|
||||
rows: SingleLineRow[];
|
||||
}
|
||||
|
||||
/** 每日打卡:单行(5 列),支持实线/虚线/留空三种样式 */
|
||||
export interface DailyCheckinRow {
|
||||
style: 'solid' | 'dashed' | 'blank';
|
||||
cells: (SingleLineCell | null)[];
|
||||
}
|
||||
|
||||
/** 每日打卡:单个字母对应一个宫格 */
|
||||
export interface DailyCheckinSection {
|
||||
letter: string;
|
||||
pageNumber: number;
|
||||
rows: DailyCheckinRow[];
|
||||
}
|
||||
|
||||
/** Highlight It 格子数据(3×6 网格,混入目标字母) */
|
||||
export interface HighlightGrid {
|
||||
rows: string[][];
|
||||
@@ -118,6 +135,10 @@ export type LetterTracingData =
|
||||
mode: 'letter-tracing-three';
|
||||
sections: TripleSection[];
|
||||
}
|
||||
| {
|
||||
mode: 'letter-tracing-daily-checkin';
|
||||
sections: DailyCheckinSection[];
|
||||
}
|
||||
| {
|
||||
mode: 'letter-tracing-upper-lower';
|
||||
upperRows: string[][];
|
||||
@@ -196,30 +217,6 @@ function rowForChar(
|
||||
};
|
||||
}
|
||||
|
||||
/** 一行内多个字母,各自占一段描红格(用于分栏对照等) */
|
||||
function rowForManyChars(
|
||||
chars: string[],
|
||||
repetitions: number,
|
||||
fadePattern: 'gradient' | 'first-only',
|
||||
): TracingRow {
|
||||
const cells: TracingCell[] = [];
|
||||
for (const ch of chars) {
|
||||
cells.push(...buildCells(ch, repetitions, fadePattern));
|
||||
}
|
||||
return {
|
||||
displayChar: chars.join(''),
|
||||
cells,
|
||||
};
|
||||
}
|
||||
|
||||
function chunk<T>(arr: T[], size: number): T[][] {
|
||||
const out: T[][] = [];
|
||||
for (let i = 0; i < arr.length; i += size) {
|
||||
out.push(arr.slice(i, i + size));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 总览区 26 字母拆成 7 + 7 + 6 + 6 四行(前两行 7 个、后两行 6 个,与四线三格排版一致) */
|
||||
function overviewRowsFrom(letters: string[]): string[][] {
|
||||
return [
|
||||
@@ -271,6 +268,37 @@ function buildSimpleCells(L: string): (SingleLineCell | null)[] {
|
||||
];
|
||||
}
|
||||
|
||||
function buildDailyFilledCells(
|
||||
L: string,
|
||||
style: 'solid' | 'dashed',
|
||||
firstBlack: boolean = false,
|
||||
): (SingleLineCell | null)[] {
|
||||
const lower = L.toLowerCase();
|
||||
return Array.from({ length: 5 }, (_, idx) => ({
|
||||
upper: L,
|
||||
lower,
|
||||
style,
|
||||
color: idx === 0 && firstBlack ? _COLOR_BLACK : _COLOR_LIGHT_RED,
|
||||
alpha: 1,
|
||||
}));
|
||||
}
|
||||
|
||||
function buildDailyBlankCells(): (SingleLineCell | null)[] {
|
||||
return Array.from({ length: 5 }, () => null);
|
||||
}
|
||||
|
||||
function buildDailyCheckinRows(L: string): DailyCheckinRow[] {
|
||||
return [
|
||||
{ style: 'solid', cells: buildDailyFilledCells(L, 'solid', true) },
|
||||
{ style: 'solid', cells: buildDailyFilledCells(L, 'solid') },
|
||||
{ style: 'blank', cells: buildDailyBlankCells() },
|
||||
{ style: 'dashed', cells: buildDailyFilledCells(L, 'dashed') },
|
||||
{ style: 'blank', cells: buildDailyBlankCells() },
|
||||
{ style: 'dashed', cells: buildDailyFilledCells(L, 'dashed') },
|
||||
{ style: 'blank', cells: buildDailyBlankCells() },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据配置生成字母描红数据
|
||||
* - picture-tracing:教学区 + 6 行描红
|
||||
@@ -377,6 +405,18 @@ export function generateLetterTracing(
|
||||
return { mode: 'letter-tracing-half', rows };
|
||||
}
|
||||
|
||||
if (mode === 'letter-tracing-daily-checkin') {
|
||||
const letters = (config.dailyLetters ?? ['A', 'B', 'C', 'D']).map(
|
||||
normalizeLetter,
|
||||
);
|
||||
const sections: DailyCheckinSection[] = letters.map((L) => ({
|
||||
letter: L,
|
||||
pageNumber: LETTERS_UPPER.indexOf(L) + 1,
|
||||
rows: buildDailyCheckinRows(L),
|
||||
}));
|
||||
return { mode: 'letter-tracing-daily-checkin', sections };
|
||||
}
|
||||
|
||||
// letter-tracing-upper-lower
|
||||
return {
|
||||
mode: 'letter-tracing-upper-lower',
|
||||
@@ -410,5 +450,11 @@ export function mergeLetterTracingConfig(
|
||||
'A-M') as AlphabetHalf,
|
||||
tripleLetters: overrides.tripleLetters ??
|
||||
base.tripleLetters ?? ['A', 'B', 'C'],
|
||||
dailyLetters: overrides.dailyLetters ?? base.dailyLetters ?? [
|
||||
'A',
|
||||
'B',
|
||||
'C',
|
||||
'D',
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -56,6 +56,12 @@ const MODES = [
|
||||
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<
|
||||
@@ -72,6 +78,21 @@ for (let i = 0; i < LETTERS_UPPER.length; i += 3) {
|
||||
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];
|
||||
@@ -88,8 +109,11 @@ type PageData = CanvasDataState & {
|
||||
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;
|
||||
@@ -118,8 +142,11 @@ createPage(
|
||||
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,
|
||||
@@ -204,6 +231,8 @@ createPage(
|
||||
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,
|
||||
@@ -221,6 +250,13 @@ createPage(
|
||||
?.letters,
|
||||
}
|
||||
: {}),
|
||||
...(isDaily
|
||||
? {
|
||||
dailyLetters:
|
||||
DAILY_GROUPS[this.data.selectedDailyGroupIdx]
|
||||
?.letters,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
},
|
||||
|
||||
@@ -293,6 +329,14 @@ createPage(
|
||||
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 控件的显隐(字母选择器、大小写切换等)
|
||||
@@ -320,9 +364,15 @@ createPage(
|
||||
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'
|
||||
@@ -343,6 +393,8 @@ createPage(
|
||||
showCaseToggle,
|
||||
showNextLetter,
|
||||
showTripleGroupPicker,
|
||||
showDailyGroupPicker,
|
||||
selectedDailyGroupIdx,
|
||||
},
|
||||
() => {
|
||||
this.initPageInfo(
|
||||
|
||||
@@ -92,6 +92,23 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{showDailyGroupPicker}}" class="lt-section">
|
||||
<text class="lt-section-title">选择字母组</text>
|
||||
<view class="lt-chip-row lt-chip-row--wrap lt-chip-row--triple">
|
||||
<view
|
||||
wx:for="{{dailyGroups}}"
|
||||
wx:key="label"
|
||||
class="lt-chip lt-chip--triple {{selectedDailyGroupIdx === index ? 'lt-chip--active' : ''}}"
|
||||
hover-class="lt-chip--pressed"
|
||||
hover-start-time="0"
|
||||
hover-stay-time="70"
|
||||
data-idx="{{index}}"
|
||||
bind:tap="onSelectDailyGroup">
|
||||
{{item.label}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
wx:if="{{showNextLetter}}"
|
||||
class="lt-shuffle-btn"
|
||||
|
||||
@@ -174,3 +174,43 @@ export function drawFourLineGrid(
|
||||
ctx.lineTo(x + w, yBot);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
/**
|
||||
* 闭合四线三格
|
||||
* - 外框:闭合矩形
|
||||
* - 中间两条:虚线
|
||||
*/
|
||||
export function drawClosedFourLineGrid(
|
||||
ctx: RenderingContext,
|
||||
x: number,
|
||||
y: number,
|
||||
w: number,
|
||||
h: number,
|
||||
style?: FourLineGridStyle,
|
||||
) {
|
||||
const ink = style?.ink ?? '#9AA9A2';
|
||||
const middleInk = style?.middleInk ?? 'rgba(154, 169, 162, 0.65)';
|
||||
const lineWidth = style?.lineWidth ?? 1;
|
||||
const dash = style?.dash ?? [3, 3];
|
||||
|
||||
const y1 = y + h / 3;
|
||||
const y2 = y + (h * 2) / 3;
|
||||
|
||||
ctx.save();
|
||||
|
||||
ctx.strokeStyle = ink;
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.setLineDash([]);
|
||||
ctx.strokeRect(x, y, w, h);
|
||||
|
||||
ctx.strokeStyle = middleInk;
|
||||
ctx.setLineDash(dash);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y1);
|
||||
ctx.lineTo(x + w, y1);
|
||||
ctx.moveTo(x, y2);
|
||||
ctx.lineTo(x + w, y2);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
@@ -23,12 +23,19 @@
|
||||
"condition": {
|
||||
"miniprogram": {
|
||||
"list": [
|
||||
{
|
||||
"name": "englishPages/letterTracing/letterTracing",
|
||||
"pathName": "englishPages/letterTracing/letterTracing",
|
||||
"query": "id=letter-tracing-daily-checkin",
|
||||
"scene": null,
|
||||
"launchMode": "default"
|
||||
},
|
||||
{
|
||||
"name": "pages/category/category",
|
||||
"pathName": "pages/category/category",
|
||||
"query": "",
|
||||
"scene": null,
|
||||
"launchMode": "default"
|
||||
"launchMode": "default",
|
||||
"scene": null
|
||||
},
|
||||
{
|
||||
"name": "mathIndex",
|
||||
@@ -58,13 +65,6 @@
|
||||
"launchMode": "default",
|
||||
"scene": null
|
||||
},
|
||||
{
|
||||
"name": "englishPages/letterTracing/letterTracing",
|
||||
"pathName": "englishPages/letterTracing/letterTracing",
|
||||
"query": "id=letter-tracing-single",
|
||||
"launchMode": "default",
|
||||
"scene": null
|
||||
},
|
||||
{
|
||||
"name": "pages/index/index",
|
||||
"pathName": "pages/index/index",
|
||||
|
||||
Reference in New Issue
Block a user