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(
|
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 UpperLowerDraw from './upperLowerDraw';
|
||||||
import SingleLineDraw from './singleLineDraw';
|
import SingleLineDraw from './singleLineDraw';
|
||||||
import TripleDraw from './tripleDraw';
|
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-two-column': TwoColumnDraw,
|
||||||
'letter-tracing-half': SingleLineDraw,
|
'letter-tracing-half': SingleLineDraw,
|
||||||
'letter-tracing-three': TripleDraw,
|
'letter-tracing-three': TripleDraw,
|
||||||
|
'letter-tracing-daily-checkin': DailyCheckinDraw,
|
||||||
}[data.mode];
|
}[data.mode];
|
||||||
|
|
||||||
const delegate = new DrawClass(this.canvas, this.ctx, this.options);
|
const delegate = new DrawClass(this.canvas, this.ctx, this.options);
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ export type LetterTracingMode =
|
|||||||
/** 5 三字母精练:每页聚焦 3 个字母,每字母 4 行(2 行示范 + 2 行简练) */
|
/** 5 三字母精练:每页聚焦 3 个字母,每字母 4 行(2 行示范 + 2 行简练) */
|
||||||
| 'letter-tracing-three'
|
| 'letter-tracing-three'
|
||||||
/** 6 单字母逐行:每行一个字母,13 字母半表(A–M 或 N–Z) */
|
/** 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';
|
export type LetterCaseMode = 'upper' | 'lower' | 'both';
|
||||||
@@ -32,6 +34,8 @@ export interface LetterTracingGeneratorConfig {
|
|||||||
alphabetHalf?: AlphabetHalf;
|
alphabetHalf?: AlphabetHalf;
|
||||||
/** 三字母精练:当前页聚焦的 3 个字母(如 ['A','B','C']) */
|
/** 三字母精练:当前页聚焦的 3 个字母(如 ['A','B','C']) */
|
||||||
tripleLetters?: string[];
|
tripleLetters?: string[];
|
||||||
|
/** 每日打卡:当前页聚焦的 4 个字母(如 ['A','B','C','D']) */
|
||||||
|
dailyLetters?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 单个描红格子的数据 */
|
/** 单个描红格子的数据 */
|
||||||
@@ -71,6 +75,19 @@ export interface TripleSection {
|
|||||||
rows: SingleLineRow[];
|
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 网格,混入目标字母) */
|
/** Highlight It 格子数据(3×6 网格,混入目标字母) */
|
||||||
export interface HighlightGrid {
|
export interface HighlightGrid {
|
||||||
rows: string[][];
|
rows: string[][];
|
||||||
@@ -118,6 +135,10 @@ export type LetterTracingData =
|
|||||||
mode: 'letter-tracing-three';
|
mode: 'letter-tracing-three';
|
||||||
sections: TripleSection[];
|
sections: TripleSection[];
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
mode: 'letter-tracing-daily-checkin';
|
||||||
|
sections: DailyCheckinSection[];
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
mode: 'letter-tracing-upper-lower';
|
mode: 'letter-tracing-upper-lower';
|
||||||
upperRows: string[][];
|
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 个,与四线三格排版一致) */
|
/** 总览区 26 字母拆成 7 + 7 + 6 + 6 四行(前两行 7 个、后两行 6 个,与四线三格排版一致) */
|
||||||
function overviewRowsFrom(letters: string[]): string[][] {
|
function overviewRowsFrom(letters: string[]): string[][] {
|
||||||
return [
|
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 行描红
|
* - picture-tracing:教学区 + 6 行描红
|
||||||
@@ -377,6 +405,18 @@ export function generateLetterTracing(
|
|||||||
return { mode: 'letter-tracing-half', rows };
|
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
|
// letter-tracing-upper-lower
|
||||||
return {
|
return {
|
||||||
mode: 'letter-tracing-upper-lower',
|
mode: 'letter-tracing-upper-lower',
|
||||||
@@ -410,5 +450,11 @@ export function mergeLetterTracingConfig(
|
|||||||
'A-M') as AlphabetHalf,
|
'A-M') as AlphabetHalf,
|
||||||
tripleLetters: overrides.tripleLetters ??
|
tripleLetters: overrides.tripleLetters ??
|
||||||
base.tripleLetters ?? ['A', 'B', 'C'],
|
base.tripleLetters ?? ['A', 'B', 'C'],
|
||||||
|
dailyLetters: overrides.dailyLetters ?? base.dailyLetters ?? [
|
||||||
|
'A',
|
||||||
|
'B',
|
||||||
|
'C',
|
||||||
|
'D',
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,12 @@ const MODES = [
|
|||||||
label: '三字母精练',
|
label: '三字母精练',
|
||||||
desc: '每页聚焦 3 个字母深度书写',
|
desc: '每页聚焦 3 个字母深度书写',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'letter-tracing-daily-checkin',
|
||||||
|
icon: 'draw-o',
|
||||||
|
label: '每日打卡',
|
||||||
|
desc: '四宫格每日字母打卡练习',
|
||||||
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const MODE_MAP = Object.fromEntries(MODES.map((m) => [m.id, m])) as Record<
|
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] });
|
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 查找页面元信息 */
|
/** 根据 worksheetId 查找页面元信息 */
|
||||||
function pageInfoLookup(id: string) {
|
function pageInfoLookup(id: string) {
|
||||||
const m = MODE_MAP[id];
|
const m = MODE_MAP[id];
|
||||||
@@ -88,8 +109,11 @@ type PageData = CanvasDataState & {
|
|||||||
showCaseToggle: boolean;
|
showCaseToggle: boolean;
|
||||||
showNextLetter: boolean;
|
showNextLetter: boolean;
|
||||||
showTripleGroupPicker: boolean;
|
showTripleGroupPicker: boolean;
|
||||||
|
showDailyGroupPicker: boolean;
|
||||||
tripleGroups: { label: string; letters: string[] }[];
|
tripleGroups: { label: string; letters: string[] }[];
|
||||||
selectedTripleGroupIdx: number;
|
selectedTripleGroupIdx: number;
|
||||||
|
dailyGroups: { label: string; letters: string[] }[];
|
||||||
|
selectedDailyGroupIdx: number;
|
||||||
letterGrid: string[];
|
letterGrid: string[];
|
||||||
modeOptions: ReadonlyArray<{ id: string; label: string; desc: string }>;
|
modeOptions: ReadonlyArray<{ id: string; label: string; desc: string }>;
|
||||||
isPreviewFavorite: boolean;
|
isPreviewFavorite: boolean;
|
||||||
@@ -118,8 +142,11 @@ createPage(
|
|||||||
showCaseToggle: false,
|
showCaseToggle: false,
|
||||||
showNextLetter: true,
|
showNextLetter: true,
|
||||||
showTripleGroupPicker: false,
|
showTripleGroupPicker: false,
|
||||||
|
showDailyGroupPicker: false,
|
||||||
tripleGroups: TRIPLE_GROUPS,
|
tripleGroups: TRIPLE_GROUPS,
|
||||||
selectedTripleGroupIdx: 0,
|
selectedTripleGroupIdx: 0,
|
||||||
|
dailyGroups: DAILY_GROUPS,
|
||||||
|
selectedDailyGroupIdx: 0,
|
||||||
letterGrid: LETTERS_PAIRS,
|
letterGrid: LETTERS_PAIRS,
|
||||||
modeOptions: [...MODES],
|
modeOptions: [...MODES],
|
||||||
isPreviewFavorite: false,
|
isPreviewFavorite: false,
|
||||||
@@ -204,6 +231,8 @@ createPage(
|
|||||||
this.data.traceMode === 'letter-tracing-case-pairing' ||
|
this.data.traceMode === 'letter-tracing-case-pairing' ||
|
||||||
this.data.traceMode === 'letter-tracing-half';
|
this.data.traceMode === 'letter-tracing-half';
|
||||||
const isTriple = this.data.traceMode === 'letter-tracing-three';
|
const isTriple = this.data.traceMode === 'letter-tracing-three';
|
||||||
|
const isDaily =
|
||||||
|
this.data.traceMode === 'letter-tracing-daily-checkin';
|
||||||
return mergeLetterTracingConfig(base, {
|
return mergeLetterTracingConfig(base, {
|
||||||
selectedLetter: this.data.selectedLetter,
|
selectedLetter: this.data.selectedLetter,
|
||||||
letterCase,
|
letterCase,
|
||||||
@@ -221,6 +250,13 @@ createPage(
|
|||||||
?.letters,
|
?.letters,
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
|
...(isDaily
|
||||||
|
? {
|
||||||
|
dailyLetters:
|
||||||
|
DAILY_GROUPS[this.data.selectedDailyGroupIdx]
|
||||||
|
?.letters,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -293,6 +329,14 @@ createPage(
|
|||||||
this.drawCanvas();
|
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 配置到页面状态
|
||||||
* 根据 worksheet 的 generatorConfig 决定 UI 控件的显隐(字母选择器、大小写切换等)
|
* 根据 worksheet 的 generatorConfig 决定 UI 控件的显隐(字母选择器、大小写切换等)
|
||||||
@@ -320,9 +364,15 @@ createPage(
|
|||||||
traceMode === 'letter-tracing-half';
|
traceMode === 'letter-tracing-half';
|
||||||
const showNextLetter = traceMode === 'letter-tracing-single';
|
const showNextLetter = traceMode === 'letter-tracing-single';
|
||||||
const showTripleGroupPicker = traceMode === 'letter-tracing-three';
|
const showTripleGroupPicker = traceMode === 'letter-tracing-three';
|
||||||
|
const showDailyGroupPicker =
|
||||||
|
traceMode === 'letter-tracing-daily-checkin';
|
||||||
const selectedLetter = options?.preserveLetter
|
const selectedLetter = options?.preserveLetter
|
||||||
? (this.data.selectedLetter ?? 'A')
|
? (this.data.selectedLetter ?? 'A')
|
||||||
: normalizeLetter(merged.selectedLetter ?? '');
|
: normalizeLetter(merged.selectedLetter ?? '');
|
||||||
|
const selectedDailyGroupIdx =
|
||||||
|
options?.preserveLetter && showDailyGroupPicker
|
||||||
|
? (this.data.selectedDailyGroupIdx ?? 0)
|
||||||
|
: getDailyGroupIndex(merged.dailyLetters);
|
||||||
const letterCaseLower =
|
const letterCaseLower =
|
||||||
traceMode === 'letter-tracing-case-pairing' ||
|
traceMode === 'letter-tracing-case-pairing' ||
|
||||||
traceMode === 'letter-tracing-half'
|
traceMode === 'letter-tracing-half'
|
||||||
@@ -343,6 +393,8 @@ createPage(
|
|||||||
showCaseToggle,
|
showCaseToggle,
|
||||||
showNextLetter,
|
showNextLetter,
|
||||||
showTripleGroupPicker,
|
showTripleGroupPicker,
|
||||||
|
showDailyGroupPicker,
|
||||||
|
selectedDailyGroupIdx,
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
this.initPageInfo(
|
this.initPageInfo(
|
||||||
|
|||||||
@@ -92,6 +92,23 @@
|
|||||||
</view>
|
</view>
|
||||||
</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
|
<view
|
||||||
wx:if="{{showNextLetter}}"
|
wx:if="{{showNextLetter}}"
|
||||||
class="lt-shuffle-btn"
|
class="lt-shuffle-btn"
|
||||||
|
|||||||
@@ -174,3 +174,43 @@ export function drawFourLineGrid(
|
|||||||
ctx.lineTo(x + w, yBot);
|
ctx.lineTo(x + w, yBot);
|
||||||
ctx.stroke();
|
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": {
|
"condition": {
|
||||||
"miniprogram": {
|
"miniprogram": {
|
||||||
"list": [
|
"list": [
|
||||||
|
{
|
||||||
|
"name": "englishPages/letterTracing/letterTracing",
|
||||||
|
"pathName": "englishPages/letterTracing/letterTracing",
|
||||||
|
"query": "id=letter-tracing-daily-checkin",
|
||||||
|
"scene": null,
|
||||||
|
"launchMode": "default"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "pages/category/category",
|
"name": "pages/category/category",
|
||||||
"pathName": "pages/category/category",
|
"pathName": "pages/category/category",
|
||||||
"query": "",
|
"query": "",
|
||||||
"scene": null,
|
"launchMode": "default",
|
||||||
"launchMode": "default"
|
"scene": null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "mathIndex",
|
"name": "mathIndex",
|
||||||
@@ -58,13 +65,6 @@
|
|||||||
"launchMode": "default",
|
"launchMode": "default",
|
||||||
"scene": null
|
"scene": null
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "englishPages/letterTracing/letterTracing",
|
|
||||||
"pathName": "englishPages/letterTracing/letterTracing",
|
|
||||||
"query": "id=letter-tracing-single",
|
|
||||||
"launchMode": "default",
|
|
||||||
"scene": null
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "pages/index/index",
|
"name": "pages/index/index",
|
||||||
"pathName": "pages/index/index",
|
"pathName": "pages/index/index",
|
||||||
|
|||||||
Reference in New Issue
Block a user