feat: 开发完成拼音每日一练
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
import { BaseDrawService } from '../../core/draw/baseDraw';
|
||||
import {
|
||||
GRID_COLORS,
|
||||
GRID_DASH,
|
||||
TRACING_COLORS,
|
||||
} from '../../core/data/tracingStyles';
|
||||
import { TONEOZ_PINYIN, fontFamilyOf } from '../../core/font/fontProfiles';
|
||||
import { PINYIN_STYLE } from './pinyinConstants';
|
||||
|
||||
export abstract class PinyinBaseDraw extends BaseDrawService {
|
||||
/**
|
||||
* 绘制一行四线三格
|
||||
*/
|
||||
protected drawFourLineRow(
|
||||
x: number,
|
||||
y: number,
|
||||
cols: number,
|
||||
cellW: number,
|
||||
cellH: number,
|
||||
) {
|
||||
const ctx = this.ctx;
|
||||
const totalW = cols * cellW;
|
||||
const yTop = y;
|
||||
const y1 = y + cellH / 3;
|
||||
const y2 = y + (cellH * 2) / 3;
|
||||
const yBot = y + cellH;
|
||||
|
||||
ctx.lineWidth = 1;
|
||||
|
||||
// 外边框(上下)
|
||||
ctx.strokeStyle = GRID_COLORS.border;
|
||||
ctx.setLineDash([]);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, yTop);
|
||||
ctx.lineTo(x + totalW, yTop);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, yBot);
|
||||
ctx.lineTo(x + totalW, yBot);
|
||||
ctx.stroke();
|
||||
|
||||
// 中间虚线
|
||||
ctx.strokeStyle = GRID_COLORS.middleLine;
|
||||
ctx.setLineDash([...GRID_DASH]);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y1);
|
||||
ctx.lineTo(x + totalW, y1);
|
||||
ctx.stroke();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y2);
|
||||
ctx.lineTo(x + totalW, y2);
|
||||
ctx.stroke();
|
||||
|
||||
// 垂直分割线
|
||||
ctx.strokeStyle = GRID_COLORS.border;
|
||||
ctx.setLineDash([]);
|
||||
for (let i = 0; i <= cols; i++) {
|
||||
const vx = x + i * cellW;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(vx, yTop);
|
||||
ctx.lineTo(vx, yBot);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制单个封闭四线三格(用于逐格绘制场景,如每日打卡)
|
||||
*/
|
||||
protected drawFourLineGrid(
|
||||
x: number,
|
||||
y: number,
|
||||
w: number,
|
||||
h: number,
|
||||
) {
|
||||
const ctx = this.ctx;
|
||||
const y1 = y + h / 3;
|
||||
const y2 = y + (h * 2) / 3;
|
||||
|
||||
ctx.save();
|
||||
|
||||
ctx.strokeStyle = GRID_COLORS.border;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.setLineDash([]);
|
||||
ctx.strokeRect(x, y, w, h);
|
||||
|
||||
ctx.strokeStyle = GRID_COLORS.middleLine;
|
||||
ctx.setLineDash([...GRID_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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 在四线三格中绘制拼音文本
|
||||
*/
|
||||
protected drawPinyinText(
|
||||
pinyin: string,
|
||||
cx: number,
|
||||
gridY: number,
|
||||
gridH: number,
|
||||
fontSizeRatio: number = PINYIN_STYLE.PINYIN_FONT_SIZE_RATIO,
|
||||
) {
|
||||
const ctx = this.ctx;
|
||||
const fontSize = Math.round(gridH * fontSizeRatio);
|
||||
const baselineY = gridY + (gridH * 2) / 3;
|
||||
|
||||
ctx.save();
|
||||
ctx.font = `${fontSize}px ${fontFamilyOf(TONEOZ_PINYIN)}`;
|
||||
ctx.fillStyle = TRACING_COLORS.guide;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'alphabetic';
|
||||
ctx.fillText(pinyin, cx, baselineY);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
export const PINYIN_STYLE = {
|
||||
MARGIN_X: 24, // 左右页边距
|
||||
CELL_H: 30, // 四线三格高度
|
||||
CELL_H_V2: 30, // V2 布局高度
|
||||
ROW_GAP: 5, // 同一分类中,行与行之间的纵向间距
|
||||
ROW_GAP_V2: 5, // V2 布局纵向间距
|
||||
SECTION_GAP: 8, // 大分类之间的间距
|
||||
TITLE_AFTER_GAP: 24, // 分类标题到其下方格子的间距
|
||||
CONTENT_TOP_GAP: 8, // Header 分割线到第一个分类标题的间距
|
||||
|
||||
// 字体
|
||||
SECTION_TITLE_FONT: 'bold 14px "Microsoft Yahei"',
|
||||
SUB_LABEL_FONT: '11px "Microsoft Yahei"',
|
||||
PINYIN_FONT_SIZE_RATIO: 0.7,
|
||||
PINYIN_FONT_SIZE_RATIO_V2: 0.72,
|
||||
|
||||
// 颜色
|
||||
SECTION_TITLE_COLOR: '#322E25',
|
||||
SUB_LABEL_COLOR: '#c0392b',
|
||||
};
|
||||
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* 拼音每日一练分组数据
|
||||
* 每页 4 个部分(四宫格),每个部分聚焦一个拼音及其相关练习
|
||||
*/
|
||||
|
||||
export interface PinyinDailyTask {
|
||||
char: string; // 核心字母,如 'a'
|
||||
variants: string[]; // 变体,如 ['ā', 'á', 'ǎ', 'à']
|
||||
type: 'shengmu' | 'yunmu' | 'zhengti';
|
||||
}
|
||||
|
||||
export const PINYIN_DAILY_GROUPS: PinyinDailyTask[][] = [
|
||||
// Day 1: 单韵母 a o e i
|
||||
[
|
||||
{ char: 'a', variants: ['ā', 'á', 'ǎ', 'à'], type: 'yunmu' },
|
||||
{ char: 'o', variants: ['ō', 'ó', 'ǒ', 'ò'], type: 'yunmu' },
|
||||
{ char: 'e', variants: ['ē', 'é', 'ě', 'è'], type: 'yunmu' },
|
||||
{ char: 'i', variants: ['ī', 'í', 'ǐ', 'ì'], type: 'yunmu' },
|
||||
],
|
||||
// Day 2: 单韵母 u ü + 声母 b p
|
||||
[
|
||||
{ char: 'u', variants: ['ū', 'ú', 'ǔ', 'ù'], type: 'yunmu' },
|
||||
{ char: 'ü', variants: ['ǖ', 'ǘ', 'ǚ', 'ǜ'], type: 'yunmu' },
|
||||
{ char: 'b', variants: [], type: 'shengmu' },
|
||||
{ char: 'p', variants: [], type: 'shengmu' },
|
||||
],
|
||||
// Day 3: 声母 m f d t
|
||||
[
|
||||
{ char: 'm', variants: [], type: 'shengmu' },
|
||||
{ char: 'f', variants: [], type: 'shengmu' },
|
||||
{ char: 'd', variants: [], type: 'shengmu' },
|
||||
{ char: 't', variants: [], type: 'shengmu' },
|
||||
],
|
||||
// Day 4: 声母 n l g k
|
||||
[
|
||||
{ char: 'n', variants: [], type: 'shengmu' },
|
||||
{ char: 'l', variants: [], type: 'shengmu' },
|
||||
{ char: 'g', variants: [], type: 'shengmu' },
|
||||
{ char: 'k', variants: [], type: 'shengmu' },
|
||||
],
|
||||
// Day 5: 声母 h j q x
|
||||
[
|
||||
{ char: 'h', variants: [], type: 'shengmu' },
|
||||
{ char: 'j', variants: [], type: 'shengmu' },
|
||||
{ char: 'q', variants: [], type: 'shengmu' },
|
||||
{ char: 'x', variants: [], type: 'shengmu' },
|
||||
],
|
||||
// Day 6: 声母 zh ch sh r
|
||||
[
|
||||
{ char: 'zh', variants: [], type: 'shengmu' },
|
||||
{ char: 'ch', variants: [], type: 'shengmu' },
|
||||
{ char: 'sh', variants: [], type: 'shengmu' },
|
||||
{ char: 'r', variants: [], type: 'shengmu' },
|
||||
],
|
||||
// Day 7: 声母 z c s y
|
||||
[
|
||||
{ char: 'z', variants: [], type: 'shengmu' },
|
||||
{ char: 'c', variants: [], type: 'shengmu' },
|
||||
{ char: 's', variants: [], type: 'shengmu' },
|
||||
{ char: 'y', variants: [], type: 'shengmu' },
|
||||
],
|
||||
// Day 8: 声母 w + 复韵母 ai ei ui
|
||||
[
|
||||
{ char: 'w', variants: [], type: 'shengmu' },
|
||||
{ char: 'ai', variants: ['āi', 'ái', 'ǎi', 'ài'], type: 'yunmu' },
|
||||
{ char: 'ei', variants: ['ēi', 'éi', 'ěi', 'èi'], type: 'yunmu' },
|
||||
{ char: 'ui', variants: ['uī', 'uí', 'uǐ', 'uì'], type: 'yunmu' },
|
||||
],
|
||||
// Day 9: 复韵母 ao ou iu ie
|
||||
[
|
||||
{ char: 'ao', variants: ['āo', 'áo', 'ǎo', 'ào'], type: 'yunmu' },
|
||||
{ char: 'ou', variants: ['ōu', 'óu', 'ǒu', 'òu'], type: 'yunmu' },
|
||||
{ char: 'iu', variants: ['iū', 'iú', 'iǔ', 'iù'], type: 'yunmu' },
|
||||
{ char: 'ie', variants: ['iē', 'ié', 'iě', 'iè'], type: 'yunmu' },
|
||||
],
|
||||
// Day 10: 复韵母 üe er + 前鼻韵母 an en
|
||||
[
|
||||
{ char: 'üe', variants: ['üē', 'üé', 'üě', 'üè'], type: 'yunmu' },
|
||||
{ char: 'er', variants: ['ēr', 'ér', 'ěr', 'èr'], type: 'yunmu' },
|
||||
{ char: 'an', variants: ['ān', 'án', 'ǎn', 'àn'], type: 'yunmu' },
|
||||
{ char: 'en', variants: ['ēn', 'én', 'ěn', 'èn'], type: 'yunmu' },
|
||||
],
|
||||
// Day 11: 前鼻韵母 in un ün + 后鼻韵母 ang
|
||||
[
|
||||
{ char: 'in', variants: ['īn', 'ín', 'ǐn', 'ìn'], type: 'yunmu' },
|
||||
{ char: 'un', variants: ['ūn', 'ún', 'ǔn', 'ùn'], type: 'yunmu' },
|
||||
{ char: 'ün', variants: ['ǖn', 'ǘn', 'ǚn', 'ǜn'], type: 'yunmu' },
|
||||
{ char: 'ang', variants: ['āng', 'áng', 'ǎng', 'àng'], type: 'yunmu' },
|
||||
],
|
||||
// Day 12: 后鼻韵母 eng ing ong + 整体认读 zhi
|
||||
[
|
||||
{ char: 'eng', variants: ['ēng', 'éng', 'ěng', 'èng'], type: 'yunmu' },
|
||||
{ char: 'ing', variants: ['īng', 'íng', 'ǐng', 'ìng'], type: 'yunmu' },
|
||||
{ char: 'ong', variants: ['ōng', 'óng', 'ǒng', 'òng'], type: 'yunmu' },
|
||||
{
|
||||
char: 'zhi',
|
||||
variants: ['zhī', 'zhí', 'zhǐ', 'zhì'],
|
||||
type: 'zhengti',
|
||||
},
|
||||
],
|
||||
// Day 13: 整体认读 chi shi ri zi
|
||||
[
|
||||
{
|
||||
char: 'chi',
|
||||
variants: ['chī', 'chí', 'chǐ', 'chì'],
|
||||
type: 'zhengti',
|
||||
},
|
||||
{
|
||||
char: 'shi',
|
||||
variants: ['shī', 'shí', 'shǐ', 'shì'],
|
||||
type: 'zhengti',
|
||||
},
|
||||
{ char: 'ri', variants: ['rī', 'rí', 'rǐ', 'rì'], type: 'zhengti' },
|
||||
{ char: 'zi', variants: ['zī', 'zí', 'zǐ', 'zì'], type: 'zhengti' },
|
||||
],
|
||||
// Day 14: 整体认读 ci si yi wu
|
||||
[
|
||||
{ char: 'ci', variants: ['cī', 'cí', 'cǐ', 'cì'], type: 'zhengti' },
|
||||
{ char: 'si', variants: ['sī', 'sí', 'sǐ', 'sì'], type: 'zhengti' },
|
||||
{ char: 'yi', variants: ['yī', 'yí', 'yǐ', 'yì'], type: 'zhengti' },
|
||||
{ char: 'wu', variants: ['wū', 'wú', 'wǔ', 'wù'], type: 'zhengti' },
|
||||
],
|
||||
// Day 15: 整体认读 yu ye yue yuan
|
||||
[
|
||||
{ char: 'yu', variants: ['yū', 'yú', 'yǔ', 'yù'], type: 'zhengti' },
|
||||
{ char: 'ye', variants: ['yē', 'yé', 'yě', 'yè'], type: 'zhengti' },
|
||||
{
|
||||
char: 'yue',
|
||||
variants: ['yuē', 'yué', 'yuě', 'yuè'],
|
||||
type: 'zhengti',
|
||||
},
|
||||
{
|
||||
char: 'yuan',
|
||||
variants: ['yuān', 'yuán', 'yuǎn', 'yuàn'],
|
||||
type: 'zhengti',
|
||||
},
|
||||
],
|
||||
// Day 16: 整体认读 yin yun ying
|
||||
[
|
||||
{
|
||||
char: 'yin',
|
||||
variants: ['yīn', 'yín', 'yǐn', 'yìn'],
|
||||
type: 'zhengti',
|
||||
},
|
||||
{
|
||||
char: 'yun',
|
||||
variants: ['yūn', 'yún', 'yǔn', 'yùn'],
|
||||
type: 'zhengti',
|
||||
},
|
||||
{
|
||||
char: 'ying',
|
||||
variants: ['yīng', 'yíng', 'yǐng', 'yìng'],
|
||||
type: 'zhengti',
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
/** 每日打卡选择器展示项 */
|
||||
export interface PinyinDailyPickerItem {
|
||||
index: number;
|
||||
label: string;
|
||||
subtitle: string;
|
||||
}
|
||||
|
||||
const CHIP_CHAR_MAX = 3;
|
||||
const CHIP_SUBTITLE_MAX = 14;
|
||||
|
||||
function shortCharForChip(char: string): string {
|
||||
if (char.length <= CHIP_CHAR_MAX) return char;
|
||||
return `${char.slice(0, CHIP_CHAR_MAX - 1)}…`;
|
||||
}
|
||||
|
||||
/** 构建「第 N 组 + 拼音摘要」选择器数据 */
|
||||
export function buildPinyinDailyPickerItems(): PinyinDailyPickerItem[] {
|
||||
return PINYIN_DAILY_GROUPS.map((group, idx) => {
|
||||
let subtitle = group.map((t) => shortCharForChip(t.char)).join('·');
|
||||
if (subtitle.length > CHIP_SUBTITLE_MAX) {
|
||||
subtitle = `${subtitle.slice(0, CHIP_SUBTITLE_MAX - 1)}…`;
|
||||
}
|
||||
return {
|
||||
index: idx,
|
||||
label: `第 ${idx + 1} 组`,
|
||||
subtitle,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { PINYIN_DAILY_GROUPS, type PinyinDailyTask } from './pinyinDailyData';
|
||||
import { TRACING_COLORS } from '../../core/data/tracingStyles';
|
||||
|
||||
export interface PinyinDailyCell {
|
||||
char: string;
|
||||
style: 'solid' | 'dashed';
|
||||
color: string;
|
||||
alpha: number;
|
||||
}
|
||||
|
||||
export interface PinyinDailyRow {
|
||||
cells: (PinyinDailyCell | null)[];
|
||||
}
|
||||
|
||||
export interface PinyinDailySection {
|
||||
task: PinyinDailyTask;
|
||||
rows: PinyinDailyRow[];
|
||||
cols: number;
|
||||
}
|
||||
|
||||
export interface PinyinDailyData {
|
||||
dayIndex: number;
|
||||
sections: PinyinDailySection[];
|
||||
}
|
||||
|
||||
const COLOR_BLACK = TRACING_COLORS.strong;
|
||||
const COLOR_LIGHT_RED = TRACING_COLORS.guide;
|
||||
|
||||
function buildRows(task: PinyinDailyTask, cols: number): PinyinDailyRow[] {
|
||||
const rows: PinyinDailyRow[] = [];
|
||||
const char = task.char;
|
||||
|
||||
// 每日打卡固定 7 行
|
||||
for (let r = 0; r < 7; r++) {
|
||||
const cells: (PinyinDailyCell | null)[] = [];
|
||||
let style: 'solid' | 'dashed' = 'solid';
|
||||
let alpha = 1;
|
||||
|
||||
// 样式:
|
||||
// 0-1行:实线描红 (第0行第0个黑色,其余浅红)
|
||||
// 2, 4, 6行:留空(null)
|
||||
// 3, 5行:虚线描红
|
||||
|
||||
if (r === 2 || r === 4 || r === 6) {
|
||||
for (let c = 0; c < cols; c++) cells.push(null);
|
||||
} else {
|
||||
if (r >= 3) {
|
||||
style = 'dashed';
|
||||
alpha = 0.5;
|
||||
}
|
||||
|
||||
for (let c = 0; c < cols; c++) {
|
||||
const color =
|
||||
r === 0 && c === 0 ? COLOR_BLACK : COLOR_LIGHT_RED;
|
||||
cells.push({ char, style, color, alpha });
|
||||
}
|
||||
}
|
||||
|
||||
rows.push({ cells });
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
export function generatePinyinDaily(dayIndex: number): PinyinDailyData {
|
||||
const group = PINYIN_DAILY_GROUPS[dayIndex] || PINYIN_DAILY_GROUPS[0];
|
||||
const sections: PinyinDailySection[] = group.map((task) => {
|
||||
const cols = task.char.length <= 2 ? 5 : 4;
|
||||
return {
|
||||
task,
|
||||
rows: buildRows(task, cols),
|
||||
cols,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
dayIndex: dayIndex + 1,
|
||||
sections,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user