feat: 数感启蒙、专注力页面重构、首页入口开发
@@ -22,24 +22,7 @@
|
||||
{
|
||||
"root": "mathPages",
|
||||
"name": "mathPages",
|
||||
"pages": [
|
||||
"numberFind/numberFind",
|
||||
"countMatch/countMatch",
|
||||
"addition/addition",
|
||||
"missingNumber/missingNumber",
|
||||
"compare/compare",
|
||||
"countingSelect/countingSelect",
|
||||
"numberDecompose/numberDecompose",
|
||||
"numberSort/numberSort",
|
||||
"numberObjectMatch/numberObjectMatch",
|
||||
"makeTen/makeTen",
|
||||
"breakTen/breakTen",
|
||||
"flatTen/flatTen",
|
||||
"borrowTen/borrowTen",
|
||||
"oneDigitAddition/oneDigitAddition",
|
||||
"calculationPractice/calculationPractice",
|
||||
"multiplicationTable/multiplicationTable"
|
||||
],
|
||||
"pages": ["mathDraw/mathDraw"],
|
||||
"independent": false
|
||||
},
|
||||
{
|
||||
@@ -51,19 +34,7 @@
|
||||
{
|
||||
"root": "focusPages",
|
||||
"name": "focusPages",
|
||||
"pages": [
|
||||
"gridDrawing/gridDrawing",
|
||||
"shape/shape",
|
||||
"positionColoring/positionColoring",
|
||||
"shapeSymbol/shapeSymbol",
|
||||
"colorPattern/colorPattern",
|
||||
"matchConnect/matchConnect",
|
||||
"lineRecognition/lineRecognition",
|
||||
"gridReasoning/gridReasoning",
|
||||
"codeConnect/codeConnect",
|
||||
"dotConnect/dotConnect",
|
||||
"colorShapeMatch/colorShapeMatch"
|
||||
],
|
||||
"pages": ["focusDraw/focusDraw", "shape/shape"],
|
||||
"independent": false
|
||||
}
|
||||
],
|
||||
|
||||
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 129 KiB |
|
Before Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 155 KiB |
|
Before Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 20 KiB |
@@ -89,160 +89,160 @@ const L = {
|
||||
// prettier-ignore
|
||||
export const ALL_WORKSHEETS: WorksheetDefinition[] = [
|
||||
sheet(
|
||||
{ id: 'number-find', title: '找数字,涂一涂', desc: '在数字方阵中找出目标数字并涂色', category: 'math', page: 'numberFind', subpackage: 'mathPages', img: '/assets/mathEntrance/number-find.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#fff3e0', previewText: '🔍 1 2 3' },
|
||||
{ id: 'number-find', title: '找数字,涂一涂', desc: '在数字方阵中找出目标数字并涂色', category: 'math', page: 'mathDraw', subpackage: 'mathPages', img: '/assets/mathEntrance/number-find.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#fff3e0', previewText: '🔍 1 2 3' },
|
||||
{ subcategory: 'number-sense', template: 'grid-coloring', generator: 'counting', sortOrder: 1, layoutConfig: { ...L.gridEx }, generatorConfig: { functionId: 'number-find' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'number-write', title: '看数字,写一写', desc: '按笔画顺序练习书写数字', category: 'math', page: 'numberFind', subpackage: 'mathPages', img: '/assets/mathEntrance/number-write.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#fff3e0', previewText: '1 2 3' },
|
||||
{ id: 'number-write', title: '看数字,写一写', desc: '按笔画顺序练习书写数字', category: 'math', page: 'mathDraw', subpackage: 'mathPages', img: '/assets/mathEntrance/number-write.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#fff3e0', previewText: '1 2 3' },
|
||||
{ subcategory: 'number-sense', template: 'tracing-writing', generator: 'counting', sortOrder: 2, layoutConfig: { ...L.trace }, generatorConfig: { functionId: 'number-write' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'number-coloring', title: '按数字,涂颜色', desc: '按指定数字给对应圆圈涂色', category: 'math', page: 'countMatch', subpackage: 'mathPages', icon: '🎨', img: '/assets/mathEntrance/number-coloring.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#fce4ec', previewText: '🎨' },
|
||||
{ id: 'number-coloring', title: '按数字,涂颜色', desc: '按指定数字给对应圆圈涂色', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '🎨', img: '/assets/mathEntrance/number-coloring.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#fce4ec', previewText: '🎨' },
|
||||
{ subcategory: 'number-sense', template: 'grid-coloring', generator: 'counting', sortOrder: 3, layoutConfig: { ...L.gridEx }, generatorConfig: { functionId: 'number-coloring' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'counting-matching', title: '数一数,连一连', desc: '连线配对数字和对应数量图形', category: 'math', page: 'countMatch', subpackage: 'mathPages', icon: '🔗', img: '/assets/mathEntrance/count-match.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e8f5e9', previewText: '3↔🍎🍎🍎' },
|
||||
{ id: 'counting-matching', title: '数一数,连一连', desc: '连线配对数字和对应数量图形', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '🔗', img: '/assets/mathEntrance/count-match.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e8f5e9', previewText: '3↔🍎🍎🍎' },
|
||||
{ subcategory: 'counting', template: 'match-connect', generator: 'counting', sortOrder: 4, layoutConfig: { ...L.gridEx }, generatorConfig: { functionId: 'counting-matching' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'number-object-match', title: '数物连线', desc: '连线相同数量的物品和数字', category: 'math', page: 'numberObjectMatch', subpackage: 'mathPages', icon: '🔗', img: '/assets/mathEntrance/number-object-match.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e8f5e9', previewText: '🔗' },
|
||||
{ id: 'number-object-match', title: '数物连线', desc: '连线相同数量的物品和数字', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '🔗', img: '/assets/mathEntrance/number-object-match.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e8f5e9', previewText: '🔗' },
|
||||
{ subcategory: 'counting', template: 'match-connect', generator: 'counting', sortOrder: 5, generatorConfig: { functionId: 'number-object-match' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'number-object-fill', title: '数物填写', desc: '数物品数量,填写对应数字', category: 'math', page: 'numberObjectMatch', subpackage: 'mathPages', icon: '✏️', img: '/assets/mathEntrance/number-object-fill.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e8f5e9', previewText: '✏️' },
|
||||
{ id: 'number-object-fill', title: '数物填写', desc: '数物品数量,填写对应数字', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '✏️', img: '/assets/mathEntrance/number-object-fill.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e8f5e9', previewText: '✏️' },
|
||||
{ subcategory: 'counting', template: 'grid-exercise', generator: 'counting', sortOrder: 6, generatorConfig: { functionId: 'number-object-fill' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'counting-select', title: '数一数,选一选', desc: '数出物品数量,圈出正确答案', category: 'math', page: 'countingSelect', subpackage: 'mathPages', icon: '✓', img: '/assets/mathEntrance/counting-select.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e8f5e9', previewText: '✓' },
|
||||
{ id: 'counting-select', title: '数一数,选一选', desc: '数出物品数量,圈出正确答案', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '✓', img: '/assets/mathEntrance/counting-select.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e8f5e9', previewText: '✓' },
|
||||
{ subcategory: 'counting', template: 'grid-exercise', generator: 'counting', sortOrder: 7, generatorConfig: { functionId: 'counting-select' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'counting-fill', title: '数一数,填一填', desc: '数出物品数量,填写数字', category: 'math', page: 'countingSelect', subpackage: 'mathPages', icon: '✏️', img: '/assets/mathEntrance/counting-fill.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e8f5e9', previewText: '✏️' },
|
||||
{ id: 'counting-fill', title: '数一数,填一填', desc: '数出物品数量,填写数字', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '✏️', img: '/assets/mathEntrance/counting-fill.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e8f5e9', previewText: '✏️' },
|
||||
{ subcategory: 'counting', template: 'grid-exercise', generator: 'counting', sortOrder: 8, generatorConfig: { functionId: 'counting-fill' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'compare', title: '数一数,比大小', desc: '比较数量,填入 ><=', category: 'math', page: 'compare', subpackage: 'mathPages', icon: '⚖️', img: '/assets/mathEntrance/compare.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e3f2fd', previewText: '3 ○ 5' },
|
||||
{ id: 'compare', title: '数一数,比大小', desc: '比较数量,填入 ><=', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '⚖️', img: '/assets/mathEntrance/compare.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e3f2fd', previewText: '3 ○ 5' },
|
||||
{ subcategory: 'counting', template: 'grid-exercise', generator: 'comparison', sortOrder: 9, generatorConfig: { functionId: 'compare' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'number-sort', title: '数字排序', desc: '写出正确的数字顺序', category: 'math', page: 'numberSort', subpackage: 'mathPages', icon: '🔢', img: '/assets/mathEntrance/number-sort.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#fff3e0', previewText: '1→2→3' },
|
||||
{ id: 'number-sort', title: '数字排序', desc: '写出正确的数字顺序', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '🔢', img: '/assets/mathEntrance/number-sort.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#fff3e0', previewText: '1→2→3' },
|
||||
{ subcategory: 'sequence', template: 'sequence-pattern', generator: 'number-sequence', sortOrder: 10, generatorConfig: { functionId: 'number-sort' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'missing-number', title: '填上缺少的数字', desc: '在数列中找出并填写缺失数字', category: 'math', page: 'missingNumber', subpackage: 'mathPages', icon: '❓', img: '/assets/mathEntrance/missing-number.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#fff3e0', previewText: '1_3_5' },
|
||||
{ id: 'missing-number', title: '填上缺少的数字', desc: '在数列中找出并填写缺失数字', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '❓', img: '/assets/mathEntrance/missing-number.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#fff3e0', previewText: '1_3_5' },
|
||||
{ subcategory: 'sequence', template: 'sequence-pattern', generator: 'number-sequence', sortOrder: 11, generatorConfig: { functionId: 'missing-number' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'number-decompose', title: '10以内数的分与合', desc: '把数字分一分,合一合', category: 'math', page: 'numberDecompose', subpackage: 'mathPages', icon: '🔢', img: '/assets/mathEntrance/number-decompose.png', ageRange: [4, 6], difficulty: 'beginner', previewBg: '#fff3e0', previewText: '8=?+?' },
|
||||
{ id: 'number-decompose', title: '10以内数的分与合', desc: '把数字分一分,合一合', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '🔢', img: '/assets/mathEntrance/number-decompose.png', ageRange: [4, 6], difficulty: 'beginner', previewBg: '#fff3e0', previewText: '8=?+?' },
|
||||
{ subcategory: 'decompose', template: 'grid-exercise', generator: 'number-decompose', sortOrder: 12, generatorConfig: { max: 10 } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'number-decompose-20', title: '20以内数的分与合', desc: '把数字分一分,合一合', category: 'math', page: 'numberDecompose', subpackage: 'mathPages', icon: '🔢', img: '/assets/mathEntrance/number-decompose-20.png', ageRange: [5, 7], difficulty: 'basic', previewBg: '#fff3e0', previewText: '15=?+?' },
|
||||
{ id: 'number-decompose-20', title: '20以内数的分与合', desc: '把数字分一分,合一合', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '🔢', img: '/assets/mathEntrance/number-decompose-20.png', ageRange: [5, 7], difficulty: 'basic', previewBg: '#fff3e0', previewText: '15=?+?' },
|
||||
{ subcategory: 'decompose', template: 'grid-exercise', generator: 'number-decompose', sortOrder: 13, generatorConfig: { max: 20 } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'one-digit-addition', title: '一位数加法', desc: '通过圆点学习一位数加法运算', category: 'math', page: 'oneDigitAddition', subpackage: 'mathPages', icon: '➕', img: '/assets/mathEntrance/one-digit-addition.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e3f2fd', previewText: '2+3=?' },
|
||||
{ id: 'one-digit-addition', title: '一位数加法', desc: '通过圆点学习一位数加法运算', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '➕', img: '/assets/mathEntrance/one-digit-addition.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e3f2fd', previewText: '2+3=?' },
|
||||
{ subcategory: 'arithmetic-visual', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 14, generatorConfig: { operators: ['+'], maxNumber: 9, showDots: true } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'addition-5', title: '5以内加法', desc: '图形化展示 5 以内加法', category: 'math', page: 'addition', subpackage: 'mathPages', icon: '➕', img: '/assets/mathEntrance/addition-5.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e3f2fd', previewText: '2+1=?' },
|
||||
{ id: 'addition-5', title: '5以内加法', desc: '图形化展示 5 以内加法', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '➕', img: '/assets/mathEntrance/addition-5.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#e3f2fd', previewText: '2+1=?' },
|
||||
{ subcategory: 'arithmetic-visual', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 15, generatorConfig: { operators: ['+'], maxNumber: 5, showDots: true } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'addition-10', title: '10以内加法', desc: '图形化展示 10 以内加法', category: 'math', page: 'addition', subpackage: 'mathPages', icon: '➕', img: '/assets/mathEntrance/addition-10.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e3f2fd', previewText: '3+5=?' },
|
||||
{ id: 'addition-10', title: '10以内加法', desc: '图形化展示 10 以内加法', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '➕', img: '/assets/mathEntrance/addition-10.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e3f2fd', previewText: '3+5=?' },
|
||||
{ subcategory: 'arithmetic-visual', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 16, generatorConfig: { operators: ['+'], maxNumber: 10, showDots: true } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'subtraction-10', title: '10以内减法', desc: '图形化展示 10 以内减法', category: 'math', page: 'addition', subpackage: 'mathPages', icon: '➖', img: '/assets/mathEntrance/subtraction-10.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e3f2fd', previewText: '8-3=?' },
|
||||
{ id: 'subtraction-10', title: '10以内减法', desc: '图形化展示 10 以内减法', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '➖', img: '/assets/mathEntrance/subtraction-10.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e3f2fd', previewText: '8-3=?' },
|
||||
{ subcategory: 'arithmetic-visual', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 17, generatorConfig: { operators: ['-'], maxNumber: 10, showDots: true } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'addition-subtraction-10', title: '10以内加减法', desc: '加减法混合运算', category: 'math', page: 'addition', subpackage: 'mathPages', icon: '±', img: '/assets/mathEntrance/addition-subtraction-10.png', ageRange: [5, 7], difficulty: 'basic', previewBg: '#e3f2fd', previewText: '±' },
|
||||
{ id: 'addition-subtraction-10', title: '10以内加减法', desc: '加减法混合运算', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '±', img: '/assets/mathEntrance/addition-subtraction-10.png', ageRange: [5, 7], difficulty: 'basic', previewBg: '#e3f2fd', previewText: '±' },
|
||||
{ subcategory: 'arithmetic-visual', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 18, generatorConfig: { operators: ['+', '-'], maxNumber: 10, showDots: true } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'make-ten', title: '凑十法练习', desc: '20 以内进位加法', category: 'math', page: 'makeTen', subpackage: 'mathPages', icon: '➕', img: '/assets/mathEntrance/make-ten.png', ageRange: [5, 7], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '7+?=10' },
|
||||
{ id: 'make-ten', title: '凑十法练习', desc: '20 以内进位加法', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '➕', img: '/assets/mathEntrance/make-ten.png', ageRange: [5, 7], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '7+?=10' },
|
||||
{ subcategory: 'methods-ten', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 19, generatorConfig: { method: 'make-ten' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'break-ten', title: '破十法练习', desc: '20 以内退位减法', category: 'math', page: 'breakTen', subpackage: 'mathPages', icon: '➖', img: '/assets/mathEntrance/break-ten.png', ageRange: [5, 7], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '15-8=?' },
|
||||
{ id: 'break-ten', title: '破十法练习', desc: '20 以内退位减法', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '➖', img: '/assets/mathEntrance/break-ten.png', ageRange: [5, 7], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '15-8=?' },
|
||||
{ subcategory: 'methods-ten', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 20, generatorConfig: { method: 'break-ten' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'flat-ten', title: '平十法练习', desc: '20 以内退位减法', category: 'math', page: 'flatTen', subpackage: 'mathPages', icon: '➖', img: '/assets/mathEntrance/flat-ten.png', ageRange: [5, 7], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '14-6=?' },
|
||||
{ id: 'flat-ten', title: '平十法练习', desc: '20 以内退位减法', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '➖', img: '/assets/mathEntrance/flat-ten.png', ageRange: [5, 7], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '14-6=?' },
|
||||
{ subcategory: 'methods-ten', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 21, generatorConfig: { method: 'flat-ten' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'borrow-ten', title: '借十法练习', desc: '20 以上退位减法', category: 'math', page: 'borrowTen', subpackage: 'mathPages', icon: '➖', img: '/assets/mathEntrance/borrow-ten.png', ageRange: [6, 8], difficulty: 'advanced', previewBg: '#e3f2fd', previewText: '32-8=?' },
|
||||
{ id: 'borrow-ten', title: '借十法练习', desc: '20 以上退位减法', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '➖', img: '/assets/mathEntrance/borrow-ten.png', ageRange: [6, 8], difficulty: 'advanced', previewBg: '#e3f2fd', previewText: '32-8=?' },
|
||||
{ subcategory: 'methods-ten', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 22, generatorConfig: { method: 'borrow-ten' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'practice-addition', title: '加法运算', desc: '10/20/50/100 以内加法', category: 'math', page: 'calculationPractice', subpackage: 'mathPages', icon: '➕', img: '/assets/mathEntrance/practice-addition.png', ageRange: [5, 8], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '25+18=?' },
|
||||
{ id: 'practice-addition', title: '加法运算', desc: '10/20/50/100 以内加法', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '➕', img: '/assets/mathEntrance/practice-addition.png', ageRange: [5, 8], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '25+18=?' },
|
||||
{ subcategory: 'practice', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 23, generatorConfig: { operators: ['+'], preset: 'vertical' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'practice-subtraction', title: '减法运算', desc: '10/20/50/100 以内减法', category: 'math', page: 'calculationPractice', subpackage: 'mathPages', icon: '➖', img: '/assets/mathEntrance/practice-subtraction.png', ageRange: [5, 8], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '43-17=?' },
|
||||
{ id: 'practice-subtraction', title: '减法运算', desc: '10/20/50/100 以内减法', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '➖', img: '/assets/mathEntrance/practice-subtraction.png', ageRange: [5, 8], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '43-17=?' },
|
||||
{ subcategory: 'practice', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 24, generatorConfig: { operators: ['-'], preset: 'vertical' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'practice-mixed', title: '混合运算', desc: '10/20/50/100 以内加减法混合', category: 'math', page: 'calculationPractice', subpackage: 'mathPages', icon: '±', img: '/assets/mathEntrance/practice-mixed.png', ageRange: [5, 8], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '±' },
|
||||
{ id: 'practice-mixed', title: '混合运算', desc: '10/20/50/100 以内加减法混合', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '±', img: '/assets/mathEntrance/practice-mixed.png', ageRange: [5, 8], difficulty: 'intermediate', previewBg: '#e3f2fd', previewText: '±' },
|
||||
{ subcategory: 'practice', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 25, generatorConfig: { operators: ['+', '-'], preset: 'vertical' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'multiplication-table', title: '九九乘法表', desc: '学习九九乘法口诀', category: 'math', page: 'multiplicationTable', subpackage: 'mathPages', icon: '✖️', img: '/assets/mathEntrance/multiplication-table.png', ageRange: [6, 8], difficulty: 'intermediate', previewBg: '#f3e5f5', previewText: '3×4=12' },
|
||||
{ id: 'multiplication-table', title: '九九乘法表', desc: '学习九九乘法口诀', category: 'math', page: 'mathDraw', subpackage: 'mathPages', icon: '✖️', img: '/assets/mathEntrance/multiplication-table.png', ageRange: [6, 8], difficulty: 'intermediate', previewBg: '#f3e5f5', previewText: '3×4=12' },
|
||||
{ subcategory: 'practice', template: 'grid-exercise', generator: 'arithmetic', sortOrder: 26, generatorConfig: { operators: ['×'], mode: 'multiplication-table' } },
|
||||
),
|
||||
|
||||
sheet(
|
||||
{ id: 'color-shape-match', title: '根据颜色画图形', desc: '根据颜色画出对应图形', category: 'focus', page: 'colorShapeMatch', subpackage: 'focusPages', icon: '🎯', img: '/assets/focusEntrance/color-shape-match.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#f3e5f5', previewText: '🎯' },
|
||||
{ id: 'color-shape-match', title: '根据颜色画图形', desc: '根据颜色画出对应图形', category: 'focus', page: 'focusDraw', subpackage: 'focusPages', icon: '🎯', img: '/assets/focusEntrance/color-shape-match.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#f3e5f5', previewText: '🎯' },
|
||||
{ subcategory: 'focus-visual', template: 'grid-exercise', generator: 'shape-grid', sortOrder: 27, generatorConfig: { functionId: 'color-shape-match' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'shape-symbol', title: '图形符号配对', desc: '根据图形画对应符号', category: 'focus', page: 'shapeSymbol', subpackage: 'focusPages', icon: '🔗', img: '/assets/focusEntrance/shape-symbol.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e8eaf6', previewText: '△→♠' },
|
||||
{ id: 'shape-symbol', title: '图形符号配对', desc: '根据图形画对应符号', category: 'focus', page: 'focusDraw', subpackage: 'focusPages', icon: '🔗', img: '/assets/focusEntrance/shape-symbol.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e8eaf6', previewText: '△→♠' },
|
||||
{ subcategory: 'focus-visual', template: 'match-connect', generator: 'shape-grid', sortOrder: 28, generatorConfig: { functionId: 'shape-symbol' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'shape-recognition', title: '识别形状', desc: '识别形状,涂一涂', category: 'focus', page: 'shape', subpackage: 'focusPages', icon: '🔍', img: '/assets/focusEntrance/shape-recognition.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#fce4ec', previewText: '△ ○ □' },
|
||||
{ id: 'shape-recognition', title: '识别形状', desc: '识别形状,涂一涂', category: 'focus', page: 'focusDraw', subpackage: 'focusPages', icon: '🔍', img: '/assets/focusEntrance/shape-recognition.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#fce4ec', previewText: '△ ○ □' },
|
||||
{ subcategory: 'focus-visual', template: 'grid-coloring', generator: 'shape-grid', sortOrder: 29, generatorConfig: { functionId: 'shape-recognition' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'position-coloring', title: '方位涂涂乐', desc: '观察位置,在方格中涂色', category: 'focus', page: 'positionColoring', subpackage: 'focusPages', icon: '📍', img: '/assets/focusEntrance/position-coloring.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e8f5e9', previewText: '📍' },
|
||||
{ id: 'position-coloring', title: '方位涂涂乐', desc: '观察位置,在方格中涂色', category: 'focus', page: 'focusDraw', subpackage: 'focusPages', icon: '📍', img: '/assets/focusEntrance/position-coloring.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#e8f5e9', previewText: '📍' },
|
||||
{ subcategory: 'focus-grid', template: 'grid-coloring', generator: 'shape-grid', sortOrder: 30, generatorConfig: { functionId: 'position-coloring' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'color-pattern', title: '颜色找规律', desc: '观察颜色规律,在空白图形中涂色', category: 'focus', page: 'colorPattern', subpackage: 'focusPages', icon: '🎨', img: '/assets/focusEntrance/color-pattern.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#fff8e1', previewText: '🎨' },
|
||||
{ id: 'color-pattern', title: '颜色找规律', desc: '观察颜色规律,在空白图形中涂色', category: 'focus', page: 'focusDraw', subpackage: 'focusPages', icon: '🎨', img: '/assets/focusEntrance/color-pattern.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#fff8e1', previewText: '🎨' },
|
||||
{ subcategory: 'focus-grid', template: 'grid-coloring', generator: 'color-pattern', sortOrder: 31, generatorConfig: { functionId: 'color-pattern' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'match-connect', title: '连连看', desc: '根据物品连一连', category: 'focus', page: 'matchConnect', subpackage: 'focusPages', icon: '🔗', img: '/assets/focusEntrance/match-connect.png', ageRange: [3, 6], difficulty: 'beginner', previewBg: '#e0f7fa', previewText: '🔗' },
|
||||
{ id: 'match-connect', title: '连连看', desc: '根据物品连一连', category: 'focus', page: 'focusDraw', subpackage: 'focusPages', icon: '🔗', img: '/assets/focusEntrance/match-connect.png', ageRange: [3, 6], difficulty: 'beginner', previewBg: '#e0f7fa', previewText: '🔗' },
|
||||
{ subcategory: 'focus-connect', template: 'match-connect', generator: 'shape-grid', sortOrder: 32, generatorConfig: { functionId: 'match-connect' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'line-recognition', title: '线条识别', desc: '认识不同线条,画出颜色对应的线条', category: 'focus', page: 'lineRecognition', subpackage: 'focusPages', icon: '📏', img: '/assets/focusEntrance/line-recognition.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#f1f8e9', previewText: '〰️' },
|
||||
{ id: 'line-recognition', title: '线条识别', desc: '认识不同线条,画出颜色对应的线条', category: 'focus', page: 'focusDraw', subpackage: 'focusPages', icon: '📏', img: '/assets/focusEntrance/line-recognition.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#f1f8e9', previewText: '〰️' },
|
||||
{ subcategory: 'focus-visual', template: 'tracing-writing', generator: 'shape-grid', sortOrder: 33, generatorConfig: { functionId: 'line-recognition' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'grid-reasoning', title: '方格推理', desc: '推理出合并方格并连线', category: 'focus', page: 'gridReasoning', subpackage: 'focusPages', icon: '🧩', img: '/assets/focusEntrance/grid-reasoning.png', ageRange: [5, 7], difficulty: 'intermediate', previewBg: '#e8eaf6', previewText: '🧩' },
|
||||
{ id: 'grid-reasoning', title: '方格推理', desc: '推理出合并方格并连线', category: 'focus', page: 'focusDraw', subpackage: 'focusPages', icon: '🧩', img: '/assets/focusEntrance/grid-reasoning.png', ageRange: [5, 7], difficulty: 'intermediate', previewBg: '#e8eaf6', previewText: '🧩' },
|
||||
{ subcategory: 'focus-logic', template: 'special-graphic', generator: 'shape-grid', sortOrder: 34, generatorConfig: { functionId: 'grid-reasoning' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'code-connect', title: '译码连线', desc: '按数字顺序将数字对应颜色连线', category: 'focus', page: 'codeConnect', subpackage: 'focusPages', icon: '🔢', img: '/assets/focusEntrance/code-connect.png', ageRange: [5, 7], difficulty: 'intermediate', previewBg: '#fff3e0', previewText: '🔢' },
|
||||
{ id: 'code-connect', title: '译码连线', desc: '按数字顺序将数字对应颜色连线', category: 'focus', page: 'focusDraw', subpackage: 'focusPages', icon: '🔢', img: '/assets/focusEntrance/code-connect.png', ageRange: [5, 7], difficulty: 'intermediate', previewBg: '#fff3e0', previewText: '🔢' },
|
||||
{ subcategory: 'focus-connect', template: 'match-connect', generator: 'dot-connect', sortOrder: 35, generatorConfig: { functionId: 'code-connect' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'dot-connect', title: '数字点连线', desc: '按数字顺序连点成图', category: 'focus', page: 'dotConnect', subpackage: 'focusPages', icon: '🔗', img: '/assets/focusEntrance/dot-connect.png', ageRange: [3, 6], difficulty: 'beginner', previewBg: '#e3f2fd', previewText: '1→2→3' },
|
||||
{ id: 'dot-connect', title: '数字点连线', desc: '按数字顺序连点成图', category: 'focus', page: 'focusDraw', subpackage: 'focusPages', icon: '🔗', img: '/assets/focusEntrance/dot-connect.png', ageRange: [3, 6], difficulty: 'beginner', previewBg: '#e3f2fd', previewText: '1→2→3' },
|
||||
{ subcategory: 'focus-connect', template: 'match-connect', generator: 'dot-connect', sortOrder: 36, generatorConfig: { functionId: 'dot-connect' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'grid-drawing-3x3', title: '格子仿画 3×3', desc: '简单有趣,培养专注力', category: 'focus', page: 'gridDrawing', subpackage: 'focusPages', icon: '🎨', mode: '3x3', img: '/assets/focusEntrance/grid-drawing-3x3.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#fce4ec', previewText: '▦' },
|
||||
{ id: 'grid-drawing-3x3', title: '格子仿画 3×3', desc: '简单有趣,培养专注力', category: 'focus', page: 'focusDraw', subpackage: 'focusPages', icon: '🎨', mode: '3x3', img: '/assets/focusEntrance/grid-drawing-3x3.png', ageRange: [3, 5], difficulty: 'beginner', previewBg: '#fce4ec', previewText: '▦' },
|
||||
{ subcategory: 'grid-copy', template: 'grid-exercise', generator: 'shape-grid', sortOrder: 37, generatorConfig: { grid: '3x3' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'grid-drawing-5x5', title: '格子仿画 5×5', desc: '创意挑战,提升观察力', category: 'focus', page: 'gridDrawing', subpackage: 'focusPages', icon: '🎨', mode: '5x5', img: '/assets/focusEntrance/grid-drawing-5x5.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#fce4ec', previewText: '▦' },
|
||||
{ id: 'grid-drawing-5x5', title: '格子仿画 5×5', desc: '创意挑战,提升观察力', category: 'focus', page: 'focusDraw', subpackage: 'focusPages', icon: '🎨', mode: '5x5', img: '/assets/focusEntrance/grid-drawing-5x5.png', ageRange: [4, 6], difficulty: 'basic', previewBg: '#fce4ec', previewText: '▦' },
|
||||
{ subcategory: 'grid-copy', template: 'grid-exercise', generator: 'shape-grid', sortOrder: 38, generatorConfig: { grid: '5x5' } },
|
||||
),
|
||||
sheet(
|
||||
{ id: 'grid-drawing-7x7', title: '格子仿画 7×7', desc: '大师挑战,锻炼耐心', category: 'focus', page: 'gridDrawing', subpackage: 'focusPages', icon: '🎨', mode: '7x7', img: '/assets/focusEntrance/grid-drawing-7x7.png', ageRange: [5, 8], difficulty: 'intermediate', previewBg: '#fce4ec', previewText: '▦' },
|
||||
{ id: 'grid-drawing-7x7', title: '格子仿画 7×7', desc: '大师挑战,锻炼耐心', category: 'focus', page: 'focusDraw', subpackage: 'focusPages', icon: '🎨', mode: '7x7', img: '/assets/focusEntrance/grid-drawing-7x7.png', ageRange: [5, 8], difficulty: 'intermediate', previewBg: '#fce4ec', previewText: '▦' },
|
||||
{ subcategory: 'grid-copy', template: 'grid-exercise', generator: 'shape-grid', sortOrder: 39, generatorConfig: { grid: '7x7' } },
|
||||
),
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"navigationBarTitleText": "译码连线",
|
||||
"navigationBarBackgroundColor": "#FFD719",
|
||||
"homeButton": true,
|
||||
"backgroundColor": "#F6F6F6",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"toy-button": "../../toy/button-v2/button",
|
||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
||||
"math-bottom-buttons": "../../components/math-bottom-buttons/math-bottom-buttons",
|
||||
"draw-ad": "../../components/draw-ad/draw-ad"
|
||||
}
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
import CodeConnectDraw from '../shared/service/codeConnectDraw';
|
||||
import { createFocusPage } from '../shared/common/focusPageMixin';
|
||||
import { CodeConnectData } from '../shared/service/codeConnectDraw';
|
||||
import { WATER_COLORS } from '../../constants/colors';
|
||||
|
||||
createFocusPage({
|
||||
canvas: null as Canvas | null,
|
||||
ctx: null as RenderingContext | null,
|
||||
boxHeight: 0,
|
||||
boxWidth: 0,
|
||||
drawService: null as CodeConnectDraw | null,
|
||||
codeData: null as CodeConnectData | null,
|
||||
|
||||
data: {
|
||||
pageTitle: '译码连线',
|
||||
functionId: '',
|
||||
hasContent: false,
|
||||
showShareDialog: false,
|
||||
},
|
||||
|
||||
onLoad(options: { id?: string }) {
|
||||
const functionId = options.id || 'code-connect';
|
||||
this.setData({
|
||||
functionId,
|
||||
});
|
||||
this.initPageInfo(functionId, '译码连线');
|
||||
},
|
||||
|
||||
onReady() {
|
||||
this.initCanvas({
|
||||
createDrawService: (
|
||||
canvas: Canvas,
|
||||
ctx: RenderingContext,
|
||||
options?: Record<string, any>,
|
||||
) => {
|
||||
return new CodeConnectDraw(canvas, ctx, options);
|
||||
},
|
||||
drawServiceOptions: {
|
||||
title: this.data.pageTitle,
|
||||
subTitle: '按照数字顺序,将数字对应的颜色连起来',
|
||||
functionId: this.data.functionId,
|
||||
},
|
||||
onCanvasReady: () => {
|
||||
// 初始随机生成
|
||||
this.onRandom();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 绘制Canvas内容
|
||||
*/
|
||||
async drawCanvas() {
|
||||
if (!this.ctx || !this.drawService || !this.codeData) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.drawService.draw(this.codeData);
|
||||
this.setData({ hasContent: true });
|
||||
} catch (error) {
|
||||
console.error('绘制失败:', error);
|
||||
this.setData({ hasContent: false });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 随机生成
|
||||
*/
|
||||
onRandom() {
|
||||
// 随机选择起始数字:1、2或3
|
||||
const startNumberOptions = [1, 2, 3];
|
||||
const startNumber =
|
||||
startNumberOptions[
|
||||
Math.floor(Math.random() * startNumberOptions.length)
|
||||
];
|
||||
|
||||
// 生成8个连续的数字(1-8、2-9或3-10)
|
||||
const numbers: number[] = [];
|
||||
for (let i = 0; i < 8; i++) {
|
||||
numbers.push(startNumber + i);
|
||||
}
|
||||
|
||||
// 从WATER_COLORS.extended24中随机选择8种不重复的颜色
|
||||
const colorIndices = Array.from(
|
||||
{ length: WATER_COLORS.extended24.length },
|
||||
(_, i) => i,
|
||||
);
|
||||
const shuffledIndices = [...colorIndices].sort(
|
||||
() => Math.random() - 0.5,
|
||||
);
|
||||
const selectedIndices = shuffledIndices.slice(0, 8);
|
||||
|
||||
// 创建颜色映射
|
||||
const colorMap = numbers.map((number, index) => ({
|
||||
number,
|
||||
color: WATER_COLORS.extended24[selectedIndices[index]].hex,
|
||||
}));
|
||||
|
||||
// 生成4个组的题目
|
||||
const groups: Array<{
|
||||
sequence: number[];
|
||||
dots: Array<{
|
||||
number: number;
|
||||
color: string;
|
||||
angle: number;
|
||||
}>;
|
||||
}> = [];
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
// 从当前数字范围中随机选择6个数字(不重复)
|
||||
const shuffledNumbers = [...numbers].sort(
|
||||
() => Math.random() - 0.5,
|
||||
);
|
||||
const sequence = shuffledNumbers.slice(0, 5);
|
||||
|
||||
// 生成8个圆点的位置(圆形分布)
|
||||
const dots: Array<{
|
||||
number: number;
|
||||
color: string;
|
||||
angle: number;
|
||||
}> = [];
|
||||
|
||||
// 8个圆点均匀分布在圆周上
|
||||
for (let j = 0; j < 8; j++) {
|
||||
const angle = (j / 8) * Math.PI * 2 - Math.PI / 2; // 从顶部开始,逆时针
|
||||
const number = numbers[j];
|
||||
const colorInfo = colorMap.find((m) => m.number === number);
|
||||
dots.push({
|
||||
number,
|
||||
color: colorInfo?.color || '#000',
|
||||
angle,
|
||||
});
|
||||
}
|
||||
|
||||
groups.push({
|
||||
sequence,
|
||||
dots,
|
||||
});
|
||||
}
|
||||
|
||||
this.codeData = {
|
||||
colorMap,
|
||||
startNumber,
|
||||
groups,
|
||||
};
|
||||
|
||||
this.drawCanvas();
|
||||
},
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
<!-- 模版不能跨包引用 -->
|
||||
<import src="../shared/templates/canvas-page-template.wxml" />
|
||||
|
||||
<template
|
||||
is="canvasPageTemplate"
|
||||
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: false, adType: 'focusDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
|
||||
@@ -1 +0,0 @@
|
||||
@import '../../base/baseDrawPage.wxss';
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"navigationBarTitleText": "颜色找规律",
|
||||
"navigationBarBackgroundColor": "#FFD719",
|
||||
"homeButton": true,
|
||||
"backgroundColor": "#F6F6F6",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"toy-button": "../../toy/button-v2/button",
|
||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
||||
"math-bottom-buttons": "../../components/math-bottom-buttons/math-bottom-buttons",
|
||||
"draw-ad": "../../components/draw-ad/draw-ad"
|
||||
}
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
import ColorPatternDraw from '../shared/service/colorPatternDraw';
|
||||
import { createFocusPage } from '../shared/common/focusPageMixin';
|
||||
import { ColorPatternData } from '../shared/service/colorPatternDraw';
|
||||
import { SHAPE_SYMBOL_SHAPES } from '../shared/shapes/shapeSymbolShapes';
|
||||
import { getNumberColors } from '../../constants/colors';
|
||||
|
||||
createFocusPage({
|
||||
canvas: null as Canvas | null,
|
||||
ctx: null as RenderingContext | null,
|
||||
boxHeight: 0,
|
||||
boxWidth: 0,
|
||||
drawService: null as ColorPatternDraw | null,
|
||||
gridData: null as ColorPatternData | null,
|
||||
|
||||
data: {
|
||||
pageTitle: '颜色找规律',
|
||||
functionId: '',
|
||||
hasContent: false,
|
||||
showShareDialog: false,
|
||||
},
|
||||
|
||||
onLoad(options: { id?: string }) {
|
||||
const functionId = options.id || 'color-pattern';
|
||||
this.setData({
|
||||
functionId,
|
||||
});
|
||||
this.initPageInfo(functionId, '颜色找规律');
|
||||
},
|
||||
|
||||
onReady() {
|
||||
this.initCanvas({
|
||||
createDrawService: (
|
||||
canvas: Canvas,
|
||||
ctx: RenderingContext,
|
||||
options?: Record<string, any>,
|
||||
) => {
|
||||
return new ColorPatternDraw(canvas, ctx, options);
|
||||
},
|
||||
drawServiceOptions: {
|
||||
title: this.data.pageTitle,
|
||||
subTitle: '观察颜色规律,在空白图形中涂上颜色',
|
||||
functionId: this.data.functionId,
|
||||
},
|
||||
onCanvasReady: () => {
|
||||
// 初始随机生成
|
||||
this.onRandom();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 绘制Canvas内容
|
||||
*/
|
||||
async drawCanvas() {
|
||||
if (!this.ctx || !this.drawService || !this.gridData) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.drawService.draw(this.gridData);
|
||||
this.setData({ hasContent: true });
|
||||
} catch (error) {
|
||||
console.error('绘制失败:', error);
|
||||
this.setData({ hasContent: false });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 随机生成
|
||||
*/
|
||||
onRandom() {
|
||||
// 从9种图形中随机选择8个不同的图形(每组使用不同的图形)
|
||||
const allShapes = [...SHAPE_SYMBOL_SHAPES];
|
||||
const shuffledShapes = allShapes.sort(() => Math.random() - 0.5);
|
||||
const selectedShapes = shuffledShapes.slice(0, 8); // 选择8个不同的图形
|
||||
|
||||
// 获取所有可用颜色
|
||||
const allColors = getNumberColors();
|
||||
|
||||
// 生成8组数据
|
||||
const groups: Array<{
|
||||
shapeId: string;
|
||||
colors: Array<string | null>; // 前4个有颜色,后6个为null
|
||||
}> = [];
|
||||
|
||||
for (let groupIndex = 0; groupIndex < 8; groupIndex++) {
|
||||
// 每一组使用不同的图形
|
||||
const selectedShape = selectedShapes[groupIndex];
|
||||
|
||||
// 每一行独立选择2种或3种颜色
|
||||
const colorCount = 2;
|
||||
const shuffledColors = [...allColors].sort(
|
||||
() => Math.random() - 0.5,
|
||||
);
|
||||
const selectedColors = shuffledColors.slice(0, colorCount);
|
||||
|
||||
// 生成前4个图形的颜色规律
|
||||
// 从选中的颜色中随机选择,形成规律(可以重复)
|
||||
// 但至少保证有两种不同的颜色,不能4个都相同
|
||||
const patternColors: string[] = [];
|
||||
for (let i = 0; i < 4; i++) {
|
||||
// 随机从选中的颜色中选择一种
|
||||
const randomColor =
|
||||
selectedColors[
|
||||
Math.floor(Math.random() * selectedColors.length)
|
||||
];
|
||||
patternColors.push(randomColor);
|
||||
}
|
||||
|
||||
// 检查是否只有一种颜色,如果是则确保至少有两种颜色
|
||||
const uniqueColors = new Set(patternColors);
|
||||
if (uniqueColors.size === 1) {
|
||||
// 如果4个都是同一种颜色,随机选择一个位置替换为另一种颜色
|
||||
const currentColor = patternColors[0];
|
||||
const otherColors = selectedColors.filter(
|
||||
(c) => c !== currentColor,
|
||||
);
|
||||
if (otherColors.length > 0) {
|
||||
const randomIndex = Math.floor(
|
||||
Math.random() * patternColors.length,
|
||||
);
|
||||
const randomOtherColor =
|
||||
otherColors[
|
||||
Math.floor(Math.random() * otherColors.length)
|
||||
];
|
||||
patternColors[randomIndex] = randomOtherColor;
|
||||
}
|
||||
}
|
||||
|
||||
// 后6个图形留空(null)
|
||||
const emptyColors: Array<string | null> = new Array(6).fill(null);
|
||||
|
||||
groups.push({
|
||||
shapeId: selectedShape.id,
|
||||
colors: [...patternColors, ...emptyColors],
|
||||
});
|
||||
}
|
||||
|
||||
this.gridData = {
|
||||
groups,
|
||||
};
|
||||
|
||||
this.drawCanvas();
|
||||
},
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
<!-- 模版不能跨包引用 -->
|
||||
<import src="../shared/templates/canvas-page-template.wxml" />
|
||||
|
||||
<template
|
||||
is="canvasPageTemplate"
|
||||
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: false, adType: 'focusDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
|
||||
@@ -1 +0,0 @@
|
||||
@import '../../base/baseDrawPage.wxss';
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"navigationBarTitleText": "根据颜色画图形",
|
||||
"navigationBarBackgroundColor": "#FFD719",
|
||||
"homeButton": true,
|
||||
"backgroundColor": "#F6F6F6",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"toy-button": "../../toy/button-v2/button",
|
||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
||||
"math-bottom-buttons": "../../components/math-bottom-buttons/math-bottom-buttons",
|
||||
"draw-ad": "../../components/draw-ad/draw-ad"
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
import ColorShapeMatchDraw from '../shared/service/colorShapeMatchDraw';
|
||||
import { createFocusPage } from '../shared/common/focusPageMixin';
|
||||
import { ColorShapeMatchData } from '../shared/service/colorShapeMatchDraw';
|
||||
import { SHAPE_SYMBOL_SHAPES } from '../shared/shapes/shapeSymbolShapes';
|
||||
import { getRandomUniqueNumberColors } from '../../constants/colors';
|
||||
|
||||
createFocusPage({
|
||||
canvas: null as Canvas | null,
|
||||
ctx: null as RenderingContext | null,
|
||||
boxHeight: 0,
|
||||
boxWidth: 0,
|
||||
drawService: null as ColorShapeMatchDraw | null,
|
||||
gridData: null as ColorShapeMatchData | null,
|
||||
|
||||
data: {
|
||||
pageTitle: '根据颜色画图形',
|
||||
functionId: '',
|
||||
hasContent: false,
|
||||
showShareDialog: false,
|
||||
},
|
||||
|
||||
onLoad(options: { id?: string }) {
|
||||
const functionId = options.id || 'color-shape-match';
|
||||
this.setData({
|
||||
functionId,
|
||||
});
|
||||
this.initPageInfo(functionId, '根据颜色画图形');
|
||||
},
|
||||
|
||||
onReady() {
|
||||
this.initCanvas({
|
||||
createDrawService: (
|
||||
canvas: Canvas,
|
||||
ctx: RenderingContext,
|
||||
options?: Record<string, any>,
|
||||
) => {
|
||||
return new ColorShapeMatchDraw(canvas, ctx, options);
|
||||
},
|
||||
drawServiceOptions: {
|
||||
title: this.data.pageTitle,
|
||||
subTitle: '根据颜色画出对应的图形',
|
||||
functionId: this.data.functionId,
|
||||
},
|
||||
onCanvasReady: () => {
|
||||
// 初始随机生成
|
||||
this.onRandom();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 绘制Canvas内容
|
||||
*/
|
||||
async drawCanvas() {
|
||||
if (!this.ctx || !this.drawService || !this.gridData) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.drawService.draw(this.gridData);
|
||||
this.setData({ hasContent: true });
|
||||
} catch (error) {
|
||||
console.error('绘制失败:', error);
|
||||
this.setData({ hasContent: false });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 随机生成
|
||||
*/
|
||||
onRandom() {
|
||||
// 从所有图形中随机选择4个
|
||||
const shapes = [...SHAPE_SYMBOL_SHAPES].filter(
|
||||
(shape) => shape.id !== 'cross',
|
||||
);
|
||||
const selectedShapes = shapes
|
||||
.sort(() => Math.random() - 0.5)
|
||||
.slice(0, 4);
|
||||
|
||||
// 获取4个不重复的颜色
|
||||
const colors = getRandomUniqueNumberColors(4);
|
||||
|
||||
// 创建阅览区域的数据,过滤掉 cross
|
||||
const legendItems = selectedShapes.map((shape, index) => ({
|
||||
shapeId: shape.id,
|
||||
color: colors[index],
|
||||
}));
|
||||
|
||||
// 创建练习区域的数据(5行5列,共25个格子)
|
||||
// 随机填充这4种颜色,确保每种颜色至少出现一次
|
||||
const practiceGrid: Array<Array<string | null>> = [];
|
||||
const allColors: (string | null)[] = new Array(25).fill(null);
|
||||
|
||||
// 先随机选择4个位置,每个位置分配一种颜色(确保每种颜色至少出现一次)
|
||||
const positions: number[] = [];
|
||||
for (let i = 0; i < 25; i++) {
|
||||
positions.push(i);
|
||||
}
|
||||
const shuffledPositions = [...positions].sort(
|
||||
() => Math.random() - 0.5,
|
||||
);
|
||||
|
||||
// 前4个位置分配4种颜色
|
||||
for (let i = 0; i < 4; i++) {
|
||||
allColors[shuffledPositions[i]] = colors[i];
|
||||
}
|
||||
|
||||
// 剩余21个位置随机分配这4种颜色
|
||||
for (let i = 4; i < 25; i++) {
|
||||
const randomColor =
|
||||
colors[Math.floor(Math.random() * colors.length)];
|
||||
allColors[shuffledPositions[i]] = randomColor;
|
||||
}
|
||||
|
||||
// 转换为5x5的二维数组
|
||||
for (let row = 0; row < 5; row++) {
|
||||
const practiceRow: Array<string | null> = [];
|
||||
for (let col = 0; col < 5; col++) {
|
||||
const index = row * 5 + col;
|
||||
practiceRow.push(allColors[index]);
|
||||
}
|
||||
practiceGrid.push(practiceRow);
|
||||
}
|
||||
|
||||
this.gridData = {
|
||||
legendItems,
|
||||
practiceGrid,
|
||||
};
|
||||
|
||||
this.drawCanvas();
|
||||
},
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
<!-- 模版不能跨包引用 -->
|
||||
<import src="../shared/templates/canvas-page-template.wxml" />
|
||||
|
||||
<template
|
||||
is="canvasPageTemplate"
|
||||
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: false, adType: 'focusDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
|
||||
@@ -1 +0,0 @@
|
||||
@import '../../base/baseDrawPage.wxss';
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"navigationBarTitleText": "数字点连线",
|
||||
"navigationBarBackgroundColor": "#FFD719",
|
||||
"homeButton": true,
|
||||
"backgroundColor": "#F6F6F6",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"toy-button": "../../toy/button-v2/button",
|
||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
||||
"math-bottom-buttons": "../../components/math-bottom-buttons/math-bottom-buttons",
|
||||
"draw-ad": "../../components/draw-ad/draw-ad"
|
||||
}
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
import DotConnectDraw from '../shared/service/dotConnectDraw';
|
||||
import { createFocusPage } from '../shared/common/focusPageMixin';
|
||||
import { DotConnectData } from '../shared/service/dotConnectDraw';
|
||||
import { getRandomUniqueNumberColors } from '../../constants/colors';
|
||||
|
||||
createFocusPage({
|
||||
canvas: null as Canvas | null,
|
||||
ctx: null as RenderingContext | null,
|
||||
boxHeight: 0,
|
||||
boxWidth: 0,
|
||||
drawService: null as DotConnectDraw | null,
|
||||
dotConnectData: null as DotConnectData | null,
|
||||
|
||||
data: {
|
||||
pageTitle: '数字点连线',
|
||||
functionId: '',
|
||||
hasContent: false,
|
||||
showShareDialog: false,
|
||||
},
|
||||
|
||||
onLoad(options: { id?: string }) {
|
||||
const functionId = options.id || 'dot-connect';
|
||||
this.setData({
|
||||
functionId,
|
||||
});
|
||||
this.initPageInfo(functionId, '数字点连线');
|
||||
},
|
||||
|
||||
onReady() {
|
||||
this.initCanvas({
|
||||
createDrawService: (
|
||||
canvas: Canvas,
|
||||
ctx: RenderingContext,
|
||||
options?: Record<string, any>,
|
||||
) => {
|
||||
return new DotConnectDraw(canvas, ctx, options);
|
||||
},
|
||||
drawServiceOptions: {
|
||||
title: this.data.pageTitle,
|
||||
functionId: this.data.functionId,
|
||||
},
|
||||
onCanvasReady: () => {
|
||||
// 初始随机生成
|
||||
this.onRandom();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 绘制Canvas内容
|
||||
*/
|
||||
async drawCanvas() {
|
||||
if (!this.ctx || !this.drawService || !this.dotConnectData) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.drawService.draw(this.dotConnectData);
|
||||
this.setData({ hasContent: true });
|
||||
} catch (error) {
|
||||
console.error('绘制失败:', error);
|
||||
this.setData({ hasContent: false });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 生成不走回头路的数字序列
|
||||
* @param length 序列长度
|
||||
* @returns 数字序列数组
|
||||
*/
|
||||
generateNonBacktrackingSequence(length: number): number[] {
|
||||
// 3x3 网格中数字的位置映射
|
||||
// 1: (0,0), 2: (1,0), 3: (2,0)
|
||||
// 4: (0,1), 5: (1,1), 6: (2,1)
|
||||
// 7: (0,2), 8: (1,2), 9: (2,2)
|
||||
const getPosition = (num: number): [number, number] => {
|
||||
const row = Math.floor((num - 1) / 3);
|
||||
const col = (num - 1) % 3;
|
||||
return [row, col];
|
||||
};
|
||||
|
||||
// 检查从位置A到位置B再到位置C是否走回头路
|
||||
const isBacktrack = (
|
||||
posA: [number, number],
|
||||
posB: [number, number],
|
||||
posC: [number, number],
|
||||
): boolean => {
|
||||
// 如果C等于A,说明走回头路了
|
||||
return posC[0] === posA[0] && posC[1] === posA[1];
|
||||
};
|
||||
|
||||
const sequence: number[] = [];
|
||||
const used = new Set<number>();
|
||||
|
||||
// 随机选择起始数字
|
||||
const startNum = Math.floor(Math.random() * 9) + 1;
|
||||
sequence.push(startNum);
|
||||
used.add(startNum);
|
||||
|
||||
let prevPos = getPosition(startNum);
|
||||
let prevPrevPos: [number, number] | null = null;
|
||||
|
||||
// 生成剩余的数字
|
||||
while (sequence.length < length) {
|
||||
const candidates: number[] = [];
|
||||
for (let num = 1; num <= 9; num++) {
|
||||
if (used.has(num)) continue;
|
||||
|
||||
const currentPos = getPosition(num);
|
||||
// 如果有前前一个位置,检查是否走回头路
|
||||
if (prevPrevPos) {
|
||||
if (isBacktrack(prevPrevPos, prevPos, currentPos)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
candidates.push(num);
|
||||
}
|
||||
|
||||
if (candidates.length === 0) {
|
||||
// 如果没有候选数字,随机选择一个未使用的数字
|
||||
const remaining = Array.from(
|
||||
{ length: 9 },
|
||||
(_, i) => i + 1,
|
||||
).filter((n) => !used.has(n));
|
||||
if (remaining.length === 0) break;
|
||||
const nextNum =
|
||||
remaining[Math.floor(Math.random() * remaining.length)];
|
||||
sequence.push(nextNum);
|
||||
used.add(nextNum);
|
||||
prevPrevPos = prevPos;
|
||||
prevPos = getPosition(nextNum);
|
||||
} else {
|
||||
// 随机选择一个候选数字
|
||||
const nextNum =
|
||||
candidates[Math.floor(Math.random() * candidates.length)];
|
||||
sequence.push(nextNum);
|
||||
used.add(nextNum);
|
||||
prevPrevPos = prevPos;
|
||||
prevPos = getPosition(nextNum);
|
||||
}
|
||||
}
|
||||
|
||||
return sequence;
|
||||
},
|
||||
|
||||
/**
|
||||
* 随机生成
|
||||
*/
|
||||
onRandom() {
|
||||
// 生成1-9的数字颜色映射(每个数字使用随机颜色)
|
||||
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||
const colors = getRandomUniqueNumberColors(9);
|
||||
const colorMap = numbers.map((number, index) => ({
|
||||
number,
|
||||
color: colors[index],
|
||||
}));
|
||||
|
||||
// 生成9个组的题目
|
||||
const groups: Array<{
|
||||
sequence: number[];
|
||||
}> = [];
|
||||
|
||||
for (let i = 0; i < 9; i++) {
|
||||
// 生成不走回头路的数字序列,长度在4-6之间
|
||||
const sequenceLength = Math.floor(Math.random() * 3) + 4; // 4, 5, 或 6
|
||||
const sequence =
|
||||
this.generateNonBacktrackingSequence(sequenceLength);
|
||||
groups.push({
|
||||
sequence,
|
||||
});
|
||||
}
|
||||
|
||||
this.dotConnectData = {
|
||||
colorMap,
|
||||
groups,
|
||||
};
|
||||
|
||||
this.drawCanvas();
|
||||
},
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
<!-- 模版不能跨包引用 -->
|
||||
<import src="../shared/templates/canvas-page-template.wxml" />
|
||||
|
||||
<template
|
||||
is="canvasPageTemplate"
|
||||
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: false, adType: 'focusDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
|
||||
@@ -1 +0,0 @@
|
||||
@import '../../base/baseDrawPage.wxss';
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"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",
|
||||
"draw-ad": "../../components/draw-ad/draw-ad",
|
||||
"preview-footer-actions": "../../components3.0/preview-footer-actions/preview-footer-actions",
|
||||
"toy-icon": "../../toy/icon/icon"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
@import '../../style/theme.less';
|
||||
|
||||
page {
|
||||
background-color: @bg-page;
|
||||
}
|
||||
|
||||
.fd-page {
|
||||
min-height: 100vh;
|
||||
padding: 0 @page-padding-x;
|
||||
padding-bottom: calc(200rpx + env(safe-area-inset-bottom));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.fd-main {
|
||||
padding-top: 24rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 64rpx;
|
||||
}
|
||||
|
||||
/* ===== 预览卡 ===== */
|
||||
.fd-preview-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.fd-preview-card {
|
||||
background: @bg-white;
|
||||
border-radius: @radius-xl;
|
||||
box-shadow: @shadow;
|
||||
padding: 48rpx;
|
||||
border: @border;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.fd-preview-kicker {
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: @fs-section-head-title;
|
||||
font-weight: bold;
|
||||
color: @text-secondary;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.fd-canvas-wrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 400rpx;
|
||||
background: @bg-gray;
|
||||
border-radius: @radius-sm;
|
||||
border: 2rpx dashed rgba(50, 46, 37, 0.12);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fd-canvas {
|
||||
max-width: 100%;
|
||||
border-radius: @radius-sm;
|
||||
box-shadow: @shadow;
|
||||
}
|
||||
|
||||
/* ===== 区块 ===== */
|
||||
.fd-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32rpx;
|
||||
}
|
||||
|
||||
.fd-section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #6d3b00;
|
||||
padding-left: 8rpx;
|
||||
}
|
||||
|
||||
/* ===== 练习类型 2列卡片网格 ===== */
|
||||
.fd-type-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 32rpx;
|
||||
}
|
||||
|
||||
.fd-type-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
padding: 32rpx 24rpx;
|
||||
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),
|
||||
box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.fd-type-card__icon {
|
||||
font-size: 42rpx;
|
||||
line-height: 1;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.fd-type-card--active {
|
||||
background: linear-gradient(145deg, #ffd709 0%, #efc900 100%);
|
||||
box-shadow: 0 8rpx 32rpx rgba(50, 46, 37, 0.06);
|
||||
}
|
||||
|
||||
.fd-type-card--pressed {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.fd-type-card__label {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: @text-secondary;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.fd-type-card--active .fd-type-card__label {
|
||||
color: #453900;
|
||||
}
|
||||
|
||||
/* ===== 子选项 Chip ===== */
|
||||
.fd-chip-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 32rpx;
|
||||
}
|
||||
|
||||
.fd-chip {
|
||||
flex: 1;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
padding: 24rpx 24rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
line-height: 40rpx;
|
||||
color: @text-secondary;
|
||||
background: @bg-card;
|
||||
border-radius: 32rpx;
|
||||
transition:
|
||||
background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
transform 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
color 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.fd-chip--pressed {
|
||||
transform: scale(0.96);
|
||||
background: @brand;
|
||||
color: @text-selected-btn;
|
||||
box-shadow: 0 2rpx 8rpx rgba(50, 46, 37, 0.08);
|
||||
}
|
||||
|
||||
.fd-chip--active {
|
||||
background: @brand;
|
||||
color: @text-selected-btn;
|
||||
box-shadow: 0 2rpx 8rpx rgba(50, 46, 37, 0.08);
|
||||
}
|
||||
|
||||
/* ===== 随机生成按钮 ===== */
|
||||
.fd-shuffle-btn {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 108rpx;
|
||||
padding: 0 32rpx;
|
||||
border-radius: 32rpx;
|
||||
border: 8rpx solid @bg-card;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
transition:
|
||||
transform 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.fd-shuffle-btn--hover {
|
||||
transform: scale(0.98);
|
||||
opacity: 0.92;
|
||||
background-color: rgba(234, 226, 208, 0.55);
|
||||
}
|
||||
|
||||
.fd-shuffle-btn toy-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.fd-shuffle-btn__text {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
color: @text-secondary;
|
||||
transition: color 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
import { createFocusPage } from '../shared/common/focusPageMixin';
|
||||
import { BaseDrawService } from '../../core/draw/baseDraw';
|
||||
import {
|
||||
FOCUS_TYPE_CONFIGS,
|
||||
findTypeByRouteId,
|
||||
type FocusTypeConfig,
|
||||
type FocusTypeAction,
|
||||
} from './registry';
|
||||
|
||||
const TYPE_LIST = FOCUS_TYPE_CONFIGS.map((t) => ({
|
||||
id: t.id,
|
||||
title: t.title,
|
||||
icon: t.icon,
|
||||
}));
|
||||
|
||||
createFocusPage({
|
||||
canvas: null as Canvas | null,
|
||||
ctx: null as RenderingContext | null,
|
||||
boxHeight: 0,
|
||||
boxWidth: 0,
|
||||
drawService: null as BaseDrawService | null,
|
||||
currentTypeConfig: null as FocusTypeConfig | null,
|
||||
currentData: null as any,
|
||||
|
||||
data: {
|
||||
pageTitle: '专注力练习',
|
||||
functionId: '',
|
||||
hasContent: false,
|
||||
showShareDialog: false,
|
||||
boxWidth: 0,
|
||||
boxHeight: 0,
|
||||
selectedTypeId: '',
|
||||
typeList: TYPE_LIST,
|
||||
showActions: false,
|
||||
actionsTitle: '选择模式',
|
||||
currentActions: [] as FocusTypeAction[],
|
||||
currentMode: '',
|
||||
},
|
||||
|
||||
onLoad(options: { id?: string; mode?: string }) {
|
||||
const routeId = options.id || FOCUS_TYPE_CONFIGS[0].id;
|
||||
const result = findTypeByRouteId(routeId);
|
||||
if (!result) return;
|
||||
|
||||
const { typeConfig, mode } = result;
|
||||
const initialMode =
|
||||
options.mode || mode || typeConfig.defaultMode || '';
|
||||
|
||||
this.currentTypeConfig = typeConfig;
|
||||
|
||||
const title = typeConfig.getTitle
|
||||
? typeConfig.getTitle(initialMode)
|
||||
: typeConfig.title;
|
||||
|
||||
this.setData({
|
||||
selectedTypeId: typeConfig.id,
|
||||
functionId: routeId,
|
||||
pageTitle: title,
|
||||
showActions: !!typeConfig.actions,
|
||||
currentActions: typeConfig.actions || [],
|
||||
actionsTitle: typeConfig.actionsTitle || '选择模式',
|
||||
currentMode: initialMode,
|
||||
});
|
||||
|
||||
this.initPageInfo(routeId, title);
|
||||
},
|
||||
|
||||
onReady() {
|
||||
if (!this.currentTypeConfig) return;
|
||||
|
||||
const typeConfig = this.currentTypeConfig;
|
||||
const mode = this.data.currentMode;
|
||||
|
||||
this.initCanvas({
|
||||
createDrawService: (
|
||||
canvas: Canvas,
|
||||
ctx: RenderingContext,
|
||||
options?: Record<string, any>,
|
||||
) => typeConfig.createDrawService(canvas, ctx, options),
|
||||
drawServiceOptions: {
|
||||
title: this.data.pageTitle,
|
||||
subTitle: typeConfig.getSubTitle
|
||||
? typeConfig.getSubTitle(mode)
|
||||
: typeConfig.subTitle,
|
||||
functionId: this.data.functionId,
|
||||
},
|
||||
onCanvasReady: () => {
|
||||
this.onRandom();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/** 切换练习类型 */
|
||||
onSelectType(e: WechatMiniprogram.TouchEvent) {
|
||||
const id = e.currentTarget.dataset.id as string;
|
||||
if (!id || id === this.data.selectedTypeId) return;
|
||||
|
||||
const typeConfig = FOCUS_TYPE_CONFIGS.find((t) => t.id === id);
|
||||
if (!typeConfig) return;
|
||||
|
||||
this.currentTypeConfig = typeConfig;
|
||||
const mode = typeConfig.defaultMode || '';
|
||||
const title = typeConfig.getTitle
|
||||
? typeConfig.getTitle(mode)
|
||||
: typeConfig.title;
|
||||
const subTitle = typeConfig.getSubTitle
|
||||
? typeConfig.getSubTitle(mode)
|
||||
: typeConfig.subTitle;
|
||||
|
||||
this.setData({
|
||||
selectedTypeId: id,
|
||||
functionId: id,
|
||||
pageTitle: title,
|
||||
showActions: !!typeConfig.actions,
|
||||
currentActions: typeConfig.actions || [],
|
||||
actionsTitle: typeConfig.actionsTitle || '选择模式',
|
||||
currentMode: mode,
|
||||
});
|
||||
|
||||
this.initPageInfo(id, title);
|
||||
|
||||
if (this.canvas && this.ctx) {
|
||||
this.drawService = typeConfig.createDrawService(
|
||||
this.canvas,
|
||||
this.ctx,
|
||||
{ title, subTitle, functionId: id },
|
||||
);
|
||||
this.onRandom();
|
||||
}
|
||||
},
|
||||
|
||||
/** 切换子选项(模式) */
|
||||
onSelectMode(e: WechatMiniprogram.TouchEvent) {
|
||||
const value = e.currentTarget.dataset.value as string;
|
||||
if (!value || value === this.data.currentMode) return;
|
||||
|
||||
const typeConfig = this.currentTypeConfig;
|
||||
if (!typeConfig) return;
|
||||
|
||||
const title = typeConfig.getTitle
|
||||
? typeConfig.getTitle(value)
|
||||
: typeConfig.title;
|
||||
const subTitle = typeConfig.getSubTitle
|
||||
? typeConfig.getSubTitle(value)
|
||||
: typeConfig.subTitle;
|
||||
|
||||
this.setData({
|
||||
currentMode: value,
|
||||
pageTitle: title,
|
||||
});
|
||||
|
||||
this.initPageInfo(this.data.functionId, title);
|
||||
|
||||
if (this.drawService) {
|
||||
(this.drawService as any).options.title = title;
|
||||
(this.drawService as any).options.subTitle = subTitle;
|
||||
}
|
||||
|
||||
this.onRandom();
|
||||
},
|
||||
|
||||
/** 执行 Canvas 绘制 */
|
||||
async drawCanvas() {
|
||||
if (!this.ctx || !this.drawService || !this.currentData) return;
|
||||
|
||||
try {
|
||||
await this.drawService.draw(this.currentData);
|
||||
this.setData({ hasContent: true });
|
||||
} catch (error) {
|
||||
console.error('绘制失败:', error);
|
||||
this.setData({ hasContent: false });
|
||||
}
|
||||
},
|
||||
|
||||
/** 随机生成数据并绘制 */
|
||||
onRandom() {
|
||||
if (!this.currentTypeConfig) return;
|
||||
|
||||
this.currentData = this.currentTypeConfig.generateData(
|
||||
this.data.currentMode,
|
||||
);
|
||||
this.drawCanvas();
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
<nav-bar title="{{pageTitle}}" />
|
||||
|
||||
<view class="fd-page">
|
||||
<view class="fd-main">
|
||||
<!-- 打印预览卡 -->
|
||||
<view class="fd-preview-wrap">
|
||||
<view class="fd-preview-card">
|
||||
<text class="fd-preview-kicker">打印预览</text>
|
||||
<view id="canvasWrapper" class="fd-canvas-wrap">
|
||||
<canvas
|
||||
type="2d"
|
||||
id="canvasContent"
|
||||
class="fd-canvas"
|
||||
style="width:{{boxWidth}}px;height:{{boxHeight}}px" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 练习类型选择(2列卡片网格) -->
|
||||
<view class="fd-section">
|
||||
<text class="fd-section-title">练习类型</text>
|
||||
<view class="fd-type-grid">
|
||||
<view
|
||||
wx:for="{{typeList}}"
|
||||
wx:key="id"
|
||||
class="fd-type-card {{selectedTypeId === item.id ? 'fd-type-card--active' : ''}}"
|
||||
data-id="{{item.id}}"
|
||||
hover-class="fd-type-card--pressed"
|
||||
hover-start-time="0"
|
||||
hover-stay-time="70"
|
||||
bindtap="onSelectType">
|
||||
<text class="fd-type-card__icon">{{item.icon}}</text>
|
||||
<text class="fd-type-card__label">{{item.title}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 子选项(当前类型有模式切换时显示) -->
|
||||
<view wx:if="{{showActions}}" class="fd-section">
|
||||
<text class="fd-section-title">{{actionsTitle}}</text>
|
||||
<view class="fd-chip-row">
|
||||
<view
|
||||
wx:for="{{currentActions}}"
|
||||
wx:key="value"
|
||||
class="fd-chip {{currentMode === item.value ? 'fd-chip--active' : ''}}"
|
||||
data-value="{{item.value}}"
|
||||
hover-class="fd-chip--pressed"
|
||||
hover-start-time="0"
|
||||
hover-stay-time="70"
|
||||
bindtap="onSelectMode">
|
||||
{{item.label}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 随机生成按钮 -->
|
||||
<view
|
||||
class="fd-shuffle-btn"
|
||||
hover-class="fd-shuffle-btn--hover"
|
||||
hover-start-time="0"
|
||||
hover-stay-time="70"
|
||||
bindtap="onRandom">
|
||||
<toy-icon
|
||||
name="refresh"
|
||||
size="40rpx"
|
||||
color="#605b50"
|
||||
custom-class="fd-shuffle-btn__icon" />
|
||||
<text class="fd-shuffle-btn__text">随机生成</text>
|
||||
</view>
|
||||
|
||||
<!-- 广告位 -->
|
||||
<draw-ad type="focusDraw"></draw-ad>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<preview-footer-actions
|
||||
disabled="{{!hasContent}}"
|
||||
bind:primary="exportToPrint"
|
||||
bind:secondary="onShare" />
|
||||
|
||||
<!-- 分享引导弹窗 -->
|
||||
<share-guide-popup
|
||||
show="{{showShareDialog}}"
|
||||
bind:onClose="onCloseShareDialog"
|
||||
bind:onShareSuccess="onShareSuccess" />
|
||||
@@ -0,0 +1,691 @@
|
||||
import { BaseDrawService } from '../../core/draw/baseDraw';
|
||||
import ColorShapeMatchDraw from '../shared/service/colorShapeMatchDraw';
|
||||
import ShapeSymbolDraw from '../shared/service/shapeSymbolDraw';
|
||||
import PositionColoringDraw from '../shared/service/positionColoringDraw';
|
||||
import ColorPatternDraw from '../shared/service/colorPatternDraw';
|
||||
import MatchConnectDraw from '../shared/service/matchConnectDraw';
|
||||
import LineRecognitionDraw from '../shared/service/lineRecognitionDraw';
|
||||
import GridReasoningDraw, {
|
||||
type GridPosition,
|
||||
} from '../shared/service/gridReasoningDraw';
|
||||
import CodeConnectDraw from '../shared/service/codeConnectDraw';
|
||||
import DotConnectDraw from '../shared/service/dotConnectDraw';
|
||||
import GridDraw, { type GridGroup } from '../shared/service/gridDraw';
|
||||
|
||||
import {
|
||||
getRandomUniqueNumberColors,
|
||||
getNumberColors,
|
||||
getRandomNumberColor,
|
||||
NUMBER_COLORS,
|
||||
WATER_COLORS,
|
||||
} from '../../constants/colors';
|
||||
import { SHAPE_SYMBOL_SHAPES } from '../shared/shapes/shapeSymbolShapes';
|
||||
import { generateCompletePattern } from '../shared/utils/gridUtils';
|
||||
import { ALL_3X3_SHAPES } from '../shared/shapes/gridShapes3x3';
|
||||
import { ALL_5X5_SHAPES } from '../shared/shapes/gridShapes5x5';
|
||||
import { ALL_7X7_SHAPES } from '../shared/shapes/gridShapes7x7';
|
||||
import type {
|
||||
ShapeTemplate,
|
||||
GridCell,
|
||||
GridConfig,
|
||||
} from '../shared/types/gridTypes';
|
||||
|
||||
// ─── Types ───
|
||||
|
||||
export interface FocusTypeAction {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface FocusTypeConfig {
|
||||
id: string;
|
||||
title: string;
|
||||
subTitle: string;
|
||||
icon: string;
|
||||
actionsTitle?: string;
|
||||
actions?: FocusTypeAction[];
|
||||
defaultMode?: string;
|
||||
getTitle?: (mode: string) => string;
|
||||
getSubTitle?: (mode: string) => string;
|
||||
createDrawService: (
|
||||
canvas: Canvas,
|
||||
ctx: RenderingContext,
|
||||
options?: Record<string, any>,
|
||||
) => BaseDrawService;
|
||||
generateData: (mode?: string) => any;
|
||||
}
|
||||
|
||||
// ─── Helper: dotConnect ───
|
||||
|
||||
function generateNonBacktrackingSequence(length: number): number[] {
|
||||
const getPosition = (num: number): [number, number] => {
|
||||
const row = Math.floor((num - 1) / 3);
|
||||
const col = (num - 1) % 3;
|
||||
return [row, col];
|
||||
};
|
||||
|
||||
const sequence: number[] = [];
|
||||
const used = new Set<number>();
|
||||
|
||||
const startNum = Math.floor(Math.random() * 9) + 1;
|
||||
sequence.push(startNum);
|
||||
used.add(startNum);
|
||||
|
||||
let prevPos = getPosition(startNum);
|
||||
let prevPrevPos: [number, number] | null = null;
|
||||
|
||||
while (sequence.length < length) {
|
||||
const candidates: number[] = [];
|
||||
for (let num = 1; num <= 9; num++) {
|
||||
if (used.has(num)) continue;
|
||||
const currentPos = getPosition(num);
|
||||
if (
|
||||
prevPrevPos &&
|
||||
currentPos[0] === prevPrevPos[0] &&
|
||||
currentPos[1] === prevPrevPos[1]
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
candidates.push(num);
|
||||
}
|
||||
|
||||
const pool =
|
||||
candidates.length > 0
|
||||
? candidates
|
||||
: Array.from({ length: 9 }, (_, i) => i + 1).filter(
|
||||
(n) => !used.has(n),
|
||||
);
|
||||
if (pool.length === 0) break;
|
||||
|
||||
const nextNum = pool[Math.floor(Math.random() * pool.length)];
|
||||
sequence.push(nextNum);
|
||||
used.add(nextNum);
|
||||
prevPrevPos = prevPos;
|
||||
prevPos = getPosition(nextNum);
|
||||
}
|
||||
|
||||
return sequence;
|
||||
}
|
||||
|
||||
// ─── Helpers: gridReasoning ───
|
||||
|
||||
function generateRandomPositions(count: number): GridPosition[] {
|
||||
const allPositions: GridPosition[] = [];
|
||||
for (let y = 0; y < 3; y++) {
|
||||
for (let x = 0; x < 3; x++) {
|
||||
allPositions.push({ x, y });
|
||||
}
|
||||
}
|
||||
const shuffled = [...allPositions].sort(() => Math.random() - 0.5);
|
||||
return shuffled.slice(0, count);
|
||||
}
|
||||
|
||||
function unionPositions(
|
||||
pos1: GridPosition[],
|
||||
pos2: GridPosition[],
|
||||
): GridPosition[] {
|
||||
const set = new Set<string>();
|
||||
const result: GridPosition[] = [];
|
||||
for (const pos of pos1) {
|
||||
const key = `${pos.x},${pos.y}`;
|
||||
if (!set.has(key)) {
|
||||
set.add(key);
|
||||
result.push(pos);
|
||||
}
|
||||
}
|
||||
for (const pos of pos2) {
|
||||
const key = `${pos.x},${pos.y}`;
|
||||
if (!set.has(key)) {
|
||||
set.add(key);
|
||||
result.push(pos);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function subtractPositions(
|
||||
pos1: GridPosition[],
|
||||
pos2: GridPosition[],
|
||||
): GridPosition[] {
|
||||
const set2 = new Set(pos2.map((p) => `${p.x},${p.y}`));
|
||||
return pos1.filter((p) => !set2.has(`${p.x},${p.y}`));
|
||||
}
|
||||
|
||||
// ─── Helpers: gridDrawing ───
|
||||
|
||||
function generateUniqueGroups(
|
||||
allShapes: ShapeTemplate[],
|
||||
count: number,
|
||||
config: GridConfig,
|
||||
): GridGroup[] {
|
||||
const groups: GridGroup[] = [];
|
||||
const usedPatterns = new Set<string>();
|
||||
|
||||
const availableShapes = [...allShapes];
|
||||
for (let i = availableShapes.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[availableShapes[i], availableShapes[j]] = [
|
||||
availableShapes[j],
|
||||
availableShapes[i],
|
||||
];
|
||||
}
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const shapeIndex = i % availableShapes.length;
|
||||
const shape = availableShapes[shapeIndex];
|
||||
const cells = generateCompletePattern(shape, config);
|
||||
const sig = cells
|
||||
.map((c: GridCell) => `${c.x},${c.y},${c.color}`)
|
||||
.sort()
|
||||
.join('|');
|
||||
|
||||
if (
|
||||
usedPatterns.has(sig) &&
|
||||
usedPatterns.size < Math.min(count, allShapes.length)
|
||||
) {
|
||||
let found = false;
|
||||
for (const nextShape of availableShapes) {
|
||||
const nextCells = generateCompletePattern(nextShape, config);
|
||||
const nextSig = nextCells
|
||||
.map((c: GridCell) => `${c.x},${c.y},${c.color}`)
|
||||
.sort()
|
||||
.join('|');
|
||||
if (!usedPatterns.has(nextSig)) {
|
||||
groups.push({ filledCells: nextCells, emptyCells: [] });
|
||||
usedPatterns.add(nextSig);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
groups.push({ filledCells: cells, emptyCells: [] });
|
||||
}
|
||||
} else {
|
||||
groups.push({ filledCells: cells, emptyCells: [] });
|
||||
usedPatterns.add(sig);
|
||||
}
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
// ─── Data Generators ───
|
||||
|
||||
function generateColorShapeMatchData() {
|
||||
const shapes = [...SHAPE_SYMBOL_SHAPES].filter((s) => s.id !== 'cross');
|
||||
const selectedShapes = shapes.sort(() => Math.random() - 0.5).slice(0, 4);
|
||||
const colors = getRandomUniqueNumberColors(4);
|
||||
|
||||
const legendItems = selectedShapes.map((shape, i) => ({
|
||||
shapeId: shape.id,
|
||||
color: colors[i],
|
||||
}));
|
||||
|
||||
const allColors: (string | null)[] = new Array(25).fill(null);
|
||||
const positions = Array.from({ length: 25 }, (_, i) => i);
|
||||
const shuffledPos = [...positions].sort(() => Math.random() - 0.5);
|
||||
for (let i = 0; i < 4; i++) allColors[shuffledPos[i]] = colors[i];
|
||||
for (let i = 4; i < 25; i++)
|
||||
allColors[shuffledPos[i]] =
|
||||
colors[Math.floor(Math.random() * colors.length)];
|
||||
|
||||
const practiceGrid: Array<Array<string | null>> = [];
|
||||
for (let row = 0; row < 5; row++) {
|
||||
practiceGrid.push(
|
||||
Array.from({ length: 5 }, (_, col) => allColors[row * 5 + col]),
|
||||
);
|
||||
}
|
||||
|
||||
return { legendItems, practiceGrid };
|
||||
}
|
||||
|
||||
function generateShapeSymbolData() {
|
||||
const shuffled = [...SHAPE_SYMBOL_SHAPES].sort(() => Math.random() - 0.5);
|
||||
const selectedShapes = shuffled.slice(0, 4);
|
||||
const symbols = ['+', '-', '×', '✓'];
|
||||
const shuffledSymbols = [...symbols].sort(() => Math.random() - 0.5);
|
||||
|
||||
const shapeColorMap = new Map<string, string>();
|
||||
const usedColors = new Set<string>();
|
||||
const legendMapping: Array<{
|
||||
shapeId: string;
|
||||
symbol: string;
|
||||
color: string;
|
||||
}> = [];
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const shape = selectedShapes[i];
|
||||
let color: string;
|
||||
if (shapeColorMap.has(shape.id)) {
|
||||
color = shapeColorMap.get(shape.id)!;
|
||||
} else {
|
||||
do {
|
||||
color = getRandomNumberColor();
|
||||
} while (usedColors.has(color));
|
||||
shapeColorMap.set(shape.id, color);
|
||||
usedColors.add(color);
|
||||
}
|
||||
legendMapping.push({
|
||||
shapeId: shape.id,
|
||||
symbol: shuffledSymbols[i],
|
||||
color,
|
||||
});
|
||||
}
|
||||
|
||||
const practiceRows: Array<
|
||||
Array<{ shapeId: string | null; symbol: string | null }>
|
||||
> = [];
|
||||
for (let g = 0; g < 4; g++) {
|
||||
const shapeRow = Array.from({ length: 8 }, () => {
|
||||
const rs =
|
||||
selectedShapes[
|
||||
Math.floor(Math.random() * selectedShapes.length)
|
||||
];
|
||||
return { shapeId: rs.id, symbol: null };
|
||||
});
|
||||
practiceRows.push(shapeRow);
|
||||
practiceRows.push(
|
||||
Array.from({ length: 8 }, () => ({ shapeId: null, symbol: null })),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
legendMapping,
|
||||
practiceRows,
|
||||
shapeColorMap: Object.fromEntries(shapeColorMap),
|
||||
};
|
||||
}
|
||||
|
||||
function generatePositionColoringData() {
|
||||
const allImageIndices = Array.from({ length: 33 }, (_, i) => i + 1);
|
||||
const shuffled = [...allImageIndices].sort(() => Math.random() - 0.5);
|
||||
const selectedImages = shuffled.slice(0, 9);
|
||||
|
||||
const referenceGrid: number[] = [...selectedImages];
|
||||
|
||||
const taskImageIndices = [...selectedImages].sort(
|
||||
() => Math.random() - 0.5,
|
||||
);
|
||||
const tasks = taskImageIndices.map((imageIndex) => {
|
||||
const gridIndex = referenceGrid.indexOf(imageIndex);
|
||||
return {
|
||||
imageIndex,
|
||||
position: { x: gridIndex % 3, y: Math.floor(gridIndex / 3) },
|
||||
};
|
||||
});
|
||||
|
||||
return { referenceGrid, tasks };
|
||||
}
|
||||
|
||||
function generateColorPatternData() {
|
||||
const allShapes = [...SHAPE_SYMBOL_SHAPES].sort(() => Math.random() - 0.5);
|
||||
const selectedShapes = allShapes.slice(0, 8);
|
||||
const allColors = getNumberColors();
|
||||
|
||||
const groups: Array<{ shapeId: string; colors: Array<string | null> }> = [];
|
||||
for (let gi = 0; gi < 8; gi++) {
|
||||
const shuffledColors = [...allColors].sort(() => Math.random() - 0.5);
|
||||
const selectedColors = shuffledColors.slice(0, 2);
|
||||
|
||||
const patternColors: string[] = [];
|
||||
for (let i = 0; i < 4; i++) {
|
||||
patternColors.push(
|
||||
selectedColors[
|
||||
Math.floor(Math.random() * selectedColors.length)
|
||||
],
|
||||
);
|
||||
}
|
||||
if (new Set(patternColors).size === 1) {
|
||||
const other = selectedColors.filter((c) => c !== patternColors[0]);
|
||||
if (other.length > 0) {
|
||||
patternColors[Math.floor(Math.random() * 4)] =
|
||||
other[Math.floor(Math.random() * other.length)];
|
||||
}
|
||||
}
|
||||
|
||||
groups.push({
|
||||
shapeId: selectedShapes[gi].id,
|
||||
colors: [...patternColors, ...new Array(6).fill(null)],
|
||||
});
|
||||
}
|
||||
|
||||
return { groups };
|
||||
}
|
||||
|
||||
function generateMatchConnectData() {
|
||||
const allImageIndices = Array.from({ length: 33 }, (_, i) => i + 1);
|
||||
const shuffled = [...allImageIndices].sort(() => Math.random() - 0.5);
|
||||
const selectedImages = shuffled.slice(0, 5);
|
||||
|
||||
const referenceSequence = [...selectedImages];
|
||||
const boxes = Array.from({ length: 6 }, () => ({
|
||||
imageIndices: [...selectedImages],
|
||||
}));
|
||||
|
||||
return { referenceSequence, boxes };
|
||||
}
|
||||
|
||||
function generateLineRecognitionData() {
|
||||
const LINE_TYPES: Array<{ type: string; name: string }> = [
|
||||
{ type: 'straight', name: '直线' },
|
||||
{ type: 'dashed', name: '虚线' },
|
||||
{ type: 'wavy', name: '波浪线' },
|
||||
{ type: 'zigzag', name: '锯齿线' },
|
||||
{ type: 'spiral', name: '电话线' },
|
||||
];
|
||||
|
||||
const colorKeys = Object.keys(NUMBER_COLORS)
|
||||
.map(Number)
|
||||
.filter((k) => k !== 11 && k !== 12);
|
||||
const shuffledColors = [...colorKeys].sort(() => Math.random() - 0.5);
|
||||
const selectedColorKeys = shuffledColors.slice(0, 5);
|
||||
|
||||
const shuffledLineTypes = [...LINE_TYPES].sort(() => Math.random() - 0.5);
|
||||
const referenceLines = shuffledLineTypes.map((lt, i) => ({
|
||||
type: lt.type,
|
||||
name: lt.name,
|
||||
color: NUMBER_COLORS[selectedColorKeys[i]],
|
||||
}));
|
||||
|
||||
const practiceBlocks: Array<{
|
||||
color: string;
|
||||
lineType: string;
|
||||
x: number;
|
||||
y: number;
|
||||
}> = [];
|
||||
for (let row = 0; row < 8; row++) {
|
||||
for (let col = 0; col < 2; col++) {
|
||||
const sel =
|
||||
referenceLines[
|
||||
Math.floor(Math.random() * referenceLines.length)
|
||||
];
|
||||
practiceBlocks.push({
|
||||
color: sel.color,
|
||||
lineType: sel.type,
|
||||
x: (Math.random() - 0.5) * 40,
|
||||
y: (Math.random() - 0.5) * 16,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { referenceLines, practiceBlocks };
|
||||
}
|
||||
|
||||
function generateGridReasoningData(mode?: string) {
|
||||
const operatorType = mode || 'mixed';
|
||||
const colorKeys = Object.keys(NUMBER_COLORS).map(Number);
|
||||
const selectedColor =
|
||||
NUMBER_COLORS[colorKeys[Math.floor(Math.random() * colorKeys.length)]];
|
||||
|
||||
const rows: Array<{
|
||||
grid1: GridPosition[];
|
||||
grid2: GridPosition[];
|
||||
operator: '+' | '-';
|
||||
result: GridPosition[];
|
||||
}> = [];
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
let operator: '+' | '-';
|
||||
if (operatorType === 'addition') operator = '+';
|
||||
else if (operatorType === 'subtraction') operator = '-';
|
||||
else operator = Math.random() > 0.5 ? '+' : '-';
|
||||
|
||||
const grid1Count =
|
||||
operator === '-'
|
||||
? Math.floor(Math.random() * 8) + 2
|
||||
: Math.floor(Math.random() * 9) + 1;
|
||||
const grid1 = generateRandomPositions(grid1Count);
|
||||
|
||||
let grid2: GridPosition[];
|
||||
let result: GridPosition[];
|
||||
if (operator === '+') {
|
||||
grid2 = generateRandomPositions(Math.floor(Math.random() * 9) + 1);
|
||||
result = unionPositions(grid1, grid2);
|
||||
} else {
|
||||
const maxG2 = grid1Count - 1;
|
||||
grid2 = generateRandomPositions(
|
||||
Math.floor(Math.random() * maxG2) + 1,
|
||||
);
|
||||
result = subtractPositions(grid1, grid2);
|
||||
}
|
||||
|
||||
rows.push({ grid1, grid2, operator, result });
|
||||
}
|
||||
|
||||
const answerOptions = rows.map((r, i) => ({
|
||||
positions: r.result,
|
||||
rowIndex: i,
|
||||
}));
|
||||
answerOptions.sort(() => Math.random() - 0.5);
|
||||
|
||||
return { rows, answerOptions, color: selectedColor };
|
||||
}
|
||||
|
||||
function generateCodeConnectData() {
|
||||
const startOptions = [1, 2, 3];
|
||||
const startNumber =
|
||||
startOptions[Math.floor(Math.random() * startOptions.length)];
|
||||
const numbers = Array.from({ length: 8 }, (_, i) => startNumber + i);
|
||||
|
||||
const colorIndices = Array.from(
|
||||
{ length: WATER_COLORS.extended24.length },
|
||||
(_, i) => i,
|
||||
);
|
||||
const shuffledIdx = [...colorIndices]
|
||||
.sort(() => Math.random() - 0.5)
|
||||
.slice(0, 8);
|
||||
|
||||
const colorMap = numbers.map((num, i) => ({
|
||||
number: num,
|
||||
color: WATER_COLORS.extended24[shuffledIdx[i]].hex,
|
||||
}));
|
||||
|
||||
const groups: Array<{
|
||||
sequence: number[];
|
||||
dots: Array<{ number: number; color: string; angle: number }>;
|
||||
}> = [];
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const sequence = [...numbers]
|
||||
.sort(() => Math.random() - 0.5)
|
||||
.slice(0, 5);
|
||||
const dots = numbers.map((num, j) => {
|
||||
const angle = (j / 8) * Math.PI * 2 - Math.PI / 2;
|
||||
const info = colorMap.find((m) => m.number === num);
|
||||
return { number: num, color: info?.color || '#000', angle };
|
||||
});
|
||||
groups.push({ sequence, dots });
|
||||
}
|
||||
|
||||
return { colorMap, startNumber, groups };
|
||||
}
|
||||
|
||||
function generateDotConnectData() {
|
||||
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||
const colors = getRandomUniqueNumberColors(9);
|
||||
const colorMap = numbers.map((num, i) => ({
|
||||
number: num,
|
||||
color: colors[i],
|
||||
}));
|
||||
|
||||
const groups = Array.from({ length: 9 }, () => {
|
||||
const seqLen = Math.floor(Math.random() * 3) + 4;
|
||||
return { sequence: generateNonBacktrackingSequence(seqLen) };
|
||||
});
|
||||
|
||||
return { colorMap, groups };
|
||||
}
|
||||
|
||||
function generateGridDrawingData(mode?: string) {
|
||||
const m = mode || '3x3';
|
||||
let allShapes: ShapeTemplate[];
|
||||
let config: GridConfig;
|
||||
let groupsPerRow: number;
|
||||
let totalRows: number;
|
||||
|
||||
if (m === '5x5') {
|
||||
allShapes = ALL_5X5_SHAPES;
|
||||
config = { rows: 5, cols: 5 };
|
||||
groupsPerRow = 2;
|
||||
totalRows = 4;
|
||||
} else if (m === '7x7') {
|
||||
allShapes = ALL_7X7_SHAPES;
|
||||
config = { rows: 7, cols: 7 };
|
||||
groupsPerRow = 1;
|
||||
totalRows = 2;
|
||||
} else {
|
||||
allShapes = ALL_3X3_SHAPES;
|
||||
config = { rows: 3, cols: 3 };
|
||||
groupsPerRow = 2;
|
||||
totalRows = 4;
|
||||
}
|
||||
|
||||
const totalGroups = groupsPerRow * totalRows;
|
||||
const groups = generateUniqueGroups(allShapes, totalGroups, config);
|
||||
|
||||
return { config, groups, groupsPerRow, totalRows };
|
||||
}
|
||||
|
||||
// ─── Registry ───
|
||||
|
||||
const GRID_DRAWING_SUBTITLES: Record<string, string> = {
|
||||
'3x3': '简单有趣,培养专注力',
|
||||
'5x5': '创意挑战,提升观察力',
|
||||
'7x7': '大师挑战,锻炼耐心',
|
||||
};
|
||||
|
||||
export const FOCUS_TYPE_CONFIGS: FocusTypeConfig[] = [
|
||||
{
|
||||
id: 'color-shape-match',
|
||||
title: '根据颜色画图形',
|
||||
subTitle: '根据颜色画出对应的图形',
|
||||
icon: '🎯',
|
||||
createDrawService: (canvas, ctx, opts) =>
|
||||
new ColorShapeMatchDraw(canvas, ctx, opts),
|
||||
generateData: generateColorShapeMatchData,
|
||||
},
|
||||
{
|
||||
id: 'shape-symbol',
|
||||
title: '图形符号配对',
|
||||
subTitle: '根据图形画对应的符号',
|
||||
icon: '🔗',
|
||||
createDrawService: (canvas, ctx, opts) =>
|
||||
new ShapeSymbolDraw(canvas, ctx, opts),
|
||||
generateData: generateShapeSymbolData,
|
||||
},
|
||||
{
|
||||
id: 'position-coloring',
|
||||
title: '方位涂涂乐',
|
||||
subTitle: '观察卡片位置,在对应的方格中涂上颜色',
|
||||
icon: '📍',
|
||||
createDrawService: (canvas, ctx, opts) =>
|
||||
new PositionColoringDraw(canvas, ctx, opts),
|
||||
generateData: generatePositionColoringData,
|
||||
},
|
||||
{
|
||||
id: 'color-pattern',
|
||||
title: '颜色找规律',
|
||||
subTitle: '观察颜色规律,在空白图形中涂上颜色',
|
||||
icon: '🎨',
|
||||
createDrawService: (canvas, ctx, opts) =>
|
||||
new ColorPatternDraw(canvas, ctx, opts),
|
||||
generateData: generateColorPatternData,
|
||||
},
|
||||
{
|
||||
id: 'match-connect',
|
||||
title: '连连看',
|
||||
subTitle: '快来根据物品连一连吧!',
|
||||
icon: '🔗',
|
||||
createDrawService: (canvas, ctx, opts) =>
|
||||
new MatchConnectDraw(canvas, ctx, opts),
|
||||
generateData: generateMatchConnectData,
|
||||
},
|
||||
{
|
||||
id: 'line-recognition',
|
||||
title: '线条识别',
|
||||
subTitle: '认识不同线条,画出颜色对应的线条',
|
||||
icon: '📏',
|
||||
createDrawService: (canvas, ctx, opts) =>
|
||||
new LineRecognitionDraw(canvas, ctx, opts),
|
||||
generateData: generateLineRecognitionData,
|
||||
},
|
||||
{
|
||||
id: 'grid-reasoning',
|
||||
title: '方格推理',
|
||||
subTitle: '仔细观察,推理出合并方格并连线',
|
||||
icon: '🧩',
|
||||
actionsTitle: '选择运算',
|
||||
actions: [
|
||||
{ value: 'addition', label: '加法运算' },
|
||||
{ value: 'subtraction', label: '减法运算' },
|
||||
{ value: 'mixed', label: '混合运算' },
|
||||
],
|
||||
defaultMode: 'mixed',
|
||||
createDrawService: (canvas, ctx, opts) =>
|
||||
new GridReasoningDraw(canvas, ctx, opts),
|
||||
generateData: generateGridReasoningData,
|
||||
},
|
||||
{
|
||||
id: 'code-connect',
|
||||
title: '译码连线',
|
||||
subTitle: '按照数字顺序,将数字对应的颜色连起来',
|
||||
icon: '🔢',
|
||||
createDrawService: (canvas, ctx, opts) =>
|
||||
new CodeConnectDraw(canvas, ctx, opts),
|
||||
generateData: generateCodeConnectData,
|
||||
},
|
||||
{
|
||||
id: 'dot-connect',
|
||||
title: '数字点连线',
|
||||
subTitle: '按数字顺序连点成图',
|
||||
icon: '🔗',
|
||||
createDrawService: (canvas, ctx, opts) =>
|
||||
new DotConnectDraw(canvas, ctx, opts),
|
||||
generateData: generateDotConnectData,
|
||||
},
|
||||
{
|
||||
id: 'grid-drawing',
|
||||
title: '格子仿画',
|
||||
subTitle: '在网格中填充颜色,形成各种形状',
|
||||
icon: '🎨',
|
||||
actionsTitle: '选择难度',
|
||||
actions: [
|
||||
{ value: '3x3', label: '3×3' },
|
||||
{ value: '5x5', label: '5×5' },
|
||||
{ value: '7x7', label: '7×7' },
|
||||
],
|
||||
defaultMode: '3x3',
|
||||
getTitle: (mode: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
'3x3': '3×3',
|
||||
'5x5': '5×5',
|
||||
'7x7': '7×7',
|
||||
};
|
||||
return `格子仿画 ${labels[mode] || '3×3'}`;
|
||||
},
|
||||
getSubTitle: (mode: string) =>
|
||||
GRID_DRAWING_SUBTITLES[mode] || GRID_DRAWING_SUBTITLES['3x3'],
|
||||
createDrawService: (canvas, ctx, opts) =>
|
||||
new GridDraw(canvas, ctx, opts),
|
||||
generateData: generateGridDrawingData,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* 通过路由 ID 查找类型配置
|
||||
* 处理 grid-drawing-3x3 → grid-drawing + mode=3x3 等别名
|
||||
*/
|
||||
export function findTypeByRouteId(
|
||||
routeId: string,
|
||||
): { typeConfig: FocusTypeConfig; mode?: string } | null {
|
||||
const direct = FOCUS_TYPE_CONFIGS.find((t) => t.id === routeId);
|
||||
if (direct) return { typeConfig: direct };
|
||||
|
||||
if (routeId.startsWith('grid-drawing-')) {
|
||||
const mode = routeId.replace('grid-drawing-', '');
|
||||
const cfg = FOCUS_TYPE_CONFIGS.find((t) => t.id === 'grid-drawing');
|
||||
if (cfg) return { typeConfig: cfg, mode };
|
||||
}
|
||||
|
||||
return { typeConfig: FOCUS_TYPE_CONFIGS[0] };
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"navigationBarTitleText": "格子仿画",
|
||||
"navigationBarBackgroundColor": "#FFD719",
|
||||
"homeButton": true,
|
||||
"backgroundColor": "#F6F6F6",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
||||
"math-type-selector": "../../components/math-type-selector/math-type-selector",
|
||||
"math-bottom-buttons": "../../components/math-bottom-buttons/math-bottom-buttons",
|
||||
"draw-ad": "../../components/draw-ad/draw-ad"
|
||||
}
|
||||
}
|
||||
@@ -1,292 +0,0 @@
|
||||
import GridDraw, { GridGroup } from '../shared/service/gridDraw';
|
||||
import { createFocusPage } from '../shared/common/focusPageMixin';
|
||||
import { GridDrawingData } from '../shared/service/gridDraw';
|
||||
import { generateCompletePattern } from '../shared/utils/gridUtils';
|
||||
import { ALL_3X3_SHAPES } from '../shared/shapes/gridShapes3x3';
|
||||
import { ALL_5X5_SHAPES } from '../shared/shapes/gridShapes5x5';
|
||||
import { ALL_7X7_SHAPES } from '../shared/shapes/gridShapes7x7';
|
||||
import { ShapeTemplate, GridCell } from '../shared/types/gridTypes';
|
||||
import { GridConfig } from '../shared/types/gridTypes';
|
||||
import { FOCUS_FUNCTION_TYPES } from '../../constants/focusFunctions';
|
||||
|
||||
createFocusPage({
|
||||
canvas: null as Canvas | null,
|
||||
ctx: null as RenderingContext | null,
|
||||
boxHeight: 0,
|
||||
boxWidth: 0,
|
||||
drawService: null as GridDraw | null,
|
||||
gridData: null as GridDrawingData | null,
|
||||
|
||||
data: {
|
||||
pageTitle: '格子仿画 5×5',
|
||||
functionId: '',
|
||||
hasContent: false,
|
||||
showShareDialog: false,
|
||||
currentMode: '3x3', // '3x3', '5x5', '7x7'
|
||||
currentModeName: '3×3',
|
||||
subTitle: '', // 副标题,从配置中获取
|
||||
typeActions: [
|
||||
{ name: '3×3', value: '3x3' },
|
||||
{ name: '5×5', value: '5x5' },
|
||||
{ name: '7×7', value: '7x7' },
|
||||
],
|
||||
},
|
||||
|
||||
onLoad(options: { id?: string; mode?: string }) {
|
||||
const functionId = options.id || 'grid-drawing';
|
||||
const mode = options.mode || '3x3'; // 从参数中读取 mode,默认为 '3x3'
|
||||
|
||||
// 根据 mode 设置对应的模式名称
|
||||
const modeNameMap: Record<string, string> = {
|
||||
'3x3': '3×3',
|
||||
'5x5': '5×5',
|
||||
'7x7': '7×7',
|
||||
};
|
||||
const currentModeName = modeNameMap[mode] || '3×3';
|
||||
|
||||
// 根据 mode 查找配置,获取 desc 作为 subTitle
|
||||
const configItem = FOCUS_FUNCTION_TYPES.find(
|
||||
(item) => item.mode === mode && item.id?.startsWith('grid-drawing'),
|
||||
);
|
||||
const subTitle = configItem?.desc || '在网格中填充颜色,形成各种形状';
|
||||
|
||||
this.setData({
|
||||
pageTitle: `格子仿画 ${mode}`,
|
||||
functionId,
|
||||
currentMode: mode,
|
||||
currentModeName,
|
||||
subTitle,
|
||||
});
|
||||
this.initPageInfo(functionId, '格子仿画');
|
||||
},
|
||||
|
||||
onReady() {
|
||||
this.initCanvas({
|
||||
createDrawService: (
|
||||
canvas: Canvas,
|
||||
ctx: RenderingContext,
|
||||
options?: Record<string, any>,
|
||||
) => {
|
||||
return new GridDraw(canvas, ctx, options);
|
||||
},
|
||||
drawServiceOptions: {
|
||||
title: this.data.pageTitle,
|
||||
subTitle:
|
||||
this.data.subTitle || '在网格中填充颜色,形成各种形状',
|
||||
functionId: this.data.functionId,
|
||||
},
|
||||
onCanvasReady: () => {
|
||||
// 初始随机生成
|
||||
this.onRandom();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 绘制Canvas内容
|
||||
*/
|
||||
async drawCanvas() {
|
||||
if (!this.ctx || !this.drawService || !this.gridData) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.drawService.draw(this.gridData);
|
||||
this.setData({ hasContent: true });
|
||||
} catch (error) {
|
||||
console.error('绘制失败:', error);
|
||||
this.setData({ hasContent: false });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查两个图案是否相同(通过比较格子的位置和颜色)
|
||||
*/
|
||||
arePatternsEqual(cells1: GridCell[], cells2: GridCell[]): boolean {
|
||||
if (cells1.length !== cells2.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 将格子转换为字符串键进行比较
|
||||
const pattern1 = cells1
|
||||
.map((c) => `${c.x},${c.y},${c.color}`)
|
||||
.sort()
|
||||
.join('|');
|
||||
const pattern2 = cells2
|
||||
.map((c) => `${c.x},${c.y},${c.color}`)
|
||||
.sort()
|
||||
.join('|');
|
||||
|
||||
return pattern1 === pattern2;
|
||||
},
|
||||
|
||||
/**
|
||||
* 生成不重复的图案组
|
||||
*/
|
||||
generateUniqueGroups(
|
||||
allShapes: ShapeTemplate[],
|
||||
count: number,
|
||||
config: GridConfig,
|
||||
): GridGroup[] {
|
||||
const groups: GridGroup[] = [];
|
||||
const usedPatterns = new Set<string>(); // 用于存储已使用的图案签名
|
||||
|
||||
// 如果需要的数量超过可用形状数量,允许重复使用
|
||||
const maxUniqueShapes = Math.min(count, allShapes.length);
|
||||
const availableShapes = [...allShapes]; // 复制数组以便打乱
|
||||
|
||||
// 打乱数组顺序
|
||||
for (let i = availableShapes.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[availableShapes[i], availableShapes[j]] = [
|
||||
availableShapes[j],
|
||||
availableShapes[i],
|
||||
];
|
||||
}
|
||||
|
||||
// 生成不重复的图案
|
||||
for (let i = 0; i < count; i++) {
|
||||
const shapeIndex = i % availableShapes.length;
|
||||
const shape = availableShapes[shapeIndex];
|
||||
const cells = generateCompletePattern(shape, config);
|
||||
|
||||
// 生成图案签名
|
||||
const patternSignature = cells
|
||||
.map((c) => `${c.x},${c.y},${c.color}`)
|
||||
.sort()
|
||||
.join('|');
|
||||
|
||||
// 如果图案已存在,尝试下一个形状
|
||||
if (usedPatterns.has(patternSignature)) {
|
||||
// 如果所有形状都用过了,允许重复
|
||||
if (usedPatterns.size >= maxUniqueShapes) {
|
||||
// 使用当前形状,即使重复
|
||||
groups.push({
|
||||
filledCells: cells,
|
||||
emptyCells: [],
|
||||
});
|
||||
continue;
|
||||
} else {
|
||||
// 尝试找到未使用的形状
|
||||
let found = false;
|
||||
for (let j = 0; j < availableShapes.length; j++) {
|
||||
const nextShape = availableShapes[j];
|
||||
const nextCells = generateCompletePattern(
|
||||
nextShape,
|
||||
config,
|
||||
);
|
||||
const nextSignature = nextCells
|
||||
.map((c) => `${c.x},${c.y},${c.color}`)
|
||||
.sort()
|
||||
.join('|');
|
||||
|
||||
if (!usedPatterns.has(nextSignature)) {
|
||||
groups.push({
|
||||
filledCells: nextCells,
|
||||
emptyCells: [],
|
||||
});
|
||||
usedPatterns.add(nextSignature);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
// 如果找不到未使用的,使用当前形状
|
||||
groups.push({
|
||||
filledCells: cells,
|
||||
emptyCells: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 图案未使用,添加到结果中
|
||||
groups.push({
|
||||
filledCells: cells,
|
||||
emptyCells: [],
|
||||
});
|
||||
usedPatterns.add(patternSignature);
|
||||
}
|
||||
}
|
||||
|
||||
return groups;
|
||||
},
|
||||
|
||||
/**
|
||||
* 随机生成
|
||||
*/
|
||||
onRandom() {
|
||||
const mode = this.data.currentMode;
|
||||
let allShapes: ShapeTemplate[];
|
||||
let config: GridConfig;
|
||||
let groupsPerRow: number;
|
||||
let totalRows: number;
|
||||
|
||||
// 根据模式选择形状和配置
|
||||
if (mode === '3x3') {
|
||||
allShapes = ALL_3X3_SHAPES;
|
||||
config = { rows: 3, cols: 3 };
|
||||
groupsPerRow = 2; // 每行2组
|
||||
totalRows = 4; // 总共3行
|
||||
} else if (mode === '5x5') {
|
||||
allShapes = ALL_5X5_SHAPES;
|
||||
config = { rows: 5, cols: 5 };
|
||||
groupsPerRow = 2;
|
||||
totalRows = 4; // 5x5可以展示2行
|
||||
} else {
|
||||
// 7x7
|
||||
allShapes = ALL_7X7_SHAPES;
|
||||
config = { rows: 7, cols: 7 };
|
||||
groupsPerRow = 1; // 7x7每行1组
|
||||
totalRows = 2; // 总共2行
|
||||
}
|
||||
|
||||
// 生成不重复的图案组
|
||||
const totalGroups = groupsPerRow * totalRows;
|
||||
const groups = this.generateUniqueGroups(
|
||||
allShapes,
|
||||
totalGroups,
|
||||
config,
|
||||
);
|
||||
|
||||
this.gridData = {
|
||||
config,
|
||||
groups,
|
||||
groupsPerRow,
|
||||
totalRows,
|
||||
};
|
||||
|
||||
this.drawCanvas();
|
||||
},
|
||||
|
||||
/** 选择类型 */
|
||||
onSelectType(event: any) {
|
||||
const { name, value } = event.detail;
|
||||
|
||||
// 根据 mode 查找配置,获取 desc 作为 subTitle
|
||||
const configItem = FOCUS_FUNCTION_TYPES.find(
|
||||
(item) =>
|
||||
item.mode === value && item.id?.startsWith('grid-drawing'),
|
||||
);
|
||||
|
||||
const pageTitle = `格子仿画 ${value}`;
|
||||
const subTitle = configItem?.desc || '在网格中填充颜色,形成各种形状';
|
||||
|
||||
// 更新 pageTitle、currentMode、currentModeName 和 subTitle
|
||||
this.setData({
|
||||
pageTitle,
|
||||
currentMode: value,
|
||||
currentModeName: name,
|
||||
subTitle,
|
||||
});
|
||||
|
||||
// 更新绘制服务的 title 和 subTitle
|
||||
if (this.drawService) {
|
||||
this.drawService.options.title = pageTitle;
|
||||
this.drawService.options.subTitle = subTitle;
|
||||
}
|
||||
|
||||
// 重新生成数据
|
||||
this.onRandom();
|
||||
},
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
<!-- 模版不能跨包引用 -->
|
||||
<import src="../shared/templates/canvas-page-template.wxml" />
|
||||
|
||||
<template
|
||||
is="canvasPageTemplate"
|
||||
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: true, currentModeName: currentModeName, typeActions: typeActions, adType: 'focusDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
|
||||
@@ -1 +0,0 @@
|
||||
@import '../../base/baseDrawPage.wxss';
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"navigationBarTitleText": "方格推理",
|
||||
"navigationBarBackgroundColor": "#FFD719",
|
||||
"homeButton": true,
|
||||
"backgroundColor": "#F6F6F6",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
||||
"math-bottom-buttons": "../../components/math-bottom-buttons/math-bottom-buttons",
|
||||
"math-type-selector": "../../components/math-type-selector/math-type-selector",
|
||||
"draw-ad": "../../components/draw-ad/draw-ad"
|
||||
}
|
||||
}
|
||||
@@ -1,261 +0,0 @@
|
||||
import GridReasoningDraw from '../shared/service/gridReasoningDraw';
|
||||
import { createFocusPage } from '../shared/common/focusPageMixin';
|
||||
import {
|
||||
GridReasoningData,
|
||||
GridPosition,
|
||||
} from '../shared/service/gridReasoningDraw';
|
||||
import { NUMBER_COLORS } from '../../constants/colors';
|
||||
|
||||
createFocusPage({
|
||||
canvas: null as Canvas | null,
|
||||
ctx: null as RenderingContext | null,
|
||||
boxHeight: 0,
|
||||
boxWidth: 0,
|
||||
drawService: null as GridReasoningDraw | null,
|
||||
gridData: null as GridReasoningData | null,
|
||||
|
||||
data: {
|
||||
pageTitle: '方格推理',
|
||||
functionId: '',
|
||||
hasContent: false,
|
||||
showShareDialog: false,
|
||||
currentOperatorType: 'mixed', // 'addition', 'subtraction', 'mixed'
|
||||
currentOperatorTypeName: '混合运算',
|
||||
operatorTypeActions: [
|
||||
{ name: '加法运算', value: 'addition' },
|
||||
{ name: '减法运算', value: 'subtraction' },
|
||||
{ name: '混合运算', value: 'mixed' },
|
||||
],
|
||||
},
|
||||
|
||||
onLoad(options: { id?: string }) {
|
||||
const functionId = options.id || 'grid-reasoning';
|
||||
this.setData({
|
||||
functionId,
|
||||
});
|
||||
this.initPageInfo(functionId, '方格推理');
|
||||
},
|
||||
|
||||
onReady() {
|
||||
this.initCanvas({
|
||||
createDrawService: (
|
||||
canvas: Canvas,
|
||||
ctx: RenderingContext,
|
||||
options?: Record<string, any>,
|
||||
) => {
|
||||
return new GridReasoningDraw(canvas, ctx, options);
|
||||
},
|
||||
drawServiceOptions: {
|
||||
title: this.data.pageTitle,
|
||||
subTitle: '仔细观察,推理出合并方格并连线',
|
||||
functionId: this.data.functionId,
|
||||
},
|
||||
onCanvasReady: () => {
|
||||
// 初始随机生成
|
||||
this.onRandom();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 绘制Canvas内容
|
||||
*/
|
||||
async drawCanvas() {
|
||||
if (!this.ctx || !this.drawService || !this.gridData) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.drawService.draw(this.gridData);
|
||||
this.setData({ hasContent: true });
|
||||
} catch (error) {
|
||||
console.error('绘制失败:', error);
|
||||
this.setData({ hasContent: false });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 选择运算类型
|
||||
*/
|
||||
onSelectType(event: any) {
|
||||
const { name, value } = event.detail;
|
||||
this.setData({
|
||||
currentOperatorType: value,
|
||||
currentOperatorTypeName: name,
|
||||
});
|
||||
// 重新生成题目
|
||||
this.onRandom();
|
||||
},
|
||||
|
||||
/**
|
||||
* 随机生成
|
||||
*/
|
||||
onRandom() {
|
||||
// 随机选择一种颜色
|
||||
const colorKeys = Object.keys(NUMBER_COLORS).map(Number);
|
||||
const randomColorKey =
|
||||
colorKeys[Math.floor(Math.random() * colorKeys.length)];
|
||||
const selectedColor = NUMBER_COLORS[randomColorKey];
|
||||
|
||||
const rows: Array<{
|
||||
grid1: GridPosition[];
|
||||
grid2: GridPosition[];
|
||||
operator: '+' | '-';
|
||||
result: GridPosition[];
|
||||
}> = [];
|
||||
|
||||
const operatorType = this.data.currentOperatorType;
|
||||
|
||||
// 生成5行的数据
|
||||
for (let i = 0; i < 5; i++) {
|
||||
// 根据选中的运算类型选择运算符
|
||||
let operator: '+' | '-';
|
||||
if (operatorType === 'addition') {
|
||||
operator = '+';
|
||||
} else if (operatorType === 'subtraction') {
|
||||
operator = '-';
|
||||
} else {
|
||||
// 混合运算:随机选择运算符
|
||||
operator = Math.random() > 0.5 ? '+' : '-';
|
||||
}
|
||||
|
||||
// 生成第一个网格的涂色位置
|
||||
// 如果是减法运算,第一个网格至少要有2个位置
|
||||
let grid1Count: number;
|
||||
if (operator === '-') {
|
||||
grid1Count = Math.floor(Math.random() * 8) + 2; // 2-9个位置
|
||||
} else {
|
||||
grid1Count = Math.floor(Math.random() * 9) + 1; // 1-9个位置
|
||||
}
|
||||
const grid1Positions = this.generateRandomPositions(grid1Count);
|
||||
|
||||
let grid2Positions: GridPosition[];
|
||||
let resultPositions: GridPosition[];
|
||||
|
||||
if (operator === '+') {
|
||||
// 加法:生成第二个网格的涂色位置(至少1个)
|
||||
const grid2Count = Math.floor(Math.random() * 9) + 1;
|
||||
grid2Positions = this.generateRandomPositions(grid2Count);
|
||||
|
||||
// 结果 = 并集
|
||||
resultPositions = this.unionPositions(
|
||||
grid1Positions,
|
||||
grid2Positions,
|
||||
);
|
||||
} else {
|
||||
// 减法:生成第二个网格的涂色位置(必须少于第一个)
|
||||
const maxGrid2Count = grid1Count - 1;
|
||||
const grid2Count =
|
||||
Math.floor(Math.random() * maxGrid2Count) + 1;
|
||||
grid2Positions = this.generateRandomPositions(grid2Count);
|
||||
|
||||
// 结果 = 差集
|
||||
resultPositions = this.subtractPositions(
|
||||
grid1Positions,
|
||||
grid2Positions,
|
||||
);
|
||||
}
|
||||
|
||||
rows.push({
|
||||
grid1: grid1Positions,
|
||||
grid2: grid2Positions,
|
||||
operator: operator as '+' | '-',
|
||||
result: resultPositions,
|
||||
});
|
||||
}
|
||||
|
||||
// 生成右侧答案选项
|
||||
// 右侧展示左侧5行的结果(result),顺序随机
|
||||
const answerOptions: Array<{
|
||||
positions: GridPosition[];
|
||||
rowIndex: number;
|
||||
}> = [];
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
answerOptions.push({
|
||||
positions: rows[i].result,
|
||||
rowIndex: i,
|
||||
});
|
||||
}
|
||||
|
||||
// 随机打乱顺序
|
||||
answerOptions.sort(() => Math.random() - 0.5);
|
||||
|
||||
this.gridData = {
|
||||
rows,
|
||||
answerOptions,
|
||||
color: selectedColor,
|
||||
};
|
||||
|
||||
this.drawCanvas();
|
||||
},
|
||||
|
||||
/**
|
||||
* 生成随机位置(不重复)
|
||||
*/
|
||||
generateRandomPositions(count: number): GridPosition[] {
|
||||
const allPositions: GridPosition[] = [];
|
||||
for (let y = 0; y < 3; y++) {
|
||||
for (let x = 0; x < 3; x++) {
|
||||
allPositions.push({ x, y });
|
||||
}
|
||||
}
|
||||
|
||||
// 随机打乱
|
||||
const shuffled = [...allPositions].sort(() => Math.random() - 0.5);
|
||||
|
||||
// 返回前count个
|
||||
return shuffled.slice(0, count);
|
||||
},
|
||||
|
||||
/**
|
||||
* 计算位置的并集
|
||||
*/
|
||||
unionPositions(pos1: GridPosition[], pos2: GridPosition[]): GridPosition[] {
|
||||
const set = new Set<string>();
|
||||
const result: GridPosition[] = [];
|
||||
|
||||
// 添加第一个数组的位置
|
||||
for (const pos of pos1) {
|
||||
const key = `${pos.x},${pos.y}`;
|
||||
if (!set.has(key)) {
|
||||
set.add(key);
|
||||
result.push(pos);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加第二个数组的位置(去重)
|
||||
for (const pos of pos2) {
|
||||
const key = `${pos.x},${pos.y}`;
|
||||
if (!set.has(key)) {
|
||||
set.add(key);
|
||||
result.push(pos);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* 计算位置的差集(pos1 - pos2)
|
||||
*/
|
||||
subtractPositions(
|
||||
pos1: GridPosition[],
|
||||
pos2: GridPosition[],
|
||||
): GridPosition[] {
|
||||
const set2 = new Set<string>();
|
||||
for (const pos of pos2) {
|
||||
set2.add(`${pos.x},${pos.y}`);
|
||||
}
|
||||
|
||||
const result: GridPosition[] = [];
|
||||
for (const pos of pos1) {
|
||||
const key = `${pos.x},${pos.y}`;
|
||||
if (!set2.has(key)) {
|
||||
result.push(pos);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
<!-- 模版不能跨包引用 -->
|
||||
<import src="../shared/templates/canvas-page-template.wxml" />
|
||||
|
||||
<template
|
||||
is="canvasPageTemplate"
|
||||
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: true, currentModeName: currentOperatorTypeName, typeActions: operatorTypeActions, adType: 'focusDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
|
||||
@@ -1 +0,0 @@
|
||||
@import '../../base/baseDrawPage.wxss';
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"navigationBarTitleText": "线条识别",
|
||||
"navigationBarBackgroundColor": "#FFD719",
|
||||
"homeButton": true,
|
||||
"backgroundColor": "#F6F6F6",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"toy-button": "../../toy/button-v2/button",
|
||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
||||
"math-bottom-buttons": "../../components/math-bottom-buttons/math-bottom-buttons",
|
||||
"draw-ad": "../../components/draw-ad/draw-ad"
|
||||
}
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
import LineRecognitionDraw from '../shared/service/lineRecognitionDraw';
|
||||
import { createFocusPage } from '../shared/common/focusPageMixin';
|
||||
import {
|
||||
LineRecognitionData,
|
||||
LineType,
|
||||
LineTypeConfig,
|
||||
} from '../shared/service/lineRecognitionDraw';
|
||||
import { NUMBER_COLORS } from '../../constants/colors';
|
||||
|
||||
/**
|
||||
* 广告位ID:adunit-89cff2de998f1146
|
||||
请返回广告位列表,获取广告位代码,将代码插入小程序页面中适合位置,发布后即展现对应广告位。
|
||||
*/
|
||||
|
||||
// 线条类型配置
|
||||
const LINE_TYPES: Array<{ type: LineType; name: string }> = [
|
||||
{ type: 'straight', name: '直线' },
|
||||
{ type: 'dashed', name: '虚线' },
|
||||
{ type: 'wavy', name: '波浪线' },
|
||||
{ type: 'zigzag', name: '锯齿线' },
|
||||
{ type: 'spiral', name: '电话线' },
|
||||
];
|
||||
|
||||
createFocusPage({
|
||||
canvas: null as Canvas | null,
|
||||
ctx: null as RenderingContext | null,
|
||||
boxHeight: 0,
|
||||
boxWidth: 0,
|
||||
drawService: null as LineRecognitionDraw | null,
|
||||
lineData: null as LineRecognitionData | null,
|
||||
|
||||
data: {
|
||||
pageTitle: '线条识别',
|
||||
functionId: '',
|
||||
hasContent: false,
|
||||
showShareDialog: false,
|
||||
},
|
||||
|
||||
onLoad(options: { id?: string }) {
|
||||
const functionId = options.id || 'line-recognition';
|
||||
this.setData({
|
||||
functionId,
|
||||
});
|
||||
this.initPageInfo(functionId, '线条识别');
|
||||
},
|
||||
|
||||
onReady() {
|
||||
this.initCanvas({
|
||||
createDrawService: (
|
||||
canvas: Canvas,
|
||||
ctx: RenderingContext,
|
||||
options?: Record<string, any>,
|
||||
) => {
|
||||
return new LineRecognitionDraw(canvas, ctx, options);
|
||||
},
|
||||
drawServiceOptions: {
|
||||
title: this.data.pageTitle,
|
||||
subTitle: '认识不同线条,画出颜色对应的线条',
|
||||
functionId: this.data.functionId,
|
||||
},
|
||||
onCanvasReady: () => {
|
||||
// 初始随机生成
|
||||
this.onRandom();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 绘制Canvas内容
|
||||
*/
|
||||
async drawCanvas() {
|
||||
if (!this.ctx || !this.drawService || !this.lineData) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.drawService.draw(this.lineData);
|
||||
this.setData({ hasContent: true });
|
||||
} catch (error) {
|
||||
console.error('绘制失败:', error);
|
||||
this.setData({ hasContent: false });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 随机生成
|
||||
*/
|
||||
onRandom() {
|
||||
// 从NUMBER_COLORS中随机选择5种颜色(排除11和12:柔和棕色和柔和黑灰)
|
||||
const colorKeys = Object.keys(NUMBER_COLORS)
|
||||
.map(Number)
|
||||
.filter((key) => key !== 11 && key !== 12); // 排除柔和棕色和柔和黑灰
|
||||
const shuffledColors = [...colorKeys].sort(() => Math.random() - 0.5);
|
||||
const selectedColorKeys = shuffledColors.slice(0, 5);
|
||||
|
||||
// 创建5个线条配置(随机排列)
|
||||
const shuffledLineTypes = [...LINE_TYPES].sort(
|
||||
() => Math.random() - 0.5,
|
||||
);
|
||||
const referenceLines: LineTypeConfig[] = shuffledLineTypes.map(
|
||||
(lineType, index) => ({
|
||||
type: lineType.type,
|
||||
name: lineType.name,
|
||||
color: NUMBER_COLORS[selectedColorKeys[index]],
|
||||
}),
|
||||
);
|
||||
|
||||
// 创建练习区域的16个色块(2列×8行)
|
||||
const practiceBlocks: Array<{
|
||||
color: string;
|
||||
lineType: LineType;
|
||||
x: number;
|
||||
y: number;
|
||||
}> = [];
|
||||
|
||||
// 为每行生成色块
|
||||
for (let row = 0; row < 8; row++) {
|
||||
for (let col = 0; col < 2; col++) {
|
||||
// 随机选择一个线条类型和颜色
|
||||
const randomLineIndex = Math.floor(
|
||||
Math.random() * referenceLines.length,
|
||||
);
|
||||
const selectedLine = referenceLines[randomLineIndex];
|
||||
|
||||
// 生成随机错落感(X轴偏移:-20到20,Y轴偏移:-8到8,确保不重叠)
|
||||
// 行高55,色块35,所以Y轴偏移最大8,确保不重叠
|
||||
const xOffset = (Math.random() - 0.5) * 40;
|
||||
const yOffset = (Math.random() - 0.5) * 16;
|
||||
|
||||
practiceBlocks.push({
|
||||
color: selectedLine.color,
|
||||
lineType: selectedLine.type,
|
||||
x: xOffset,
|
||||
y: yOffset,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.lineData = {
|
||||
referenceLines,
|
||||
practiceBlocks,
|
||||
};
|
||||
|
||||
this.drawCanvas();
|
||||
},
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
<!-- 模版不能跨包引用 -->
|
||||
<import src="../shared/templates/canvas-page-template.wxml" />
|
||||
|
||||
<template
|
||||
is="canvasPageTemplate"
|
||||
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: false, adType: 'focusDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
|
||||
@@ -1 +0,0 @@
|
||||
@import '../../base/baseDrawPage.wxss';
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"navigationBarTitleText": "连连看",
|
||||
"navigationBarBackgroundColor": "#FFD719",
|
||||
"homeButton": true,
|
||||
"backgroundColor": "#F6F6F6",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"toy-button": "../../toy/button-v2/button",
|
||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
||||
"math-bottom-buttons": "../../components/math-bottom-buttons/math-bottom-buttons",
|
||||
"draw-ad": "../../components/draw-ad/draw-ad"
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
import MatchConnectDraw from '../shared/service/matchConnectDraw';
|
||||
import { createFocusPage } from '../shared/common/focusPageMixin';
|
||||
import { MatchConnectData } from '../shared/service/matchConnectDraw';
|
||||
|
||||
createFocusPage({
|
||||
canvas: null as Canvas | null,
|
||||
ctx: null as RenderingContext | null,
|
||||
boxHeight: 0,
|
||||
boxWidth: 0,
|
||||
drawService: null as MatchConnectDraw | null,
|
||||
matchData: null as MatchConnectData | null,
|
||||
|
||||
data: {
|
||||
pageTitle: '连连看',
|
||||
functionId: '',
|
||||
hasContent: false,
|
||||
showShareDialog: false,
|
||||
},
|
||||
|
||||
onLoad(options: { id?: string }) {
|
||||
const functionId = options.id || 'match-connect';
|
||||
this.setData({
|
||||
functionId,
|
||||
});
|
||||
this.initPageInfo(functionId, '连连看');
|
||||
},
|
||||
|
||||
onReady() {
|
||||
this.initCanvas({
|
||||
createDrawService: (
|
||||
canvas: Canvas,
|
||||
ctx: RenderingContext,
|
||||
options?: Record<string, any>,
|
||||
) => {
|
||||
return new MatchConnectDraw(canvas, ctx, options);
|
||||
},
|
||||
drawServiceOptions: {
|
||||
title: this.data.pageTitle,
|
||||
subTitle: '快来根据物品连一连吧!',
|
||||
functionId: this.data.functionId,
|
||||
},
|
||||
onCanvasReady: () => {
|
||||
// 初始随机生成
|
||||
this.onRandom();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 绘制Canvas内容
|
||||
*/
|
||||
async drawCanvas() {
|
||||
if (!this.ctx || !this.drawService || !this.matchData) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.drawService.draw(this.matchData);
|
||||
this.setData({ hasContent: true });
|
||||
} catch (error) {
|
||||
console.error('绘制失败:', error);
|
||||
this.setData({ hasContent: false });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 随机生成
|
||||
*/
|
||||
onRandom() {
|
||||
// 从33张图片中随机选择5张不重复的图片
|
||||
const allImageIndices = Array.from({ length: 33 }, (_, i) => i + 1);
|
||||
const shuffled = [...allImageIndices].sort(() => Math.random() - 0.5);
|
||||
const selectedImages = shuffled.slice(0, 5);
|
||||
|
||||
// 创建参考序列(顺序)
|
||||
const referenceSequence = [...selectedImages];
|
||||
|
||||
// 创建6个框,每个框包含5张图片的索引(位置由绘制服务随机生成)
|
||||
const boxes: Array<{
|
||||
imageIndices: number[];
|
||||
}> = [];
|
||||
|
||||
for (let boxIndex = 0; boxIndex < 6; boxIndex++) {
|
||||
// 每个框使用相同的5张图片
|
||||
boxes.push({
|
||||
imageIndices: [...selectedImages],
|
||||
});
|
||||
}
|
||||
|
||||
this.matchData = {
|
||||
referenceSequence,
|
||||
boxes,
|
||||
};
|
||||
|
||||
this.drawCanvas();
|
||||
},
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
<!-- 模版不能跨包引用 -->
|
||||
<import src="../shared/templates/canvas-page-template.wxml" />
|
||||
|
||||
<template
|
||||
is="canvasPageTemplate"
|
||||
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: false, adType: 'focusDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
|
||||
@@ -1 +0,0 @@
|
||||
@import '../../base/baseDrawPage.wxss';
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"navigationBarTitleText": "方位涂涂乐",
|
||||
"navigationBarBackgroundColor": "#FFD719",
|
||||
"homeButton": true,
|
||||
"backgroundColor": "#F6F6F6",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"toy-button": "../../toy/button-v2/button",
|
||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
||||
"math-bottom-buttons": "../../components/math-bottom-buttons/math-bottom-buttons",
|
||||
"draw-ad": "../../components/draw-ad/draw-ad"
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
import PositionColoringDraw from '../shared/service/positionColoringDraw';
|
||||
import { createFocusPage } from '../shared/common/focusPageMixin';
|
||||
import { PositionColoringData } from '../shared/service/positionColoringDraw';
|
||||
|
||||
createFocusPage({
|
||||
canvas: null as Canvas | null,
|
||||
ctx: null as RenderingContext | null,
|
||||
boxHeight: 0,
|
||||
boxWidth: 0,
|
||||
drawService: null as PositionColoringDraw | null,
|
||||
gridData: null as PositionColoringData | null,
|
||||
|
||||
data: {
|
||||
pageTitle: '方位涂涂乐',
|
||||
functionId: '',
|
||||
hasContent: false,
|
||||
showShareDialog: false,
|
||||
},
|
||||
|
||||
onLoad(options: { id?: string }) {
|
||||
const functionId = options.id || 'position-coloring';
|
||||
this.setData({
|
||||
functionId,
|
||||
});
|
||||
this.initPageInfo(functionId, '方位涂涂乐');
|
||||
},
|
||||
|
||||
onReady() {
|
||||
this.initCanvas({
|
||||
createDrawService: (
|
||||
canvas: Canvas,
|
||||
ctx: RenderingContext,
|
||||
options?: Record<string, any>,
|
||||
) => {
|
||||
return new PositionColoringDraw(canvas, ctx, options);
|
||||
},
|
||||
drawServiceOptions: {
|
||||
title: this.data.pageTitle,
|
||||
subTitle: '观察卡片位置,在对应的方格中涂上颜色',
|
||||
functionId: this.data.functionId,
|
||||
},
|
||||
onCanvasReady: () => {
|
||||
// 初始随机生成
|
||||
this.onRandom();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 绘制Canvas内容
|
||||
*/
|
||||
async drawCanvas() {
|
||||
if (!this.ctx || !this.drawService || !this.gridData) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.drawService.draw(this.gridData);
|
||||
this.setData({ hasContent: true });
|
||||
} catch (error) {
|
||||
console.error('绘制失败:', error);
|
||||
this.setData({ hasContent: false });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 随机生成
|
||||
*/
|
||||
onRandom() {
|
||||
// 生成9张不重复的图片索引(1-33),33 是图片总数
|
||||
const allImageIndices = Array.from({ length: 33 }, (_, i) => i + 1);
|
||||
const shuffled = [...allImageIndices].sort(() => Math.random() - 0.5);
|
||||
const selectedImages = shuffled.slice(0, 9);
|
||||
|
||||
// 创建3x3网格的图片位置映射
|
||||
const referenceGrid: number[] = [];
|
||||
for (let i = 0; i < 9; i++) {
|
||||
referenceGrid[i] = selectedImages[i];
|
||||
}
|
||||
|
||||
// 创建任务组:9个任务,每个任务包含一个图片和对应的位置
|
||||
// 第一行需要标记位置(涂色),其他行不标记
|
||||
const tasks: Array<{
|
||||
imageIndex: number;
|
||||
position: { x: number; y: number };
|
||||
}> = [];
|
||||
|
||||
// 打乱顺序选择图片用于任务
|
||||
const taskImageIndices = [...selectedImages].sort(
|
||||
() => Math.random() - 0.5,
|
||||
);
|
||||
|
||||
for (let i = 0; i < 9; i++) {
|
||||
const imageIndex = taskImageIndices[i];
|
||||
// 找到这个图片在参考网格中的位置
|
||||
const gridIndex = referenceGrid.indexOf(imageIndex);
|
||||
const x = gridIndex % 3;
|
||||
const y = Math.floor(gridIndex / 3);
|
||||
tasks.push({
|
||||
imageIndex,
|
||||
position: { x, y },
|
||||
});
|
||||
}
|
||||
|
||||
this.gridData = {
|
||||
referenceGrid,
|
||||
tasks,
|
||||
};
|
||||
|
||||
this.drawCanvas();
|
||||
},
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
<!-- 模版不能跨包引用 -->
|
||||
<import src="../shared/templates/canvas-page-template.wxml" />
|
||||
|
||||
<template
|
||||
is="canvasPageTemplate"
|
||||
data="{{boxWidth: boxWidth, boxHeight: boxHeight, hasTypeSelector: false, adType: 'focusDraw', disabled: !hasContent, showShareDialog: showShareDialog}}" />
|
||||
@@ -1 +0,0 @@
|
||||
@import '../../base/baseDrawPage.wxss';
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"navigationBarTitleText": "图形符号配对",
|
||||
"navigationBarBackgroundColor": "#FFD719",
|
||||
"homeButton": true,
|
||||
"backgroundColor": "#F6F6F6",
|
||||
"enablePullDownRefresh": false,
|
||||
"usingComponents": {
|
||||
"toy-button": "../../toy/button-v2/button",
|
||||
"share-guide-popup": "../../components/share-guide-popup/share-guide-popup",
|
||||
"math-bottom-buttons": "../../components/math-bottom-buttons/math-bottom-buttons",
|
||||
"draw-ad": "../../components/draw-ad/draw-ad"
|
||||
}
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
import ShapeSymbolDraw from '../shared/service/shapeSymbolDraw';
|
||||
import { createFocusPage } from '../shared/common/focusPageMixin';
|
||||
import { ShapeSymbolData } from '../shared/service/shapeSymbolDraw';
|
||||
import { SHAPE_SYMBOL_SHAPES } from '../shared/shapes/shapeSymbolShapes';
|
||||
import { getRandomNumberColor } from '../../constants/colors';
|
||||
|
||||
createFocusPage({
|
||||
canvas: null as Canvas | null,
|
||||
ctx: null as RenderingContext | null,
|
||||
boxHeight: 0,
|
||||
boxWidth: 0,
|
||||
drawService: null as ShapeSymbolDraw | null,
|
||||
gridData: null as ShapeSymbolData | null,
|
||||
|
||||
data: {
|
||||
pageTitle: '图形符号配对',
|
||||
functionId: '',
|
||||
hasContent: false,
|
||||
showShareDialog: false,
|
||||
},
|
||||
|
||||
onLoad(options: { id?: string }) {
|
||||
const functionId = options.id || 'shape-symbol';
|
||||
this.setData({
|
||||
functionId,
|
||||
});
|
||||
this.initPageInfo(functionId, '图形符号配对');
|
||||
},
|
||||
|
||||
onReady() {
|
||||
this.initCanvas({
|
||||
createDrawService: (
|
||||
canvas: Canvas,
|
||||
ctx: RenderingContext,
|
||||
options?: Record<string, any>,
|
||||
) => {
|
||||
return new ShapeSymbolDraw(canvas, ctx, options);
|
||||
},
|
||||
drawServiceOptions: {
|
||||
title: this.data.pageTitle,
|
||||
subTitle: '根据图形画对应的符号',
|
||||
functionId: this.data.functionId,
|
||||
},
|
||||
onCanvasReady: () => {
|
||||
// 初始随机生成
|
||||
this.onRandom();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 绘制Canvas内容
|
||||
*/
|
||||
async drawCanvas() {
|
||||
if (!this.ctx || !this.drawService || !this.gridData) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.drawService.draw(this.gridData);
|
||||
this.setData({ hasContent: true });
|
||||
} catch (error) {
|
||||
console.error('绘制失败:', error);
|
||||
this.setData({ hasContent: false });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 随机生成
|
||||
*/
|
||||
onRandom() {
|
||||
// 从9种图形中随机选择4个
|
||||
const shuffled = [...SHAPE_SYMBOL_SHAPES].sort(
|
||||
() => Math.random() - 0.5,
|
||||
);
|
||||
const selectedShapes = shuffled.slice(0, 4);
|
||||
|
||||
// 符号类型:加号、减号、乘号、对号
|
||||
const symbols = ['+', '-', '×', '✓'];
|
||||
// 随机打乱符号顺序
|
||||
const shuffledSymbols = [...symbols].sort(() => Math.random() - 0.5);
|
||||
|
||||
// 创建图形和符号的映射关系(按位置对应),并为每个图形分配不同的颜色
|
||||
const shapeColorMap = new Map<string, string>(); // 存储每个图形ID对应的颜色
|
||||
const usedColors = new Set<string>(); // 记录已使用的颜色,确保4个图形颜色不同
|
||||
const legendMapping: Array<{
|
||||
shapeId: string;
|
||||
symbol: string;
|
||||
color: string;
|
||||
}> = [];
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const shape = selectedShapes[i];
|
||||
|
||||
// 为每个图形分配颜色,确保4个图形颜色都不相同
|
||||
let color: string;
|
||||
if (shapeColorMap.has(shape.id)) {
|
||||
// 如果这个图形已经分配过颜色,使用之前的颜色
|
||||
color = shapeColorMap.get(shape.id)!;
|
||||
} else {
|
||||
// 生成新颜色,确保不与已使用的颜色重复
|
||||
do {
|
||||
color = getRandomNumberColor();
|
||||
} while (usedColors.has(color));
|
||||
shapeColorMap.set(shape.id, color);
|
||||
usedColors.add(color);
|
||||
}
|
||||
|
||||
legendMapping.push({
|
||||
shapeId: shape.id,
|
||||
symbol: shuffledSymbols[i],
|
||||
color: color,
|
||||
});
|
||||
}
|
||||
|
||||
// 生成练习区域的数据(6行7列,每两行一组)
|
||||
const practiceRows: Array<
|
||||
Array<{ shapeId: string | null; symbol: string | null }>
|
||||
> = [];
|
||||
for (let groupIndex = 0; groupIndex < 4; groupIndex++) {
|
||||
// 每组第一行:随机展示图形
|
||||
const shapeRow: Array<{
|
||||
shapeId: string | null;
|
||||
symbol: string | null;
|
||||
}> = [];
|
||||
for (let col = 0; col < 8; col++) {
|
||||
// 随机选择一个图形(从选中的4个中选)
|
||||
const randomShape =
|
||||
selectedShapes[
|
||||
Math.floor(Math.random() * selectedShapes.length)
|
||||
];
|
||||
shapeRow.push({ shapeId: randomShape.id, symbol: null });
|
||||
}
|
||||
practiceRows.push(shapeRow);
|
||||
|
||||
// 每组第二行:空着,让小朋友涂符号
|
||||
const symbolRow: Array<{
|
||||
shapeId: string | null;
|
||||
symbol: string | null;
|
||||
}> = [];
|
||||
for (let col = 0; col < 8; col++) {
|
||||
symbolRow.push({ shapeId: null, symbol: null });
|
||||
}
|
||||
practiceRows.push(symbolRow);
|
||||
}
|
||||
|
||||
this.gridData = {
|
||||
legendMapping,
|
||||
practiceRows,
|
||||
shapeColorMap: Object.fromEntries(shapeColorMap), // 转换为普通对象传递给绘制服务
|
||||
};
|
||||
|
||||
this.drawCanvas();
|
||||
},
|
||||
});
|
||||
@@ -1,38 +0,0 @@
|
||||
<view class="page-container">
|
||||
<view class="wrapper">
|
||||
<text class="wrapper-title">预览打印效果</text>
|
||||
|
||||
<!-- 预览打印效果 -->
|
||||
<view id="canvasWrapper" class="canvas-wrapper">
|
||||
<canvas
|
||||
type="2d"
|
||||
id="canvasContent"
|
||||
class="canvas-content"
|
||||
style="width:{{boxWidth}}px;height:{{boxHeight}}px" />
|
||||
</view>
|
||||
|
||||
<!-- 随机生成按钮 -->
|
||||
<view class="random-button-area">
|
||||
<toy-button
|
||||
class="random-button"
|
||||
type="primary"
|
||||
bind:click="onRandom"
|
||||
width="100%"
|
||||
height="80rpx"
|
||||
icon="refresh"
|
||||
icon-class-prefix="toy-icon">
|
||||
随机生成
|
||||
</toy-button>
|
||||
</view>
|
||||
<draw-ad type="focusDraw"></draw-ad>
|
||||
</view>
|
||||
<view class="empty"></view>
|
||||
</view>
|
||||
<math-bottom-buttons
|
||||
disabled="{{!hasContent}}"
|
||||
bind:share="onShareAppMessage"
|
||||
bind:export="exportToPrint" />
|
||||
<share-guide-popup
|
||||
show="{{showShareDialog}}"
|
||||
bind:onClose="onCloseShareDialog"
|
||||
bind:onShareSuccess="onShareSuccess" />
|
||||
@@ -1 +0,0 @@
|
||||
@import '../../base/baseDrawPage.wxss';
|
||||
@@ -238,9 +238,9 @@ class LineRecognitionDraw extends BaseDrawService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制线条
|
||||
* 按线条类型绘制预览/练习区中的线条(避免与 BaseDrawService.drawLine 同名冲突)
|
||||
*/
|
||||
private drawLine(
|
||||
private drawLineVariant(
|
||||
ctx: RenderingContext,
|
||||
lineType: LineType,
|
||||
x: number,
|
||||
@@ -317,7 +317,7 @@ class LineRecognitionDraw extends BaseDrawService {
|
||||
const lineX = blockX + lineMargin;
|
||||
const lineY = blockY + referenceBlockHeight / 2;
|
||||
const lineWidth = referenceBlockWidth - lineMargin * 2;
|
||||
this.drawLine(ctx, config.type, lineX, lineY, lineWidth);
|
||||
this.drawLineVariant(ctx, config.type, lineX, lineY, lineWidth);
|
||||
|
||||
// 绘制文字标签
|
||||
ctx.fillStyle = '#000';
|
||||
@@ -359,7 +359,7 @@ class LineRecognitionDraw extends BaseDrawService {
|
||||
const lineX = blockX + lineMargin;
|
||||
const lineY = blockY + referenceBlockHeight / 2;
|
||||
const lineWidth = referenceBlockWidth - lineMargin * 2;
|
||||
this.drawLine(ctx, config.type, lineX, lineY, lineWidth);
|
||||
this.drawLineVariant(ctx, config.type, lineX, lineY, lineWidth);
|
||||
|
||||
// 绘制文字标签
|
||||
ctx.fillStyle = '#000';
|
||||
|
||||
@@ -116,7 +116,7 @@ class ShapeSymbolDraw extends BaseDrawService {
|
||||
// 第二行:绘制符号(使用 emoji)
|
||||
const symbolX = legendStartX + index * cellSize + cellSize / 2;
|
||||
const symbolY = legendStartY + cellSize + cellSize / 2;
|
||||
this.drawSymbol(ctx, symbolX, symbolY, mapping.symbol, 30);
|
||||
this.drawLegendEmojiSymbol(ctx, symbolX, symbolY, mapping.symbol, 30);
|
||||
});
|
||||
|
||||
const dividerY = legendStartY + legendRows * cellSize + 30;
|
||||
@@ -178,9 +178,9 @@ class ShapeSymbolDraw extends BaseDrawService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制符号(使用路径绘制,类似 shapeSymbolShapes.ts)
|
||||
* 图例区绘制 emoji 符号(避免与 BaseDrawService.drawSymbol 同名冲突)
|
||||
*/
|
||||
private drawSymbol(
|
||||
private drawLegendEmojiSymbol(
|
||||
ctx: RenderingContext,
|
||||
x: number,
|
||||
y: number,
|
||||
|
||||