From 7126d3a94190d9b3df7a40dde68b8ec0430dc15d Mon Sep 17 00:00:00 2001 From: R524809 Date: Thu, 30 Apr 2026 16:44:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=A6=96=E9=A1=B5=E3=80=81=E5=88=86?= =?UTF-8?q?=E7=B1=BB=E9=A1=B5=E5=8A=A0=E8=BD=BD=E6=95=B0=E6=8D=AE=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../english/letter-tracing-case-pairing.jpg | Bin .../english/letter-tracing-case-pairing2.jpg | Bin .../english/letter-tracing-daily-checkin.jpg | Bin .../english/letter-tracing-half.jpg | Bin .../english/letter-tracing-single.jpg | Bin .../english/letter-tracing-single2.jpg | Bin .../english/letter-tracing-three.jpg | Bin .../english/letter-tracing-two-column.jpg | Bin .../english/letter-tracing-upper-lower.jpg | Bin .../entrancePicture/focus/code-connect.png | Bin .../entrancePicture/focus/color-pattern.png | Bin .../focus/color-shape-match.png | Bin .../entrancePicture/focus/dot-connect.png | Bin .../focus/grid-drawing-3x3.png | Bin .../focus/grid-drawing-5x5.png | Bin .../focus/grid-drawing-7x7.png | Bin .../entrancePicture/focus/grid-reasoning.png | Bin .../focus/line-recognition.png | Bin .../entrancePicture/focus/match-connect.png | Bin .../focus/position-coloring.png | Bin .../focus/shape-recognition.png | Bin .../entrancePicture/focus/shape-symbol.png | Bin .../entrancePicture/math/addition-10.png | Bin .../entrancePicture/math/addition-5.png | Bin .../math/addition-subtraction-10.png | Bin .../entrancePicture/math/borrow-ten.png | Bin .../entrancePicture/math/break-ten.png | Bin .../entrancePicture/math/compare.png | Bin .../entrancePicture/math/count-match.png | Bin .../entrancePicture/math/counting-fill.png | Bin .../entrancePicture/math/counting-select.png | Bin .../entrancePicture/math/flat-ten.png | Bin .../entrancePicture/math/make-ten.png | Bin .../entrancePicture/math/missing-number.png | Bin .../math/multiplication-table.png | Bin .../entrancePicture/math/number-coloring.png | Bin .../math/number-decompose-20.png | Bin .../entrancePicture/math/number-decompose.png | Bin .../entrancePicture/math/number-find.png | Bin .../math/number-object-fill.png | Bin .../math/number-object-match.png | Bin .../entrancePicture/math/number-sort.png | Bin .../entrancePicture/math/number-write.png | Bin .../math/one-digit-addition.png | Bin .../math/practice-addition.png | Bin .../entrancePicture/math/practice-mixed.png | Bin .../math/practice-subtraction.png | Bin .../entrancePicture/math/subtraction-10.png | Bin cloudfunctions/pageContentBuild/index.js | 10 +- docs/页面内容管理方案.md | 546 +++++++++--------- miniprogram/app.json | 6 +- miniprogram/app.ts | 100 +++- .../focusPages/focusDraw/focusDraw.config.ts | 6 +- .../mathPages/mathDraw/mathDraw.config.ts | 35 +- miniprogram/pages/category/category.data.ts | 1 + miniprogram/pages/category/category.less | 46 +- miniprogram/pages/category/category.ts | 98 +++- miniprogram/pages/category/category.wxml | 22 +- miniprogram/pages/home/home.data.ts | 54 +- miniprogram/pages/home/home.less | 98 +++- miniprogram/pages/home/home.ts | 139 +++-- miniprogram/pages/home/home.wxml | 58 +- typings/index.d.ts | 19 + 63 files changed, 805 insertions(+), 433 deletions(-) rename {miniprogram/assets => backup}/entrancePicture/english/letter-tracing-case-pairing.jpg (100%) rename {miniprogram/assets => backup}/entrancePicture/english/letter-tracing-case-pairing2.jpg (100%) rename {miniprogram/assets => backup}/entrancePicture/english/letter-tracing-daily-checkin.jpg (100%) rename {miniprogram/assets => backup}/entrancePicture/english/letter-tracing-half.jpg (100%) rename {miniprogram/assets => backup}/entrancePicture/english/letter-tracing-single.jpg (100%) rename {miniprogram/assets => backup}/entrancePicture/english/letter-tracing-single2.jpg (100%) rename {miniprogram/assets => backup}/entrancePicture/english/letter-tracing-three.jpg (100%) rename {miniprogram/assets => backup}/entrancePicture/english/letter-tracing-two-column.jpg (100%) rename {miniprogram/assets => backup}/entrancePicture/english/letter-tracing-upper-lower.jpg (100%) rename {miniprogram/assets => backup}/entrancePicture/focus/code-connect.png (100%) rename {miniprogram/assets => backup}/entrancePicture/focus/color-pattern.png (100%) rename {miniprogram/assets => backup}/entrancePicture/focus/color-shape-match.png (100%) rename {miniprogram/assets => backup}/entrancePicture/focus/dot-connect.png (100%) rename {miniprogram/assets => backup}/entrancePicture/focus/grid-drawing-3x3.png (100%) rename {miniprogram/assets => backup}/entrancePicture/focus/grid-drawing-5x5.png (100%) rename {miniprogram/assets => backup}/entrancePicture/focus/grid-drawing-7x7.png (100%) rename {miniprogram/assets => backup}/entrancePicture/focus/grid-reasoning.png (100%) rename {miniprogram/assets => backup}/entrancePicture/focus/line-recognition.png (100%) rename {miniprogram/assets => backup}/entrancePicture/focus/match-connect.png (100%) rename {miniprogram/assets => backup}/entrancePicture/focus/position-coloring.png (100%) rename {miniprogram/assets => backup}/entrancePicture/focus/shape-recognition.png (100%) rename {miniprogram/assets => backup}/entrancePicture/focus/shape-symbol.png (100%) rename {miniprogram/assets => backup}/entrancePicture/math/addition-10.png (100%) rename {miniprogram/assets => backup}/entrancePicture/math/addition-5.png (100%) rename {miniprogram/assets => backup}/entrancePicture/math/addition-subtraction-10.png (100%) rename {miniprogram/assets => backup}/entrancePicture/math/borrow-ten.png (100%) rename {miniprogram/assets => backup}/entrancePicture/math/break-ten.png (100%) rename {miniprogram/assets => backup}/entrancePicture/math/compare.png (100%) rename {miniprogram/assets => backup}/entrancePicture/math/count-match.png (100%) rename {miniprogram/assets => backup}/entrancePicture/math/counting-fill.png (100%) rename {miniprogram/assets => backup}/entrancePicture/math/counting-select.png (100%) rename {miniprogram/assets => backup}/entrancePicture/math/flat-ten.png (100%) rename {miniprogram/assets => backup}/entrancePicture/math/make-ten.png (100%) rename {miniprogram/assets => backup}/entrancePicture/math/missing-number.png (100%) rename {miniprogram/assets => backup}/entrancePicture/math/multiplication-table.png (100%) rename {miniprogram/assets => backup}/entrancePicture/math/number-coloring.png (100%) rename {miniprogram/assets => backup}/entrancePicture/math/number-decompose-20.png (100%) rename {miniprogram/assets => backup}/entrancePicture/math/number-decompose.png (100%) rename {miniprogram/assets => backup}/entrancePicture/math/number-find.png (100%) rename {miniprogram/assets => backup}/entrancePicture/math/number-object-fill.png (100%) rename {miniprogram/assets => backup}/entrancePicture/math/number-object-match.png (100%) rename {miniprogram/assets => backup}/entrancePicture/math/number-sort.png (100%) rename {miniprogram/assets => backup}/entrancePicture/math/number-write.png (100%) rename {miniprogram/assets => backup}/entrancePicture/math/one-digit-addition.png (100%) rename {miniprogram/assets => backup}/entrancePicture/math/practice-addition.png (100%) rename {miniprogram/assets => backup}/entrancePicture/math/practice-mixed.png (100%) rename {miniprogram/assets => backup}/entrancePicture/math/practice-subtraction.png (100%) rename {miniprogram/assets => backup}/entrancePicture/math/subtraction-10.png (100%) diff --git a/miniprogram/assets/entrancePicture/english/letter-tracing-case-pairing.jpg b/backup/entrancePicture/english/letter-tracing-case-pairing.jpg similarity index 100% rename from miniprogram/assets/entrancePicture/english/letter-tracing-case-pairing.jpg rename to backup/entrancePicture/english/letter-tracing-case-pairing.jpg diff --git a/miniprogram/assets/entrancePicture/english/letter-tracing-case-pairing2.jpg b/backup/entrancePicture/english/letter-tracing-case-pairing2.jpg similarity index 100% rename from miniprogram/assets/entrancePicture/english/letter-tracing-case-pairing2.jpg rename to backup/entrancePicture/english/letter-tracing-case-pairing2.jpg diff --git a/miniprogram/assets/entrancePicture/english/letter-tracing-daily-checkin.jpg b/backup/entrancePicture/english/letter-tracing-daily-checkin.jpg similarity index 100% rename from miniprogram/assets/entrancePicture/english/letter-tracing-daily-checkin.jpg rename to backup/entrancePicture/english/letter-tracing-daily-checkin.jpg diff --git a/miniprogram/assets/entrancePicture/english/letter-tracing-half.jpg b/backup/entrancePicture/english/letter-tracing-half.jpg similarity index 100% rename from miniprogram/assets/entrancePicture/english/letter-tracing-half.jpg rename to backup/entrancePicture/english/letter-tracing-half.jpg diff --git a/miniprogram/assets/entrancePicture/english/letter-tracing-single.jpg b/backup/entrancePicture/english/letter-tracing-single.jpg similarity index 100% rename from miniprogram/assets/entrancePicture/english/letter-tracing-single.jpg rename to backup/entrancePicture/english/letter-tracing-single.jpg diff --git a/miniprogram/assets/entrancePicture/english/letter-tracing-single2.jpg b/backup/entrancePicture/english/letter-tracing-single2.jpg similarity index 100% rename from miniprogram/assets/entrancePicture/english/letter-tracing-single2.jpg rename to backup/entrancePicture/english/letter-tracing-single2.jpg diff --git a/miniprogram/assets/entrancePicture/english/letter-tracing-three.jpg b/backup/entrancePicture/english/letter-tracing-three.jpg similarity index 100% rename from miniprogram/assets/entrancePicture/english/letter-tracing-three.jpg rename to backup/entrancePicture/english/letter-tracing-three.jpg diff --git a/miniprogram/assets/entrancePicture/english/letter-tracing-two-column.jpg b/backup/entrancePicture/english/letter-tracing-two-column.jpg similarity index 100% rename from miniprogram/assets/entrancePicture/english/letter-tracing-two-column.jpg rename to backup/entrancePicture/english/letter-tracing-two-column.jpg diff --git a/miniprogram/assets/entrancePicture/english/letter-tracing-upper-lower.jpg b/backup/entrancePicture/english/letter-tracing-upper-lower.jpg similarity index 100% rename from miniprogram/assets/entrancePicture/english/letter-tracing-upper-lower.jpg rename to backup/entrancePicture/english/letter-tracing-upper-lower.jpg diff --git a/miniprogram/assets/entrancePicture/focus/code-connect.png b/backup/entrancePicture/focus/code-connect.png similarity index 100% rename from miniprogram/assets/entrancePicture/focus/code-connect.png rename to backup/entrancePicture/focus/code-connect.png diff --git a/miniprogram/assets/entrancePicture/focus/color-pattern.png b/backup/entrancePicture/focus/color-pattern.png similarity index 100% rename from miniprogram/assets/entrancePicture/focus/color-pattern.png rename to backup/entrancePicture/focus/color-pattern.png diff --git a/miniprogram/assets/entrancePicture/focus/color-shape-match.png b/backup/entrancePicture/focus/color-shape-match.png similarity index 100% rename from miniprogram/assets/entrancePicture/focus/color-shape-match.png rename to backup/entrancePicture/focus/color-shape-match.png diff --git a/miniprogram/assets/entrancePicture/focus/dot-connect.png b/backup/entrancePicture/focus/dot-connect.png similarity index 100% rename from miniprogram/assets/entrancePicture/focus/dot-connect.png rename to backup/entrancePicture/focus/dot-connect.png diff --git a/miniprogram/assets/entrancePicture/focus/grid-drawing-3x3.png b/backup/entrancePicture/focus/grid-drawing-3x3.png similarity index 100% rename from miniprogram/assets/entrancePicture/focus/grid-drawing-3x3.png rename to backup/entrancePicture/focus/grid-drawing-3x3.png diff --git a/miniprogram/assets/entrancePicture/focus/grid-drawing-5x5.png b/backup/entrancePicture/focus/grid-drawing-5x5.png similarity index 100% rename from miniprogram/assets/entrancePicture/focus/grid-drawing-5x5.png rename to backup/entrancePicture/focus/grid-drawing-5x5.png diff --git a/miniprogram/assets/entrancePicture/focus/grid-drawing-7x7.png b/backup/entrancePicture/focus/grid-drawing-7x7.png similarity index 100% rename from miniprogram/assets/entrancePicture/focus/grid-drawing-7x7.png rename to backup/entrancePicture/focus/grid-drawing-7x7.png diff --git a/miniprogram/assets/entrancePicture/focus/grid-reasoning.png b/backup/entrancePicture/focus/grid-reasoning.png similarity index 100% rename from miniprogram/assets/entrancePicture/focus/grid-reasoning.png rename to backup/entrancePicture/focus/grid-reasoning.png diff --git a/miniprogram/assets/entrancePicture/focus/line-recognition.png b/backup/entrancePicture/focus/line-recognition.png similarity index 100% rename from miniprogram/assets/entrancePicture/focus/line-recognition.png rename to backup/entrancePicture/focus/line-recognition.png diff --git a/miniprogram/assets/entrancePicture/focus/match-connect.png b/backup/entrancePicture/focus/match-connect.png similarity index 100% rename from miniprogram/assets/entrancePicture/focus/match-connect.png rename to backup/entrancePicture/focus/match-connect.png diff --git a/miniprogram/assets/entrancePicture/focus/position-coloring.png b/backup/entrancePicture/focus/position-coloring.png similarity index 100% rename from miniprogram/assets/entrancePicture/focus/position-coloring.png rename to backup/entrancePicture/focus/position-coloring.png diff --git a/miniprogram/assets/entrancePicture/focus/shape-recognition.png b/backup/entrancePicture/focus/shape-recognition.png similarity index 100% rename from miniprogram/assets/entrancePicture/focus/shape-recognition.png rename to backup/entrancePicture/focus/shape-recognition.png diff --git a/miniprogram/assets/entrancePicture/focus/shape-symbol.png b/backup/entrancePicture/focus/shape-symbol.png similarity index 100% rename from miniprogram/assets/entrancePicture/focus/shape-symbol.png rename to backup/entrancePicture/focus/shape-symbol.png diff --git a/miniprogram/assets/entrancePicture/math/addition-10.png b/backup/entrancePicture/math/addition-10.png similarity index 100% rename from miniprogram/assets/entrancePicture/math/addition-10.png rename to backup/entrancePicture/math/addition-10.png diff --git a/miniprogram/assets/entrancePicture/math/addition-5.png b/backup/entrancePicture/math/addition-5.png similarity index 100% rename from miniprogram/assets/entrancePicture/math/addition-5.png rename to backup/entrancePicture/math/addition-5.png diff --git a/miniprogram/assets/entrancePicture/math/addition-subtraction-10.png b/backup/entrancePicture/math/addition-subtraction-10.png similarity index 100% rename from miniprogram/assets/entrancePicture/math/addition-subtraction-10.png rename to backup/entrancePicture/math/addition-subtraction-10.png diff --git a/miniprogram/assets/entrancePicture/math/borrow-ten.png b/backup/entrancePicture/math/borrow-ten.png similarity index 100% rename from miniprogram/assets/entrancePicture/math/borrow-ten.png rename to backup/entrancePicture/math/borrow-ten.png diff --git a/miniprogram/assets/entrancePicture/math/break-ten.png b/backup/entrancePicture/math/break-ten.png similarity index 100% rename from miniprogram/assets/entrancePicture/math/break-ten.png rename to backup/entrancePicture/math/break-ten.png diff --git a/miniprogram/assets/entrancePicture/math/compare.png b/backup/entrancePicture/math/compare.png similarity index 100% rename from miniprogram/assets/entrancePicture/math/compare.png rename to backup/entrancePicture/math/compare.png diff --git a/miniprogram/assets/entrancePicture/math/count-match.png b/backup/entrancePicture/math/count-match.png similarity index 100% rename from miniprogram/assets/entrancePicture/math/count-match.png rename to backup/entrancePicture/math/count-match.png diff --git a/miniprogram/assets/entrancePicture/math/counting-fill.png b/backup/entrancePicture/math/counting-fill.png similarity index 100% rename from miniprogram/assets/entrancePicture/math/counting-fill.png rename to backup/entrancePicture/math/counting-fill.png diff --git a/miniprogram/assets/entrancePicture/math/counting-select.png b/backup/entrancePicture/math/counting-select.png similarity index 100% rename from miniprogram/assets/entrancePicture/math/counting-select.png rename to backup/entrancePicture/math/counting-select.png diff --git a/miniprogram/assets/entrancePicture/math/flat-ten.png b/backup/entrancePicture/math/flat-ten.png similarity index 100% rename from miniprogram/assets/entrancePicture/math/flat-ten.png rename to backup/entrancePicture/math/flat-ten.png diff --git a/miniprogram/assets/entrancePicture/math/make-ten.png b/backup/entrancePicture/math/make-ten.png similarity index 100% rename from miniprogram/assets/entrancePicture/math/make-ten.png rename to backup/entrancePicture/math/make-ten.png diff --git a/miniprogram/assets/entrancePicture/math/missing-number.png b/backup/entrancePicture/math/missing-number.png similarity index 100% rename from miniprogram/assets/entrancePicture/math/missing-number.png rename to backup/entrancePicture/math/missing-number.png diff --git a/miniprogram/assets/entrancePicture/math/multiplication-table.png b/backup/entrancePicture/math/multiplication-table.png similarity index 100% rename from miniprogram/assets/entrancePicture/math/multiplication-table.png rename to backup/entrancePicture/math/multiplication-table.png diff --git a/miniprogram/assets/entrancePicture/math/number-coloring.png b/backup/entrancePicture/math/number-coloring.png similarity index 100% rename from miniprogram/assets/entrancePicture/math/number-coloring.png rename to backup/entrancePicture/math/number-coloring.png diff --git a/miniprogram/assets/entrancePicture/math/number-decompose-20.png b/backup/entrancePicture/math/number-decompose-20.png similarity index 100% rename from miniprogram/assets/entrancePicture/math/number-decompose-20.png rename to backup/entrancePicture/math/number-decompose-20.png diff --git a/miniprogram/assets/entrancePicture/math/number-decompose.png b/backup/entrancePicture/math/number-decompose.png similarity index 100% rename from miniprogram/assets/entrancePicture/math/number-decompose.png rename to backup/entrancePicture/math/number-decompose.png diff --git a/miniprogram/assets/entrancePicture/math/number-find.png b/backup/entrancePicture/math/number-find.png similarity index 100% rename from miniprogram/assets/entrancePicture/math/number-find.png rename to backup/entrancePicture/math/number-find.png diff --git a/miniprogram/assets/entrancePicture/math/number-object-fill.png b/backup/entrancePicture/math/number-object-fill.png similarity index 100% rename from miniprogram/assets/entrancePicture/math/number-object-fill.png rename to backup/entrancePicture/math/number-object-fill.png diff --git a/miniprogram/assets/entrancePicture/math/number-object-match.png b/backup/entrancePicture/math/number-object-match.png similarity index 100% rename from miniprogram/assets/entrancePicture/math/number-object-match.png rename to backup/entrancePicture/math/number-object-match.png diff --git a/miniprogram/assets/entrancePicture/math/number-sort.png b/backup/entrancePicture/math/number-sort.png similarity index 100% rename from miniprogram/assets/entrancePicture/math/number-sort.png rename to backup/entrancePicture/math/number-sort.png diff --git a/miniprogram/assets/entrancePicture/math/number-write.png b/backup/entrancePicture/math/number-write.png similarity index 100% rename from miniprogram/assets/entrancePicture/math/number-write.png rename to backup/entrancePicture/math/number-write.png diff --git a/miniprogram/assets/entrancePicture/math/one-digit-addition.png b/backup/entrancePicture/math/one-digit-addition.png similarity index 100% rename from miniprogram/assets/entrancePicture/math/one-digit-addition.png rename to backup/entrancePicture/math/one-digit-addition.png diff --git a/miniprogram/assets/entrancePicture/math/practice-addition.png b/backup/entrancePicture/math/practice-addition.png similarity index 100% rename from miniprogram/assets/entrancePicture/math/practice-addition.png rename to backup/entrancePicture/math/practice-addition.png diff --git a/miniprogram/assets/entrancePicture/math/practice-mixed.png b/backup/entrancePicture/math/practice-mixed.png similarity index 100% rename from miniprogram/assets/entrancePicture/math/practice-mixed.png rename to backup/entrancePicture/math/practice-mixed.png diff --git a/miniprogram/assets/entrancePicture/math/practice-subtraction.png b/backup/entrancePicture/math/practice-subtraction.png similarity index 100% rename from miniprogram/assets/entrancePicture/math/practice-subtraction.png rename to backup/entrancePicture/math/practice-subtraction.png diff --git a/miniprogram/assets/entrancePicture/math/subtraction-10.png b/backup/entrancePicture/math/subtraction-10.png similarity index 100% rename from miniprogram/assets/entrancePicture/math/subtraction-10.png rename to backup/entrancePicture/math/subtraction-10.png diff --git a/cloudfunctions/pageContentBuild/index.js b/cloudfunctions/pageContentBuild/index.js index ad915b0..a502575 100644 --- a/cloudfunctions/pageContentBuild/index.js +++ b/cloudfunctions/pageContentBuild/index.js @@ -45,7 +45,7 @@ function toHomeDisplayItem(ws) { ageBand: ageBand(ws.ageMin || 0, ws.ageMax || 0), difficulty: DIFFICULTY_LABELS[ws.difficulty] || '基础', icon: '', - img: ws.previewImg || '', + previewImg: ws.previewImg || '', badge: ws.isNew ? 'NEW' : ws.isHot ? 'HOT' : '', path: ws.path || '', available: true, @@ -141,7 +141,9 @@ async function buildCategoryData(db) { } async function buildHomeData(db, event) { - const featuredIds = Array.isArray(event.featuredIds) ? event.featuredIds : []; + const featuredIds = Array.isArray(event.featuredIds) + ? event.featuredIds + : []; const hotIds = Array.isArray(event.hotIds) ? event.hotIds : []; const sectionConfigs = Array.isArray(event.sections) ? event.sections : []; @@ -264,9 +266,7 @@ exports.main = async (event) => { return { success: false, message: - error instanceof Error - ? error.message - : '生成分类页数据失败', + error instanceof Error ? error.message : '生成分类页数据失败', }; } }; diff --git a/docs/页面内容管理方案.md b/docs/页面内容管理方案.md index 7bd2f50..058bcef 100644 --- a/docs/页面内容管理方案.md +++ b/docs/页面内容管理方案.md @@ -1,7 +1,7 @@ # 页面内容管理方案 -> 版本:v1.0  |  最后更新:2026-04-29 -> 配套文档:[Worksheet 发布方案](./Worksheet发布方案.md)  |  [小程序云开发方案](./小程序云开发方案.md) +> 版本:v2.0 | 最后更新:2026-04-30 +> 配套文档:[Worksheet 发布方案](./Worksheet发布方案.md) | [小程序云开发方案](./小程序云开发方案.md) --- @@ -9,36 +9,68 @@ worksheet 已可通过 Debug 发布进入云数据库,但三个展示页仍使用本地静态数据: - | 页面 | 当前数据来源 | 问题 | | --- | --------------------------------- | ---------------------- | | 首页 | `pages/home/home.data.ts` | featured、hot、分区靠代码手动维护 | | 分类页 | `pages/category/category.data.ts` | 新 worksheet 发布后不会自动出现 | | 分龄页 | `pages/age/age.ts` 内 Mock | 推荐内容和周路线无法运营配置 | - -**目标:** 通过 Debug 管理页配置三个页面的内容,生成统一 JSON 上传云存储,小程序启动时通过**数据预拉取**获取并渲染。 +**目标:** 通过 Debug 管理页配置三个页面的内容,生成统一配置写入云数据库并同步至云存储,小程序启动时通过**数据预拉取**获取并渲染。 --- ## 二、整体架构 -### 2.1 数据流 +### 2.1 数据存储与同步 + +**数据源(Source of Truth):** 云数据库 `page_configs` 集合,文档 `_id = 'current'` + +**数据交付(Delivery):** 云存储 `content/page-config.json`(供小程序预拉取使用) + +每次更新时,云函数先写数据库,再同步上传至云存储,保证两者一致。各管理页更新时只修改配置中自己负责的字段,不影响其他页面数据。 + +```ts +// page_configs/current 文档结构 +type PageConfig = { + home: HomePageData; // 首页配置 + category: CategoryPageData; // 分类页配置 + age: AgePageData; // 分龄页配置 + version: number; // 版本号,每次更新递增 + updatedAt: string; // 最后更新时间 +}; +``` + +读写流程: + +``` +readCurrentConfig() → 从 page_configs/current 读取当前完整配置 +config[page] = data → 只覆盖对应页面字段(home / category / age) +saveConfig(config) → 写回数据库 → 上传云存储 content/page-config.json +``` + +### 2.2 数据流 ``` ┌──────────────────────────────────────────────────────────────────┐ -│ Debug 内容管理页(三个独立 tab) │ +│ Debug 内容管理页(独立页面) │ │ │ -│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ -│ │ 首页管理 │ │ 分类页管理 │ │ 分龄页管理 │ │ -│ └────┬────┘ └────┬────┘ └────┬────┘ │ -│ │ │ │ │ -│ └──────┬──────┘──────┬──────┘ │ -│ ▼ ▼ │ -│ 点击「更新」按钮 → 调用云函数 pageContentUpdate │ -│ │ │ -│ ▼ │ -│ 更新云存储 content/page-config.json 中对应页面的配置 │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ 首页内容管理 │ │ 分类页内容管理 │ │ 分龄页内容管理 │ │ +│ │ homeContent │ │ categoryContent│ │ ageContent │ │ +│ │ Manage │ │ Manage │ │ Manage │ │ +│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ +│ │ │ │ │ +│ └────────┬─────────┘─────────┬─────────┘ │ +│ ▼ ▼ │ +│ 点击「更新」按钮 → 调用云函数 pageContentBuild │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────────────────────────┐ │ +│ │ 1. 从 page_configs/current 读取当前配置 │ │ +│ │ 2. 只更新对应页面字段(home/category/age) │ │ +│ │ 3. version++ → 写回数据库 │ │ +│ │ 4. 同步上传至云存储 content/page-config.json│ │ +│ └─────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────────┘ ▼ @@ -47,122 +79,194 @@ worksheet 已可通过 Debug 发布进入云数据库,但三个展示页仍使 │ 小程序启动 │ │ │ │ app.onLaunch │ -│ └─ 数据预拉取 → 获取 content/page-config.json │ +│ └─ wx.getBackgroundFetchData('pre') 获取预拉取结果 │ +│ ├─ 成功 → 存入 globalData + storage 缓存 │ +│ └─ 失败 → 读取 storage 缓存 │ +│ ├─ 命中 → 存入 globalData │ +│ └─ 未命中 → 调用 pageConfigFetch 云函数兜底 │ +│ └─ 成功 → 存入 globalData + storage │ │ │ -│ 页面 onLoad │ -│ ├─ 1. 使用本地 *.data.ts / age.config.ts 渲染兜底首屏 │ -│ ├─ 2. 读取预拉取结果中对应页面的配置 │ -│ └─ 3. 成功则 setData 更新页面 │ +│ TabBar 页面 onLoad(首页 / 分类页 / 分龄页) │ +│ ├─ 配置已就绪 → 从 globalData.pageConfig 读取对应字段 → 渲染 │ +│ └─ 配置未就绪 → 显示加载态 → 监听配置就绪回调 → 渲染 │ +│ │ +│ TabBar 页面 onShow(后续切换) │ +│ └─ 直接从 globalData.pageConfig 读取,零延迟 │ └──────────────────────────────────────────────────────────────────┘ ``` -### 2.2 统一 JSON 格式 +### 2.3 数据预拉取 -云存储路径:`content/page-config.json` +小程序数据预拉取通过云函数 `pageConfigFetch` 实现: -```ts -// content/page-config.json 结构 -type PageConfig = { - home: HomePageData; // 首页配置 - category: CategoryPageData; // 分类页配置 - age: AgePageData; // 分龄页配置 - version: number; // 版本号,每次更新递增 - updatedAt: string; // 最后更新时间 +- 在 `app.json` 中配置 `fetchDataUrl` 指向 `pageConfigFetch` 云函数 +- 小程序冷启动时由微信客户端自动调用,不占用页面加载时间 +- 云函数从 `page_configs/current` 读取完整配置并返回 + +```js +// cloudfunctions/pageConfigFetch/index.js +exports.main = async () => { + const db = cloud.database(); + const { data } = await db.collection('page_configs').doc('current').get(); + return { success: true, data }; }; ``` -每个管理页更新时,只修改 JSON 中自己负责的字段,通过云函数读取当前 JSON → 合并更新 → 写回云存储。 +### 2.4 页面加载策略 -### 2.3 页面加载策略 - -三个页面统一采用**数据预拉取 + 本地兜底**的加载策略: +三个展示页(首页、分类页、分龄页)均为 TabBar 页面,不再使用本地 `.data.ts` 静态数据渲染,改为**统一从内存读取预拉取配置**,并通过 storage 缓存保证离线可用: ``` -页面 onLoad +app.onLaunch │ - ├─ Step 1: 使用本地静态数据渲染首屏(零延迟) - │ 首页 → home.data.ts - │ 分类页 → category.data.ts - │ 分龄页 → age.config.ts + ├─ Step 1: wx.getBackgroundFetchData('pre') 获取预拉取结果 + │ 成功 → 存入 globalData.pageConfig + │ → wx.setStorageSync('pageConfig', data) 写入缓存 + │ → 通知页面就绪 │ - ├─ Step 2: 读取数据预拉取结果 - │ wx.getBackgroundFetchData('pre') 获取 page-config.json - │ 取出当前页面对应的配置(home / category / age) + ├─ Step 2: 预拉取失败 → wx.getStorageSync('pageConfig') 读取缓存 + │ 命中 → 存入 globalData.pageConfig → 通知页面就绪 │ - └─ Step 3: 成功 → setData 更新页面内容 - 失败 → 保持本地兜底数据,用户无感知 + └─ Step 3: 缓存也为空 → 调用 pageConfigFetch 云函数兜底 + 成功 → 存入 globalData.pageConfig + → wx.setStorageSync('pageConfig', data) 写入缓存 + → 通知页面就绪 + +TabBar 页面 onLoad(仅首次进入触发) + │ + ├─ globalData.pageConfig 已就绪 + │ → 读取对应字段(home / category / age)→ setData 渲染 + │ + └─ globalData.pageConfig 未就绪 + → 显示加载态(骨架屏 / loading) + → 注册回调,配置就绪后自动渲染 + +TabBar 页面 onShow(后续每次切换触发) + │ + └─ 直接从 globalData.pageConfig 读取,零延迟,无网络请求 ``` -> **数据预拉取**在 `app.json` 中配置 `fetchDataUrl`,小程序冷启动时由微信客户端自动发起,不占用页面加载时间。预拉取的数据指向云存储中的 `content/page-config.json`。 +**三级降级策略:** 预拉取 → storage 缓存 → 云函数调用 -### 2.4 云函数设计 +| 层级 | 数据来源 | 耗时 | 适用场景 | +| --- | ----------------------------------- | ------- | -------------------- | +| L1 | `wx.getBackgroundFetchData('pre')` | 0ms | 正常冷启动,预拉取已完成 | +| L2 | `wx.getStorageSync('pageConfig')` | ~1ms | 预拉取失败,但之前成功过 | +| L3 | `pageConfigFetch` 云函数 | 200-500ms | 首次使用或缓存被清理 | -新增一个统一云函数 `pageContentUpdate`: +**设计要点:** -```ts -// cloudfunctions/pageContentUpdate/index.js -type PageContentUpdatePayload = { - page: 'home' | 'category' | 'age'; // 要更新的页面 - data: HomePageData | CategoryPageData | AgePageData; // 该页面的完整配置 -}; -``` +- 配置在 `app.onLaunch` 时加载一次,存入 `globalData.pageConfig`,三个页面共享同一份内存数据 +- 每次预拉取或云函数获取成功后,同步写入 storage 作为下次启动的缓存兜底 +- TabBar 页面 `onLoad` 只执行一次,`onShow` 每次切换都触发,天然适合「启动时加载一次,后续复用」的模式 +- 不再维护 `home.data.ts`、`category.data.ts` 等本地静态数据文件,消除双数据源不一致的问题 +- storage 缓存的 key 为 `pageConfig`,使用 `wx.setStorageSync` / `wx.getStorageSync` 同步读写 -执行逻辑: +### 2.5 云函数总览 -``` -接收参数 { page, data } - → 从云存储读取当前 content/page-config.json - → 合并:config[page] = data - → config.version++ - → config.updatedAt = new Date() - → 写回云存储 content/page-config.json - → 返回 { success, version } -``` +| 云函数 | 职责 | +| ---------------------- | ------------------------------------------------- | +| `pageContentBuild` | 生成指定页面配置(home / category),写入数据库 + 云存储 | +| `pageConfigFetch` | 数据预拉取接口,从数据库读取完整配置返回给客户端 | +| `homeAutoRefresh` | 定时任务,每日 22:00 自动更新 featured 和 hot | +| `homeTimerControl` | 暂停/启动首页定时刷新任务(更新 settings 集合标记位) | +| `worksheetsQuery` | 查询 worksheet 列表(支持按分类、状态筛选) | +| `worksheetsUpdateStatus` | 更新单个 worksheet 的状态 | --- ## 三、首页内容管理 -### 3.1 管理 UI +### 3.1 管理页入口 -首页管理页分为以下区块: +`supportPages/homeContentManage/homeContentManage` +进入页面时并行调用两个云函数加载数据: -| 区块 | 对应字段 | 管理方式 | +``` +onLoad + → Promise.all([ + worksheetsQuery({ status: 'active' }), // 获取所有 active worksheet + pageConfigFetch() // 获取当前已保存的配置 + ]) + → featured: 始终按 downloads desc 取前 5 自动填充 + → hot: 始终按 likes desc 取前 5 自动填充 + → sections: 从 config.home.sections 回显已保存的分类分区数据 +``` + +### 3.2 管理 UI + +| 区块 | 对应字段 | 数据来源 | | ------ | -------------- | ----------------------------------- | -| 分类 tab | `categoryTabs` | 跟随 `CATEGORY_LIST_WITH_ALL`,一般不单独管理 | -| 年龄入口 | `ageBands` | 跟随 `AGE_BANDS`,可管理描述文案 | -| 今日推荐 | `featured` | 自动取 downloads 最多的 5 个 active worksheet,支持手动微调 | -| 热门推荐 | `hot` | 自动取 likes 最多的 5 个 active worksheet,支持手动微调 | -| 分类分区 | `sections` | 每个分类选择若干 active worksheet,支持排序 | +| 定时任务卡片 | — | 显示定时任务状态(运行中/已暂停),可切换 | +| 今日推荐 | `featured` | 自动:downloads 最多的 5 个 active worksheet | +| 热门推荐 | `hot` | 自动:likes 最多的 5 个 active worksheet | +| 分类分区 | `sections` | 从已保存配置回显,支持手动添加/移除 worksheet | +### 3.3 数据回显逻辑 -### 3.2 自动刷新与定时任务 +**featured / hot(自动填充,不回显历史配置):** + +每次进入管理页,始终从当前 active worksheets 中实时计算: +- featured = 按 `downloads` 降序取前 5 +- hot = 按 `likes` 降序取前 5 + +用户可在自动填充基础上手动添加/移除,点击「更新首页数据」后生效。 + +**sections(从配置回显):** + +从 `pageConfigFetch` 返回的 `home.sections` 中恢复已保存的分区数据: + +``` +CATEGORY_LIST.map(category => { + saved = savedSectionMap.get(category.id) + items = saved.items → 通过 wsMap 还原为完整 worksheet 对象 + morePath = category.path // 从 CATEGORY_LIST 获取,如 /pages/category/category?id=math +}) +``` + +- `name` / `icon` 始终从本地 `CATEGORY_LIST` 取(保证最新) +- `morePath` 从 `CATEGORY_LIST` 的 `path` 字段获取,指向对应分类页并自动选中该分类 +- `items` 通过 worksheet id 从 active worksheets 中匹配还原,已下架的 worksheet 自动过滤 + +### 3.4 自动刷新与定时任务 今日推荐和热门推荐采用**自动化 + 手动微调**的方式: -- **默认数据**:页面加载时自动查询 active worksheets,按 `downloads desc` 取前 5 填入 featured,按 `likes desc` 取前 5 填入 hot - **定时任务**:云函数 `homeAutoRefresh` 配置定时触发器,每日 22:00 自动执行,查询最新 top5 数据更新 `page-config.json` 中的 `home.featured` 和 `home.hot` - **开关控制**:通过 `settings` 集合中 `homeAutoRefresh` 文档的 `enabled` 字段控制。管理页提供暂停/启动按钮,调用 `homeTimerControl` 云函数切换状态。`homeAutoRefresh` 执行时先检查此标记,`enabled === false` 则跳过 -- **手动微调**:用户可在自动填充的基础上手动添加/移除 worksheet,点击「更新首页数据」手动生成 +- **手动更新**:用户可在管理页手动添加/移除 worksheet,点击「更新首页数据」调用 `pageContentBuild({ page: 'home', ... })` 生成 ``` -管理页 onLoad - → 查询 active worksheets - → 按 downloads desc 取前 5 → featured - → 按 likes desc 取前 5 → hot - → 用户可手动微调 - → 点击「更新首页数据」→ pageContentBuild({ page: 'home', ... }) - 定时任务(每日 22:00) → homeAutoRefresh 云函数 → 检查 settings/homeAutoRefresh.enabled + → enabled === false → 跳过执行 → 查询 top5 downloads → featured → 查询 top5 likes → hot - → 更新 page-config.json 中 home.featured 和 home.hot + → readCurrentConfig() → 只更新 home.featured 和 home.hot → saveConfig() ``` -### 3.3 数据结构 +### 3.5 更新首页数据流程 + +``` +点击「更新首页数据」 + → pageContentBuild({ + page: 'home', + featuredIds: [...], + hotIds: [...], + sections: [ + { id: 'math', subtitle: '', morePath: '/pages/category/category?id=math', worksheetIds: [...] }, + { id: 'english', subtitle: '', morePath: '/pages/category/category?id=english', worksheetIds: [...] }, + ... + ] + }) + → 云函数批量查询所有引用的 worksheet + → 构建 home 配置(categoryTabs + ageBands + featured + hot + sections) + → readCurrentConfig() → config.home = homeData → saveConfig() +``` + +### 3.6 数据结构 ```ts type HomePageData = { @@ -173,41 +277,38 @@ type HomePageData = { hot: HomeDisplayItem[]; sections: HomeDisplaySection[]; }; + +type HomeDisplaySection = { + id: string; // 分类 id + title: string; // 分类名称 + subtitle: string; + morePath: string; // 跳转路径,如 /pages/category/category?id=math + items: HomeDisplayItem[]; +}; ``` -自动生成规则: - - -| 模块 | 默认规则 | +| 模块 | 生成规则 | | -------------- | ---------------------------------------------- | | `categoryTabs` | 由 `CATEGORY_LIST_WITH_ALL` 生成 | | `ageBands` | 由 `AGE_BANDS` 生成 | | `featured` | 自动:downloads 最多的 5 个 active worksheet | | `hot` | 自动:likes 最多的 5 个 active worksheet | -| `sections` | 使用配置中的 worksheetIds,不足时从该分类 active 内容补齐 | - - -### 3.4 相关云函数 - - -| 云函数 | 职责 | -| ------------------- | ------------------------------------------- | -| `pageContentBuild` | 手动更新首页全量数据(featured + hot + sections) | -| `homeAutoRefresh` | 定时任务,每日 22:00 自动更新 featured 和 hot | -| `homeTimerControl` | 暂停/启动定时任务(更新 settings 集合标记位) | - +| `sections` | 管理页配置的 worksheetIds,morePath 来自 CATEGORY_LIST | --- ## 四、分类页内容管理 -### 4.1 管理 UI +### 4.1 管理页入口 -分类页管理 tab 顶部展示分类选择器(来源 `CATEGORY_LIST`),默认选中 `math`。 +`supportPages/categoryContentManage/categoryContentManage` + +### 4.2 管理 UI + +分类页管理顶部展示分类选择器(来源 `CATEGORY_LIST`),默认选中 `math`。 分类头部统计: - | 指标 | 查询规则 | | --- | ----------------------------- | | 总数 | `worksheets.category == 当前分类` | @@ -215,21 +316,29 @@ type HomePageData = { | 线上 | `status == active` | | 已隐藏 | `status == hidden` | - -列表展示当前分类下所有 worksheet(按 `status` → `sortOrder` → `updatedAt` 排序),每项显示预览图、标题、ID、年龄/难度/标签、状态、排序权重。 +列表展示当前分类下所有 worksheet(按 `draft → hidden → active` 排序),每项显示预览图、标题、ID、年龄/难度/标签、状态、操作按钮。 操作按钮: +| 当前状态 | 按钮样式 | 可执行操作 | +| -------- | ------- | --------------- | +| `draft` | 绿色按钮 | 激活上线 | +| `active` | 灰色按钮 | 下架为 hidden | +| `hidden` | 黄色按钮 | 恢复为 draft 或直接激活 | -| 当前状态 | 可执行操作 | -| -------- | --------------- | -| `draft` | 激活上线 | -| `active` | 下架为 hidden | -| `hidden` | 恢复为 draft 或直接激活 | -| 任意 | 调整排序、预览跳转、刷新 | +### 4.3 分类页支持 id 参数 +分类页 `/pages/category/category` 支持通过 URL 参数 `id` 指定默认选中的分类: -### 4.2 数据结构 +``` +/pages/category/category?id=english → 默认选中「英语启蒙」 +/pages/category/category?id=math → 默认选中「数感启蒙」 +/pages/category/category → 默认选中「全部」 +``` + +首页 sections 的「查看更多」链接使用此参数跳转到对应分类。 + +### 4.4 数据结构 ```ts type CategoryPageData = { @@ -245,14 +354,14 @@ type CategoryPageData = { 生成时从 `worksheets` 集合查询 `status == active` 的内容,按分类分组、按 `sortOrder` 排序,转换为 `CategoryItem`。 -### 4.3 更新流程 +### 4.5 更新流程 ``` -分类页管理 tab → 管理 worksheet 状态和排序 +分类页管理 → 管理 worksheet 状态和排序 → 点击「更新分类数据」 → 查询所有 active worksheets,按 category 分组 - → 调用 pageContentUpdate({ page: 'category', data: ... }) - → 云函数更新 page-config.json 中的 category 字段 + → 调用 pageContentBuild({ page: 'category' }) + → 云函数更新 page_configs/current 中的 category 字段 + 同步云存储 ``` --- @@ -264,23 +373,6 @@ type CategoryPageData = { 在 `miniprogram/pages/age/` 下新增 `age.config.ts`,将年龄段划分、能力目标、默认 worksheet 写死在配置中,作为兜底数据和管理页的基础结构。 ```ts -// miniprogram/pages/age/age.config.ts - -import { AGE_BANDS, type AgeBandKey } from '../../core/data/difficulty'; - -export type AbilityItem = { - icon: string; - title: string; - desc: string; -}; - -export type WeekPlan = { - week: number; - theme: string; - worksheetIds: string[]; // worksheet 业务 ID - worksheetTitles: string[]; // 兜底展示标题 -}; - export type AgeBandConfig = { key: AgeBandKey; label: string; @@ -288,61 +380,13 @@ export type AgeBandConfig = { abilities: AbilityItem[]; weeks: WeekPlan[]; }; - -export const AGE_CONFIG: AgeBandConfig[] = [ - { - key: '3-4', - label: '3-4 岁', - subLabel: '启蒙认知', - abilities: [ - { icon: '🔢', title: '数感', desc: '认读 1-5、点数对应' }, - { icon: '✏️', title: '书写', desc: '涂鸦线条、简单描红' }, - { icon: '🧩', title: '思维', desc: '找相同、简单配对' }, - ], - weeks: [ - { week: 1, theme: '数感启蒙', - worksheetIds: [], worksheetTitles: ['找数字涂一涂', '5以内加法', '数数连一连'] }, - { week: 2, theme: '形状与连线', - worksheetIds: [], worksheetTitles: ['识别形状', '线条识别', '数字点连线'] }, - { week: 3, theme: '趣味专注', - worksheetIds: [], worksheetTitles: ['颜色找规律', '方格推理', '格子仿画'] }, - { week: 4, theme: '综合练习', - worksheetIds: [], worksheetTitles: ['10以内加减法', '识字卡', '连连看'] }, - ], - }, - { - key: '4-5', - label: '4-5 岁', - subLabel: '基础练习', - abilities: [ - { icon: '🔢', title: '数感', desc: '10 以内数数、比大小' }, - { icon: '✏️', title: '书写', desc: '笔画模仿、图形描边' }, - { icon: '🧩', title: '思维', desc: '规律排序、图形分类' }, - ], - weeks: [ - { week: 1, theme: '数感启蒙', - worksheetIds: [], worksheetTitles: ['找数字涂一涂', '5以内加法', '数数连一连'] }, - { week: 2, theme: '形状与连线', - worksheetIds: [], worksheetTitles: ['识别形状', '线条识别', '数字点连线'] }, - { week: 3, theme: '趣味专注', - worksheetIds: [], worksheetTitles: ['颜色找规律', '方格推理', '格子仿画'] }, - { week: 4, theme: '综合练习', - worksheetIds: [], worksheetTitles: ['10以内加减法', '识字卡', '连连看'] }, - ], - }, - // 5-6、6-7、7-8 结构相同,abilities 和 weeks 内容不同 - // ... 完整配置见 age.config.ts 文件 -]; ``` > `worksheetIds` 初始为空数组,由分龄管理页选择后填入。`worksheetTitles` 作为兜底展示。 ### 5.2 管理 UI -分龄管理 tab 按年龄段切换:`3-4 岁 | 4-5 岁 | 5-6 岁 | 6-7 岁 | 7-8 岁` - -每个年龄段下的管理内容: - +分龄管理按年龄段切换:`3-4 岁 | 4-5 岁 | 5-6 岁 | 6-7 岁 | 7-8 岁` | 模块 | 操作 | 说明 | | ---- | ------------ | -------------------------------- | @@ -350,59 +394,14 @@ export const AGE_CONFIG: AgeBandConfig[] = [ | 四周路线 | 选择 worksheet | 每周选择 4 个 worksheet,主题来自 config | | 推荐内容 | 选择 worksheet | 动态数据,展示 likes 数量最高的 6 个worksheet | - -worksheet 选择器默认筛选条件: +### 5.3 更新流程 ``` -status == active -ageMin <= 当前年龄段 maxAge -ageMax >= 当前年龄段 minAge -``` - -**核心简化:** 分龄管理页不需要配置能力目标和周主题(这些写死在 `age.config.ts` 中),只需要为不同年龄段、不同周选择对应的 worksheet。 - -### 5.3 数据结构 - -```ts -type AgePageData = { - ageTabs: Array<{ - key: AgeBandKey; - rangeText: string; - subLabel: string; - }>; - bands: Record; - }>; - recommendedItems: Array<{ - id: string; - title: string; - image?: string; - path: string; - }>; - }>; -}; -``` - -### 5.4 更新流程 - -``` -分龄管理 tab → 选择年龄段 - → 为每周选择 worksheet - → 选择推荐内容 +分龄管理 → 选择年龄段 → 为每周选择 worksheet → 选择推荐内容 → 点击「更新分龄数据」 → 合并 age.config.ts 的能力目标 + 管理页选择的 worksheet 详情 - → 调用 pageContentUpdate({ page: 'age', data: ... }) - → 云函数更新 page-config.json 中的 age 字段 + → 调用 pageContentBuild({ page: 'age', data: ... }) + → 云函数更新 page_configs/current 中的 age 字段 + 同步云存储 ``` --- @@ -411,70 +410,75 @@ type AgePageData = { ``` supportPages/debug/debug - ├─ 分类基础数据管理 (已有:同步 CATEGORY_LIST → categories) - ├─ Worksheet 内容池 (已有:状态、分类、上下架、预览) - └─ 页面内容管理 (新增) - ├─ Tab: 首页管理 → 编排 featured / hot / sections - ├─ Tab: 分类页管理 → 管理 worksheet 状态和排序 - └─ Tab: 分龄页管理 → 按年龄段选择周 worksheet + ├─ 分类基础数据管理 (已有:同步 CATEGORY_LIST → categories) + ├─ Worksheet 内容池 (已有:状态、分类、上下架、预览) + └─ 页面内容管理 (新增) + ├─ 📋 分类页内容管理 → supportPages/categoryContentManage + ├─ 🏠 首页内容管理 → supportPages/homeContentManage + └─ 📅 分龄页内容管理 → supportPages/ageContentManage(待开发) ``` --- ## 七、实施计划 -### Phase 1:基础设施 +### Phase 1:基础设施 ✅ -1. 配置 `app.json` 数据预拉取,指向 `content/page-config.json` -2. 实现 `pageContentUpdate` 云函数 -3. 上传初始 `page-config.json`(从现有 `*.data.ts` 转换) -4. 三个页面统一接入预拉取加载逻辑 +1. 实现 `pageContentBuild` 云函数(读写数据库 + 同步云存储) +2. 实现 `pageConfigFetch` 云函数(数据预拉取接口) +3. 实现 `worksheetsQuery` / `worksheetsUpdateStatus` 云函数 -### Phase 2:分类页管理 +### Phase 2:分类页管理 ✅ -1. 实现分类页管理 tab(worksheet 列表、状态管理、排序) -2. 实现「更新分类数据」→ 生成 category 配置 → 调用云函数更新 +1. 实现分类页管理页(worksheet 列表、状态管理) +2. 实现「更新分类数据」→ 生成 category 配置 +3. 分类页支持 `?id=xxx` 参数默认选中分类 -### Phase 3:首页管理 +### Phase 3:首页管理 ✅ -1. 实现首页管理 tab(featured / hot / sections 编排) +1. 实现首页管理页(featured / hot 自动填充 + sections 配置回显) 2. 实现 worksheet 选择器弹窗 -3. 实现「更新首页数据」→ 生成 home 配置 → 调用云函数更新 +3. 实现定时任务(homeAutoRefresh + homeTimerControl) +4. 实现「更新首页数据」→ 生成 home 配置 -### Phase 4:分龄页管理 +### Phase 4:分龄页管理(待开发) 1. 新增 `age.config.ts` 配置文件 -2. 实现分龄管理 tab(年龄段切换、周 worksheet 选择) -3. 实现「更新分龄数据」→ 合并 config + worksheet 详情 → 调用云函数更新 +2. 实现分龄管理页(年龄段切换、周 worksheet 选择) +3. 实现「更新分龄数据」→ 合并 config + worksheet 详情 --- ## 八、风险与约束 - | 风险 | 处理方式 | | ----------------- | ----------------------------------------- | -| 预拉取失败 | 本地 `*.data.ts` / `age.config.ts` 兜底,用户无感知 | -| JSON 生成失败 | 云函数返回错误,管理页提示重试,线上数据不受影响 | -| 首页配置引用了 hidden 内容 | 生成时只允许 active 内容,不足时返回警告 | +| 预拉取失败 | 依次降级:storage 缓存 → pageConfigFetch 云函数兜底 | +| 配置生成失败 | 云函数返回错误,管理页提示重试,线上数据不受影响 | +| 首页配置引用了 hidden 内容 | sections 回显时通过 wsMap 匹配,已下架的自动过滤 | +| 更新某页数据清空其他页 | 云函数从数据库读取完整配置,只覆盖对应字段,其他页面数据不受影响 | | 分龄内容不适龄 | 选择器按年龄段默认过滤 | | 并发更新冲突 | 云函数使用 version 乐观锁,冲突时提示重新加载后重试 | - --- ## 九、关键文件索引 - -| 文件 / 模块 | 职责 | -| ----------------------------------- | ------------------------------ | -| `supportPages/contentManage/` | 三 tab 内容管理页 | -| `cloudfunctions/pageContentBuild/` | 生成 `page-config.json` 中指定页面的配置 | -| `cloudfunctions/homeAutoRefresh/` | 定时任务,每日自动更新首页推荐位数据 | -| `cloudfunctions/homeTimerControl/` | 暂停/启动首页定时刷新任务 | -| `pages/age/age.config.ts` | 分龄页兜底配置(年龄段、能力目标、默认 worksheet) | -| `pages/home/home.data.ts` | 首页兜底数据 | -| `pages/category/category.data.ts` | 分类页兜底数据 | -| `content/page-config.json`(云存储) | 三个页面的统一配置 JSON | - - +| 文件 / 模块 | 职责 | +| -------------------------------------------- | ------------------------------ | +| `supportPages/homeContentManage/` | 首页内容管理页 | +| `supportPages/categoryContentManage/` | 分类页内容管理页 | +| `cloudfunctions/pageContentBuild/` | 生成 page-config 中指定页面的配置 | +| `cloudfunctions/pageConfigFetch/` | 数据预拉取接口,返回完整页面配置 | +| `cloudfunctions/homeAutoRefresh/` | 定时任务,每日自动更新首页推荐位数据 | +| `cloudfunctions/homeTimerControl/` | 暂停/启动首页定时刷新任务 | +| `cloudfunctions/worksheetsQuery/` | 查询 worksheet 列表 | +| `cloudfunctions/worksheetsUpdateStatus/` | 更新 worksheet 状态 | +| `pages/category/category.ts` | 分类页,支持 `?id=xxx` 参数选中分类 | +| `pages/age/age.config.ts` | 分龄页兜底配置 | +| `pages/home/home.data.ts` | 首页兜底数据 | +| `pages/category/category.data.ts` | 分类页兜底数据 | +| `core/data/categories.ts` | CATEGORY_LIST 分类定义(id/name/icon/path) | +| `content/page-config.json`(云存储) | 三个页面的统一配置 JSON | +| `page_configs/current`(云数据库) | 配置数据源(Source of Truth) | +| `settings/homeAutoRefresh`(云数据库) | 定时任务开关标记 | diff --git a/miniprogram/app.json b/miniprogram/app.json index 9a0bb66..3710321 100644 --- a/miniprogram/app.json +++ b/miniprogram/app.json @@ -45,7 +45,11 @@ "preloadRule": { "pages/home/home": { "network": "all", - "packages": ["mathPages", "focusPages", "englishPages"] + "packages": ["mathPages", "focusPages"] + }, + "pages/category/category": { + "network": "all", + "packages": ["focusPages", "englishPages"] }, "pages/profile/profile": { "network": "all", diff --git a/miniprogram/app.ts b/miniprogram/app.ts index b267f41..4562034 100644 --- a/miniprogram/app.ts +++ b/miniprogram/app.ts @@ -1,12 +1,19 @@ import { defaultPrintConfig } from './config/config'; import { generateUUID, getAppUUID, setAppUUID } from './utils/uuid'; +const STORAGE_KEY_PAGE_CONFIG = 'pageConfig'; + +// Pending resolvers for getPageConfig() callers waiting on config +let _configResolvers: Array<(config: PageConfigData | null) => void> = []; + // app.ts App({ globalData: { env: 'release', uuid: '', printConfig: defaultPrintConfig, + pageConfig: undefined, + pageConfigReady: false, }, onLaunch() { wx.cloud.init({ @@ -16,15 +23,8 @@ App({ const accountInfo = wx.getAccountInfoSync(); const env = accountInfo.miniProgram.envVersion || 'release'; - /** - * 设置环境变量 - * 开发环境:develop - * 体验环境:trial - * 正式环境:release - */ this.globalData.env = env; - // 从 localStorage 读取 UUID,如果没有则生成并存储 let uuid = getAppUUID(); if (!uuid) { uuid = generateUUID(); @@ -38,15 +38,13 @@ App({ wx.getStorageSync('printConfig') || defaultPrintConfig; this.globalData.printConfig = printConfig; } + + void this.loadPageConfig(); }, - onShow() { - // 小程序显示时的生命周期 - }, + onShow() {}, - onHide() { - // 小程序隐藏时的生命周期 - }, + onHide() {}, getPrintConfig(): PrintConfig { return this.globalData.printConfig || defaultPrintConfig; @@ -56,4 +54,80 @@ App({ this.globalData.printConfig = config; wx.setStorageSync('printConfig', config); }, + + async loadPageConfig() { + let config: PageConfigData | null = null; + + // L1: 数据预拉取 + try { + const res = await new Promise<{ fetchedData: string }>( + (resolve, reject) => { + wx.getBackgroundFetchData({ + fetchType: 'pre', + success: resolve as any, + fail: reject, + }); + }, + ); + if (res.fetchedData) { + const parsed = JSON.parse(res.fetchedData); + config = parsed?.data || parsed; + } + } catch { + // 预拉取失败,继续降级 + } + + // L2: storage 缓存 + if (!config) { + try { + const cached = wx.getStorageSync(STORAGE_KEY_PAGE_CONFIG); + if (cached && typeof cached === 'object') { + config = cached; + } + } catch { + // storage 读取失败,继续降级 + } + } + + // L3: 云函数兜底 + if (!config) { + try { + const res = (await wx.cloud.callFunction({ + name: 'pageConfigFetch', + })) as { result?: { success?: boolean; data?: any } }; + if (res.result?.success && res.result.data) { + config = res.result.data; + } + } catch { + // 云函数也失败,config 保持 null + } + } + + // 写入 globalData + if (config) { + this.globalData.pageConfig = config; + // 成功获取后写入 storage 缓存 + try { + wx.setStorageSync(STORAGE_KEY_PAGE_CONFIG, config); + } catch { + // storage 写入失败不影响主流程 + } + } + + // 标记就绪,通知等待中的页面 + this.globalData.pageConfigReady = true; + for (const resolve of _configResolvers) { + resolve(config); + } + _configResolvers = []; + }, + + getPageConfig(): Promise { + if (this.globalData.pageConfigReady) { + return Promise.resolve(this.globalData.pageConfig || null); + } + return new Promise((resolve) => { + _configResolvers.push(resolve); + }); + }, }); diff --git a/miniprogram/focusPages/focusDraw/focusDraw.config.ts b/miniprogram/focusPages/focusDraw/focusDraw.config.ts index 3e08fb5..92ebf05 100644 --- a/miniprogram/focusPages/focusDraw/focusDraw.config.ts +++ b/miniprogram/focusPages/focusDraw/focusDraw.config.ts @@ -46,7 +46,7 @@ const FOCUS_DRAW_DEFINITIONS = [ { id: 'color-pattern', title: '颜色找规律', - subtitle: '观察颜色规律,在空白图形中涂色', + subtitle: '观察颜色规律,涂色', ageMin: 4, ageMax: 6, difficulty: 2, @@ -66,7 +66,7 @@ const FOCUS_DRAW_DEFINITIONS = [ { id: 'line-recognition', title: '线条识别', - subtitle: '认识不同线条,画出颜色对应的线条', + subtitle: '认识不同线条,画对应线条', ageMin: 3, ageMax: 5, difficulty: 1, @@ -86,7 +86,7 @@ const FOCUS_DRAW_DEFINITIONS = [ { id: 'code-connect', title: '译码连线', - subtitle: '按数字顺序将数字对应颜色连线', + subtitle: '按数字顺序连线', ageMin: 5, ageMax: 7, difficulty: 3, diff --git a/miniprogram/mathPages/mathDraw/mathDraw.config.ts b/miniprogram/mathPages/mathDraw/mathDraw.config.ts index eec59aa..a745dca 100644 --- a/miniprogram/mathPages/mathDraw/mathDraw.config.ts +++ b/miniprogram/mathPages/mathDraw/mathDraw.config.ts @@ -16,7 +16,8 @@ const MATH_DRAW_DEFINITIONS = [ { id: 'number-find', title: '找数字,涂一涂', - subtitle: '在数字方阵中找出目标数字并涂色', + subtitle: '找出目标数字并涂色', + ageMin: 3, ageMax: 5, difficulty: 1, @@ -26,7 +27,7 @@ const MATH_DRAW_DEFINITIONS = [ { id: 'number-write', title: '看数字,写一写', - subtitle: '按笔画顺序练习书写数字', + subtitle: '按笔画顺序书写数字', ageMin: 3, ageMax: 5, difficulty: 1, @@ -36,7 +37,7 @@ const MATH_DRAW_DEFINITIONS = [ { id: 'number-coloring', title: '按数字,涂颜色', - subtitle: '按指定数字给对应圆圈涂色', + subtitle: '按指定数字涂色', ageMin: 3, ageMax: 5, difficulty: 1, @@ -46,7 +47,7 @@ const MATH_DRAW_DEFINITIONS = [ { id: 'counting-matching', title: '数一数,连一连', - subtitle: '连线配对数字和对应数量图形', + subtitle: '连线配对数字和数量', ageMin: 3, ageMax: 5, difficulty: 1, @@ -56,7 +57,7 @@ const MATH_DRAW_DEFINITIONS = [ { id: 'number-object-match', title: '数物连线', - subtitle: '连线相同数量的物品和数字', + subtitle: '连线数量相同的物体和数字', ageMin: 3, ageMax: 5, difficulty: 1, @@ -66,7 +67,7 @@ const MATH_DRAW_DEFINITIONS = [ { id: 'number-object-fill', title: '数物填写', - subtitle: '数物品数量,填写对应数字', + subtitle: '数出数量,填写对应数字', ageMin: 3, ageMax: 5, difficulty: 1, @@ -76,7 +77,7 @@ const MATH_DRAW_DEFINITIONS = [ { id: 'counting-select', title: '数一数,选一选', - subtitle: '数出物品数量,圈出正确答案', + subtitle: '数出数量,圈出正确答案', ageMin: 3, ageMax: 5, difficulty: 1, @@ -116,7 +117,7 @@ const MATH_DRAW_DEFINITIONS = [ { id: 'missing-number', title: '填上缺少的数字', - subtitle: '在数列中找出并填写缺失数字', + subtitle: '找出并填写缺失数字', ageMin: 4, ageMax: 6, difficulty: 2, @@ -146,7 +147,7 @@ const MATH_DRAW_DEFINITIONS = [ { id: 'one-digit-addition', title: '一位数加法', - subtitle: '通过圆点学习一位数加法运算', + subtitle: '通过圆点学习一位数加法', ageMin: 4, ageMax: 6, difficulty: 2, @@ -156,7 +157,7 @@ const MATH_DRAW_DEFINITIONS = [ { id: 'addition-5', title: '5以内加法', - subtitle: '图形化展示 5 以内加法', + subtitle: '图形化展示5以内加法', ageMin: 3, ageMax: 5, difficulty: 1, @@ -166,7 +167,7 @@ const MATH_DRAW_DEFINITIONS = [ { id: 'addition-10', title: '10以内加法', - subtitle: '图形化展示 10 以内加法', + subtitle: '图形化展示10以内加法', ageMin: 4, ageMax: 6, difficulty: 2, @@ -176,7 +177,7 @@ const MATH_DRAW_DEFINITIONS = [ { id: 'subtraction-10', title: '10以内减法', - subtitle: '图形化展示 10 以内减法', + subtitle: '图形化展示10以内减法', ageMin: 4, ageMax: 6, difficulty: 2, @@ -196,7 +197,7 @@ const MATH_DRAW_DEFINITIONS = [ { id: 'make-ten', title: '凑十法练习', - subtitle: '20 以内进位加法', + subtitle: '20以内进位加法', ageMin: 5, ageMax: 7, difficulty: 3, @@ -206,7 +207,7 @@ const MATH_DRAW_DEFINITIONS = [ { id: 'break-ten', title: '破十法练习', - subtitle: '20 以内退位减法', + subtitle: '20以内退位减法', ageMin: 5, ageMax: 7, difficulty: 3, @@ -216,7 +217,7 @@ const MATH_DRAW_DEFINITIONS = [ { id: 'flat-ten', title: '平十法练习', - subtitle: '20 以内退位减法', + subtitle: '20以内退位减法', ageMin: 5, ageMax: 7, difficulty: 3, @@ -246,7 +247,7 @@ const MATH_DRAW_DEFINITIONS = [ { id: 'practice-subtraction', title: '减法运算', - subtitle: '10/20/50/100 以内减法', + subtitle: '10/20/50/100以内减法', ageMin: 5, ageMax: 8, difficulty: 3, @@ -256,7 +257,7 @@ const MATH_DRAW_DEFINITIONS = [ { id: 'practice-mixed', title: '混合运算', - subtitle: '10/20/50/100 以内加减法混合', + subtitle: '10/20/50/100以内加减法混合', ageMin: 5, ageMax: 8, difficulty: 3, diff --git a/miniprogram/pages/category/category.data.ts b/miniprogram/pages/category/category.data.ts index 9c02cc3..68461f8 100644 --- a/miniprogram/pages/category/category.data.ts +++ b/miniprogram/pages/category/category.data.ts @@ -77,6 +77,7 @@ function item(input: ItemInput): CategoryItem { }; } +/** 目前只作为兜底数据 */ const CATEGORY_ITEMS_BY_ID: Record = { math: [ item({ diff --git a/miniprogram/pages/category/category.less b/miniprogram/pages/category/category.less index b92dc81..fc786ec 100644 --- a/miniprogram/pages/category/category.less +++ b/miniprogram/pages/category/category.less @@ -142,8 +142,8 @@ align-items: center; justify-content: center; overflow: hidden; - border: 1rpx solid #bbb; - background: #f0f0f0; + border: 1rpx solid rgba(50, 46, 37, 0.08); + background: #f5edd8; } .cat-card__thumb-img { @@ -261,4 +261,46 @@ .cat-empty__text { font-size: 28rpx; color: @text-gray; +} + +// ── Skeleton ── +@keyframes skeleton-pulse { + 0% { + opacity: 0.6; + } + + 50% { + opacity: 0.3; + } + + 100% { + opacity: 0.6; + } +} + +.cat-card--skeleton { + pointer-events: none; + + .cat-card__thumb { + background: #e8e2d5; + animation: skeleton-pulse 1.5s ease-in-out infinite; + } + + .cat-card__title, + .cat-card__subtitle { + background: #e8e2d5; + border-radius: 8rpx; + color: transparent; + animation: skeleton-pulse 1.5s ease-in-out infinite; + } + + .cat-card__title { + width: 60%; + height: 32rpx; + } + + .cat-card__subtitle { + width: 80%; + height: 24rpx; + } } \ No newline at end of file diff --git a/miniprogram/pages/category/category.ts b/miniprogram/pages/category/category.ts index 9269959..d80a57f 100644 --- a/miniprogram/pages/category/category.ts +++ b/miniprogram/pages/category/category.ts @@ -7,6 +7,16 @@ type CategoryTab = { icon: string; }; +type CategoryDataset = { + searchPlaceholder: string; + categories: Array<{ + id: string; + name: string; + icon: string; + items: CategoryItem[]; + }>; +}; + const SIDEBAR_TABS: CategoryTab[] = CATEGORY_LIST_WITH_ALL.map( ({ id, name, icon }) => ({ id, @@ -15,25 +25,34 @@ const SIDEBAR_TABS: CategoryTab[] = CATEGORY_LIST_WITH_ALL.map( }), ); +const TAB_BAR_PATHS = new Set([ + '/pages/home/home', + '/pages/category/category', + '/pages/age/age', + '/pages/favorites/favorites', + '/pages/profile/profile', +]); + +// Page-level mutable state (shared across methods, set once on load) +let _categoryData: CategoryDataset | null = null; + function buildAllItems(): CategoryItem[] { + if (!_categoryData) return []; const items: CategoryItem[] = []; - for (const group of CATEGORY_DATA.categories) { + for (const group of _categoryData.categories) { for (const item of group.items) { - items.push({ - ...item, - }); + items.push({ ...item }); } } return items; } function getItemsByCategory(categoryId: string): CategoryItem[] { + if (!_categoryData) return []; if (categoryId === 'all') return buildAllItems(); - const group = CATEGORY_DATA.categories.find((c) => c.id === categoryId); + const group = _categoryData.categories.find((c) => c.id === categoryId); if (!group) return []; - return group.items.map((item) => ({ - ...item, - })); + return group.items.map((item) => ({ ...item })); } function filterByKeyword( @@ -49,33 +68,64 @@ function filterByKeyword( ); } -const TAB_BAR_PATHS = new Set([ - '/pages/home/home', - '/pages/category/category', - '/pages/age/age', - '/pages/favorites/favorites', - '/pages/profile/profile', -]); +// Skeleton placeholder items for loading state +const SKELETON_ITEMS = Array.from({ length: 4 }, (_, i) => ({ + id: `skeleton-${i}`, + title: '\u00A0', + subtitle: '\u00A0', + skeleton: true, +})); Page({ data: { - searchPlaceholder: CATEGORY_DATA.searchPlaceholder, + searchPlaceholder: '搜索练习纸...', searchKeyword: '', sidebarTabs: SIDEBAR_TABS, activeCategoryId: 'all', - displayItems: buildAllItems() as CategoryItem[], + displayItems: SKELETON_ITEMS as any[], scrollIntoViewId: '', + loading: true, }, onLoad(options: Record) { - const id = options.id; - if (id && id !== 'all') { - const items = getItemsByCategory(id); - this.setData({ - activeCategoryId: id, - displayItems: items, - }); + void this.initData(options); + }, + + async initData(options: Record) { + wx.showLoading({ title: '加载中', mask: false }); + + const app = getApp(); + const config = await app.getPageConfig(); + + // Use cloud config if available, otherwise fallback to local static data + if (config?.category?.categories?.length) { + _categoryData = { + searchPlaceholder: + config.category.searchPlaceholder || '搜索练习纸...', + categories: config.category.categories.map((cat) => ({ + id: cat.id, + name: cat.name, + icon: cat.icon, + items: (cat.items || []).map( + (raw) => raw as unknown as CategoryItem, + ), + })), + }; + } else { + _categoryData = CATEGORY_DATA; } + + const activeId = options.id && options.id !== 'all' ? options.id : 'all'; + const items = getItemsByCategory(activeId); + + this.setData({ + loading: false, + searchPlaceholder: _categoryData.searchPlaceholder, + activeCategoryId: activeId, + displayItems: items, + }); + + wx.hideLoading(); }, onTapCategory(e: WechatMiniprogram.TouchEvent) { diff --git a/miniprogram/pages/category/category.wxml b/miniprogram/pages/category/category.wxml index 2c26eee..e1abb94 100644 --- a/miniprogram/pages/category/category.wxml +++ b/miniprogram/pages/category/category.wxml @@ -2,7 +2,7 @@ - + + bindtap="{{item.skeleton ? '' : 'onTapItem'}}"> - {{item.icon}} @@ -76,7 +78,9 @@ {{item.subtitle}} - + {{item.ageBand}} @@ -84,7 +88,9 @@ >{{item.difficultyLabel}} - + 🔍 没有找到匹配的内容 diff --git a/miniprogram/pages/home/home.data.ts b/miniprogram/pages/home/home.data.ts index 79338b6..9d0d3d1 100644 --- a/miniprogram/pages/home/home.data.ts +++ b/miniprogram/pages/home/home.data.ts @@ -12,7 +12,7 @@ export type HomeDisplayItem = { difficulty: '入门' | '基础' | '进阶' | '挑战'; icon: string; /** 横向滑动卡片顶部图,有则优先展示,可与 icon 二选一 */ - img?: string; + previewImg?: string; badge?: string; path: string; available: boolean; @@ -139,7 +139,7 @@ export const HOME_PRODUCT_TARGET_DATA: HomeDisplayDataset = { difficulty: '基础', icon: '➕', badge: '热门', - img: '/assets/entrancePicture/math/addition-5.png', + previewImg: '/assets/entrancePicture/math/addition-5.png', path: '/mathPages/mathDraw/mathDraw?id=addition', available: true, source: '现有加减法页面可承接', @@ -160,7 +160,7 @@ export const HOME_PRODUCT_TARGET_DATA: HomeDisplayDataset = { ageBand: '3-4岁', difficulty: '入门', icon: '🔢', - img: '/assets/entrancePicture/math/number-find.png', + previewImg: '/assets/entrancePicture/math/number-find.png', path: '/mathPages/mathDraw/mathDraw?id=number-find', available: true, source: '现有 找数字/写数字 能力', @@ -173,7 +173,7 @@ export const HOME_PRODUCT_TARGET_DATA: HomeDisplayDataset = { ageBand: '4-5岁', difficulty: '基础', icon: '🧮', - img: '/assets/entrancePicture/math/count-match.png', + previewImg: '/assets/entrancePicture/math/count-match.png', path: '/mathPages/mathDraw/mathDraw?id=counting-matching', available: true, source: '现有 countMatch/countingSelect', @@ -186,7 +186,7 @@ export const HOME_PRODUCT_TARGET_DATA: HomeDisplayDataset = { ageBand: '5-7岁', difficulty: '基础', icon: '➕', - img: '/assets/entrancePicture/math/addition-5.png', + previewImg: '/assets/entrancePicture/math/addition-5.png', path: '/mathPages/mathDraw/mathDraw?id=addition', available: true, source: '现有 addition/calculationPractice', @@ -355,7 +355,7 @@ export const HOME_PRODUCT_TARGET_DATA: HomeDisplayDataset = { ageBand: '4-6岁', difficulty: '基础', icon: '🧵', - img: '/assets/entrancePicture/focus/dot-connect.png', + previewImg: '/assets/entrancePicture/focus/dot-connect.png', path: '/focusPages/focusDraw/focusDraw?id=dot-connect', available: true, source: '现有 dotConnect/matchConnect', @@ -435,7 +435,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = { difficulty: '基础', icon: '➕', badge: 'HOT', - img: '/assets/entrancePicture/math/addition-5.png', + previewImg: '/assets/entrancePicture/math/addition-5.png', path: '/mathPages/mathDraw/mathDraw?id=addition', available: true, source: '现有功能清单 addition', @@ -464,7 +464,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = { difficulty: '基础', icon: '🔗', badge: '热门', - img: '/assets/entrancePicture/focus/dot-connect.png', + previewImg: '/assets/entrancePicture/focus/dot-connect.png', path: '/focusPages/focusDraw/focusDraw?id=dot-connect', available: true, source: '现有功能清单 dot-connect', @@ -480,7 +480,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = { difficulty: '入门', icon: '🔢', badge: '热门', - img: '/assets/entrancePicture/math/number-find.png', + previewImg: '/assets/entrancePicture/math/number-find.png', path: '/mathPages/mathDraw/mathDraw?id=number-find', available: true, source: 'number-find', @@ -494,7 +494,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = { difficulty: '基础', icon: '🧮', badge: '热门', - img: '/assets/entrancePicture/math/count-match.png', + previewImg: '/assets/entrancePicture/math/count-match.png', path: '/mathPages/mathDraw/mathDraw?id=counting-matching', available: true, source: 'counting-matching', @@ -508,7 +508,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = { difficulty: '进阶', icon: '🟨', badge: '热门', - img: '/assets/entrancePicture/focus/grid-drawing-5x5.png', + previewImg: '/assets/entrancePicture/focus/grid-drawing-5x5.png', path: '/focusPages/focusDraw/focusDraw?id=grid-drawing', available: true, source: 'grid-drawing-5x5', @@ -529,7 +529,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = { ageBand: '3-4岁', difficulty: '入门', icon: '🔢', - img: '/assets/entrancePicture/math/number-find.png', + previewImg: '/assets/entrancePicture/math/number-find.png', path: '/mathPages/mathDraw/mathDraw?id=number-find', available: true, source: 'number-find / number-write', @@ -542,7 +542,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = { ageBand: '4-5岁', difficulty: '基础', icon: '🧮', - img: '/assets/entrancePicture/math/count-match.png', + previewImg: '/assets/entrancePicture/math/count-match.png', path: '/mathPages/mathDraw/mathDraw?id=counting-matching', available: true, source: 'countMatch / countingSelect', @@ -555,7 +555,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = { ageBand: '4-5岁', difficulty: '基础', icon: '📈', - img: '/assets/entrancePicture/math/number-sort.png', + previewImg: '/assets/entrancePicture/math/number-sort.png', path: '/mathPages/mathDraw/mathDraw?id=number-sort', available: true, source: 'numberSort / missingNumber', @@ -568,7 +568,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = { ageBand: '5-7岁', difficulty: '基础', icon: '➕', - img: '/assets/entrancePicture/math/addition-5.png', + previewImg: '/assets/entrancePicture/math/addition-5.png', path: '/mathPages/mathDraw/mathDraw?id=addition', available: true, source: 'addition / calculationPractice', @@ -581,7 +581,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = { ageBand: '7-8岁', difficulty: '挑战', icon: '✖️', - img: '/assets/entrancePicture/math/multiplication-table.png', + previewImg: '/assets/entrancePicture/math/multiplication-table.png', path: '/mathPages/mathDraw/mathDraw?id=multiplication-table', available: true, source: 'multiplicationTable', @@ -646,7 +646,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = { ageBand: '4-6岁', difficulty: '基础', icon: '🔗', - img: '/assets/entrancePicture/focus/dot-connect.png', + previewImg: '/assets/entrancePicture/focus/dot-connect.png', path: '/focusPages/focusDraw/focusDraw?id=dot-connect', available: true, source: 'dot-connect', @@ -659,7 +659,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = { ageBand: '4-6岁', difficulty: '基础', icon: '🧵', - img: '/assets/entrancePicture/focus/match-connect.png', + previewImg: '/assets/entrancePicture/focus/match-connect.png', path: '/focusPages/focusDraw/focusDraw?id=match-connect', available: true, source: 'match-connect', @@ -672,7 +672,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = { ageBand: '4-6岁', difficulty: '基础', icon: '🌈', - img: '/assets/entrancePicture/focus/color-pattern.png', + previewImg: '/assets/entrancePicture/focus/color-pattern.png', path: '/focusPages/focusDraw/focusDraw?id=color-pattern', available: true, source: 'color-pattern', @@ -685,7 +685,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = { ageBand: '5-7岁', difficulty: '进阶', icon: '🧠', - img: '/assets/entrancePicture/focus/grid-reasoning.png', + previewImg: '/assets/entrancePicture/focus/grid-reasoning.png', path: '/focusPages/focusDraw/focusDraw?id=grid-reasoning', available: true, source: 'grid-reasoning', @@ -698,7 +698,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = { ageBand: '4-7岁', difficulty: '进阶', icon: '🟨', - img: '/assets/entrancePicture/focus/grid-drawing-5x5.png', + previewImg: '/assets/entrancePicture/focus/grid-drawing-5x5.png', path: '/focusPages/focusDraw/focusDraw?id=grid-drawing', available: true, source: 'grid-drawing', @@ -719,7 +719,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = { ageBand: '4-7岁', difficulty: '入门', icon: '', - img: '/assets/entrancePicture/english/letter-tracing-single.jpg', + previewImg: '/assets/entrancePicture/english/letter-tracing-single.jpg', path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-single', available: true, source: 'letter-tracing-single', @@ -732,7 +732,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = { ageBand: '4-7岁', difficulty: '入门', icon: '', - img: '/assets/entrancePicture/english/letter-tracing-upper-lower.jpg', + previewImg: '/assets/entrancePicture/english/letter-tracing-upper-lower.jpg', path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-upper-lower', available: true, source: 'letter-tracing-upper-lower', @@ -745,7 +745,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = { ageBand: '4-7岁', difficulty: '入门', icon: '', - img: '/assets/entrancePicture/english/letter-tracing-case-pairing.jpg', + previewImg: '/assets/entrancePicture/english/letter-tracing-case-pairing.jpg', path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-case-pairing', available: true, source: 'letter-tracing-case-pairing', @@ -758,7 +758,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = { ageBand: '4-7岁', difficulty: '入门', icon: '', - img: '/assets/entrancePicture/english/letter-tracing-two-column.jpg', + previewImg: '/assets/entrancePicture/english/letter-tracing-two-column.jpg', path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-two-column', available: true, source: 'letter-tracing-two-column', @@ -771,7 +771,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = { ageBand: '4-7岁', difficulty: '入门', icon: '', - img: '/assets/entrancePicture/english/letter-tracing-half.jpg', + previewImg: '/assets/entrancePicture/english/letter-tracing-half.jpg', path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-half', available: true, source: 'letter-tracing-half', @@ -784,7 +784,7 @@ export const HOME_CURRENT_CAPABILITY_DATA: HomeDisplayDataset = { ageBand: '4-7岁', difficulty: '入门', icon: '', - img: '/assets/entrancePicture/english/letter-tracing-three.jpg', + previewImg: '/assets/entrancePicture/english/letter-tracing-three.jpg', path: '/englishPages/letterTracing/letterTracing?id=letter-tracing-three', available: true, source: 'letter-tracing-three', diff --git a/miniprogram/pages/home/home.less b/miniprogram/pages/home/home.less index bca9abe..66ad936 100644 --- a/miniprogram/pages/home/home.less +++ b/miniprogram/pages/home/home.less @@ -83,6 +83,15 @@ border-top-left-radius: 32rpx; border-top-right-radius: 32rpx; overflow: hidden; + border-bottom: 1rpx solid rgba(50, 46, 37, 0.08); + background: #f5edd8; +} + +.home-hero-card__cover { + width: 100%; + height: 100%; + display: block; + object-fit: cover; } .home-hero-card__icon { @@ -216,6 +225,8 @@ display: flex; align-items: flex-start; justify-content: center; + border-bottom: 1rpx solid rgba(50, 46, 37, 0.08); + background: #f5edd8; } .home-track-card__cover { @@ -242,7 +253,6 @@ padding: 20rpx @page-padding-inner-x 22rpx; background: #ffffff; white-space: normal; - border-top: 1rpx solid #c5c5c5; } .home-track-card__title { @@ -264,7 +274,7 @@ box-sizing: border-box; width: 100%; margin-top: 8rpx; - font-size: 22rpx; + font-size: 20rpx; color: @text-gray; line-height: 1.45; display: -webkit-box; @@ -350,12 +360,22 @@ .home-hot-card__thumb { width: 144rpx; - height: 144rpx; - border-radius: 24rpx; + height: 180rpx; + border-radius: 18rpx; display: flex; align-items: center; justify-content: center; flex-shrink: 0; + overflow: hidden; + border: 1rpx solid rgba(50, 46, 37, 0.08); + background: #f5edd8; +} + +.home-hot-card__thumb-img { + width: 100%; + height: 100%; + display: block; + object-fit: cover; } .home-hot-card__thumb-emoji { @@ -367,6 +387,9 @@ flex-direction: column; min-width: 0; flex: 1; + justify-content: space-between; + align-self: stretch; + padding: 6rpx 0; } .home-hot-card__title { @@ -438,4 +461,71 @@ background: @brand; color: @text-selected-btn; font-weight: 600; +} + +// ── Skeleton ── +@keyframes home-skeleton-pulse { + 0% { + opacity: 0.6; + } + + 50% { + opacity: 0.3; + } + + 100% { + opacity: 0.6; + } +} + +.home-hero-card--skeleton { + pointer-events: none; + + .home-hero-card__top { + animation: home-skeleton-pulse 1.5s ease-in-out infinite; + } + + .home-hero-card__title, + .home-hero-card__sub { + background: #e8e2d5; + border-radius: 8rpx; + color: transparent; + animation: home-skeleton-pulse 1.5s ease-in-out infinite; + } + + .home-hero-card__title { + width: 50%; + height: 36rpx; + } + + .home-hero-card__sub { + width: 70%; + height: 26rpx; + } +} + +.home-hot-card--skeleton { + pointer-events: none; + + .home-hot-card__thumb { + animation: home-skeleton-pulse 1.5s ease-in-out infinite; + } + + .home-hot-card__title, + .home-hot-card__desc { + background: #e8e2d5; + border-radius: 8rpx; + color: transparent; + animation: home-skeleton-pulse 1.5s ease-in-out infinite; + } + + .home-hot-card__title { + width: 40%; + height: 32rpx; + } + + .home-hot-card__desc { + width: 65%; + height: 24rpx; + } } \ No newline at end of file diff --git a/miniprogram/pages/home/home.ts b/miniprogram/pages/home/home.ts index 06e7b12..0c4e41b 100644 --- a/miniprogram/pages/home/home.ts +++ b/miniprogram/pages/home/home.ts @@ -3,6 +3,7 @@ import { HOME_CURRENT_CAPABILITY_DATA, type HomeDisplayItem, type HomeDisplaySection, + type HomeDisplayDataset, } from './home.data'; type AgeBandKey = string; @@ -21,7 +22,7 @@ type HomeAgeBandView = { }; type HomeDisplayItemView = HomeDisplayItem & { - topBg: string; + skeleton?: boolean; }; type HomeDisplaySectionView = HomeDisplaySection & { @@ -31,14 +32,6 @@ type HomeDisplaySectionView = HomeDisplaySection & { const AGE_BAND_ICONS = ['🌱', '🚀', '🎓', '🧠', '🏆'] as const; -const CATEGORY_TOP_BG: Record = { - math: '#fff3c4', - chinese: '#d8f0d0', - english: '#e8f4ff', - puzzle: '#f5e6dc', - craft: '#fce4ec', -}; - const SECTION_ANCHOR_PREFIX = 'section-'; const TAB_BAR_PATHS = new Set([ '/pages/home/home', @@ -50,7 +43,6 @@ const TAB_BAR_PATHS = new Set([ function toItemView(item: HomeDisplayItem): HomeDisplayItemView { return { ...item, - topBg: CATEGORY_TOP_BG[item.category], }; } @@ -64,12 +56,7 @@ function sortSectionsByTabs( }, {}); return [...sections] - .sort((a, b) => { - const aOrder = orderMap[a.id]; - const bOrder = orderMap[b.id]; - // 注意:0 需要保留(不能用 || 进行兜底) - return (aOrder ?? 999) - (bOrder ?? 999); - }) + .sort((a, b) => (orderMap[a.id] ?? 999) - (orderMap[b.id] ?? 999)) .map((section) => ({ ...section, anchorId: `${SECTION_ANCHOR_PREFIX}${section.id}`, @@ -78,6 +65,7 @@ function sortSectionsByTabs( } function navigateByPath(path?: string, fallbackTitle?: string) { + console.log('path-----:', path); if (!path) { wx.showToast({ title: fallbackTitle ? `${fallbackTitle} 即将上线` : '即将上线', @@ -94,31 +82,109 @@ function navigateByPath(path?: string, fallbackTitle?: string) { wx.navigateTo({ url: path }); } +// ── Skeleton data ── + +function skeletonItem(id: string): HomeDisplayItemView { + return { + id, + title: '\u00A0', + subtitle: '\u00A0', + category: 'math' as any, + ageBand: '', + difficulty: '基础', + icon: '', + path: '', + available: false, + skeleton: true, + }; +} + +const SKELETON_FEATURED = Array.from({ length: 3 }, (_, i) => + skeletonItem(`skeleton-featured-${i}`), +); +const SKELETON_HOT = Array.from({ length: 3 }, (_, i) => + skeletonItem(`skeleton-hot-${i}`), +); + const win = wx.getWindowInfo(); -const HOME_DATA = HOME_CURRENT_CAPABILITY_DATA; -const HOME_TABS: HomeTab[] = HOME_DATA.categoryTabs.map((item) => ({ - id: item.id, - name: item.name, -})); -const HOME_SECTIONS = sortSectionsByTabs(HOME_TABS, HOME_DATA.sections); -console.log('HOME_DATA', HOME_DATA); -console.log('HOME_SECTIONS', HOME_SECTIONS); + +// Page-level mutable state +let _homeData: HomeDisplayDataset | null = null; + +function applyHomeData(data: HomeDisplayDataset) { + _homeData = data; + const tabs: HomeTab[] = data.categoryTabs.map((item) => ({ + id: item.id, + name: item.name, + })); + const sections = sortSectionsByTabs(tabs, data.sections); + const ageBands: HomeAgeBandView[] = data.ageBands.map((item, idx) => ({ + ...item, + icon: AGE_BAND_ICONS[idx] || '🌟', + })); + + return { tabs, sections, ageBands }; +} Page({ data: { - searchPlaceholder: HOME_DATA.searchPlaceholder, - tabs: HOME_TABS, + searchPlaceholder: '搜索你喜欢的练习册...', + tabs: [] as HomeTab[], activeTab: 'all', - ageBands: HOME_DATA.ageBands.map((item, idx) => ({ - ...item, - icon: AGE_BAND_ICONS[idx] || '🌟', - })) as HomeAgeBandView[], - activeAgeBand: HOME_DATA.ageBands[0]?.key || ('3-4' as AgeBandKey), - featuredItems: HOME_DATA.featured.map(toItemView), - hotRecommends: HOME_DATA.hot.map(toItemView), - sections: HOME_SECTIONS, + ageBands: [] as HomeAgeBandView[], + activeAgeBand: '3-4' as AgeBandKey, + featuredItems: SKELETON_FEATURED, + hotRecommends: SKELETON_HOT, + sections: [] as HomeDisplaySectionView[], + loading: true, }, + onLoad() { + void this.initData(); + }, + + async initData() { + wx.showLoading({ title: '加载中', mask: false }); + + const app = getApp(); + const config = await app.getPageConfig(); + + let homeData: HomeDisplayDataset; + + if (config?.home) { + // Cloud config available — map to HomeDisplayDataset + const raw = config.home as Record; + homeData = { + searchPlaceholder: + raw.searchPlaceholder || '搜索你喜欢的练习册...', + categoryTabs: raw.categoryTabs || [], + ageBands: raw.ageBands || [], + featured: (raw.featured || []) as HomeDisplayItem[], + hot: (raw.hot || []) as HomeDisplayItem[], + sections: (raw.sections || []) as HomeDisplaySection[], + }; + } else { + homeData = HOME_CURRENT_CAPABILITY_DATA; + } + + const { tabs, sections, ageBands } = applyHomeData(homeData); + + this.setData({ + loading: false, + searchPlaceholder: homeData.searchPlaceholder, + tabs, + ageBands, + activeAgeBand: homeData.ageBands[0]?.key || '3-4', + featuredItems: homeData.featured.map(toItemView), + hotRecommends: homeData.hot.slice(0, 3).map(toItemView), + sections, + }); + + wx.hideLoading(); + }, + + // ── Tab & Navigation ── + onTabChange(e: WechatMiniprogram.CustomEvent) { const id = e.detail.id as string | undefined; if (!id) return; @@ -126,10 +192,7 @@ Page({ this.setData({ activeTab: id }); if (id === 'all') { - wx.pageScrollTo({ - scrollTop: 0, - duration: 400, - }); + wx.pageScrollTo({ scrollTop: 0, duration: 400 }); return; } diff --git a/miniprogram/pages/home/home.wxml b/miniprogram/pages/home/home.wxml index 90be3ed..77fceff 100644 --- a/miniprogram/pages/home/home.wxml +++ b/miniprogram/pages/home/home.wxml @@ -27,14 +27,21 @@ - - {{item.icon}} + bindtap="{{item.skeleton ? '' : 'onTapFeatured'}}"> + + + {{item.icon}} {{item.title}} - {{item.ageBand}} · {{item.difficulty}} @@ -98,14 +107,19 @@ - - + + + {{item.icon}} @@ -114,7 +128,9 @@ {{item.subtitle}} - + {{item.ageBand}} @@ -123,7 +139,11 @@ > - + @@ -157,13 +177,11 @@ data-title="{{item.title}}" data-path="{{item.path}}" bindtap="onTapTrackCard"> - + {{item.icon}} /// +interface PageConfigData { + home?: Record; + category?: { + searchPlaceholder?: string; + categories?: Array<{ + id: string; + name: string; + icon: string; + items: Array>; + }>; + }; + age?: Record; + version?: number; + updatedAt?: string; +} + interface IAppOption { globalData: { uuid: string; userInfo?: WechatMiniprogram.UserInfo; env: string; printConfig?: PrintConfig; + pageConfig?: PageConfigData; + pageConfigReady?: boolean; }; userInfoReadyCallback?: WechatMiniprogram.GetUserInfoSuccessCallback; + getPageConfig(): Promise; [key: string]: any; }