feat: 添加控笔页

This commit is contained in:
R524809
2026-05-28 16:46:30 +08:00
parent 824e83c887
commit 2f0c4ab591
21 changed files with 1406 additions and 169 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 297 KiB

+2 -1
View File
@@ -52,7 +52,8 @@
"name": "chinesePages", "name": "chinesePages",
"pages": [ "pages": [
"wordColoring/wordColoring", "wordColoring/wordColoring",
"handwritingSheet/handwritingSheet" "handwritingSheet/handwritingSheet",
"penControlSheet/penControlSheet"
], ],
"independent": false "independent": false
} }
+1 -1
View File
@@ -34,7 +34,7 @@
content: "\e634"; content: "\e634";
} }
.toy-icon-iconify-material-symbols_check-rounded:before { .toy-icon-check:before {
content: "\e632"; content: "\e632";
} }
@@ -0,0 +1,268 @@
/** 控笔图形:100×100 坐标系,Y 轴向下 */
export type PenControlCategory = 'line' | 'curve' | 'shape' | 'combo';
export const PEN_CONTROL_CATEGORY_LABEL: Record<PenControlCategory, string> = {
line: '直线',
curve: '曲线',
shape: '形状',
combo: '组合',
};
export type PenControlRenderMode = 'stroke' | 'fill' | 'both';
export interface PenControlPattern {
id: string;
name: string;
category: PenControlCategory;
categoryLabel?: string;
tags: string[];
paths: string[];
render: PenControlRenderMode;
strokeScale?: number;
}
const RAW_PEN_CONTROL_PATTERNS: PenControlPattern[] = [
{
id: 'horizontal-lines',
name: '横线',
category: 'line',
tags: ['基础', '直线'],
render: 'stroke',
paths: [
'M 14 26 L 86 26',
'M 14 42 L 86 42',
'M 14 58 L 86 58',
'M 14 74 L 86 74',
],
},
{
id: 'vertical-lines',
name: '竖线',
category: 'line',
tags: ['基础', '直线'],
render: 'stroke',
paths: [
'M 26 14 L 26 86',
'M 42 14 L 42 86',
'M 58 14 L 58 86',
'M 74 14 L 74 86',
],
},
{
id: 'diagonal-lr',
name: '左斜线',
category: 'line',
tags: ['斜线'],
render: 'stroke',
paths: [
'M 14 14 L 86 86',
'M 14 32 L 68 86',
'M 32 14 L 86 68',
'M 14 50 L 50 86',
'M 50 14 L 86 50',
],
},
{
id: 'diagonal-rl',
name: '右斜线',
category: 'line',
tags: ['斜线'],
render: 'stroke',
paths: [
'M 86 14 L 14 86',
'M 86 32 L 32 86',
'M 68 14 L 14 68',
'M 86 50 L 50 86',
'M 50 14 L 14 50',
],
},
{
id: 'zigzag',
name: '锯齿线',
category: 'line',
tags: ['折线'],
render: 'stroke',
paths: ['M 14 18 L 26 82 L 38 18 L 50 82 L 62 18 L 74 82 L 86 18'],
},
{
id: 'loop-single',
name: '回字圈',
category: 'shape',
tags: ['形状'],
render: 'stroke',
paths: [
'M 14 14 L 86 14 L 86 86 L 14 86 Z',
'M 28 28 L 72 28 L 72 72 L 28 72 Z',
'M 42 42 L 58 42 L 58 58 L 42 58 Z',
],
},
{
id: 'spiral',
name: '蜗牛线',
category: 'combo',
tags: ['组合'],
render: 'stroke',
strokeScale: 0.95,
paths: [
'M 88 50 Q 88 86, 50 86 Q 14 86, 14 50 Q 14 18, 50 18 Q 80 18, 80 50 Q 80 74, 50 74 Q 26 74, 26 50 Q 26 30, 50 30 Q 68 30, 68 50 Q 68 62, 50 62 Q 38 62, 38 50 Q 38 44, 50 44 Q 56 44, 56 50',
],
},
{
id: 'mountain',
name: '山峰线',
category: 'line',
tags: ['折线'],
render: 'stroke',
paths: ['M 14 76 L 32 28 L 50 64 L 68 28 L 86 76'],
},
{
id: 'stair',
name: '台阶线',
category: 'line',
tags: ['折线'],
render: 'stroke',
paths: [
'M 14 58 L 14 43 L 38 43 L 38 28 L 62 28 L 62 13 L 86 13',
'M 14 88 L 14 73 L 38 73 L 38 58 L 62 58 L 62 43 L 86 43',
],
},
{
id: 'corner-turn',
name: '转折线',
category: 'line',
tags: ['折线'],
render: 'stroke',
paths: [
'M 86 86 L 14 86 L 14 14 L 86 14 L 86 74 L 26 74 L 26 26 L 74 26 L 74 62 L 38 62 L 38 38 L 62 38 L 62 50',
],
},
{
id: 'wave',
name: '波浪线',
category: 'curve',
tags: ['曲线'],
render: 'stroke',
paths: ['M 14 50 Q 32 18, 50 50 Q 68 82, 86 50'],
},
{
id: 'horizontal-curve',
name: '横曲线',
category: 'curve',
tags: ['曲线'],
render: 'stroke',
paths: [
'M 14 24 Q 20 8, 26 24 Q 32 40, 38 24 Q 44 8, 50 24 Q 56 40, 62 24 Q 68 8, 74 24 Q 80 40, 86 24',
'M 14 50 Q 20 34, 26 50 Q 32 66, 38 50 Q 44 34, 50 50 Q 56 66, 62 50 Q 68 34, 74 50 Q 80 66, 86 50',
'M 14 76 Q 20 60, 26 76 Q 32 92, 38 76 Q 44 60, 50 76 Q 56 92, 62 76 Q 68 60, 74 76 Q 80 92, 86 76',
],
},
{
id: 'vertical-curve',
name: '竖曲线',
category: 'curve',
tags: ['曲线'],
render: 'stroke',
paths: [
'M 24 14 Q 8 20, 24 26 Q 40 32, 24 38 Q 8 44, 24 50 Q 40 56, 24 62 Q 8 68, 24 74 Q 40 80, 24 86',
'M 50 14 Q 34 20, 50 26 Q 66 32, 50 38 Q 34 44, 50 50 Q 66 56, 50 62 Q 34 68, 50 74 Q 66 80, 50 86',
'M 76 14 Q 60 20, 76 26 Q 92 32, 76 38 Q 60 44, 76 50 Q 92 56, 76 62 Q 60 68, 76 74 Q 92 80, 76 86',
],
},
{
id: 's-curve',
name: 'S 弯',
category: 'curve',
tags: ['曲线'],
render: 'stroke',
strokeScale: 1.1,
paths: ['M 80 16 Q 18 18, 50 50 Q 82 82, 20 84'],
},
{
id: 'arc-up',
name: '左弧线',
category: 'curve',
tags: ['弧线'],
render: 'stroke',
paths: [
'M 52 13 Q 13 13, 13 52',
'M 69 30 Q 30 30, 30 69',
'M 86 47 Q 47 47, 47 86',
],
},
{
id: 'arc-down',
name: '右弧线',
category: 'curve',
tags: ['弧线'],
render: 'stroke',
paths: [
'M 13 52 Q 52 52, 52 13',
'M 30 69 Q 69 69, 69 30',
'M 47 86 Q 86 86, 86 47',
],
},
{
id: 'figure-eight',
name: '长8线',
category: 'combo',
tags: ['组合'],
render: 'stroke',
strokeScale: 0.95,
paths: [
'M 20 50 C 10 41, 13 20, 27 13 C 41 6, 42 28, 34 40 C 30 45, 25 49, 20 50 C 30 59, 27 80, 14 87 C 5 92, 5 72, 8 60 C 11 55, 15 51, 20 50',
'M 50 50 C 40 41, 43 20, 57 13 C 71 6, 72 28, 64 40 C 60 45, 55 49, 50 50 C 60 59, 57 80, 43 87 C 32 92, 31 72, 36 60 C 40 55, 45 51, 50 50',
'M 80 50 C 70 41, 73 20, 87 13 C 99 7, 100 28, 94 40 C 90 45, 85 49, 80 50 C 90 59, 87 80, 73 87 C 62 92, 61 72, 66 60 C 70 55, 75 51, 80 50',
],
},
{
id: 'cross-hatch',
name: '小太阳',
category: 'combo',
tags: ['组合'],
render: 'stroke',
strokeScale: 0.9,
paths: [
'M 62 50 C 62 56.6, 56.6 62, 50 62 C 43.4 62, 38 56.6, 38 50 C 38 43.4, 43.4 38, 50 38 C 56.6 38, 62 43.4, 62 50 Z',
'M 50 30 L 50 12',
'M 50 70 L 50 88',
'M 30 50 L 12 50',
'M 70 50 L 88 50',
'M 36 36 L 22 22',
'M 64 36 L 78 22',
'M 36 64 L 22 78',
'M 64 64 L 78 78',
],
},
];
export const PEN_CONTROL_PATTERNS: PenControlPattern[] =
RAW_PEN_CONTROL_PATTERNS.map((p) => ({
...p,
categoryLabel: PEN_CONTROL_CATEGORY_LABEL[p.category],
}));
const PATTERN_BY_ID = Object.fromEntries(
PEN_CONTROL_PATTERNS.map((p) => [p.id, p]),
) as Record<string, PenControlPattern>;
export function getPenControlPattern(
id: string,
): PenControlPattern | undefined {
return PATTERN_BY_ID[id];
}
export const PEN_CONTROL_PATTERN_IDS = PEN_CONTROL_PATTERNS.map((p) => p.id);
export function pickRandomPatternIds(
count: number,
exclude: string[] = [],
): string[] {
const pool = PEN_CONTROL_PATTERNS.map((p) => p.id).filter(
(id) => !exclude.includes(id),
);
const n = Math.max(0, Math.min(count, pool.length));
const shuffled = [...pool].sort(() => Math.random() - 0.5);
return shuffled.slice(0, n);
}
@@ -0,0 +1,169 @@
import { BaseDrawService } from '../../../core/draw/baseDraw';
import { drawTianZiGrid } from '../../shared/drawUtils';
import { getPenControlPattern } from '../data/penControlPatterns';
import type { PenControlSheetData } from '../generators/penControlGenerator';
import {
drawPenControlInCell,
type PenControlCellStyle,
} from '../shared/penControlDrawUtils';
/** 控笔练习固定 8 列 × 10 行 */
const LAYOUT = {
topGap: 17,
leftMargin: 40,
rightMargin: 40,
bottomMargin: 40,
targetCols: 8,
targetRows: 10,
minColGap: 6,
rowGap: 8,
} as const;
export default class PenControlDrawService extends BaseDrawService {
constructor(
canvas: Canvas,
ctx: RenderingContext,
options?: Record<string, unknown>,
) {
super(canvas, ctx, {
title: '控笔组合练习',
subTitle: '每种图形占两行田字格',
...options,
});
}
getMaxGridLayout(): { maxRow: number; maxCol: number } {
return { maxRow: LAYOUT.targetRows, maxCol: LAYOUT.targetCols };
}
computeSheetLayout(maxRow: number, maxCol: number) {
const content = this.getContentRect();
const { minColGap, rowGap } = LAYOUT;
const cellByWidth =
(content.width - (maxCol - 1) * minColGap) / maxCol;
const cellByHeight =
(content.height - (maxRow - 1) * rowGap) / maxRow;
const cellSize = Math.max(
16,
Math.floor(Math.min(cellByWidth, cellByHeight)),
);
const actualColGap =
maxCol > 1
? (content.width - maxCol * cellSize) / (maxCol - 1)
: 0;
const actualRowGap =
maxRow > 1
? Math.min(
rowGap,
(content.height - maxRow * cellSize) / (maxRow - 1),
)
: 0;
return {
maxRow,
maxCol,
cellSize,
startX: content.left,
startY: content.top,
colGap: actualColGap,
rowGap: actualRowGap,
};
}
async draw(data: PenControlSheetData) {
this.prepareDraw();
await this.drawHeaderAndDivider();
const layout = this.computeSheetLayout(
data.layout.maxRow,
data.layout.maxCol,
);
this.drawSheet({ ...data, layout });
this.drawPrintFooter();
}
private getContentRect() {
const topGap = LAYOUT.topGap;
const leftMargin = LAYOUT.leftMargin;
const rightMargin = LAYOUT.rightMargin;
const bottomMargin = LAYOUT.bottomMargin;
const contentTop = this.currentY + topGap;
const contentWidth = this.canvasWidth - leftMargin - rightMargin;
const contentHeight =
this.canvasHeight - contentTop - bottomMargin;
return {
top: contentTop,
left: leftMargin,
width: contentWidth,
height: contentHeight,
};
}
private drawSheet(data: PenControlSheetData) {
const { ctx } = this;
const { layout, blocks } = data;
const {
startX,
startY,
cellSize,
colGap,
rowGap,
maxCol,
maxRow,
} = layout;
for (let row = 0; row < maxRow; row++) {
for (let col = 0; col < maxCol; col++) {
const cx = startX + col * (cellSize + colGap) + cellSize / 2;
const cy = startY + row * (cellSize + rowGap) + cellSize / 2;
drawTianZiGrid({ ctx, cx, cy, size: cellSize });
}
}
let globalRow = 0;
for (const block of blocks) {
for (const rowSpec of block.rows) {
if (globalRow >= maxRow) break;
const pattern = getPenControlPattern(block.patternId);
if (pattern) {
for (let col = 0; col < maxCol; col++) {
const cx =
startX + col * (cellSize + colGap) + cellSize / 2;
const cy =
startY +
globalRow * (cellSize + rowGap) +
cellSize / 2;
const style: PenControlCellStyle =
rowSpec.cells[col] ?? 'guide';
const dashed =
rowSpec.role === 'pair-second' && col >= 1;
drawPenControlInCell(
ctx,
pattern,
cx,
cy,
cellSize,
style,
{ dashed },
);
}
}
globalRow++;
}
if (globalRow >= maxRow) break;
}
}
/** 供页面在绘制前计算 layout */
buildLayoutForGenerate(): ReturnType<
PenControlDrawService['computeSheetLayout']
> {
const { maxRow, maxCol } = this.getMaxGridLayout();
return this.computeSheetLayout(maxRow, maxCol);
}
}
@@ -0,0 +1,123 @@
import {
getPenControlPattern,
PEN_CONTROL_PATTERN_IDS,
} from '../data/penControlPatterns';
import type { PenControlCellStyle } from '../shared/penControlDrawUtils';
export const PEN_CONTROL_MODE = 'pen-control-mix' as const;
export type PenControlMode = typeof PEN_CONTROL_MODE;
export const ROWS_PER_PATTERN = 2;
export const MIN_PATTERNS = 0;
export const MAX_PATTERNS = 5;
export const DEFAULT_PATTERN_COUNT = 5;
export interface PenControlRowSpec {
role: 'pair-first' | 'pair-second';
cells: PenControlCellStyle[];
}
export interface PenControlPatternBlock {
patternId: string;
patternName: string;
rows: [PenControlRowSpec, PenControlRowSpec];
}
export interface PenControlSheetLayout {
maxRow: number;
maxCol: number;
cellSize: number;
startX: number;
startY: number;
colGap: number;
rowGap: number;
}
export interface PenControlSheetData {
mode: PenControlMode;
patternCount: number;
blocks: PenControlPatternBlock[];
layout: PenControlSheetLayout;
}
export function clampPatternCount(
requested: number,
maxRow: number,
): number {
const capByRows = Math.floor(maxRow / ROWS_PER_PATTERN);
const capped = Math.min(
MAX_PATTERNS,
Math.max(MIN_PATTERNS, requested),
Math.max(MIN_PATTERNS, capByRows),
);
return capped;
}
function buildRowCells(
maxCol: number,
_role: 'pair-first' | 'pair-second',
): PenControlCellStyle[] {
const cells: PenControlCellStyle[] = [];
for (let c = 0; c < maxCol; c++) {
cells.push(c === 0 ? 'reference' : 'guide');
}
return cells;
}
function normalizePatternIds(
patternIds: string[],
patternCount: number,
): string[] {
const valid = patternIds.filter((id) =>
PEN_CONTROL_PATTERN_IDS.includes(id),
);
const unique: string[] = [];
for (const id of valid) {
if (!unique.includes(id)) unique.push(id);
}
if (unique.length >= patternCount) {
return unique.slice(0, patternCount);
}
const rest = PEN_CONTROL_PATTERN_IDS.filter((id) => !unique.includes(id));
const shuffled = [...rest].sort(() => Math.random() - 0.5);
while (unique.length < patternCount && shuffled.length > 0) {
unique.push(shuffled.shift()!);
}
return unique.slice(0, patternCount);
}
export function generatePenControlMix(options: {
patternIds: string[];
patternCount: number;
layout: PenControlSheetLayout;
}): PenControlSheetData {
const { layout } = options;
const patternCount = clampPatternCount(
options.patternCount,
layout.maxRow,
);
const ids = normalizePatternIds(options.patternIds, patternCount);
const blocks: PenControlPatternBlock[] = ids.map((patternId) => {
const pattern = getPenControlPattern(patternId);
const pairFirst = buildRowCells(layout.maxCol, 'pair-first');
const pairSecond = buildRowCells(layout.maxCol, 'pair-second');
return {
patternId,
patternName: pattern?.name ?? patternId,
rows: [
{ role: 'pair-first', cells: pairFirst },
{ role: 'pair-second', cells: pairSecond },
],
};
});
return {
mode: PEN_CONTROL_MODE,
patternCount: blocks.length,
blocks,
layout,
};
}
@@ -0,0 +1,69 @@
import type { DebugPublishMeta } from '../../utils/debugPublish';
import { inferGradeFromAge } from '../../utils/debugPublish';
import { PEN_CONTROL_MODE } from './generators/penControlGenerator';
interface PenControlWorksheetDefinition {
id: typeof PEN_CONTROL_MODE;
icon: string;
title: string;
subtitle: string;
ageMin: number;
ageMax: number;
difficulty: 1 | 2 | 3 | 4;
tags: string[];
sortOrder: number;
}
export const PEN_CONTROL_WORKSHEET_DEFINITIONS = [
{
id: PEN_CONTROL_MODE,
icon: 'edit',
title: '控笔组合练习',
subtitle: '3–6 种图形,每种占两行田字格',
ageMin: 3,
ageMax: 6,
difficulty: 1,
tags: ['控笔', '运笔', '田字格', '学前'],
sortOrder: 40,
},
] as const satisfies ReadonlyArray<PenControlWorksheetDefinition>;
type PenControlWorksheetRow = (typeof PEN_CONTROL_WORKSHEET_DEFINITIONS)[number];
const WORKSHEET_BY_ID = Object.fromEntries(
PEN_CONTROL_WORKSHEET_DEFINITIONS.map((m) => [m.id, m]),
) as Record<string, PenControlWorksheetRow>;
export const PEN_CONTROL_WORKSHEET_ID = PEN_CONTROL_WORKSHEET_DEFINITIONS[0].id;
export function getModeInfo(id: string) {
const m = WORKSHEET_BY_ID[id];
return m ? { title: m.title, desc: m.subtitle } : undefined;
}
export function isValidMode(id: string): boolean {
return id in WORKSHEET_BY_ID;
}
export function getPublishMetaByMode(id: string): DebugPublishMeta | null {
const m = WORKSHEET_BY_ID[id];
if (!m) return null;
return {
id: m.id,
title: m.title,
subtitle: m.subtitle,
category: 'chinese',
subcategory: 'pen-control',
path: `/chinesePages/penControlSheet/penControlSheet?id=${m.id}`,
ageMin: m.ageMin,
ageMax: m.ageMax,
grade: inferGradeFromAge(m.ageMin, m.ageMax),
difficulty: m.difficulty,
previewImg: '',
tags: [...m.tags],
isNew: true,
isHot: false,
sortOrder: m.sortOrder,
status: 'draft',
};
}
@@ -0,0 +1,16 @@
{
"navigationStyle": "custom",
"navigationBarTitleText": "控笔组合练习",
"navigationBarBackgroundColor": "#F8F0E0",
"navigationBarTextStyle": "black",
"backgroundColor": "#FEF6E7",
"enablePullDownRefresh": false,
"usingComponents": {
"nav-bar": "../../components3.0/nav-bar/nav-bar",
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
"preview-footer-actions": "../../components3.0/preview-footer-actions/preview-footer-actions",
"debug-publish-tools": "../../components3.0/debug-publish-tools/debug-publish-tools",
"toy-icon": "../../toy/icon/icon",
"preview-card": "../../components3.0/preview-card/preview-card"
}
}
@@ -0,0 +1,123 @@
@import '../../style/theme.less';
page {
background-color: @bg-page;
}
.pc-page {
min-height: 100vh;
padding: 0 @page-padding-x;
padding-bottom: calc(200rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
}
.pc-main {
padding-top: 24rpx;
display: flex;
flex-direction: column;
gap: 32rpx;
}
.pc-section {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.pc-section-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.pc-section-title {
font-size: 30rpx;
font-weight: 700;
color: #6d3b00;
padding-left: 8rpx;
}
.pc-pattern-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24rpx;
}
.pc-pattern-card {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8rpx;
padding: 28rpx 12rpx;
border-radius: @radius-lg;
background: rgba(255, 255, 255, 0.6);
border: 4rpx solid rgba(179, 172, 159, 0.1);
box-shadow: @shadow;
transition: transform 0.12s, box-shadow 0.12s, border-color 0.12s,
background 0.12s;
}
.pc-pattern-card--active {
background: #ffffff;
border-color: @brand;
box-shadow: @shadow;
}
.pc-pattern-card--pressed {
transform: scale(0.95);
}
.pc-pattern-card__name {
font-size: 28rpx;
font-weight: 700;
color: @text-title;
line-height: 1.3;
}
.pc-pattern-card__cat {
font-size: 20rpx;
color: @text-secondary;
text-align: center;
line-height: 1.3;
text-transform: capitalize;
}
.pc-pattern-card--active .pc-pattern-card__name {
color: @text-title;
}
.pc-pattern-card__check {
position: absolute;
top: 50%;
left: 18rpx;
transform: translateY(-50%);
display: flex;
align-items: center;
justify-content: center;
width: 32rpx;
height: 32rpx;
border-radius: 50%;
background-color: #9cd343;
}
.pc-shuffle-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
padding: 24rpx;
border-radius: 24rpx;
background: #f8f0e0;
}
.pc-shuffle-btn--hover {
opacity: 0.85;
}
.pc-shuffle-btn__text {
font-size: 28rpx;
font-weight: 600;
color: @text-secondary;
}
@@ -0,0 +1,235 @@
import PenControlDrawService from './draw/penControlDrawService';
import {
PEN_CONTROL_PATTERNS,
pickRandomPatternIds,
} from './data/penControlPatterns';
import {
DEFAULT_PATTERN_COUNT,
generatePenControlMix,
MAX_PATTERNS,
} from './generators/penControlGenerator';
import {
getModeInfo,
getPublishMetaByMode,
isValidMode,
PEN_CONTROL_WORKSHEET_ID,
} from './penControlSheet.config';
import { createPage, type CanvasDataState } from '../../base/pageMixin';
import { defaultShareConfig } from '../../config/config';
import type { DebugPublishMeta } from '../../utils/debugPublish';
import {
addFavorite,
removeFavorite,
batchCheckFavorited,
} from '../../utils/favorites';
const pageInfoLookup = getModeInfo;
function pickRandomPatternSet(count: number, current: string[]): string[] {
const next = pickRandomPatternIds(count, current);
if (next.length >= count) return next.slice(0, count);
return pickRandomPatternIds(count);
}
function buildSelectedMap(ids: string[]): Record<string, boolean> {
const map: Record<string, boolean> = {};
for (const id of ids) map[id] = true;
return map;
}
type PageData = CanvasDataState & {
worksheetId: string;
selectedPatternIds: string[];
selectedMap: Record<string, boolean>;
patternList: typeof PEN_CONTROL_PATTERNS;
isPreviewFavorite: boolean;
isDevEnv: boolean;
debugPublishVisible: boolean;
debugPublishLoading: boolean;
debugPublishMeta: DebugPublishMeta | null;
};
createPage(
{
canvas: null as Canvas | null,
ctx: null as RenderingContext | null,
boxHeight: 0,
boxWidth: 0,
drawService: null as PenControlDrawService | null,
_favoritedMap: {} as Record<string, boolean>,
data: {
pageTitle: '控笔组合练习',
functionId: PEN_CONTROL_WORKSHEET_ID,
hasContent: false,
showShareDialog: false,
worksheetId: PEN_CONTROL_WORKSHEET_ID,
selectedPatternIds: [] as string[],
selectedMap: {} as Record<string, boolean>,
patternList: PEN_CONTROL_PATTERNS,
isPreviewFavorite: false,
isDevEnv: false,
debugPublishVisible: false,
debugPublishLoading: false,
debugPublishMeta: null,
} as unknown as PageData,
onLoad(options: { id?: string }) {
this.syncDebugPublishEnv();
console.log('PEN_CONTROL_PATTERNS', PEN_CONTROL_PATTERNS);
const worksheetId =
options.id && isValidMode(options.id)
? options.id
: PEN_CONTROL_WORKSHEET_ID;
const initialPatterns = PEN_CONTROL_PATTERNS.slice(
0,
DEFAULT_PATTERN_COUNT,
).map((p) => p.id);
console.log('initialPatterns', initialPatterns);
this.setData({
worksheetId,
functionId: worksheetId,
selectedPatternIds: initialPatterns,
selectedMap: buildSelectedMap(initialPatterns),
});
this.initPageInfo(worksheetId, '控笔练习');
this.loadFavoritedMap();
},
onCanvasReady(e: WechatMiniprogram.CustomEvent) {
this.initCanvasFromComponent(e.detail, {
createDrawService: (
canvas: Canvas,
ctx: RenderingContext,
opts?: Record<string, unknown>,
) => new PenControlDrawService(canvas, ctx, opts),
drawServiceOptions: {
title: this.data.pageTitle,
},
onCanvasReady: () => {
this.drawCanvas();
},
});
},
async drawCanvas() {
if (!this.drawService) return;
try {
const layout = (
this.drawService as PenControlDrawService
).buildLayoutForGenerate();
const data = generatePenControlMix({
patternIds: this.data.selectedPatternIds,
patternCount: this.data.selectedPatternIds.length,
layout,
});
const ids = data.blocks.map((b) => b.patternId);
if (ids.join(',') !== this.data.selectedPatternIds.join(',')) {
this.setData({
selectedPatternIds: ids,
selectedMap: buildSelectedMap(ids),
});
}
await (this.drawService as PenControlDrawService).draw(data);
this.setData({ hasContent: true });
} catch (e) {
console.error('penControlSheet draw failed', e);
this.setData({ hasContent: false });
}
},
onTogglePattern(e: WechatMiniprogram.TouchEvent) {
const id = e.currentTarget.dataset.id as string;
if (!id) return;
let ids = [...this.data.selectedPatternIds];
const idx = ids.indexOf(id);
if (idx >= 0) {
ids.splice(idx, 1);
} else {
if (ids.length >= MAX_PATTERNS) {
wx.showToast({
title: `最多选择 ${MAX_PATTERNS}`,
icon: 'none',
});
return;
}
ids.push(id);
}
this.setData(
{
selectedPatternIds: ids,
selectedMap: buildSelectedMap(ids),
},
() => this.drawCanvas(),
);
},
onPreviewRefresh() {
const count =
this.data.selectedPatternIds.length || DEFAULT_PATTERN_COUNT;
const next = pickRandomPatternSet(
count,
this.data.selectedPatternIds,
);
this.setData(
{
selectedPatternIds: next,
selectedMap: buildSelectedMap(next),
},
() => this.drawCanvas(),
);
},
onShufflePatterns() {
this.onPreviewRefresh();
},
async onPreviewFavorite() {
const next = !this.data.isPreviewFavorite;
this.setData({ isPreviewFavorite: next });
const id = this.data.worksheetId;
if (id) {
this._favoritedMap[id] = next;
if (next) {
addFavorite(id);
} else {
removeFavorite(id);
}
}
wx.showToast({
title: next ? '收藏成功' : '已取消收藏',
icon: 'none',
});
},
async loadFavoritedMap() {
const ids = [PEN_CONTROL_WORKSHEET_ID];
this._favoritedMap = await batchCheckFavorited(ids);
if (this._favoritedMap[this.data.worksheetId]) {
this.setData({ isPreviewFavorite: true });
}
},
getPublishMeta(): DebugPublishMeta {
const meta = getPublishMetaByMode(this.data.worksheetId);
if (!meta) {
throw new Error('当前题型配置不存在');
}
return meta;
},
},
{
shareConfig: defaultShareConfig,
pageInfoLookup,
},
);
@@ -0,0 +1,77 @@
<nav-bar title="控笔组合练习" />
<view class="pc-page">
<view class="pc-main">
<preview-card
id="previewCard"
showRefresh="{{true}}"
showFavorite="{{true}}"
favorited="{{isPreviewFavorite}}"
bind:canvas-ready="onCanvasReady"
bind:refresh="onPreviewRefresh"
bind:favorite="onPreviewFavorite" />
<view class="pc-section">
<view class="pc-section-header">
<text class="pc-section-title"
>选择图形 ({{selectedPatternIds.length}}/5)</text
>
</view>
<view class="pc-pattern-grid">
<view
wx:for="{{patternList}}"
wx:key="id"
class="pc-pattern-card {{selectedMap[item.id] ? 'pc-pattern-card--active' : ''}}"
data-id="{{item.id}}"
hover-class="pc-pattern-card--pressed"
hover-start-time="0"
hover-stay-time="70"
bindtap="onTogglePattern">
<view
wx:if="{{selectedMap[item.id]}}"
class="pc-pattern-card__check">
<toy-icon name="check" size="20rpx" color="#fff" />
</view>
<text class="pc-pattern-card__name">{{item.name}}</text>
<text class="pc-pattern-card__cat"
>{{item.categoryLabel}}</text
>
</view>
</view>
</view>
<view
class="pc-shuffle-btn"
hover-class="pc-shuffle-btn--hover"
hover-start-time="0"
hover-stay-time="70"
bindtap="onShufflePatterns">
<toy-icon
name="refresh"
size="40rpx"
color="#605b50"
custom-class="pc-shuffle-btn__icon" />
<text class="pc-shuffle-btn__text">换一批图形</text>
</view>
</view>
</view>
<preview-footer-actions
disabled="{{!hasContent}}"
bind:primary="exportToPrint"
bind:secondary="onShare" />
<debug-publish-tools
wx:if="{{isDevEnv && hasContent}}"
id="debugPublishTools"
visible="{{debugPublishVisible}}"
loading="{{debugPublishLoading}}"
meta="{{debugPublishMeta}}"
bind:open="onOpenDebugPublish"
bind:close="onCloseDebugPublish"
bind:confirm="onConfirmDebugPublish" />
<share-guide-popup
show="{{showShareDialog}}"
bind:onClose="onCloseShareDialog"
bind:onShareSuccess="onShareSuccess" />
@@ -0,0 +1,103 @@
import { TRACING_COLORS } from '../../../core/data/tracingStyles';
import { drawSvgPath } from '../../shared/drawUtils';
import type {
PenControlPattern,
PenControlRenderMode,
} from '../data/penControlPatterns';
export const PEN_CONTROL_VIEW_SIZE = 100;
export interface PenControlTransform {
xOffset: number;
yOffset: number;
scale: number;
}
export function getPenControlTransform(
width: number,
height: number,
padding: number,
): PenControlTransform {
const availableWidth = width - 2 * padding;
const availableHeight = height - 2 * padding;
const scale = Math.min(
availableWidth / PEN_CONTROL_VIEW_SIZE,
availableHeight / PEN_CONTROL_VIEW_SIZE,
);
const scaledW = PEN_CONTROL_VIEW_SIZE * scale;
const scaledH = PEN_CONTROL_VIEW_SIZE * scale;
const xOffset = padding + (availableWidth - scaledW) / 2;
const yOffset = padding + (availableHeight - scaledH) / 2;
return { xOffset, yOffset, scale };
}
export type PenControlCellStyle = 'reference' | 'guide';
function colorForStyle(style: PenControlCellStyle): string {
return style === 'reference' ? TRACING_COLORS.strong : TRACING_COLORS.guide;
}
function drawPathWithMode(
ctx: RenderingContext,
pathD: string,
render: PenControlRenderMode,
color: string,
lineWidth: number,
dashed: boolean,
) {
ctx.beginPath();
drawSvgPath(ctx, pathD);
if (render === 'fill' || render === 'both') {
ctx.fillStyle = color;
ctx.fill();
}
if (render === 'stroke' || render === 'both') {
ctx.strokeStyle = color;
ctx.lineWidth = lineWidth;
if (dashed) {
// setLineDash 接收一个数组,表示虚线的样式。
// 第一个参数 (lineWidth * 2.6):表示每段实线的长度
// 第二个参数 (lineWidth * 2.4):表示每段虚线的间隙长度
// 旧虚线效果不明显,调整为更清晰的虚线样式
ctx.setLineDash([lineWidth * 1.8, lineWidth * 2.3]);
} else {
ctx.setLineDash([]);
}
ctx.stroke();
}
}
/**
* 在田字格中心绘制控笔图形
*/
export function drawPenControlInCell(
ctx: RenderingContext,
pattern: PenControlPattern,
cx: number,
cy: number,
cellSize: number,
style: PenControlCellStyle,
options?: { dashed?: boolean },
) {
const color = colorForStyle(style);
const padding = cellSize * 0.1;
const transform = getPenControlTransform(cellSize, cellSize, padding);
const lineWidth =
(cellSize * 0.0236 * (pattern.strokeScale ?? 1)) / transform.scale;
const dashed = options?.dashed === true;
ctx.save();
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.translate(cx - cellSize / 2, cy - cellSize / 2);
ctx.translate(transform.xOffset, transform.yOffset);
ctx.scale(transform.scale, transform.scale);
for (const pathD of pattern.paths) {
drawPathWithMode(ctx, pathD, pattern.render, color, lineWidth, dashed);
}
ctx.setLineDash([]);
ctx.restore();
}
+7 -2
View File
@@ -42,10 +42,10 @@ export function getScalingTransform(
} }
/** /**
* SVG 路径解析与绘制(支持 M/L/Q/Z 命令) * SVG 路径解析与绘制(支持 M/L/Q/C/Z 命令)
*/ */
export function drawSvgPath(ctx: RenderingContext, pathD: string) { export function drawSvgPath(ctx: RenderingContext, pathD: string) {
const commandRegex = /([MLQZ])([^MLQZ]*?)(?=[MLQZ]|$)/gi; const commandRegex = /([MLQCZ])([^MLQCZ]*?)(?=[MLQCZ]|$)/gi;
const commands: Array<{ cmd: string; coords: string }> = []; const commands: Array<{ cmd: string; coords: string }> = [];
let match; let match;
while ((match = commandRegex.exec(pathD)) !== null) { while ((match = commandRegex.exec(pathD)) !== null) {
@@ -69,6 +69,11 @@ export function drawSvgPath(ctx: RenderingContext, pathD: string) {
case 'Q': case 'Q':
if (v.length >= 4) ctx.quadraticCurveTo(v[0], v[1], v[2], v[3]); if (v.length >= 4) ctx.quadraticCurveTo(v[0], v[1], v[2], v[3]);
break; break;
case 'C':
if (v.length >= 6) {
ctx.bezierCurveTo(v[0], v[1], v[2], v[3], v[4], v[5]);
}
break;
case 'Z': case 'Z':
ctx.closePath(); ctx.closePath();
break; break;
+1 -1
View File
@@ -32,4 +32,4 @@ export const downloadFreeBatchSize = 4;
* - develop:为 true 时启用下载配额/分享/广告(便于联调);为 false 时不限制 * - develop:为 true 时启用下载配额/分享/广告(便于联调);为 false 时不限制
* - 体验版 / 正式版:始终启用,不受此开关影响 * - 体验版 / 正式版:始终启用,不受此开关影响
*/ */
export const enableDownloadLimitInDevelop = true; export const enableDownloadLimitInDevelop = false;
@@ -136,7 +136,8 @@ export const FOCUS_WORKSHEET_DEFINITIONS = [
}, },
] as const satisfies ReadonlyArray<FocusWorksheetDefinition>; ] as const satisfies ReadonlyArray<FocusWorksheetDefinition>;
type FocusWorksheetDefinitionItem = (typeof FOCUS_WORKSHEET_DEFINITIONS)[number]; type FocusWorksheetDefinitionItem =
(typeof FOCUS_WORKSHEET_DEFINITIONS)[number];
const FOCUS_WORKSHEET_BY_ID = Object.fromEntries( const FOCUS_WORKSHEET_BY_ID = Object.fromEntries(
FOCUS_WORKSHEET_DEFINITIONS.map((item) => [item.id, item]), FOCUS_WORKSHEET_DEFINITIONS.map((item) => [item.id, item]),
+15 -6
View File
@@ -6,9 +6,16 @@ import {
type FocusTypeConfig, type FocusTypeConfig,
type FocusTypeAction, type FocusTypeAction,
} from './registry'; } from './registry';
import { getPublishMetaByFocusState, FOCUS_WORKSHEET_DEFINITIONS } from './focusDraw.config'; import {
getPublishMetaByFocusState,
FOCUS_WORKSHEET_DEFINITIONS,
} from './focusDraw.config';
import type { DebugPublishMeta } from '../../utils/debugPublish'; import type { DebugPublishMeta } from '../../utils/debugPublish';
import { addFavorite, removeFavorite, batchCheckFavorited } from '../../utils/favorites'; import {
addFavorite,
removeFavorite,
batchCheckFavorited,
} from '../../utils/favorites';
const TYPE_LIST = FOCUS_TYPE_CONFIGS.map((t) => ({ const TYPE_LIST = FOCUS_TYPE_CONFIGS.map((t) => ({
id: t.id, id: t.id,
@@ -124,9 +131,10 @@ createFocusPage({
selectedTypeId: id, selectedTypeId: id,
functionId: id, functionId: id,
pageTitle: title, pageTitle: title,
isPreviewFavorite: !!this._favoritedMap[ isPreviewFavorite:
getPublishMetaByFocusState(id, id, mode)?.id || id !!this._favoritedMap[
], getPublishMetaByFocusState(id, id, mode)?.id || id
],
showActions: !!typeConfig.actions, showActions: !!typeConfig.actions,
currentActions: typeConfig.actions || [], currentActions: typeConfig.actions || [],
actionsTitle: typeConfig.actionsTitle || '选择模式', actionsTitle: typeConfig.actionsTitle || '选择模式',
@@ -163,7 +171,8 @@ createFocusPage({
this.setData({ this.setData({
currentMode: value, currentMode: value,
pageTitle: title, pageTitle: title,
isPreviewFavorite: !!this._favoritedMap[this.getWorksheetStatsIdFor(value)], isPreviewFavorite:
!!this._favoritedMap[this.getWorksheetStatsIdFor(value)],
}); });
this.initPageInfo(this.data.functionId, title); this.initPageInfo(this.data.functionId, title);
+15 -6
View File
@@ -6,9 +6,16 @@ import {
type MathTypeConfig, type MathTypeConfig,
type MathTypeAction, type MathTypeAction,
} from './registry'; } from './registry';
import { getPublishMetaByMathState, MATH_WORKSHEET_DEFINITIONS } from './mathDraw.config'; import {
getPublishMetaByMathState,
MATH_WORKSHEET_DEFINITIONS,
} from './mathDraw.config';
import type { DebugPublishMeta } from '../../utils/debugPublish'; import type { DebugPublishMeta } from '../../utils/debugPublish';
import { addFavorite, removeFavorite, batchCheckFavorited } from '../../utils/favorites'; import {
addFavorite,
removeFavorite,
batchCheckFavorited,
} from '../../utils/favorites';
const TYPE_LIST = MATH_TYPE_CONFIGS.map((t) => ({ const TYPE_LIST = MATH_TYPE_CONFIGS.map((t) => ({
id: t.id, id: t.id,
@@ -135,9 +142,10 @@ createMathPage({
selectedTypeId: id, selectedTypeId: id,
functionId: id, functionId: id,
pageTitle: title, pageTitle: title,
isPreviewFavorite: !!this._favoritedMap[ isPreviewFavorite:
getPublishMetaByMathState(id, id, mode)?.id || id !!this._favoritedMap[
], getPublishMetaByMathState(id, id, mode)?.id || id
],
showActions: !!typeConfig.actions, showActions: !!typeConfig.actions,
currentActions: typeConfig.actions || [], currentActions: typeConfig.actions || [],
actionsTitle: typeConfig.actionsTitle || '选择模式', actionsTitle: typeConfig.actionsTitle || '选择模式',
@@ -178,7 +186,8 @@ createMathPage({
this.setData({ this.setData({
currentMode: value, currentMode: value,
pageTitle: title, pageTitle: title,
isPreviewFavorite: !!this._favoritedMap[this.getWorksheetStatsIdFor(value)], isPreviewFavorite:
!!this._favoritedMap[this.getWorksheetStatsIdFor(value)],
}); });
this.initPageInfo(this.data.functionId, title); this.initPageInfo(this.data.functionId, title);
+6
View File
@@ -153,6 +153,12 @@ export const AGE_WEEK_PLANS: Record<AgeBandKey, WeekPlan[]> = {
subtitle: '自定义田字格练字帖', subtitle: '自定义田字格练字帖',
path: '/chinesePages/handwritingSheet/handwritingSheet?id=hanzi-sheet', path: '/chinesePages/handwritingSheet/handwritingSheet?id=hanzi-sheet',
}, },
{
_id: 'pen-control-mix',
title: '控笔组合练习',
subtitle: '36 种图形,每种占两行',
path: '/chinesePages/penControlSheet/penControlSheet?id=pen-control-mix',
},
{ {
_id: 'letter-tracing-single', _id: 'letter-tracing-single',
title: '默认字帖', title: '默认字帖',
@@ -568,6 +568,16 @@ const CATEGORY_ITEMS_BY_ID: Record<string, CategoryItem[]> = {
path: '/pages/copyBook/copyBook', path: '/pages/copyBook/copyBook',
available: true, available: true,
}), }),
item({
id: 'pen-control-mix',
title: '控笔组合练习',
subtitle: '3–6 种图形,每种占两行田字格',
icon: '✍️',
ageBand: age(3, 6),
difficulty: 1,
path: '/chinesePages/penControlSheet/penControlSheet?id=pen-control-mix',
available: true,
}),
], ],
english: [ english: [
item({ item({
@@ -51,7 +51,6 @@ function inferGradeFromAge(ageMin: number, ageMax: number): number {
return ageGradeMap[centerAge] ?? 0; return ageGradeMap[centerAge] ?? 0;
} }
type ConfigSource = { type ConfigSource = {
label: string; label: string;
data: readonly LocalDef[]; data: readonly LocalDef[];
@@ -398,9 +397,10 @@ Page({
const startedAt = Date.now(); const startedAt = Date.now();
try { try {
const queryRes = await callCloudFunction< const queryRes = await callCloudFunction<Array<{ _id: string }>>(
Array<{ _id: string }> 'worksheetsQuery',
>('worksheetsQuery', {}); {},
);
if (!queryRes.success || !queryRes.data) { if (!queryRes.success || !queryRes.data) {
throw new Error(queryRes.message || '查询 worksheet 列表失败'); throw new Error(queryRes.message || '查询 worksheet 列表失败');
@@ -443,8 +443,7 @@ Page({
this.setData({ statusText: text }); this.setData({ statusText: text });
wx.showToast({ title: '已重置', icon: 'success' }); wx.showToast({ title: '已重置', icon: 'success' });
} catch (error) { } catch (error) {
const msg = const msg = error instanceof Error ? error.message : '重置失败';
error instanceof Error ? error.message : '重置失败';
this.setData({ statusText: msg }); this.setData({ statusText: msg });
wx.showToast({ title: msg, icon: 'none' }); wx.showToast({ title: msg, icon: 'none' });
} finally { } finally {
+158 -144
View File
@@ -1,148 +1,162 @@
{ {
"description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html", "description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
"projectname": "doodle-mini", "projectname": "doodle-mini",
"setting": { "setting": {
"compileHotReLoad": true, "compileHotReLoad": true,
"urlCheck": true, "urlCheck": true,
"coverView": false, "coverView": false,
"lazyloadPlaceholderEnable": false, "lazyloadPlaceholderEnable": false,
"skylineRenderEnable": true, "skylineRenderEnable": true,
"preloadBackgroundData": false, "preloadBackgroundData": false,
"autoAudits": false, "autoAudits": false,
"useApiHook": true, "useApiHook": true,
"useApiHostProcess": true, "useApiHostProcess": true,
"showShadowRootInWxmlPanel": false, "showShadowRootInWxmlPanel": false,
"useStaticServer": false, "useStaticServer": false,
"useLanDebug": false, "useLanDebug": false,
"showES6CompileOption": false, "showES6CompileOption": false,
"checkInvalidKey": true, "checkInvalidKey": true,
"ignoreDevUnusedFiles": true, "ignoreDevUnusedFiles": true,
"bigPackageSizeSupport": false "bigPackageSizeSupport": false
}, },
"libVersion": "3.7.12", "libVersion": "3.7.12",
"condition": { "condition": {
"miniprogram": { "miniprogram": {
"list": [ "list": [
{ {
"name": "chinesePages/handwritingSheet/handwritingSheet", "name": "chinesePages/penControlSheet/penControlSheet",
"pathName": "chinesePages/handwritingSheet/handwritingSheet", "pathName": "chinesePages/penControlSheet/penControlSheet",
"query": "id=hanzi-sheet", "query": "",
"scene": null, "scene": null,
"launchMode": "default" "launchMode": "default"
}, },
{ {
"name": "chinesePages/handwritingSheet/handwritingSheet", "name": "chinesePages/penControlSheet/penControlSheet?id=pen-control-mix",
"pathName": "chinesePages/handwritingSheet/handwritingSheet", "pathName": "chinesePages/penControlSheet/penControlSheet?id=pen-control-mix",
"query": "id=hanzi-sheet", "query": "",
"launchMode": "default", "launchMode": "default",
"scene": null "scene": null
}, },
{ {
"name": "pinyinPages/pinyinDictation/pinyinDictation", "name": "chinesePages/handwritingSheet/handwritingSheet",
"pathName": "pinyinPages/pinyinDictation/pinyinDictation", "pathName": "chinesePages/handwritingSheet/handwritingSheet",
"query": "", "query": "id=hanzi-sheet",
"launchMode": "default", "launchMode": "default",
"scene": null "scene": null
}, },
{ {
"name": "chinesePages/handwritingSheet/handwritingSheet", "name": "chinesePages/handwritingSheet/handwritingSheet",
"pathName": "chinesePages/handwritingSheet/handwritingSheet", "pathName": "chinesePages/handwritingSheet/handwritingSheet",
"query": "", "query": "id=hanzi-sheet",
"launchMode": "default", "launchMode": "default",
"scene": null "scene": null
}, },
{ {
"name": "chinesePages/wordColoring/wordColoring", "name": "pinyinPages/pinyinDictation/pinyinDictation",
"pathName": "chinesePages/wordColoring/wordColoring", "pathName": "pinyinPages/pinyinDictation/pinyinDictation",
"query": "", "query": "",
"launchMode": "default", "launchMode": "default",
"scene": null "scene": null
}, },
{ {
"name": "pages/profile/profile", "name": "chinesePages/handwritingSheet/handwritingSheet",
"pathName": "pages/profile/profile", "pathName": "chinesePages/handwritingSheet/handwritingSheet",
"query": "", "query": "",
"launchMode": "default", "launchMode": "default",
"scene": null "scene": null
}, },
{ {
"name": "pages/age/age", "name": "chinesePages/wordColoring/wordColoring",
"pathName": "pages/age/age", "pathName": "chinesePages/wordColoring/wordColoring",
"query": "", "query": "",
"launchMode": "default", "launchMode": "default",
"scene": null "scene": null
}, },
{ {
"name": "supportPages/debug/debug", "name": "pages/profile/profile",
"pathName": "supportPages/debug/debug", "pathName": "pages/profile/profile",
"query": "", "query": "",
"launchMode": "default", "launchMode": "default",
"scene": null "scene": null
}, },
{ {
"name": "englishPages/letterTracing/letterTracing", "name": "pages/age/age",
"pathName": "englishPages/letterTracing/letterTracing", "pathName": "pages/age/age",
"query": "", "query": "",
"launchMode": "default", "launchMode": "default",
"scene": null "scene": null
}, },
{ {
"name": "supportPages/debug/debug", "name": "supportPages/debug/debug",
"pathName": "supportPages/debug/debug", "pathName": "supportPages/debug/debug",
"query": "", "query": "",
"launchMode": "default", "launchMode": "default",
"scene": null "scene": null
}, },
{ {
"name": "pages/category/category", "name": "englishPages/letterTracing/letterTracing",
"pathName": "pages/category/category", "pathName": "englishPages/letterTracing/letterTracing",
"query": "", "query": "",
"launchMode": "default", "launchMode": "default",
"scene": null "scene": null
}, },
{ {
"name": "mathIndex", "name": "supportPages/debug/debug",
"pathName": "mathPages/mathDraw/mathDraw", "pathName": "supportPages/debug/debug",
"query": "", "query": "",
"launchMode": "default", "launchMode": "default",
"scene": null "scene": null
}, },
{ {
"name": "focusDraw", "name": "pages/category/category",
"pathName": "focusPages/focusDraw/focusDraw", "pathName": "pages/category/category",
"query": "", "query": "",
"launchMode": "default", "launchMode": "default",
"scene": null "scene": null
}, },
{ {
"name": "oldFocusIndex", "name": "mathIndex",
"pathName": "supportPages/focusIndex/focusIndex", "pathName": "mathPages/mathDraw/mathDraw",
"query": "", "query": "",
"launchMode": "default", "launchMode": "default",
"scene": null "scene": null
}, },
{ {
"name": "oldMathIndex", "name": "focusDraw",
"pathName": "supportPages/mathIndex/mathIndex", "pathName": "focusPages/focusDraw/focusDraw",
"query": "", "query": "",
"launchMode": "default", "launchMode": "default",
"scene": null "scene": null
}, },
{ {
"name": "pages/index/index", "name": "oldFocusIndex",
"pathName": "pages/index/index", "pathName": "supportPages/focusIndex/focusIndex",
"query": "", "query": "",
"launchMode": "default", "launchMode": "default",
"scene": null "scene": null
}, },
{ {
"name": "englishPages/letterTracing/letterTracing", "name": "oldMathIndex",
"pathName": "englishPages/letterTracing/letterTracing", "pathName": "supportPages/mathIndex/mathIndex",
"query": "id=letter-tracing-single", "query": "",
"launchMode": "default", "launchMode": "default",
"scene": null "scene": null
} },
] {
"name": "pages/index/index",
"pathName": "pages/index/index",
"query": "",
"launchMode": "default",
"scene": null
},
{
"name": "englishPages/letterTracing/letterTracing",
"pathName": "englishPages/letterTracing/letterTracing",
"query": "id=letter-tracing-single",
"launchMode": "default",
"scene": null
} }
]
} }
}
} }