diff --git a/96qiAacPdaLu78de7d9f9dfe65085df526e0ad9e2fea.jpg b/96qiAacPdaLu78de7d9f9dfe65085df526e0ad9e2fea.jpg new file mode 100644 index 0000000..4134ca0 Binary files /dev/null and b/96qiAacPdaLu78de7d9f9dfe65085df526e0ad9e2fea.jpg differ diff --git a/miniprogram/chinesePages/handwritingSheet/draw/wordDailyCheckinDraw.ts b/miniprogram/chinesePages/handwritingSheet/draw/wordDailyCheckinDraw.ts new file mode 100644 index 0000000..5860be6 --- /dev/null +++ b/miniprogram/chinesePages/handwritingSheet/draw/wordDailyCheckinDraw.ts @@ -0,0 +1,353 @@ +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 { loadFontFace } from '../../../core/font/fontLoader'; +import { + SECTION_PADDING_TOP, + SECTION_PADDING_BOTTOM, + HEADER_TITLE_GAP, + HEADER_META_GAP, + CONTENT_TOP_GAP, + FOOTER_PHRASE_GAP, + FOOTER_INDEX_GAP, + drawSectionHeader, + drawSectionFooter, + getRandomEncouragementPhrase, +} from '../../../core/draw/dailyPracticeDrawHelper'; +import { + getScalingTransform, + drawSvgPath, + drawTianZiGrid, +} from '../../shared/drawUtils'; + +export interface DailyCheckinHanziItem { + character: string; + pinyin: string; + strokes: string[]; +} + +const PAGE_MARGIN = 14; +const SECTION_PADDING_X = 18; + +const COLUMNS = 11; +const ROWS_PER_SECTION = 4; +const BLOCK_GAP = 0; +const PINYIN_ROW_RATIO = 0.52; // 拼音/笔画行高度占田字格高度比例 + +interface DrawSingleStrokeParams { + ctx: RenderingContext; + strokes: string[]; + offsetX: number; + offsetY: number; + width: number; + height: number; + fromIndex: number; + toIndex: number; + fillStyle: string; +} + +function drawStrokesRange({ + ctx, + strokes, + offsetX, + offsetY, + width, + height, + fromIndex, + toIndex, + fillStyle, +}: DrawSingleStrokeParams) { + const padding = Math.min(width, height) * 0.1; + const transform = getScalingTransform(width, height, padding); + + ctx.save(); + ctx.imageSmoothingEnabled = true; + ctx.imageSmoothingQuality = 'high'; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + ctx.miterLimit = 10; + ctx.fillStyle = fillStyle; + + for (let s = fromIndex; s <= toIndex && s < strokes.length; s++) { + ctx.save(); + ctx.translate( + offsetX + transform.xOffset, + offsetY + height - transform.yOffset, + ); + ctx.scale(transform.scale, -transform.scale); + ctx.beginPath(); + drawSvgPath(ctx, strokes[s]); + ctx.fill(); + ctx.restore(); + } + + ctx.restore(); +} + +export default class WordDailyCheckinDraw extends BaseDrawService { + async draw(items: DailyCheckinHanziItem[]) { + this.prepareDraw(); + await loadFontFace(TONEOZ_PINYIN); + + this.drawPageDivider(); + + const sectionRects = this.getSectionRects(); + const itemsPerSection = ROWS_PER_SECTION; + + for (let i = 0; i < sectionRects.length; i++) { + const rect = sectionRects[i]; + const sectionItems = items.slice( + i * itemsPerSection, + (i + 1) * itemsPerSection, + ); + if (sectionItems.length === 0) continue; + this.drawSection(rect, sectionItems, i + 1); + } + } + + private drawPageDivider() { + const cy = this.canvasHeight / 2; + this.drawLine(PAGE_MARGIN, cy, this.canvasWidth - PAGE_MARGIN, cy, { + isDashed: true, + dashPattern: [5, 5], + color: '#D6D2CC', + lineWidth: 1, + }); + } + + private getSectionRects() { + const halfH = this.canvasHeight / 2; + const fullW = this.canvasWidth; + return [ + { + x: PAGE_MARGIN, + y: PAGE_MARGIN, + w: fullW - PAGE_MARGIN * 2, + h: halfH - PAGE_MARGIN, + }, + { + x: PAGE_MARGIN, + y: halfH, + w: fullW - PAGE_MARGIN * 2, + h: this.canvasHeight - PAGE_MARGIN - halfH, + }, + ]; + } + + private drawSection( + rect: { x: number; y: number; w: number; h: number }, + items: DailyCheckinHanziItem[], + sectionIndex: number, + ) { + const phrase = getRandomEncouragementPhrase(); + drawSectionHeader(this.ctx, rect, '汉字每日打卡'); + drawSectionFooter(this.ctx, rect, phrase, `- ${sectionIndex} -`); + this.drawSectionContent(rect, items); + } + + private drawSectionContent( + rect: { x: number; y: number; w: number; h: number }, + items: DailyCheckinHanziItem[], + ) { + 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 contentLeft = rect.x + SECTION_PADDING_X; + const contentWidth = rect.w - SECTION_PADDING_X * 2; + + // 单元尺寸:田字格为正方形,受宽高约束取较小值 + const cellWFromWidth = contentWidth / COLUMNS; + // 每个 hanzi 块高度 = pinyinRow + tianzigeRow,取 pinyin = ratio * tianzige + const blockHeightFactor = 1 + PINYIN_ROW_RATIO; + const totalGap = (ROWS_PER_SECTION - 1) * BLOCK_GAP; + const cellWFromHeight = + (contentHeight - totalGap) / (blockHeightFactor * ROWS_PER_SECTION); + + const cellW = Math.min(cellWFromWidth, cellWFromHeight); + const tianzigeH = cellW; + const pinyinH = cellW * PINYIN_ROW_RATIO; + const blockH = tianzigeH + pinyinH; + const totalH = ROWS_PER_SECTION * blockH + totalGap; + + const startX = contentLeft + (contentWidth - cellW * COLUMNS) / 2; + const startY = contentTop + (contentHeight - totalH) / 2; + + for (let i = 0; i < items.length && i < ROWS_PER_SECTION; i++) { + const blockY = startY + i * (blockH + BLOCK_GAP); + this.drawHanziBlock(items[i], startX, blockY, cellW, pinyinH); + } + } + + private drawHanziBlock( + item: DailyCheckinHanziItem, + startX: number, + startY: number, + cellW: number, + pinyinH: number, + ) { + const tianzigeH = cellW; + const totalW = cellW * COLUMNS; + + // 拼音 + 笔画行(顶部) + this.drawPinyinStrokeRow(item, startX, startY, totalW, pinyinH, cellW); + + // 田字格练习行(底部) + const tianzigeTop = startY + pinyinH; + this.drawTianzigeRow(item, startX, tianzigeTop, cellW, tianzigeH); + } + + private drawPinyinStrokeRow( + item: DailyCheckinHanziItem, + startX: number, + startY: number, + totalW: number, + rowH: number, + cellW: number, + ) { + const ctx = this.ctx; + const y1 = startY + rowH / 3; + const y2 = startY + (rowH * 2) / 3; + const yBot = startY + rowH; + + ctx.save(); + + // 外边框 + ctx.strokeStyle = GRID_COLORS.border; + ctx.lineWidth = 1; + ctx.setLineDash([]); + ctx.strokeRect(startX, startY, totalW, rowH); + + // 中间两条虚线(四线三格)—— 仅第一格 + ctx.strokeStyle = GRID_COLORS.middleLine; + ctx.setLineDash([...GRID_DASH]); + ctx.beginPath(); + ctx.moveTo(startX, y1); + ctx.lineTo(startX + cellW, y1); + ctx.moveTo(startX, y2); + ctx.lineTo(startX + cellW, y2); + ctx.stroke(); + ctx.beginPath(); + ctx.setLineDash([]); + const vx = startX + cellW; + ctx.moveTo(vx, startY); + ctx.lineTo(vx, yBot); + ctx.stroke(); + ctx.restore(); + + // 列分隔线 + // ctx.strokeStyle = GRID_COLORS.border; + // ctx.setLineDash([]); + // for (let c = 1; c < COLUMNS; c++) { + // const vx = startX + c * cellW; + // ctx.beginPath(); + // ctx.moveTo(vx, startY); + // ctx.lineTo(vx, yBot); + // ctx.stroke(); + // } + + // 第一格:绘制拼音 + if (item.pinyin) { + const cx = startX + cellW / 2; + const fontSize = Math.round(rowH * 0.65); + ctx.save(); + ctx.font = `${fontSize}px ${fontFamilyOf(TONEOZ_PINYIN)}`; + ctx.fillStyle = TRACING_COLORS.strong; + ctx.textAlign = 'center'; + ctx.textBaseline = 'alphabetic'; + ctx.fillText(item.pinyin, cx, y2); + ctx.restore(); + } + + // 笔画格:第 2 格起,按笔画顺序展示当前进度 + const strokeCount = item.strokes.length; + for (let k = 1; k <= strokeCount && k < COLUMNS; k++) { + const cellX = startX + cellW / 2 + k * 15; + const currentIdx = k - 1; + + // 已绘制的历史笔画:浅灰 + if (currentIdx > 0) { + drawStrokesRange({ + ctx, + strokes: item.strokes, + offsetX: cellX, + offsetY: startY, + width: cellW, + height: rowH, + fromIndex: 0, + toIndex: currentIdx - 1, + fillStyle: TRACING_COLORS.guide, + }); + } + // 当前笔画:红色高亮 + drawStrokesRange({ + ctx, + strokes: item.strokes, + offsetX: cellX, + offsetY: startY, + width: cellW, + height: rowH, + fromIndex: currentIdx, + toIndex: currentIdx, + fillStyle: '#D94B4B', + }); + } + } + + private drawTianzigeRow( + item: DailyCheckinHanziItem, + startX: number, + startY: number, + cellW: number, + cellH: number, + ) { + const ctx = this.ctx; + const TRACING_INDICES = new Set([5, 8]); // 第 6 个和第 9 个(0-based: 5, 8) + + for (let c = 0; c < COLUMNS; c++) { + const cellX = startX + c * cellW; + const cx = cellX + cellW / 2; + const cy = startY + cellH / 2; + drawTianZiGrid({ ctx, cx, cy, size: cellW }); + + if (c === 0) { + drawStrokesRange({ + ctx, + strokes: item.strokes, + offsetX: cellX, + offsetY: startY, + width: cellW, + height: cellH, + fromIndex: 0, + toIndex: item.strokes.length - 1, + fillStyle: TRACING_COLORS.strong, + }); + } else if (TRACING_INDICES.has(c)) { + drawStrokesRange({ + ctx, + strokes: item.strokes, + offsetX: cellX, + offsetY: startY, + width: cellW, + height: cellH, + fromIndex: 0, + toIndex: item.strokes.length - 1, + fillStyle: TRACING_COLORS.guide, + }); + } + } + } +} diff --git a/miniprogram/chinesePages/handwritingSheet/draw/wordDrawService.ts b/miniprogram/chinesePages/handwritingSheet/draw/wordDrawService.ts index f429a7b..7286a87 100644 --- a/miniprogram/chinesePages/handwritingSheet/draw/wordDrawService.ts +++ b/miniprogram/chinesePages/handwritingSheet/draw/wordDrawService.ts @@ -9,123 +9,13 @@ * - symbol:符号 */ import { BaseDrawService } from '../../../core/draw/baseDraw'; -import { GRID_COLORS, TRACING_COLORS } from '../../../core/data/tracingStyles'; +import { TRACING_COLORS } from '../../../core/data/tracingStyles'; import { CharacterItem } from '../../../types/characterType'; - -/** - * cnchar-data 坐标系配置 - * 参考 hanzi-writer 的实现逻辑 - * 字符数据的边界框:左上角 (0, -124),右下角 (1024, 900) - */ -const CHAR_BOUNDS = { - minX: 0, - minY: -124, - maxX: 1024, - maxY: 900, -}; -const CHAR_WIDTH = CHAR_BOUNDS.maxX - CHAR_BOUNDS.minX; // 1024 -const CHAR_HEIGHT = CHAR_BOUNDS.maxY - CHAR_BOUNDS.minY; // 1024 - -/** - * 计算缩放变换参数(模仿 cnchar.draw 的 getScalingTransform 函数) - * 用于将 1024x1024 坐标系的字符数据变换到目标画布尺寸 - */ -interface ScalingTransform { - xOffset: number; - yOffset: number; - scale: number; -} - -function getScalingTransform( - width: number, - height: number, - padding: number, -): ScalingTransform { - // 计算可用空间 - const availableWidth = width - 2 * padding; - const availableHeight = height - 2 * padding; - - // 计算缩放比例(取较小的比例以确保字符完整显示) - const scaleX = availableWidth / CHAR_WIDTH; - const scaleY = availableHeight / CHAR_HEIGHT; - const scale = Math.min(scaleX, scaleY); - - // 计算偏移量(居中显示) - const scaledWidth = CHAR_WIDTH * scale; - const scaledHeight = CHAR_HEIGHT * scale; - const centerX = padding + (availableWidth - scaledWidth) / 2; - const centerY = padding + (availableHeight - scaledHeight) / 2; - - // 计算 xOffset 和 yOffset(考虑字符边界框的偏移) - const xOffset = -CHAR_BOUNDS.minX * scale + centerX; - const yOffset = -CHAR_BOUNDS.minY * scale + centerY; - - return { xOffset, yOffset, scale }; -} - -/** - * SVG 路径解析与绘制(支持 M/L/Q/Z 命令) - * 参考 cnchar.draw 的实现,使用 Canvas transform 进行坐标变换 - */ -interface DrawSvgPathParams { - ctx: RenderingContext; - pathD: string; -} - -function drawSvgPath({ ctx, pathD }: DrawSvgPathParams) { - // 使用正则表达式匹配 SVG 路径命令 - // 支持 M(moveTo)、L(lineTo)、Q(二次贝塞尔曲线)、Z(closePath) - const commandRegex = /([MLQZ])([^MLQZ]*?)(?=[MLQZ]|$)/gi; - const commands: Array<{ cmd: string; coords: string }> = []; - - let match; - while ((match = commandRegex.exec(pathD)) !== null) { - const cmd = match[1].toUpperCase(); - const coords = match[2].trim(); - commands.push({ cmd, coords }); - } - - // 解析坐标的辅助函数 - const parseCoords = (coords: string): number[] => { - return coords - .split(/[\s,]+/) - .filter((part) => part.trim() !== '') - .map((part) => parseFloat(part)); - }; - - for (const { cmd, coords } of commands) { - const coordValues = parseCoords(coords); - - switch (cmd) { - case 'M': - if (coordValues.length >= 2) { - ctx.moveTo(coordValues[0], coordValues[1]); - } - break; - case 'L': - if (coordValues.length >= 2) { - ctx.lineTo(coordValues[0], coordValues[1]); - } - break; - case 'Q': - // 二次贝塞尔曲线:Q cpx cpy x y - if (coordValues.length >= 4) { - ctx.quadraticCurveTo( - coordValues[0], - coordValues[1], - coordValues[2], - coordValues[3], - ); - } - break; - case 'Z': - ctx.closePath(); - break; - default: - console.warn(`未知的 SVG 路径命令: ${cmd}`); - } - } -} +import { + getScalingTransform, + drawSvgPath, + drawTianZiGrid, +} from '../../shared/drawUtils'; interface DrawStrokesParams { ctx: RenderingContext; @@ -187,7 +77,7 @@ function drawStrokes({ // 绘制路径 ctx.beginPath(); - drawSvgPath({ ctx, pathD: strokes[s] }); + drawSvgPath(ctx, strokes[s]); // 使用 fill 绘制(cnchar 只使用 fill) ctx.fillStyle = fillStyle; @@ -197,48 +87,6 @@ function drawStrokes({ } } -interface DrawTianZiGridParams { - ctx: RenderingContext; - x: number; - y: number; - size: number; - lineColor?: string; - boldColor?: string; -} - -function drawTianZiGrid({ - ctx, - x, - y, - size, - lineColor = GRID_COLORS.middleLine, - boldColor = GRID_COLORS.border, -}: DrawTianZiGridParams) { - ctx.strokeStyle = boldColor; - ctx.lineWidth = 1; - ctx.strokeRect(x - size / 2, y - size / 2, size, size); - - ctx.strokeStyle = lineColor; - ctx.lineWidth = 1; - ctx.beginPath(); - ctx.moveTo(x, y - size / 2); - ctx.lineTo(x, y + size / 2); - ctx.moveTo(x - size / 2, y); - ctx.lineTo(x + size / 2, y); - ctx.stroke(); - ctx.closePath(); - - ctx.strokeStyle = GRID_COLORS.diagonal; - ctx.lineWidth = 1; - ctx.beginPath(); - ctx.moveTo(x - size / 2, y - size / 2); - ctx.lineTo(x + size / 2, y + size / 2); - ctx.moveTo(x + size / 2, y - size / 2); - ctx.lineTo(x - size / 2, y + size / 2); - ctx.stroke(); - ctx.closePath(); -} - class WordDrawService extends BaseDrawService { constructor( canvas: Canvas, @@ -421,7 +269,7 @@ class WordDrawService extends BaseDrawService { for (let col = 0; col < maxCol; col++) { const x = startX + col * (cellSize + colGap) + cellSize / 2; const y = startY + row * (cellSize + rowGap) + cellSize / 2; - drawTianZiGrid({ ctx, x, y, size: cellSize }); + drawTianZiGrid({ ctx, cx: x, cy: y, size: cellSize }); } } } diff --git a/miniprogram/chinesePages/handwritingSheet/handwritingSheet.config.ts b/miniprogram/chinesePages/handwritingSheet/handwritingSheet.config.ts index 4b8b72b..782d149 100644 --- a/miniprogram/chinesePages/handwritingSheet/handwritingSheet.config.ts +++ b/miniprogram/chinesePages/handwritingSheet/handwritingSheet.config.ts @@ -1,8 +1,13 @@ import type { DebugPublishMeta } from '../../utils/debugPublish'; import { inferGradeFromAge } from '../../utils/debugPublish'; +export type HandwritingSheetMode = + | 'handwriting-sheet' + | 'handwriting-daily-checkin'; + interface HandwritingSheetWorksheetDefinition { - id: string; + id: HandwritingSheetMode; + icon: string; title: string; subtitle: string; ageMin: number; @@ -12,18 +17,31 @@ interface HandwritingSheetWorksheetDefinition { sortOrder: number; } -export const HANDWRITING_SHEET_WORKSHEET_DEFINITIONS = [ - { - id: 'handwriting-sheet', - title: '自定义练字帖', - subtitle: '自定义田字格练字帖', - ageMin: 4, - ageMax: 8, - difficulty: 2, - tags: ['练字', '字帖', '田字格', '汉字'], - sortOrder: 35, - }, -] as const satisfies ReadonlyArray; +export const HANDWRITING_SHEET_WORKSHEET_DEFINITIONS: HandwritingSheetWorksheetDefinition[] = + [ + { + id: 'handwriting-sheet', + icon: 'draw-o', + title: '自定义练字帖', + subtitle: '自定义田字格练字帖', + ageMin: 4, + ageMax: 8, + difficulty: 2, + tags: ['练字', '字帖', '田字格', '汉字'], + sortOrder: 35, + }, + { + id: 'handwriting-daily-checkin', + icon: 'task-o', + title: '汉字每日打卡', + subtitle: '每日 8 字带拼音笔顺打卡', + ageMin: 5, + ageMax: 8, + difficulty: 2, + tags: ['汉字', '每日练习', '打卡', '拼音', '笔顺'], + sortOrder: 36, + }, + ]; type HandwritingSheetWorksheetRow = (typeof HANDWRITING_SHEET_WORKSHEET_DEFINITIONS)[number]; @@ -32,6 +50,9 @@ const HANDWRITING_SHEET_WORKSHEET_BY_ID = Object.fromEntries( HANDWRITING_SHEET_WORKSHEET_DEFINITIONS.map((item) => [item.id, item]), ) as Record; +export const HANDWRITING_SHEET_MODE_OPTIONS = + HANDWRITING_SHEET_WORKSHEET_DEFINITIONS; + export const HANDWRITING_SHEET_WORKSHEET_ID = HANDWRITING_SHEET_WORKSHEET_DEFINITIONS[0].id; diff --git a/miniprogram/chinesePages/handwritingSheet/handwritingSheet.less b/miniprogram/chinesePages/handwritingSheet/handwritingSheet.less index 05d6a83..d361bac 100644 --- a/miniprogram/chinesePages/handwritingSheet/handwritingSheet.less +++ b/miniprogram/chinesePages/handwritingSheet/handwritingSheet.less @@ -24,6 +24,65 @@ margin-bottom: 20rpx; } +.cb-mode-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 24rpx; +} + +.cb-mode-card { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8rpx; + padding: 28rpx 20rpx; + border-radius: 24rpx; + background: #f8f0e0; + box-sizing: border-box; + transition: + transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), + background 0.2s cubic-bezier(0.4, 0, 0.2, 1); +} + +.cb-mode-card__icon { + display: inline-flex; + align-items: center; + justify-content: center; +} + +.cb-mode-card--active { + background: linear-gradient(145deg, #ffd709 0%, #efc900 100%); + box-shadow: 0 8rpx 32rpx rgba(50, 46, 37, 0.06); +} + +.cb-mode-card--pressed { + transform: scale(0.96); +} + +.cb-mode-card__label { + font-size: 28rpx; + font-weight: 700; + color: @text-secondary; + text-align: center; +} + +.cb-mode-card--active .cb-mode-card__label { + color: #453900; +} + +.cb-mode-card__desc { + font-size: 22rpx; + color: @text-secondary; + text-align: center; + opacity: 0.7; +} + +.cb-mode-card--active .cb-mode-card__desc { + color: #453900; + opacity: 0.85; +} + .cb-content-panel { background: rgba(240, 231, 214, 0.5); border-radius: @radius-lg; diff --git a/miniprogram/chinesePages/handwritingSheet/handwritingSheet.ts b/miniprogram/chinesePages/handwritingSheet/handwritingSheet.ts index 93d16f9..740441d 100644 --- a/miniprogram/chinesePages/handwritingSheet/handwritingSheet.ts +++ b/miniprogram/chinesePages/handwritingSheet/handwritingSheet.ts @@ -1,5 +1,13 @@ import WordDrawService from './draw/wordDrawService'; -import { getWordsSvgData } from '../../utils/getWordsSvgJson'; +import WordDailyCheckinDraw, { + DailyCheckinHanziItem, +} from './draw/wordDailyCheckinDraw'; +import { + getWordsSvgData, + getHanziReadingsData, + pickPrimaryPinyin, + HanziReadingsMap, +} from '../shared/getWordsSvgJson'; import { WORDS } from '../../core/data/words'; import { CharacterItem } from '../../types/characterType'; import tracker from '../../utils/tracker'; @@ -13,26 +21,43 @@ import { import type { DebugPublishMeta } from '../../utils/debugPublish'; import { HANDWRITING_SHEET_WORKSHEET_DEFINITIONS, + HANDWRITING_SHEET_MODE_OPTIONS, HANDWRITING_SHEET_WORKSHEET_ID, + HandwritingSheetMode, getModeInfo, getPublishMetaByMode, + isValidMode, } from './handwritingSheet.config'; -const MAX_WORDS = 11; +const CUSTOM_MAX_WORDS = 11; +const CHECKIN_MAX_WORDS = 8; const RANDOM_WORD_COUNT = 8; -const DEFAULT_WORDS = '东西南北日月山河风雨'; -const CATEGORY_TAGS = WORDS.slice(0, 3).map((cat) => ({ +const CUSTOM_DEFAULT_WORDS = '东西南北日月山河风雨'; +const CUSTOM_CATEGORY_TAGS = WORDS.slice(0, 3).map((cat) => ({ categoryId: cat.categoryId, icon: cat.icon, categoryName: cat.categoryName, })); +function collectFirstGradeWords(): string[] { + const grade = WORDS.find((cat) => cat.categoryId === 26); + if (!grade || !grade.sections) return []; + const merged: string[] = []; + for (const section of grade.sections) { + for (const w of section.words) merged.push(w); + } + return Array.from(new Set(merged)); +} + createPage( { canvas: null as Canvas | null, ctx: null as RenderingContext | null, wordDrawService: null as WordDrawService | null, + dailyDrawService: null as WordDailyCheckinDraw | null, svgWords: {} as Record, + readings: {} as HanziReadingsMap, + readingsLoaded: false, maxRow: 0 as number, maxCol: 0 as number, _favoritedMap: {} as Record, @@ -40,6 +65,9 @@ createPage( data: { pageTitle: HANDWRITING_SHEET_WORKSHEET_DEFINITIONS[0].title, functionId: HANDWRITING_SHEET_WORKSHEET_ID, + currentMode: + HANDWRITING_SHEET_WORKSHEET_ID as HandwritingSheetMode, + modeOptions: HANDWRITING_SHEET_MODE_OPTIONS, hasContent: false, words: [] as string[], inputWords: [] as string[], @@ -47,7 +75,9 @@ createPage( inputValue: '', showSelectWordPopup: false, pickerCurrentTab: 0, - categoryTags: CATEGORY_TAGS, + pickerMax: CUSTOM_MAX_WORDS, + categoryTags: CUSTOM_CATEGORY_TAGS, + showCustomPanel: true, showShareDialog: false, isPreviewFavorite: false, isDevEnv: false, @@ -56,13 +86,16 @@ createPage( debugPublishMeta: null as DebugPublishMeta | null, }, - async onLoad() { + async onLoad(options: { id?: string }) { this.syncDebugPublishEnv(); this.loadFavoritedMap(); - this.initPageInfo( - HANDWRITING_SHEET_WORKSHEET_ID, - HANDWRITING_SHEET_WORKSHEET_DEFINITIONS[0].title, - ); + + const initialMode: HandwritingSheetMode = + options.id && isValidMode(options.id) + ? (options.id as HandwritingSheetMode) + : HANDWRITING_SHEET_WORKSHEET_ID; + + this.applyMode(initialMode, { redraw: false }); await this.loadSvgWords(); this.initDefaultWords(); }, @@ -94,6 +127,16 @@ createPage( } }, + async ensureReadingsLoaded(): Promise { + if (this.readingsLoaded) return; + try { + this.readings = await getHanziReadingsData(); + this.readingsLoaded = true; + } catch (error) { + console.error('加载汉字读音数据失败:', error); + } + }, + async onCanvasReady(e: WechatMiniprogram.CustomEvent) { const { canvas, ctx } = e.detail; if (!canvas || !ctx) return; @@ -101,31 +144,81 @@ createPage( this.canvas = canvas; this.ctx = ctx; this.wordDrawService = new WordDrawService(canvas, ctx); + this.dailyDrawService = new WordDailyCheckinDraw(canvas, ctx); - await this.wordDrawService.drawLayout(); - - const { maxRow, maxCol } = this.wordDrawService.getMaxGridLayout(); - this.maxRow = maxRow; - this.maxCol = maxCol; + await this.prepareCanvasForCurrentMode(); if (this.data.words.length > 0) { await this.renderPracticeContent(); } }, + async prepareCanvasForCurrentMode() { + if (!this.canvas) return; + if (this.data.currentMode === 'handwriting-sheet') { + if (!this.wordDrawService) return; + await this.wordDrawService.drawLayout(); + const { maxRow, maxCol } = + this.wordDrawService.getMaxGridLayout(); + this.maxRow = maxRow; + this.maxCol = maxCol; + } else { + this.dailyDrawService?.prepareDraw(); + } + }, + + applyMode( + mode: HandwritingSheetMode, + options?: { redraw?: boolean }, + ) { + const isCheckin = mode === 'handwriting-daily-checkin'; + const max = isCheckin ? CHECKIN_MAX_WORDS : CUSTOM_MAX_WORDS; + + this.setData( + { + currentMode: mode, + functionId: mode, + pickerMax: max, + showCustomPanel: !isCheckin, + }, + () => { + this.initPageInfo(mode); + if (options?.redraw) { + this.resetForModeChange(); + } + }, + ); + }, + + async onSelectMode(e: WechatMiniprogram.TouchEvent) { + const id = e.currentTarget.dataset.id as + | HandwritingSheetMode + | undefined; + if (!id || id === this.data.currentMode) return; + this.applyMode(id, { redraw: true }); + }, + + async resetForModeChange() { + await this.prepareCanvasForCurrentMode(); + + // 切换模式后用对应模式的默认数据填充 + this.initDefaultWords(); + }, + onPreviewRefresh() { this.refreshWords(); }, async onPreviewFavorite() { + const id = this.data.currentMode; const next = !this.data.isPreviewFavorite; this.setData({ isPreviewFavorite: next }); - this._favoritedMap[HANDWRITING_SHEET_WORKSHEET_ID] = next; + this._favoritedMap[id] = next; if (next) { - await addFavorite(HANDWRITING_SHEET_WORKSHEET_ID); + await addFavorite(id); } else { - await removeFavorite(HANDWRITING_SHEET_WORKSHEET_ID); + await removeFavorite(id); } wx.showToast({ @@ -135,11 +228,12 @@ createPage( }, async loadFavoritedMap() { - this._favoritedMap = await batchCheckFavorited([ - HANDWRITING_SHEET_WORKSHEET_ID, - ]); + const ids = HANDWRITING_SHEET_WORKSHEET_DEFINITIONS.map( + (d) => d.id, + ); + this._favoritedMap = await batchCheckFavorited(ids); - if (this._favoritedMap[HANDWRITING_SHEET_WORKSHEET_ID]) { + if (this._favoritedMap[this.data.currentMode]) { this.setData({ isPreviewFavorite: true }); } }, @@ -158,9 +252,16 @@ createPage( this.clearWords(); }, + getMaxWordsForMode(): number { + return this.data.currentMode === 'handwriting-daily-checkin' + ? CHECKIN_MAX_WORDS + : CUSTOM_MAX_WORDS; + }, + syncWordsFromInput(value: string, showLimitToast: boolean) { + const max = this.getMaxWordsForMode(); const selectedWords = this.data.selectedWords as string[]; - const maxInputCount = Math.max(0, MAX_WORDS - selectedWords.length); + const maxInputCount = Math.max(0, max - selectedWords.length); const nextInputWords = this.splitToSingleChars(value).slice( 0, maxInputCount, @@ -204,12 +305,13 @@ createPage( }, onChangeWord(e: WechatMiniprogram.CustomEvent) { + const max = this.getMaxWordsForMode(); const nextSelectedWords = e.detail.selectedWords as string[]; const inputWords = this.data.inputWords as string[]; - if (inputWords.length + nextSelectedWords.length > MAX_WORDS) { + if (inputWords.length + nextSelectedWords.length > max) { wx.showToast({ - title: `最多只能添加 ${MAX_WORDS} 个字`, + title: `最多只能添加 ${max} 个字`, icon: 'none', }); return; @@ -273,9 +375,14 @@ createPage( }, initDefaultWords() { + if (this.data.currentMode === 'handwriting-daily-checkin') { + this.refreshWords(); + return; + } + const defaults = this.getSupportedWords( - this.splitToSingleChars(DEFAULT_WORDS), - ).slice(0, MAX_WORDS); + this.splitToSingleChars(CUSTOM_DEFAULT_WORDS), + ).slice(0, CUSTOM_MAX_WORDS); if (defaults.length === 0) { this.refreshWords(); @@ -294,17 +401,21 @@ createPage( }, refreshWords() { - const candidates = WORDS.slice(0, 3).reduce( - (acc: string[], category) => acc.concat(category.words), - [] as string[], - ); + const isCheckin = + this.data.currentMode === 'handwriting-daily-checkin'; + const candidates = isCheckin + ? collectFirstGradeWords() + : WORDS.slice(0, 3).reduce( + (acc: string[], category) => + acc.concat(category.words || []), + [] as string[], + ); + const shuffled = Array.from(new Set(candidates)).sort( () => Math.random() - 0.5, ); - const supported = this.getSupportedWords(shuffled).slice( - 0, - RANDOM_WORD_COUNT, - ); + const limit = isCheckin ? CHECKIN_MAX_WORDS : RANDOM_WORD_COUNT; + const supported = this.getSupportedWords(shuffled).slice(0, limit); if (supported.length === 0) { wx.showToast({ @@ -317,9 +428,9 @@ createPage( this.setData( { words: supported, - inputWords: supported, - selectedWords: [], - inputValue: supported.join(''), + inputWords: isCheckin ? [] : supported, + selectedWords: isCheckin ? supported : [], + inputValue: isCheckin ? '' : supported.join(''), }, () => this.renderPracticeContent().catch(console.error), ); @@ -331,9 +442,21 @@ createPage( }, async renderPracticeContent() { - if (!this.canvas || !this.wordDrawService) return; + if (!this.canvas) return; const words = this.data.words as string[]; + + if (this.data.currentMode === 'handwriting-daily-checkin') { + await this.renderCheckinContent(words); + return; + } + + await this.renderCustomContent(words); + }, + + async renderCustomContent(words: string[]) { + if (!this.wordDrawService) return; + if (words.length === 0) { await this.wordDrawService.drawContentEmpty(); this.setData({ hasContent: false }); @@ -368,6 +491,34 @@ createPage( this.setData({ hasContent: characterData.length > 0 }); }, + async renderCheckinContent(words: string[]) { + if (!this.dailyDrawService) return; + + const supportedWords = this.getSupportedWords(words).slice( + 0, + CHECKIN_MAX_WORDS, + ); + + if (supportedWords.length === 0) { + this.dailyDrawService.prepareDraw(); + this.setData({ hasContent: false }); + return; + } + + await this.ensureReadingsLoaded(); + + const items: DailyCheckinHanziItem[] = supportedWords.map( + (char: string) => ({ + character: char, + pinyin: pickPrimaryPinyin(this.readings, char), + strokes: this.svgWords[char] || [], + }), + ); + + await this.dailyDrawService.draw(items); + this.setData({ hasContent: items.length > 0 }); + }, + getSupportedWords(words: string[]): string[] { return words.filter((word) => this.svgWords[word]); }, @@ -401,7 +552,7 @@ createPage( tracker.reportShare('练字贴'); return { ...defaultShareConfig, - path: `/chinesePages/handwritingSheet/handwritingSheet?id=${HANDWRITING_SHEET_WORKSHEET_ID}`, + path: `/chinesePages/handwritingSheet/handwritingSheet?id=${this.data.currentMode}`, }; }, @@ -409,12 +560,12 @@ createPage( tracker.reportShare('练字贴'); return { ...defaultShareConfig, - query: `id=${HANDWRITING_SHEET_WORKSHEET_ID}`, + query: `id=${this.data.currentMode}`, }; }, getPublishMeta(): DebugPublishMeta { - const meta = getPublishMetaByMode(HANDWRITING_SHEET_WORKSHEET_ID); + const meta = getPublishMetaByMode(this.data.currentMode); if (!meta) { throw new Error('当前题型配置不存在'); } diff --git a/miniprogram/chinesePages/handwritingSheet/handwritingSheet.wxml b/miniprogram/chinesePages/handwritingSheet/handwritingSheet.wxml index 0ea2ee1..5a5fd76 100644 --- a/miniprogram/chinesePages/handwritingSheet/handwritingSheet.wxml +++ b/miniprogram/chinesePages/handwritingSheet/handwritingSheet.wxml @@ -11,13 +11,38 @@ bind:refresh="onPreviewRefresh" bind:favorite="onPreviewFavorite" /> + + 练习类型 + + + + {{item.title}} + {{item.subtitle}} + + + + 内容设置 - + 自定义文字 - 最多输入11个字 + 最多输入{{pickerMax}}个字 - 已选文字 + 已选文字 ({{words.length}}/{{pickerMax}}) 点击文字可删除 @@ -123,7 +150,7 @@ show="{{showSelectWordPopup}}" selectedWords="{{selectedWords}}" currentTab="{{pickerCurrentTab}}" - max="11" + max="{{pickerMax}}" bind:onClose="closeSelectWordPopup" bind:onChange="onChangeWord" /> diff --git a/miniprogram/chinesePages/shared/drawUtils.ts b/miniprogram/chinesePages/shared/drawUtils.ts new file mode 100644 index 0000000..f1d763f --- /dev/null +++ b/miniprogram/chinesePages/shared/drawUtils.ts @@ -0,0 +1,137 @@ +import { GRID_COLORS } from '../../core/data/tracingStyles'; + +/** + * cnchar-data 1024×1024 坐标系 + */ +export const CHAR_BOUNDS = { + minX: 0, + minY: -124, + maxX: 1024, + maxY: 900, +}; +export const CHAR_WIDTH = CHAR_BOUNDS.maxX - CHAR_BOUNDS.minX; +export const CHAR_HEIGHT = CHAR_BOUNDS.maxY - CHAR_BOUNDS.minY; + +export interface ScalingTransform { + xOffset: number; + yOffset: number; + scale: number; +} + +export function getScalingTransform( + width: number, + height: number, + padding: number, +): ScalingTransform { + const availableWidth = width - 2 * padding; + const availableHeight = height - 2 * padding; + + const scaleX = availableWidth / CHAR_WIDTH; + const scaleY = availableHeight / CHAR_HEIGHT; + const scale = Math.min(scaleX, scaleY); + + const scaledWidth = CHAR_WIDTH * scale; + const scaledHeight = CHAR_HEIGHT * scale; + const centerX = padding + (availableWidth - scaledWidth) / 2; + const centerY = padding + (availableHeight - scaledHeight) / 2; + + const xOffset = -CHAR_BOUNDS.minX * scale + centerX; + const yOffset = -CHAR_BOUNDS.minY * scale + centerY; + + return { xOffset, yOffset, scale }; +} + +/** + * SVG 路径解析与绘制(支持 M/L/Q/Z 命令) + */ +export function drawSvgPath(ctx: RenderingContext, pathD: string) { + const commandRegex = /([MLQZ])([^MLQZ]*?)(?=[MLQZ]|$)/gi; + const commands: Array<{ cmd: string; coords: string }> = []; + let match; + while ((match = commandRegex.exec(pathD)) !== null) { + commands.push({ cmd: match[1].toUpperCase(), coords: match[2].trim() }); + } + const parseCoords = (coords: string): number[] => + coords + .split(/[\s,]+/) + .filter((part) => part.trim() !== '') + .map((part) => parseFloat(part)); + + for (const { cmd, coords } of commands) { + const v = parseCoords(coords); + switch (cmd) { + case 'M': + if (v.length >= 2) ctx.moveTo(v[0], v[1]); + break; + case 'L': + if (v.length >= 2) ctx.lineTo(v[0], v[1]); + break; + case 'Q': + if (v.length >= 4) ctx.quadraticCurveTo(v[0], v[1], v[2], v[3]); + break; + case 'Z': + ctx.closePath(); + break; + } + } +} + +const GRID_CROSS_DASH = [4, 3]; + +export interface DrawTianZiGridParams { + ctx: RenderingContext; + /** 田字格中心 x */ + cx: number; + /** 田字格中心 y */ + cy: number; + /** 田字格边长 */ + size: number; + borderColor?: string; + middleLineColor?: string; + diagonalColor?: string; + /** 是否绘制对角线(X线),默认不绘制 */ + showDiagonal?: boolean; +} + +export function drawTianZiGrid({ + ctx, + cx, + cy, + size, + borderColor = GRID_COLORS.border, + middleLineColor = GRID_COLORS.middleLine, + diagonalColor = GRID_COLORS.diagonal, + showDiagonal = false, +}: DrawTianZiGridParams) { + const half = size / 2; + + // 外边框 — 实线 + ctx.strokeStyle = borderColor; + ctx.lineWidth = 1; + ctx.setLineDash([]); + ctx.strokeRect(cx - half, cy - half, size, size); + + // 十字线 — 虚线 + ctx.strokeStyle = middleLineColor; + ctx.setLineDash(GRID_CROSS_DASH); + ctx.beginPath(); + ctx.moveTo(cx, cy - half); + ctx.lineTo(cx, cy + half); + ctx.moveTo(cx - half, cy); + ctx.lineTo(cx + half, cy); + ctx.stroke(); + + // 对角线 — 虚线 + if (showDiagonal) { + ctx.strokeStyle = diagonalColor; + ctx.setLineDash(GRID_CROSS_DASH); + ctx.beginPath(); + ctx.moveTo(cx - half, cy - half); + ctx.lineTo(cx + half, cy + half); + ctx.moveTo(cx + half, cy - half); + ctx.lineTo(cx - half, cy + half); + ctx.stroke(); + } + + ctx.setLineDash([]); +} diff --git a/miniprogram/utils/getWordsSvgJson.ts b/miniprogram/chinesePages/shared/getWordsSvgJson.ts similarity index 66% rename from miniprogram/utils/getWordsSvgJson.ts rename to miniprogram/chinesePages/shared/getWordsSvgJson.ts index 9d5accd..2f2d900 100644 --- a/miniprogram/utils/getWordsSvgJson.ts +++ b/miniprogram/chinesePages/shared/getWordsSvgJson.ts @@ -1,4 +1,4 @@ -import { getJson } from './http'; +import { getJson } from '../../utils/http'; /** * SVG汉字数据缓存配置 @@ -9,9 +9,30 @@ const CACHE_CONFIG = { // url: 'https://cdn.joeyone.cn/doodle/words-svg-all.json' // 远程数据地址 // url: 'https://cdn.joeyone.cn/doodle/words-svg-3000.json' // 远程数据地址 // url: 'https://cdn.joeyone.cn/doodle/char_svg_3500.json' // 远程数据地址 - url: 'https://cdn.joeyone.cn/doodle/char_common_stroke.json', // 远程数据地址 + url: 'https://cdn.joeyone.cn/doodle/hanzi/hanzi_stroke_3500.json', // 远程数据地址 + hanziReadingsUrl: + 'https://cdn.joeyone.cn/doodle/hanzi/hanzi_readings_3500.json', // 远程数据地址 }; +/** + * 汉字读音数据缓存配置 + */ +const READINGS_CACHE_CONFIG = { + key: 'hanziReadingsData', + expireDays: 7, + url: CACHE_CONFIG.hanziReadingsUrl, +}; + +/** + * 汉字读音原始数据条目: + * - 单音字:[pinyin, gloss, introduce] + * - 多音字:[[pinyin, gloss, introduce], ...] + */ +export type HanziReadingEntry = + | [string, string, string] + | [string, string, string][]; +export type HanziReadingsMap = Record; + /** * 缓存数据结构 */ @@ -135,6 +156,65 @@ export async function getWordsSvgData(): Promise> { } } +/** + * 获取汉字读音数据(带缓存机制) + * 数据格式参见 HanziReadingsMap 类型说明,单音字为一维数组,多音字为二维数组。 + */ +export async function getHanziReadingsData(): Promise { + try { + const cacheStr = wx.getStorageSync(READINGS_CACHE_CONFIG.key); + if (cacheStr) { + const cacheData = JSON.parse(cacheStr) as { + data: HanziReadingsMap; + timestamp: number; + }; + const expireMs = + READINGS_CACHE_CONFIG.expireDays * 24 * 60 * 60 * 1000; + if (Date.now() - cacheData.timestamp <= expireMs) { + return cacheData.data; + } + wx.removeStorageSync(READINGS_CACHE_CONFIG.key); + } + } catch (error) { + console.error('读取汉字读音缓存失败:', error); + } + + const remote = await getJson(READINGS_CACHE_CONFIG.url, { + timeout: 15000, + header: { + Accept: 'application/json', + 'Cache-Control': 'no-cache', + }, + }); + + try { + wx.setStorageSync( + READINGS_CACHE_CONFIG.key, + JSON.stringify({ data: remote, timestamp: Date.now() }), + ); + } catch (error) { + console.error('保存汉字读音缓存失败:', error); + } + + return remote; +} + +/** + * 取出汉字的主拼音(多音字取第一个发音),未命中或数据非法时返回空串。 + */ +export function pickPrimaryPinyin( + readings: HanziReadingsMap, + char: string, +): string { + const entry = readings[char]; + if (!entry) return ''; + if (typeof entry[0] === 'string') { + return (entry as [string, string, string])[0] || ''; + } + const first = (entry as [string, string, string][])[0]; + return Array.isArray(first) ? first[0] || '' : ''; +} + /** * 清除SVG汉字数据缓存 */ diff --git a/miniprogram/components3.0/word-picker/word-picker.less b/miniprogram/components3.0/word-picker/word-picker.less index e15323c..5800c3d 100644 --- a/miniprogram/components3.0/word-picker/word-picker.less +++ b/miniprogram/components3.0/word-picker/word-picker.less @@ -37,6 +37,34 @@ overflow-y: auto; height: 500rpx; + .word-section { + margin-bottom: 36rpx; + + &:last-child { + margin-bottom: 0; + } + + .word-section-title { + font-size: 26rpx; + color: #888888; + padding-left: 28rpx; + margin-bottom: 20rpx; + font-weight: bold; + display: flex; + align-items: center; + + &::before { + content: ""; + display: inline-block; + width: 6rpx; + height: 24rpx; + background-color: #93d333; + margin-right: 12rpx; + border-radius: 3rpx; + } + } + } + .word-list { display: grid; grid-template-columns: repeat(6, 72rpx); diff --git a/miniprogram/components3.0/word-picker/word-picker.wxml b/miniprogram/components3.0/word-picker/word-picker.wxml index b469d3d..8dcb98c 100644 --- a/miniprogram/components3.0/word-picker/word-picker.wxml +++ b/miniprogram/components3.0/word-picker/word-picker.wxml @@ -31,17 +31,42 @@ wx:key="categoryIndex" title="{{wordList[categoryIndex].icon}} {{wordList[categoryIndex].categoryName}}"> - + - {{item}} - + class="word-section" + wx:for="{{wordList[categoryIndex].sections}}" + wx:for-item="section" + wx:for-index="sectionIndex" + wx:key="sectionIndex"> + {{section.sectionName}} + + + {{item}} + + - + + + + + {{item}} + + + + diff --git a/miniprogram/core/data/words.ts b/miniprogram/core/data/words.ts index 8141327..9e169ad 100644 --- a/miniprogram/core/data/words.ts +++ b/miniprogram/core/data/words.ts @@ -1,6 +1,44 @@ // 为避免保存时的自动格式化将数组元素强制换行,保持紧凑展示 // prettier-ignore export const WORDS = [ + { + categoryId: 26, + icon: '🎒', + categoryName: '一年级', + sections: [ + { + sectionName: '一年级上册', + words: [ + '天', '地', '人', '你', '我', '他', '一', '二', '三', '四', '五', '上', '下', '口', '耳', '目', '手', '足', '站', '坐', '日', '月', '水', '火', '山', '石', '田', '禾', '对', '云', '雨', '风', '花', '鸟', '虫', '六', '七', '八', '九', '十', '爸', '妈', '马', '土', '不', '画', '打', '棋', '鸡', '字', '词', '语', '句', '子', '桌', '纸', '文', '数', '学', '音', '乐', '妹', '奶', '白', '皮', '草', '家', '是', '小', '桥', '台', '车', '羊', '走', '也', '雪', '儿', '秋', '气', '了', '树', '叶', '片', '大', '飞', '会', '个', '的', '船', '两', '头', '在', '里', '看', '见', '闪', '星', '江', '南', '可', '采', '莲', '鱼', '东', '西', '北', '尖', '说', '春', '青', '蛙', '夏', '弯', '冬', '男', '女', '开', '关', '正', '反', '远', '有', '色', '近', '听', '无', '声', '去', '还', '来', '多', '少', '黄', '牛', '只', '猫', '边', '鸭', '苹', '果', '杏', '桃', '书', '包', '尺', '作', '业', '本', '笔', '刀', '课', '早', '校', '明', '力', '尘', '从', '众', '双', '木', '林', '森', '条', '心', '升', '国', '旗', '中', '红', '歌', '起', '么', '美', '丽', '立', '午', '晚', '昨', '今', '年', '影', '前', '后', '黑', '狗', '左', '右', '它', '好', '朋', '友', '比', '尾', '巴', '谁', '长', '短', '把', '伞', '兔', '最', '公', '写', '诗', '点', '要', '过', '给', '当', '串', '们', '以', '成', '彩', '半', '空', '问', '到', '方', '没', '更', '绿', '出', '睡', '那', '海', '真', '老', '师', '吗', '同', '什', '才', '亮', '时', '候', '觉', '得', '自', '己', '很', '穿', '衣', '服', '门', '快', '蓝', '又', '笑', '着', '向', '和', '贝', '娃', '挂', '活', '金', '哥', '姐', '弟', '叔', '爷', '群', '竹', '牙', '用', '几', '步', '为', '参', '加', '洞', '乌', '鸦', '处', '找', '办', '旁', '许', '法', '放', '进', '高', '住', '孩', '玩', '吧', '发', '芽', '爬', '呀', '久', '回', '全', '变', '工', '厂', '医', '院', '生' + ] + }, + { + sectionName: '一年级下册', + words: [ + '春', '夏', '秋', '冬', '风', '雪', '花', '入', '吹', '落', '降', '飘', '池', '游', '姓', '氏', '李', '张', '古', '吴', '赵', '钱', '孙', '周', '王', '官', '什', '么', '双', '国', '方', '青', '清', '气', '晴', '情', '请', '生', '眼', '睛', '保', '护', '害', '事', '让', '病', '相', '遇', '喜', '欢', '怕', '言', '互', '令', '动', '万', '纯', '净', '阴', '雷', '电', '阵', '雨', '冰', '冻', '夹', '吃', '忘', '井', '村', '叫', '毛', '主', '席', '乡', '亲', '战', '士', '面', '想', '告', '诉', '路', '京', '安', '门', '广', '非', '常', '壮', '观', '接', '觉', '再', '做', '各', '种', '样', '梦', '伙', '伴', '却', '趣', '这', '些', '都', '太', '阳', '道', '送', '忙', '尝', '香', '甜', '温', '暖', '该', '颜', '因', '只', '窝', '孤', '单', '邻', '居', '招', '呼', '静', '夜', '床', '光', '疑', '举', '望', '低', '故', '色', '外', '看', '爸', '晚', '笑', '偏', '散', '勇', '敢', '往', '胆', '微', '端', '粽', '节', '叶', '分', '总', '米', '间', '肉', '带', '知', '据', '念', '红', '小', '豆', '虹', '座', '浇', '提', '洒', '水', '挑', '兴', '镜', '照', '千', '秋', '裙', '蜻', '体', '之', '初', '性', '善', '专', '幼', '玉', '器', '义', '教', '首', '眠', '处', '闻', '散', '歌', '惜', '柔', '露', '荷', '珠', '摇', '篮', '晶', '停', '坪', '透', '翅', '膀', '蹲', '嘻', '展', '叹', '哈', '呀', '钟', '元', '洗', '背', '汽', '决', '定', '交', '共', '已', '经', '物', '外', '看', '爸', '晚', '笑', '偏', '散', '勇', '敢', '往', '胆', '微', '端', '粽', '节', '叶', '分', '总', '米', '间', '肉', '带', '知', '据', '念', '红', '小', '豆', '虹', '座', '浇', '提', '洒', '水', '挑', '兴', '镜', '照', '千', '秋', '裙', '蜻', '蜓', '迷', '藏', '造', '蚂', '蚁', '食', '粮', '蜘', '蛛', '网', '凉', '细', '夕', '李', '语', '香', '操', '场', '拔', '拍', '跑', '踢', '铃', '热', '闹', '锻', '炼', '体', '之', '初', '性', '善', '专', '幼', '玉', '器', '义', '教', '首', '眠', '处', '闻', '散', '歌', '惜', '柔', '露', '荷', '珠', '摇', '篮', '晶', '停', '坪', '透', '翅', '膀', '蹲', '嘻', '展', '叹', '哈', '呀', '钟', '元', '洗', '背', '汽', '决', '定', '交', '共', '已', '经', '物', '虎', '熊', '准', '备', '第', '次', '播', '百', '齐', '盼', '店', '迈', '卖', '怎', '独', '绳', '讲', '得', '阻', '领', '壁', '借', '健', '康', '寿', '新' + ] + } + ] + }, + { + categoryId: 27, + icon: '🏫', + categoryName: '二年级', + sections: [ + { + sectionName: '二年级上册', + words: [ + '塘', '脑', '袋', '灰', '哇', '教', '捕', '迎', '阿', '姨', '宽', '龟', '顶', '披', '鼓', '两', '哪', '睛', '肚', '皮', '孩', '跳', '晒', '极', '傍', '管', '越', '滴', '溪', '奔', '坏', '淹', '没', '冲', '毁', '屋', '猜', '变', '片', '海', '作', '给', '带', '植', '如', '为', '旅', '靠', '备', '纷', '刺', '底', '炸', '离', '察', '识', '粗', '得', '套', '帽', '登', '鞋', '裤', '图', '意', '指', '针', '滩', '艘', '军', '舰', '帆', '稻', '园', '孔', '翠', '队', '铜', '号', '领', '巾', '杨', '壮', '桐', '枫', '松', '柏', '装', '桦', '耐', '守', '疆', '银', '杉', '化', '桂', '牢', '记', '歌', '丛', '深', '处', '六', '熊', '猫', '九', '朋', '友', '季', '蝴', '蝶', '麦', '苗', '嫩', '桑', '肥', '农', '归', '戴', '场', '谷', '粒', '虽', '辛', '苦', '洋', '了', '葡', '萄', '紫', '狐', '狸', '笨', '酸', '曹', '称', '员', '柱', '议', '论', '重', '杆', '秤', '砍', '倒', '割', '线', '止', '量', '级', '术', '由', '挥', '粉', '板', '妙', '瓶', '合', '盛', '丑', '兴', '奋', '干', '标', '补', '认', '哄', '先', '闭', '紧', '润', '蛋', '等', '吸', '发', '粘', '意', '额', '沙', '乏', '弹', '钢', '琴', '泥', '滚', '铁', '环', '荡', '滑', '梯', '楼', '依', '尽', '欲', '穷', '层', '瀑', '布', '炉', '烟', '遥', '川', '闻', '名', '景', '区', '省', '部', '秀', '神', '尤', '其', '仙', '巨', '位', '著', '形', '状', '湖', '绕', '围', '胜', '央', '岛', '华', '隐', '约', '纱', '童', '境', '引', '客', '沟', '产', '梨', '份', '种', '搭', '棚', '淡', '够', '好', '收', '城', '市', '留', '定', '利', '分', '味', '康', '寿', '轿', '救', '摩', '托', '防', '渔', '货', '科', '考', '宿', '寺', '危', '辰', '恐', '惊', '似', '庐', '笼', '苍', '茫', '雾', '淘', '气', '于', '暗', '岸', '街', '梁', '甚', '至', '切', '躲', '失', '呀', '累', '淋', '灭', '激', '唱', '赶', '旺', '旁', '浑', '谁', '轻', '汽', '假', '威', '寻', '扑', '转', '扯', '嗓', '派', '抗', '爪', '趟', '神', '猪', '受', '骗', '酪', '捡', '俩', '始', '拌', '帮', '匀', '嚷', '瞧', '剩', '整', '筝', '鼠', '折', '漂', '啦', '抓', '幸', '福', '受', '愿', '哭', '取', '抽', '续', '吸', '极', '弯', '表', '示', '汗', '责', '伤', '路', '急', '帮', '泼', '族', '民', '度', '敲', '龙', '驶', '铺', '盛', '碗', '朱', '德', '扁', '担', '志', '伍', '敌', '抽', '仗', '难', '陡', '难', '占', '攻', '洪', '毒', '蛇', '兽', '伤', '灾', '难', '退', '被', '耕', '恢', '复', '治', '葫', '芦', '藤', '谢', '蚜', '盯', '赛', '感', '怪', '慢', '治' + ] + }, + { + sectionName: '二年级下册', + words: [ + '诗', '村', '童', '碧', '妆', '绿', '丝', '剪', '冲', '寻', '姑', '娘', '吐', '柳', '荡', '桃', '杏', '鲜', '邮', '递', '员', '原', '叔', '局', '堆', '礼', '邓', '植', '格', '引', '注', '满', '休', '息', '锋', '昨', '冒', '留', '弯', '背', '洒', '温', '暖', '能', '桌', '味', '买', '具', '甘', '甜', '菜', '劳', '匹', '妹', '波', '纹', '像', '景', '恋', '舍', '求', '州', '湾', '岛', '港', '民', '族', '谊', '齐', '奋', '贴', '街', '舟', '艾', '敬', '转', '团', '热', '闹', '贝', '壳', '甲', '骨', '钱', '币', '与', '财', '关', '烧', '茄', '烤', '鸭', '肉', '鸡', '蛋', '炒', '饭', '彩', '梦', '森', '拉', '结', '苹', '般', '精', '灵', '伞', '边', '意', '弟', '便', '教', '游', '戏', '母', '周', '围', '句', '补', '充', '药', '合', '死', '记', '屁', '股', '尿', '净', '屎', '幸', '使', '劲', '亡', '牢', '钻', '劝', '丢', '告', '筋', '疲', '图', '课', '摆', '座', '交', '哈', '页', '抢', '嘻', '愿', '意', '麦', '该', '伯', '刻', '突', '掉', '湖', '莲', '穷', '荷', '绝', '含', '岭', '吴', '博', '馆', '览', '器', '梢', '帮', '助', '导', '碰', '特', '积', '密', '稀', '针', '沟', '闯', '川', '灾', '户', '容', '易', '损', '失', '赚', '赔', '购', '贫', '富', '竖', '茸', '兔', '撑', '扇', '慢', '遇', '丰', '最', '喂', '痛', '耷', '拉', '视', '频', '织', '编', '怎', '抽', '纺', '阻', '碍', '阻', '怜', '扁', '敏', '挣', '脱', '绒', '毛', '扫', '拂', '帚', '抹', '擦', '拖', '倒', '桶', '缸', '脏', '熟', '祖', '浓', '望', '蓝', '摘', '掏', '赛', '忆', '双', '啊', '逮', '蝈', '蛛', '蜘', '蝉', '挖', '铲', '由', '先', '绿', '边', '天', '野', '果', '雀', '跑', '红', '蜻', '蜓', '捉', '简', '单', '纪', '幼', '粗', '糙', '睁', '式', '此', '重', '复' + ] + } + ] + }, { categoryId: 0, icon: '🌟', diff --git a/miniprogram/englishPages/letterTracing/letterTracing.config.ts b/miniprogram/englishPages/letterTracing/letterTracing.config.ts index 1b25635..864604b 100644 --- a/miniprogram/englishPages/letterTracing/letterTracing.config.ts +++ b/miniprogram/englishPages/letterTracing/letterTracing.config.ts @@ -19,7 +19,7 @@ export const LETTER_TRACING_WORKSHEET_DEFINITIONS = [ { id: 'letter-tracing-single', icon: 'start-a', - title: '默认字帖', + title: '字母默认字帖', subtitle: '配图、例句与描红', ageMin: 4, ageMax: 7, @@ -30,7 +30,7 @@ export const LETTER_TRACING_WORKSHEET_DEFINITIONS = [ { id: 'letter-tracing-upper-lower', icon: 'draw-o', - title: '基础描红', + title: '字母基础描红', subtitle: 'Uppercase / Lowercase 总览', ageMin: 5, ageMax: 7, diff --git a/miniprogram/utils/index.ts b/miniprogram/utils/index.ts index 1e51d20..858f654 100644 --- a/miniprogram/utils/index.ts +++ b/miniprogram/utils/index.ts @@ -1,9 +1,6 @@ // 导出HTTP模块 export * from './http'; -// 导出SVG汉字数据获取模块 -export * from './getWordsSvgJson'; - export async function getMiniCodeImage(canvas: Canvas) { return getImage(canvas, '/assets/imgs/doodle-mini-logo.jpg'); } diff --git a/project.private.config.json b/project.private.config.json index 7bb9455..1d5d4cd 100644 --- a/project.private.config.json +++ b/project.private.config.json @@ -23,6 +23,20 @@ "condition": { "miniprogram": { "list": [ + { + "name": "chinesePages/handwritingSheet/handwritingSheet", + "pathName": "chinesePages/handwritingSheet/handwritingSheet", + "query": "id=handwriting-sheet", + "scene": null, + "launchMode": "default" + }, + { + "name": "chinesePages/handwritingSheet/handwritingSheet", + "pathName": "chinesePages/handwritingSheet/handwritingSheet", + "query": "id=handwriting-sheet", + "launchMode": "default", + "scene": null + }, { "name": "pinyinPages/pinyinDictation/pinyinDictation", "pathName": "pinyinPages/pinyinDictation/pinyinDictation",