From c732111e12e764c4d0f607ab27fa575eae63a00b Mon Sep 17 00:00:00 2001 From: R524809 Date: Thu, 7 May 2026 16:32:49 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=88=91=E7=9A=84=E3=80=81=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E3=80=81=E6=89=93=E5=8D=B0=E6=8C=87=E5=8D=97=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E5=BC=80=E5=8F=91=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cloudfunctions/pageContentBuild/index.js | 9 +- cloudfunctions/userDownloadLogs/index.js | 8 + cloudfunctions/userFavorites/index.js | 8 + cloudfunctions/userLogin/index.js | 31 ++- cloudfunctions/userSettings/index.js | 51 ++++ cloudfunctions/userSettings/package.json | 8 + docs/页面内容管理方案.md | 242 +++++++++-------- miniprogram/app.json | 1 + miniprogram/app.ts | 11 +- miniprogram/assets/fonts/demo_index.html | 27 +- miniprogram/assets/fonts/iconfont.js | 2 +- miniprogram/assets/fonts/iconfont.json | 7 + miniprogram/assets/fonts/iconfont.less | 8 +- miniprogram/assets/fonts/iconfont.ttf | Bin 14292 -> 14664 bytes miniprogram/assets/fonts/iconfont.woff | Bin 9240 -> 9484 bytes miniprogram/assets/fonts/iconfontV3.less | 239 ----------------- miniprogram/assets/fonts/iconfontV4.less | 243 +++++++++++++++++ miniprogram/assets/fonts/iconfontv1.less | 239 ----------------- miniprogram/assets/imgs/doodle-head.png | Bin 0 -> 28981 bytes miniprogram/pages/category/category.data.ts | 2 + miniprogram/pages/category/category.ts | 126 +++++++-- miniprogram/pages/favorites/favorites.less | 4 +- miniprogram/pages/favorites/favorites.wxml | 23 +- miniprogram/pages/home/home.wxml | 7 +- miniprogram/pages/profile/profile.json | 4 +- miniprogram/pages/profile/profile.less | 148 +++++++++- miniprogram/pages/profile/profile.ts | 151 ++++++++++- miniprogram/pages/profile/profile.wxml | 115 ++++++-- .../categoryContentManage.ts | 44 +-- .../categoryContentManage.wxml | 3 +- miniprogram/supportPages/guide/guide.json | 9 +- miniprogram/supportPages/guide/guide.less | 252 ++++++++++++++---- miniprogram/supportPages/guide/guide.ts | 72 ++++- miniprogram/supportPages/guide/guide.wxml | 165 ++++++++---- .../supportPages/settings/settings.json | 10 + .../supportPages/settings/settings.less | 59 ++++ miniprogram/supportPages/settings/settings.ts | 74 +++++ .../supportPages/settings/settings.wxml | 16 ++ miniprogram/utils/auth.ts | 66 ++++- miniprogram/utils/downloadLogs.ts | 18 ++ miniprogram/utils/favorites.ts | 18 ++ project.private.config.json | 18 +- typings/index.d.ts | 1 + 43 files changed, 1702 insertions(+), 837 deletions(-) create mode 100644 cloudfunctions/userSettings/index.js create mode 100644 cloudfunctions/userSettings/package.json delete mode 100644 miniprogram/assets/fonts/iconfontV3.less create mode 100644 miniprogram/assets/fonts/iconfontV4.less delete mode 100644 miniprogram/assets/fonts/iconfontv1.less create mode 100644 miniprogram/assets/imgs/doodle-head.png create mode 100644 miniprogram/supportPages/settings/settings.json create mode 100644 miniprogram/supportPages/settings/settings.less create mode 100644 miniprogram/supportPages/settings/settings.ts create mode 100644 miniprogram/supportPages/settings/settings.wxml diff --git a/cloudfunctions/pageContentBuild/index.js b/cloudfunctions/pageContentBuild/index.js index ee62e90..cc8327d 100644 --- a/cloudfunctions/pageContentBuild/index.js +++ b/cloudfunctions/pageContentBuild/index.js @@ -287,6 +287,10 @@ exports.main = async (event) => { if (page === 'home') { const { homeData } = await buildHomeData(db, homeEvent); config.home = homeData; + // [v3.0] 清空 category/age 字段,分类页已改为实时接口查询 + // 如需恢复静态配置,注释掉以下两行即可 + config.category = null; + config.age = null; config.version = (config.version || 0) + 1; config.updatedAt = new Date().toISOString(); @@ -307,7 +311,10 @@ exports.main = async (event) => { if (page === 'all') { const { categoryData, stats } = await buildCategoryData(db); const { homeData } = await buildHomeData(db, homeEvent); - config.category = categoryData; + // [v3.0] 清空 category/age 字段,分类页已改为实时接口查询 + // 如需恢复静态配置,将下面 null 改回 categoryData 即可 + config.category = null; + config.age = null; config.home = homeData; config.version = (config.version || 0) + 1; config.updatedAt = new Date().toISOString(); diff --git a/cloudfunctions/userDownloadLogs/index.js b/cloudfunctions/userDownloadLogs/index.js index 6586b36..e93a273 100644 --- a/cloudfunctions/userDownloadLogs/index.js +++ b/cloudfunctions/userDownloadLogs/index.js @@ -27,6 +27,8 @@ exports.main = async (event) => { return await handleAdd(userId, event); case 'list': return await handleList(userId, event); + case 'count': + return await handleCount(userId); default: return { code: -1, message: '未知操作' }; } @@ -96,3 +98,9 @@ async function handleList(userId, event) { return { code: 0, data: result }; } + +async function handleCount(userId) { + const collection = db.collection('download_logs'); + const res = await collection.where({ userId }).count(); + return { code: 0, data: { count: res.total || 0 } }; +} diff --git a/cloudfunctions/userFavorites/index.js b/cloudfunctions/userFavorites/index.js index 451f54f..0adcfb4 100644 --- a/cloudfunctions/userFavorites/index.js +++ b/cloudfunctions/userFavorites/index.js @@ -29,6 +29,8 @@ exports.main = async (event) => { return await handleRemove(userId, event); case 'list': return await handleList(userId, event); + case 'count': + return await handleCount(userId); case 'check': return await handleCheck(userId, event); case 'batchCheck': @@ -153,6 +155,12 @@ async function handleList(userId, event) { return { code: 0, data: result }; } +async function handleCount(userId) { + const collection = db.collection('favorites'); + const res = await collection.where({ userId }).count(); + return { code: 0, data: { count: res.total || 0 } }; +} + async function handleCheck(userId, event) { const { worksheetId } = event; if (!worksheetId) return { code: -1, message: 'worksheetId 不能为空' }; diff --git a/cloudfunctions/userLogin/index.js b/cloudfunctions/userLogin/index.js index d887c18..05b573c 100644 --- a/cloudfunctions/userLogin/index.js +++ b/cloudfunctions/userLogin/index.js @@ -18,6 +18,20 @@ exports.main = async (event, context) => { } }; +function serializeUser(user) { + return { + ...user, + createdAt: + user.createdAt instanceof Date + ? user.createdAt + : user.createdAt?.toDate?.() || user.createdAt || null, + lastActiveAt: + user.lastActiveAt instanceof Date + ? user.lastActiveAt + : user.lastActiveAt?.toDate?.() || user.lastActiveAt || null, + }; +} + async function handleSilentLogin(openid, unionid) { const usersCollection = db.collection('users'); @@ -32,10 +46,13 @@ async function handleSilentLogin(openid, unionid) { updateData.unionid = unionid; } await usersCollection.doc(user._id).update({ data: updateData }); + const latest = ( + await usersCollection.doc(user._id).get() + ).data; return { code: 0, - data: { ...user, ...updateData, lastActiveAt: new Date() }, + data: serializeUser(latest), }; } else { const newUser = { @@ -48,15 +65,11 @@ async function handleSilentLogin(openid, unionid) { lastActiveAt: now, }; const { _id } = await usersCollection.add({ data: newUser }); + const latest = (await usersCollection.doc(_id).get()).data; return { code: 0, - data: { - _id, - ...newUser, - createdAt: new Date(), - lastActiveAt: new Date(), - }, + data: serializeUser(latest), }; } } @@ -74,11 +87,13 @@ async function handleUpdateProfile(event, openid) { const updateData = {}; if (nickName !== undefined) updateData.nickName = nickName; if (avatarUrl !== undefined) updateData.avatarUrl = avatarUrl; + updateData.lastActiveAt = db.serverDate(); await usersCollection.doc(data[0]._id).update({ data: updateData }); + const latest = (await usersCollection.doc(data[0]._id).get()).data; return { code: 0, - data: { ...data[0], ...updateData }, + data: serializeUser(latest), }; } diff --git a/cloudfunctions/userSettings/index.js b/cloudfunctions/userSettings/index.js new file mode 100644 index 0000000..7fb9ab9 --- /dev/null +++ b/cloudfunctions/userSettings/index.js @@ -0,0 +1,51 @@ +const cloud = require('wx-server-sdk'); +cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); + +const db = cloud.database(); + +exports.main = async (event) => { + const wxContext = cloud.getWXContext(); + const openid = wxContext.OPENID; + const { action } = event; + + switch (action) { + case 'clearProfile': + return await handleClearProfile(openid); + case 'deleteAccount': + return await handleDeleteAccount(openid); + default: + return { code: -1, message: '未知操作' }; + } +}; + +async function handleClearProfile(openid) { + const usersCollection = db.collection('users'); + const { data } = await usersCollection.where({ openid }).limit(1).get(); + + if (data.length === 0) { + return { code: -1, message: '用户不存在' }; + } + + await usersCollection.doc(data[0]._id).update({ + data: { + nickName: null, + avatarUrl: null, + lastActiveAt: db.serverDate(), + }, + }); + + return { code: 0, message: '已清除头像和昵称' }; +} + +async function handleDeleteAccount(openid) { + const usersCollection = db.collection('users'); + const { data } = await usersCollection.where({ openid }).limit(1).get(); + + if (data.length === 0) { + return { code: -1, message: '用户不存在' }; + } + + await usersCollection.doc(data[0]._id).remove(); + + return { code: 0, message: '账户已注销' }; +} diff --git a/cloudfunctions/userSettings/package.json b/cloudfunctions/userSettings/package.json new file mode 100644 index 0000000..59f5e5c --- /dev/null +++ b/cloudfunctions/userSettings/package.json @@ -0,0 +1,8 @@ +{ + "name": "user-settings", + "version": "1.0.0", + "main": "index.js", + "dependencies": { + "wx-server-sdk": "^3.0.4" + } +} diff --git a/docs/页面内容管理方案.md b/docs/页面内容管理方案.md index e8fcd1e..c1ee6cb 100644 --- a/docs/页面内容管理方案.md +++ b/docs/页面内容管理方案.md @@ -1,6 +1,6 @@ # 页面内容管理方案 -> 版本:v2.0 | 最后更新:2026-04-30 +> 版本:v3.0 | 最后更新:2026-05-07 > 配套文档:[Worksheet 发布方案](./Worksheet发布方案.md) | [小程序云开发方案](./小程序云开发方案.md) --- @@ -15,7 +15,11 @@ worksheet 已可通过 Debug 发布进入云数据库,但三个展示页仍使 | 分类页 | `pages/category/category.data.ts` | 新 worksheet 发布后不会自动出现 | | 分龄页 | `pages/age/age.ts` 内 Mock | 推荐内容和周路线无法运营配置 | -**目标:** 通过 Debug 管理页配置三个页面的内容,生成统一配置写入云数据库并同步至云存储,小程序启动时通过**数据预拉取**获取并渲染。 +**目标:** + +- **首页**:通过 Debug 管理页配置内容,生成配置写入云数据库并同步至云存储,小程序启动时通过数据预拉取获取并渲染 +- **分类页**:每次进入时通过云函数实时查询所有已上线 worksheet,前端分组排序展示(v3.0 调整,因需展示收藏/下载等实时数据) +- **分龄页**:独立方案(详见分龄页文档) --- @@ -23,18 +27,24 @@ worksheet 已可通过 Debug 发布进入云数据库,但三个展示页仍使 ### 2.1 数据存储与同步 -**数据源(Source of Truth):** 云数据库 `page_configs` 集合,文档 `_id = 'current'` +**首页数据:** 通过 `page-config.json` 预拉取方式加载(保持不变) -**数据交付(Delivery):** 云存储 `content/page-config.json`(供小程序预拉取使用) +**分类页数据:** 通过云函数接口实时查询(v3.0 调整) -每次更新时,云函数先写数据库,再同步上传至云存储,保证两者一致。各管理页更新时只修改配置中自己负责的字段,不影响其他页面数据。 +分类页需要展示收藏数和下载数等实时数据,不再适合通过定期更新 `page-config.json` 的方式同步。改为每次进入页面时通过云函数实时查询所有已上线 worksheet,前端本地排序和分类展示。 + +**数据源(Source of Truth):** 云数据库 `page_configs` 集合,文档 `_id = 'current'`(仅存储 home 配置) + +**数据交付(Delivery):** 云存储 `content/page-config.json`(仅包含 home 数据,供小程序预拉取使用) + +每次首页更新时,云函数先写数据库,再同步上传至云存储。 ```ts // page_configs/current 文档结构 type PageConfig = { home: HomePageData; // 首页配置 - category: CategoryPageData; // 分类页配置 - age: AgePageData; // 分龄页配置 + // category: 已移除,改为实时接口查询 + // age: 已移除,改为独立方案 version: number; // 版本号,每次更新递增 updatedAt: string; // 最后更新时间 }; @@ -44,7 +54,7 @@ type PageConfig = { ``` readCurrentConfig() → 从 page_configs/current 读取当前完整配置 -config[page] = data → 只覆盖对应页面字段(home / category / age) +config.home = data → 只覆盖首页字段 saveConfig(config) → 写回数据库 → 上传云存储 content/page-config.json ``` @@ -60,16 +70,18 @@ saveConfig(config) → 写回数据库 → 上传云存储 content/page-config │ │ Manage │ │ Manage │ │ Manage │ │ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │ │ │ │ │ -│ └────────┬─────────┘─────────┬─────────┘ │ -│ ▼ ▼ │ -│ 点击「更新」按钮 → 调用云函数 pageContentBuild │ -│ │ │ -│ ▼ │ +│ ▼ ▼ ▼ │ +│ 点击「更新首页」→ 仅管理上下线状态 待开发 │ +│ 调用 pageContentBuild (不再生成分类页配置) │ +│ 只更新 home 配置 │ +│ │ │ +│ ▼ │ │ ┌─────────────────────────────────────────┐ │ │ │ 1. 从 page_configs/current 读取当前配置 │ │ -│ │ 2. 只更新对应页面字段(home/category/age) │ │ -│ │ 3. version++ → 写回数据库 │ │ -│ │ 4. 同步上传至云存储 content/page-config.json│ │ +│ │ 2. 更新 home 字段 │ │ +│ │ 3. 清空 category / age 字段(注释保留) │ │ +│ │ 4. version++ → 写回数据库 │ │ +│ │ 5. 同步上传至云存储 content/page-config.json│ │ │ └─────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────────┘ @@ -86,22 +98,22 @@ saveConfig(config) → 写回数据库 → 上传云存储 content/page-config │ └─ 未命中 → 调用 pageConfigFetch 云函数兜底 │ │ └─ 成功 → 存入 globalData + storage │ │ │ -│ TabBar 页面 onLoad(首页 / 分类页 / 分龄页) │ -│ ├─ 配置已就绪 → 从 globalData.pageConfig 读取对应字段 → 渲染 │ -│ └─ 配置未就绪 → 显示加载态 → 监听配置就绪回调 → 渲染 │ +│ 首页 onLoad │ +│ └─ 从 globalData.pageConfig.home 读取 → 渲染 │ │ │ -│ TabBar 页面 onShow(后续切换) │ -│ └─ 直接从 globalData.pageConfig 读取,零延迟 │ +│ 分类页 onShow(每次进入都重新请求) │ +│ └─ 调用 worksheetsQuery({ status: 'active' }) │ +│ → 前端按分类分组 + 排序 → 渲染 │ └──────────────────────────────────────────────────────────────────┘ ``` ### 2.3 数据预拉取 -小程序数据预拉取通过云函数 `pageConfigFetch` 实现: +小程序数据预拉取通过云函数 `pageConfigFetch` 实现(仅用于首页数据): - 在 `app.json` 中配置 `fetchDataUrl` 指向 `pageConfigFetch` 云函数 - 小程序冷启动时由微信客户端自动调用,不占用页面加载时间 -- 云函数从 `page_configs/current` 读取完整配置并返回 +- 云函数从 `page_configs/current` 读取配置并返回(主要是 home 字段) ```js // cloudfunctions/pageConfigFetch/index.js @@ -114,39 +126,37 @@ exports.main = async () => { ### 2.4 页面加载策略 -三个展示页(首页、分类页、分龄页)均为 TabBar 页面,不再使用本地 `.data.ts` 静态数据渲染,改为**统一从内存读取预拉取配置**,并通过 storage 缓存保证离线可用: +**首页**:通过预拉取 + storage 缓存加载(保持不变) ``` app.onLaunch │ ├─ Step 1: wx.getBackgroundFetchData('pre') 获取预拉取结果 - │ 成功 → 存入 globalData.pageConfig - │ → wx.setStorageSync('pageConfig', data) 写入缓存 - │ → 通知页面就绪 + │ 成功 → 存入 globalData.pageConfig → 写入 storage 缓存 │ ├─ Step 2: 预拉取失败 → wx.getStorageSync('pageConfig') 读取缓存 - │ 命中 → 存入 globalData.pageConfig → 通知页面就绪 + │ 命中 → 存入 globalData.pageConfig │ └─ Step 3: 缓存也为空 → 调用 pageConfigFetch 云函数兜底 - 成功 → 存入 globalData.pageConfig - → wx.setStorageSync('pageConfig', data) 写入缓存 - → 通知页面就绪 + 成功 → 存入 globalData.pageConfig + storage 缓存 -TabBar 页面 onLoad(仅首次进入触发) - │ - ├─ globalData.pageConfig 已就绪 - │ → 读取对应字段(home / category / age)→ setData 渲染 - │ - └─ globalData.pageConfig 未就绪 - → 显示加载态(骨架屏 / loading) - → 注册回调,配置就绪后自动渲染 - -TabBar 页面 onShow(后续每次切换触发) - │ - └─ 直接从 globalData.pageConfig 读取,零延迟,无网络请求 +首页 onLoad + └─ 从 globalData.pageConfig.home 读取 → 渲染 ``` -**三级降级策略:** 预拉取 → storage 缓存 → 云函数调用 +**分类页**:每次 onShow 实时查询(v3.0 新方案) + +``` +分类页 onShow(每次进入触发) + │ + ├─ 清空当前列表数据,显示 loading + ├─ 调用 worksheetsQuery({ status: 'active' }) 获取所有已上线 worksheet + ├─ 前端按分类分组 + ├─ 每个分类内排序:updatedAt 最新的 2 个置顶 → 其余按 downloads 降序 + └─ setData 渲染,切换分类时无需再次请求 +``` + +**三级降级策略(首页):** 预拉取 → storage 缓存 → 云函数调用 | 层级 | 数据来源 | 耗时 | 适用场景 | | --- | ----------------------------------- | ------- | -------------------- | @@ -154,24 +164,16 @@ TabBar 页面 onShow(后续每次切换触发) | L2 | `wx.getStorageSync('pageConfig')` | ~1ms | 预拉取失败,但之前成功过 | | L3 | `pageConfigFetch` 云函数 | 200-500ms | 首次使用或缓存被清理 | -**设计要点:** - -- 配置在 `app.onLaunch` 时加载一次,存入 `globalData.pageConfig`,三个页面共享同一份内存数据 -- 每次预拉取或云函数获取成功后,同步写入 storage 作为下次启动的缓存兜底 -- TabBar 页面 `onLoad` 只执行一次,`onShow` 每次切换都触发,天然适合「启动时加载一次,后续复用」的模式 -- 不再维护 `home.data.ts`、`category.data.ts` 等本地静态数据文件,消除双数据源不一致的问题 -- storage 缓存的 key 为 `pageConfig`,使用 `wx.setStorageSync` / `wx.getStorageSync` 同步读写 - ### 2.5 云函数总览 -| 云函数 | 职责 | -| ---------------------- | ------------------------------------------------- | -| `pageContentBuild` | 生成指定页面配置(home / category),写入数据库 + 云存储 | -| `pageConfigFetch` | 数据预拉取接口,从数据库读取完整配置返回给客户端 | -| `homeAutoRefresh` | 定时任务,每日 22:00 自动更新 featured 和 hot | -| `homeTimerControl` | 暂停/启动首页定时刷新任务(更新 settings 集合标记位) | -| `worksheetsQuery` | 查询 worksheet 列表(支持按分类、状态筛选) | -| `worksheetsUpdateStatus` | 更新单个 worksheet 的状态 | +| 云函数 | 职责 | +| ----------------------- | ------------------------------------------------- | +| `pageContentBuild` | 生成首页配置(仅 home),写入数据库 + 云存储 | +| `pageConfigFetch` | 数据预拉取接口,从数据库读取配置返回给客户端(首页用) | +| `homeAutoRefresh` | 定时任务,每日 22:00 自动更新 featured 和 hot | +| `homeTimerControl` | 暂停/启动首页定时刷新任务(更新 settings 集合标记位) | +| `worksheetsQuery` | 查询 worksheet 列表(支持按分类、状态筛选,分类页实时调用)| +| `worksheetsUpdateStatus`| 更新单个 worksheet 的状态 | --- @@ -263,9 +265,15 @@ CATEGORY_LIST.map(category => { }) → 云函数批量查询所有引用的 worksheet → 构建 home 配置(categoryTabs + ageBands + featured + hot + sections) - → readCurrentConfig() → config.home = homeData → saveConfig() + → readCurrentConfig() + → config.home = homeData + → config.category = null // 清空分类页配置(注释保留,后期可调整) + → config.age = null // 清空分龄页配置(注释保留,后期可调整) + → saveConfig() ``` +> **注意:** 清空 category 和 age 字段的代码需加注释标记,方便后期对这部分逻辑进行调整或注销。分类页已改为实时接口查询,不再依赖 page-config.json 中的 category 数据。 + ### 3.6 数据结构 ```ts @@ -297,15 +305,63 @@ type HomeDisplaySection = { --- -## 四、分类页内容管理 +## 四、分类页 -### 4.1 管理页入口 +### 4.1 数据加载方式(v3.0 调整) + +分类页不再通过 `page-config.json` 获取数据,改为**每次 onShow 时实时查询云端数据**: + +``` +分类页 onShow + │ + ├─ 清空 displayItems,显示 loading + ├─ 调用 worksheetsQuery({ status: 'active' }) + │ → 一次性获取所有分类下已上线的 worksheet + ├─ 前端按 CATEGORY_LIST 分组 + ├─ 每个分类内排序(含「全部」): + │ 1. updatedAt 最新的 2 个置顶(最近修改优先展示) + │ 2. 其余按 downloads 降序排列 + └─ setData 渲染 + → 切换分类时直接从内存数据筛选,无需再次请求 +``` + +**设计要点:** + +- 每次进入分类页都重新请求,保证收藏数、下载数等实时数据的准确性 +- 一次请求获取全部 active worksheet,切换分类时前端本地筛选,体验流畅 +- 排序规则统一:先展示 2 个最近更新的(让用户看到新内容),再按热度(下载量)排序 + +### 4.2 排序规则详解 + +所有分类(包括「全部」)使用相同的排序逻辑: + +```ts +function sortWorksheets(items: Worksheet[]): Worksheet[] { + // 按 updatedAt 降序,取前 2 个作为「最新」 + const sorted = [...items].sort((a, b) => + new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime() + ); + const recent = sorted.slice(0, 2); + const recentIds = new Set(recent.map(w => w._id)); + + // 剩余按 downloads 降序 + const rest = sorted.filter(w => !recentIds.has(w._id)) + .sort((a, b) => (b.downloads || 0) - (a.downloads || 0)); + + return [...recent, ...rest]; +} +``` + +| 位置 | 排序依据 | 说明 | +| --- | --- | --- | +| 前 2 个 | `updatedAt` 降序 | 最近修改/新上线的 worksheet 优先曝光 | +| 第 3 个起 | `downloads` 降序 | 按热度排序,下载多的排前面 | + +### 4.3 内容管理页(仅管理上下线) `supportPages/categoryContentManage/categoryContentManage` -### 4.2 管理 UI - -分类页管理顶部展示分类选择器(来源 `CATEGORY_LIST`),默认选中 `math`。 +分类页内容管理页**仅保留 worksheet 上下线管理功能**,不再提供「更新分类数据」按钮: 分类头部统计: @@ -326,7 +382,9 @@ type HomeDisplaySection = { | `active` | 灰色按钮 | 下架为 hidden | | `hidden` | 黄色按钮 | 恢复为 draft 或直接激活 | -### 4.3 分类页支持 id 参数 +> **已移除:**「更新分类数据」按钮及相关代码已注释,分类页数据改为实时接口查询。 + +### 4.4 分类页支持 id 参数 分类页 `/pages/category/category` 支持通过 URL 参数 `id` 指定默认选中的分类: @@ -338,32 +396,6 @@ type HomeDisplaySection = { 首页 sections 的「查看更多」链接使用此参数跳转到对应分类。 -### 4.4 数据结构 - -```ts -type CategoryPageData = { - searchPlaceholder: string; - categories: Array<{ - id: string; - name: string; - icon: string; - items: CategoryItem[]; - }>; -}; -``` - -生成时从 `worksheets` 集合查询 `status == active` 的内容,按分类分组、按 `sortOrder` 排序,转换为 `CategoryItem`。 - -### 4.5 更新流程 - -``` -分类页管理 → 管理 worksheet 状态和排序 - → 点击「更新分类数据」 - → 查询所有 active worksheets,按 category 分组 - → 调用 pageContentBuild({ page: 'category' }) - → 云函数更新 page_configs/current 中的 category 字段 + 同步云存储 -``` - --- ## 五、分龄页内容管理 @@ -405,11 +437,12 @@ supportPages/debug/debug 2. 实现 `pageConfigFetch` 云函数(数据预拉取接口) 3. 实现 `worksheetsQuery` / `worksheetsUpdateStatus` 云函数 -### Phase 2:分类页管理 ✅ +### Phase 2:分类页管理 ✅(v3.0 调整) 1. 实现分类页管理页(worksheet 列表、状态管理) -2. 实现「更新分类数据」→ 生成 category 配置 +2. ~~实现「更新分类数据」→ 生成 category 配置~~(已移除,改为实时接口) 3. 分类页支持 `?id=xxx` 参数默认选中分类 +4. 分类页改为 onShow 实时查询 worksheetsQuery + 前端排序 ### Phase 3:首页管理 ✅ @@ -432,10 +465,11 @@ supportPages/debug/debug | 风险 | 处理方式 | | ----------------- | ----------------------------------------- | -| 预拉取失败 | 依次降级:storage 缓存 → pageConfigFetch 云函数兜底 | +| 首页预拉取失败 | 依次降级:storage 缓存 → pageConfigFetch 云函数兜底 | +| 分类页接口请求失败 | 显示错误提示 + 重试按钮,或降级使用本地 category.data.ts | | 配置生成失败 | 云函数返回错误,管理页提示重试,线上数据不受影响 | | 首页配置引用了 hidden 内容 | sections 回显时通过 wsMap 匹配,已下架的自动过滤 | -| 更新某页数据清空其他页 | 云函数从数据库读取完整配置,只覆盖对应字段,其他页面数据不受影响 | +| 分类页每次请求性能 | 一次查询所有 active worksheet(通常 < 100 条),前端分组排序,耗时可控 | | 分龄内容不适龄 | 选择器按年龄段默认过滤 | | 并发更新冲突 | 云函数使用 version 乐观锁,冲突时提示重新加载后重试 | @@ -446,20 +480,20 @@ supportPages/debug/debug | 文件 / 模块 | 职责 | | -------------------------------------------- | ------------------------------ | | `supportPages/homeContentManage/` | 首页内容管理页 | -| `supportPages/categoryContentManage/` | 分类页内容管理页 | -| `cloudfunctions/pageContentBuild/` | 生成 page-config 中指定页面的配置 | -| `cloudfunctions/pageConfigFetch/` | 数据预拉取接口,返回完整页面配置 | +| `supportPages/categoryContentManage/` | 分类页内容管理页(仅上下线管理) | +| `cloudfunctions/pageContentBuild/` | 生成首页配置,写入数据库 + 云存储 | +| `cloudfunctions/pageConfigFetch/` | 数据预拉取接口,返回首页配置 | | `cloudfunctions/homeAutoRefresh/` | 定时任务,每日自动更新首页推荐位数据 | | `cloudfunctions/homeTimerControl/` | 暂停/启动首页定时刷新任务 | -| `cloudfunctions/worksheetsQuery/` | 查询 worksheet 列表 | +| `cloudfunctions/worksheetsQuery/` | 查询 worksheet 列表(分类页实时调用) | | `cloudfunctions/worksheetsUpdateStatus/` | 更新 worksheet 状态 | -| `pages/category/category.ts` | 分类页,支持 `?id=xxx` 参数选中分类 | +| `pages/category/category.ts` | 分类页,onShow 实时查询 + 前端排序 | | `pages/age/age.config.ts` | 分龄页配置(详见 [分龄页方案](./分龄页内容管理方案.md)) | | `skills/generate-age-abilities/SKILL.md` | AI Skill:生成分龄页能力目标 | | `skills/generate-age-weekly-plans/SKILL.md` | AI Skill:生成分龄页学习路线 | | `pages/home/home.data.ts` | 首页兜底数据 | -| `pages/category/category.data.ts` | 分类页兜底数据 | +| `pages/category/category.data.ts` | 分类页兜底数据(接口失败时降级使用) | | `core/data/categories.ts` | CATEGORY_LIST 分类定义(id/name/icon/path) | -| `content/page-config.json`(云存储) | 三个页面的统一配置 JSON | +| `content/page-config.json`(云存储) | 首页配置 JSON(仅 home 字段有效) | | `page_configs/current`(云数据库) | 配置数据源(Source of Truth) | | `settings/homeAutoRefresh`(云数据库) | 定时任务开关标记 | diff --git a/miniprogram/app.json b/miniprogram/app.json index 6f5ebd3..ce9b63f 100644 --- a/miniprogram/app.json +++ b/miniprogram/app.json @@ -12,6 +12,7 @@ "name": "supportPages", "pages": [ "guide/guide", + "settings/settings", "debug/debug", "worksheetSync/worksheetSync", "categoryManage/categoryManage", diff --git a/miniprogram/app.ts b/miniprogram/app.ts index 09f6302..da0a635 100644 --- a/miniprogram/app.ts +++ b/miniprogram/app.ts @@ -12,6 +12,7 @@ App({ globalData: { env: 'release', uuid: '', + user: null, printConfig: defaultPrintConfig, pageConfig: undefined, pageConfigReady: false, @@ -41,9 +42,13 @@ App({ } // 静默登录(不阻塞页面渲染) - getUser().catch((err) => { - console.error('静默登录失败', err); - }); + getUser() + .then((user) => { + this.globalData.user = user; + }) + .catch((err) => { + console.error('静默登录失败', err); + }); void this.loadPageConfig(); }, diff --git a/miniprogram/assets/fonts/demo_index.html b/miniprogram/assets/fonts/demo_index.html index 74b6ba7..185dfa9 100644 --- a/miniprogram/assets/fonts/demo_index.html +++ b/miniprogram/assets/fonts/demo_index.html @@ -54,6 +54,12 @@
    +
  • + +
    iconify-tabler_mood-empty
    +
    &#xe62f;
    +
  • +
  • loading
    @@ -415,8 +421,8 @@ >@font-face { font-family: 'iconfont'; src: - url('data:application/x-font-woff;charset=utf-8;base64,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') format('woff'), - url('iconfont.ttf?t=1778054314485') format('truetype'); + url('data:application/x-font-woff;charset=utf-8;base64,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') format('woff'), + url('iconfont.ttf?t=1778142126728') format('truetype'); }

    第二步:定义使用 iconfont 的样式

    @@ -442,6 +448,15 @@
      +
    • + +
      + iconify-tabler_mood-empty +
      +
      .icon-iconify-tabler_mood-empty +
      +
    • +
    • @@ -982,6 +997,14 @@
        +
      • + +
        iconify-tabler_mood-empty
        +
        #icon-iconify-tabler_mood-empty
        +
      • +
      • VrxVo{*yH*hIU(K zAEhUppH6Z+L9(J^P!Da5QH>+w9rc%%B(~lORMDozhG+V`1M>Xv@;hR2&`LA35R37A z1sP1_6`lxDKMNDKO>b9I)%r=Zf;|CK%Fu>E=O=a3%*C@jo__1$F066GGnh>zv^8%c zU8R_h09@vDNi$6z3Ah79U zFYR$sgrVAozAa)K^VS(!KN75Q{+@4+v0pP#q}j%nr~0(wab;WSPrsMgczSe9XU@ZG z3}(b}KZ(UgSV9^tRBTr-tA+N%w&FBPD~oZsD2Ghhi*d?kpC-Q>>Vcgkest_c_)$|B zg@FUTmKBzUt;ap5FIt2F-Hb47RaJ}<+%V2S=%U9o<7}+Oo(rmY%nYr2+1u#?JLL|u z`wR<&&y!D=)Kd%IYE4~MpHsP=#)9+l!q)4hycxm50^N5?N(Avkhm||Udw*N%E1V6$ zrHqxlOH?~}Rq(c{qmM8lv_YvBXo4C^rM=3#714#@orI7&h6K)o?FRE9n^1;Ff3Rkd zE$FJibj5{L%MXnqCwL>EhFWSQAWyDA_T0>fYLMjzPQua<>_DgU1_o##ymmd+V(qp9 zSB^W=!$yTdqbI>6TzC@w&|Iy^v1^oJq!&DY7SH~HS2`xQku&~lu)tlWgj=fgTwEsL z;_BefrHaMYKU@POUubJ=nzGNiayX8jNRb{NcyDWvsQ;nhc(;6MSN(S(xwtSgQyJq7 zOB2urh6tQjK?{NJ_VHJbe<=1B)}TD!c11%sGJzISI$Oe4;yceg&jlg%Dpm-*>8L6V zF=FH;ci|MZ)AAG~0rK+;2Y*7v*y(Y*9_Y$(@d~c#$Qy6Zyf$qOwhe3s&mL zBk%65>-YOEHn%tY95~kYa_B4kQ5-vTF`4EF52-K(&^M#*b)D>^4p{f!^~7 z*vxjl^_w3D=wg@98QF1UAlz57Sp_S>Y`|$D?t-H$=sJS;UjrSbkL$IL*m1Ra(=@)0HihGuM ztRhGj;Rad)Z%*y$5x0m~0;rRTR&H+=ICl zC4d>f&4e#wh?HII;-Qsx#WG3*F(POOB#@!yxUe$Uwt18zp=b?%@DXU?3+|4taG^`c z-%U}>)&hkK2I9=O&KZb*UCnte!j1a|oMII@Ph4b?@&9nKgCTPLF$2qNkhTc_GU+ zzr7Q1hXyrpOk2|{{TekcboB4S;EWeJYN?S-|9R7HZUTqh%a&DnkAQ8HT!_|U#mV#N zZ;+{?6a>Wjtw{G$jvrMSb=~x!IlvuJ)EC~QGMvZG86~qBE*%VO{MK6cv|+2NYKvLd zT@L&FXrbk{T0bV_ANaZHodU~oZS6FWA&WXlI%_F%Ac--FP=_l0Q*Y#;jWd=*9#NQ1 zYUtfCT06Twg{CZV)|xWsmoP zvtOyA22UGbf`x>boM?NUY!plrQ)micB(s zwYI-xq4IH$#G_G94=ZKaV3@)O2%_QKXRS5QLhaJ%HS-}b4lciyM1lhsB?bhHjixAd zPc}7+WE0l@I;iCz0Y9X~pAMs`78vUyl_tnjrqg1W;!6#Xi)EHD0XvA>QbEp^5*3xY zWhgk!RPV9K4kcBiEio%dj>+T}Cyj4WV~LeG6x87dxq~(H7JH7@iDC4kd@nH~P)})h zUySGncLF#nuj$`o|I{`)McWzC_yKxuyX2!-E{pBmfDrStj?&n{eJBxn47s>7s@Hov zjoPz{N^oOT`a912TNsV<_5==P=mf0W0*AP`CpKd-4AqgLN|>Ms2Xgd)ox)~3m;t7q>dYIOUBIGzR>XbdM6UqPXt1JTs~iUn}-;GWrGHt`Wgc5 z5Qp}Tj*ij}J|YyD1+=L0W%?Nk>5NhWlQynf&Uh7km7`{XY+6xJ%yd95J|Bp#IbU%+ zzFUFRfD_NIc8B-(f8SlWAsGSquvzRRagIDvHh5O)!jYMviC}}(=|UNvW+CjD8PdTK z>3@GPt6ad~z)x(AM`D%so7fj$y3J_h6@M$FqcRz6EGWA0>1mt17l5zCAIv?Qx{R52 zv+NZfMc9nNp%r5hnes1fxKO_Q$Uz#tpRg>cmuA3cb$dp8 zrkd;X^j1sL^gi&Fm4J})b2bG3NHiQpfr#TWuI!KRYx0t&Qq1So>lw3h1hncFMsj{)n9 zWS3_pbd?QK1qplcRMLOR-f!ZnGhrHo7y*>u58E8XM=(T2lmIB~=(F>aFf}fkx^e4J zTj7yvf9PkDp0po}=RsHz7qJz<^ag~q5E4S*n2%?3oEq(|3?+LvZL|&>&s}MJaD{{$ zK*6|@L-LSRnCU=IOqNk6@oF@86fB%LZDw<4&Friz>Jg$mB5y|@OkT)pV{*iMfU)0r zT6q!a%7irH4GJv-kuOEzFU4DA2{+9wmY! z;u1~Jl{^4B_*EMO4JHNcrS&Mkuet(T35g;>ZugPMx{-OwaK|Qn03IFdbeXBOOeZ!z zjm?Z+N5$|&aTOmHJwk|Bs>mH?d>w1Hw6MJ_CDMA@{dK%1|IOr}9{+4G#64Q4VmXGA zN$mT*stg}B+X*yF|MXBMQKI&U7RS$-F3S+59;yD8s)9e(sPqoca5ShUhQ85_pWOzz}98(aUW_aGQ29pD) z&nz~UvEh1rY?&goU^@;dBOukvur&!b3U_+Xd*w4!Rz~Pat3W}xHJfUC1Nw=J;IQFs_18n|yYtjR6D`j_e>@+VHfz?ubI0>tu0MH|nB`{E|!69ASA~ zfw0H58LOM|zMY9ze1&3byOkX2^j-bNbp3x0su0LFC=A{;P&Z}lv_a)bOYtO;#Y)7P zGK(-VycS9z&!8>^Y*-II^Ws4Zy}Qkgo0F6nI~uUliqj8-QJ&B{bLNy>GO&>RmE{u@@PKAlkDU|^H z`@IV-5?nbo_rL4-^jdzZRZXdGlEfC<w~P>1EC;SnIBMGc2D(;h!%)SLl8ydio}WYbs1ampiUp|6Gxei?aO zB%~LL6N1xw!Gy84B%E)wS1erNpFA9I&9sV0Nlb<23IjRr{bVP#%EF$P1ZmJymAQm$ z8^O>!z1^N+@X)6}{<^Vo{FAZIyHs(_d&5#d2>f~|uE(HFj}zk}-d<=aM?W{FX|ne# zX`2WF_+NlTpkcrAHV*7|(A3&-!+R;}1WH38H;*CBugmdHmMq7(8?W9yxGJ4#)ED1H zSh>lzA-x?uu31G^jIC{YjEAPpMuXtG71?1j^JK0@#!jP`BtjB1HMe2x#P>#((*!t2 zx=$;kJk)_p_1V-_D?ey+7Ko0q%gE#VwdFleDheYbwg!C-*u5?=na#sD;80jG6X2tv z&0goUJDplF*7z`iK8hbrv+n!KF^kw#voWy3$w}M~2S>ZEsvKGjj$@_3cCWE!|0y%6 zxRo7kKV8?~dpxOrL2P7$-YfvpBt+=Wch#MC?HQYSg%WaK*DGD_F&F2p^QcsqXt<)R z^oV`^7E46uKNo=!BUr*}V30rp)a8f^kIH;-3bEfkTL<@`4B}$EaM*qEW7peFP6>{N z#5f94a462i45;a&kcVp8$X(;@-C@M`*hbzChjd7WU$yIlbrC1sv@NFWWNQ;gR zq(<}4ACRab)bX8`Mc zF_NKwnzNAyPup5E@AB=GPNN5UiO8xw1Pt&He2Yv?Q|J8mVPI?<0FL$?M-!#!Em)!4Xbk*0=Q!EvEG zt1zb4?#_M^->ggx4wMff7J3L_Vab?*)=FgUunZ}ozDUj;&iJNPKZclRS9z9%rU9&B z>2K|ouHu0iTKE@|*9Q9ny#)VdAKW_&kgcnr!-e?6k)n(Jqm68VF|1Mni=vtNB0*TR z6E?%Bt_v>^;e?QXC9)u9rdh-DIU>-q`Thy`ry=9vfRJ51kv&wAEwL|I+E!UXqIhsJ zg7YK8+!EM{z-#=ZRJa9$y-<|286DM>pZsr4CRE?Z09Ist1wi>8h60_@kX%UDtzdPw z8CajjQe{C|H2OfQ8X_fDX5_Tz6;Egj-t)L+|B~@j6=uagvn_%v^-WD zhNqECdkKbBYy_M^*5-6QVbqxT1hY2jywY!XWK4wY+QH3ZZu&ZZJRQ3MkT`vJNy{)Y z0MGmH2rY(Osm>Da>bfWwqs|Vd+=!7F*ZH8<@~~NMp#e~J<(XFClq)$1Ks#uyGCyEr z=ft~Rb7M%2&U#fe+>s-q$TsQFKIs2ig)}3voa%(Cpt!8kUM^}0N zr&uC<6pFoA`h)2HEEQdym=A7(YMEciwhNjX2e9ucYV!9D+|>st)3W0h+=02I3-4f* z*WIY)KT@Oyo1UE#T*5`-iPvZ&ZOM`EMBLh)&XfUuo0gW0iEmv?tbanAxBlz*K_m+W ztD3ceZWCast4Axn6(COka&eQ_?qw7WOK7OF=%kc6`?Jt#h*{EX@DwYV*KxU(``3AI zkI(2PkQ$XD(DLz!ga)l31f5K1QVSNvq94k5wQzQ+?RZuEg^puprNs3r0&YBj{F!_v z_Cn2k>@gF_SX$v&juZP30|NYj?6yJbSvFwZ-d#MG-V61ilMUro@e3WAxRDKnvB|piT*fdq8kh_;1*kjlMGg*Rdm$V@4SVK zl93le1V9?G>-`ibkbGhIlUx14dAxDk*t)`n#4eUqVH7<{y(df(M{^|8Z147BMGFJVKo$h| z-|Xut5TIfNL!0TJ#@B=#WNp6ITFtD>)4QO%A4=~6)3%04IK|~w&61_R^-}<^nCG~J zJ=*)PwkpNI#toC49ivq)2|)VJnmRYXk@vtr zC1HKCYnBf@w1KPO9O@oBb?Lk2F!L2|SZKnJC6&M0A)6m?+RqN;a4@CdKsL%ubM>}g zJIfmzd@W{lP;JKR-*BT=iDmO3<0w2X<5ifABCa)VFG1B@F6&WUpQKj<%yR}B2y~m; zMkZ0$FSiEoP`m5=_*zv70ZFuSIW ze<6Ek*j+Y3kWiANzZTW?nGZ$B$Tb1xGe;6_imfGYFKoctXmw_%=j}NXSf7oG!0jH= zyBU= z3J~`ZWi+?@1aoG_{Ns8u-R!)1tx9qV?(t)*#rmvv2dN9^db=aPeO>jM<+V$OfV<8_ zET9Lmn&!(unoB_q5siTxs8(z{=ivPN_g*U&4%LVu1&hU_xyii5vv^r#%5;8u?AB6n z8qX)f2JB?tK_LaiEsUS1FxtRsd$(l4E{+S9CCC&8+=7-KI_kkFNRkXWD!Jg#^XTfX z2%C?l$Y`DQ4lfN}w6X)AJk&Km1c^;`-m0pNE58kQoBLYr&aaz|uji$&Tu-yTCGR)k zuHSEmBmbaKwZ!iC=bKO8Grl9A^}^pO=dhBzHGF{;2A?AuU(tvz88Lx&=GX*vUWAl2 zA%1$BmR|*HA9eZn!AFeeJVny)k&!h7#L9!nvA?KjhTqkh>pA7Kj^=`$pRNtWFx=_) zvIN8~)JS;dtuzhzeCcp&t@gRxHHhfv7PQ)YGH%b&*NbPH;%*SECe@J_|Nj~ z;hZ9@zr4I7pMk~9V~0xorItrio_lWWGL+(y#r$!JGi-+&^z zS9`_Q2>2jMZ{lvSK&BME|JUAGwng=RZ=W90p}Pbm1ZiZD7)n5CNu`Dkk&qf<=x#(L zBt=T3q;qJHjxSw9gVfM5F!Sg467J{sb?is`;kB=0@4Y_jJkQ0%848>4D`A90&kA#N z-+U8JCuG;LWLo15z-8}37^e5!4Mlim+=_1HY@%@tNv|eLPk*&4A7umP3`~i^2 z+=8AEK)oJ6E^YbRhc+mtqcF<+mHXlRun;`pizQ+!E}LY)0@UJ)v0hblFlDqL(CGYw zXUE-Z(9|(~3uJc3nhO5>;f>2ox2ih#>j2Z~J>ETf1iSv9D1{;CDDw{--9rRIHr2me zT6s4Kl~|u~=*zK3W6ja3;r+yxh7Er+fQLdaXN(py<2U*Ph{vY#M<`qXrOK4*D2BT` z9o1Ab;lUq0OkFg|uf0Lp=oSB@4!4_|INTDgc2W?5KIpQYj-n)q**9=pP{@NX!1ut= z6*7j4C$>2+kbq4z*{`f9TV|^mwPTn;p>D^e3t1?yiVZWq8f<`wzpf3-1gZ7E&bGD< zTe1_p(1%@eEH{O`k88xHaBesi>sHwV2=KQ>v-)n$7+LrqUGn4w;3BtDwL7jB%d0y6 z3O-MGW1V44BT0(2NB34lQn1^B{qv|hD`9u;o106BTVLX-T@$l>55CH_A3DB(psG5!zrk>TLuRqjHz%N z>e9$7af*2Zx&*nifBaQFME6e-s$joI+w-a|^rZbd5ZRv^Ph8zd=RlC9N4;4QD{c=c zR~TF|${)@-!Kn(c?Ykc$@>an+ImUfFshV_{$MgieI=Xe9RrkZ*0^HWNWJ4F{46s=A ztP1lcz!47YS&5Atiht=NPeM_NJVF3ux{Z-rO!xHj6m92)K7ludJ`9BmPw#I=c^P9O z?Fa>(yrf1V8h%jF705%BoA6^0BV3B{W^X0r!%oBq2-$&wXH-`w?Tz>}UxY2#`$?F5 zJ74y-i>rwVhrLLYB1rSSdh6z`u7q8SCA*pfNbC?t{=lk4VZFn~_{WNE_DoisxoDU^ zZP`d?<~R8|BZiQ5oVT@SrZe87kcLsPNOE-u^5~ucKV_%M2-LA!+7xyO&59N4Y22Tp zjc~nWCVknc@iCka9uEL$3#SIpKGY=><7NZYLQ@gy!rb?D^meCBOm;Jm6w@c-t>DWS3)s<9K)zl+LjBeItoFN1iSRo zQAfv862{x7#kA%@2Pp8}0)(|q2?u)+dE5#PLXm+I=%?TGLkE5*6Y|N{7T}NeItZJ3 z_=J)C8fufimJPy@RpC&~B7(dMH3hwa-frzHjURej{`K4bZ!sS#Rm0bA|5WYbm8^yT%$Mt8Z|oJ=P!u;VxUm;ZJ788k0i{K`jw%w1dC`$ zW{W)E^bOR}ov#-+W~S#>B%r5Sjqh)&e-^92$3l&bp#VUNdjLyLJ{lzQ?wS6(9oKLa z$r+u6OoC-tof_5>;v$A>07OF&Oq8_X>}_Gb{qM?7OfHwMu|3@Yd>dZrBMwMH;BbR~Drc=FvebN^Q{ zn35}#63&23G4v@FW2MF#oDYf|%P%CixxtEg{F%DsC;kbh>{5f;!O|scJVa;QD(SXO zk?j6;3kP+^k0>0vO!aP6Q+lZX2m71BT%H@UWdw?a&!Lx#i&bsS#?F z^78WfQmA~j+THiHt%AbShCG4b8|+5>%+DZiw-fXVD(}vgn!NThAlxh%U7Jq38EkWHS#M;#h2InMJ(OMWxGB`l|tFN7wWAuMd)8=sYq?!hSND z@as+u$YRL*TBesTkS5vR_&6g2Ot}KSoux1gSapB;tx^1YDat%B&O?y1+pJys!{W#F z3(>w#Ef%F>d0}SNtMBdzTrGTW+JY>6oGWoP-I9M7=&Z%7gcr5n($ni-{x&f}jMmaT z3Qdo5{N-c<{1DWBQ3zI6>6?q=J^~7Yc?$w_D`mH%MNEeh&SOT_P6iu+HOlpn9cS)a*9={Rx89i?9uSK)oe*u^QPvj z0pQmbff1GJCu|QzRjXIGf75|1Q9S#ssr$7OGGT1MdnVCdPYkT5-_&mb$@lsxPb+c$ z$LhMe`i_9v{y8}20AphTM|<@%;NgaBVWUp515-ako*RZi+f{4}G<2dqI-QuCHnYrRsWkLiB>)#266i1zgfW=|?sB8(e!MOhk|cP#5bj-82| znzX+XLYh?2sz;}EZ4iIe_ZAlyp8!-=?2{DcJ<2)AdGCp-2;tq@>dH`iax&4QHK06K z+v(zCAb}3hg3-++zjsKay{xRPwDhQWT{0=~B6g|vPo&NFKfV*UVx0ggdqr>WQ8=##PlO&EAsZKv}5=Y#@a4U{{%(0|_@9f2ja@zE@@K zY#m8JkcwgVFhWKKbAj#)jH8LQ85PzQ8Q;CENiuo2fJ9RHDE+O~1+m3-yv4zeL5kQo z$b5Bj#UT1WGCiM5@S$ruSJr!`g@6$JYX0S>x@G z>mDuC3eP)ir4s##Kd7~+ZAL9E*XhO zbrT0YNYd}4+l%$kH4i|uki3RG|9WqilIr7uNS-Rg)~7$rEmTcXOv6dGWoOX=jg%t) z+pvO5Y~>p=ItN~~tW@^*M{17b5pvSV^9s+UL?g#q^W^a?#jD|%^(2oRxx8`QT%zBy zCNK{QGOX_09FG17p(GaB+y6Q9m;H~MtHTmasYw~Wcx@M2dn|s=Qq-KM_^KOv99*V4 z-^rLI_fW78586vo!+*4+tgzV#uk*0cD?_gVRkEQWfziwj=@R-ct%h50OJ3jzeB0Q- zm-fNTh3o=5g5sNrogK9c40vZBiqfCtanDgqC-WrbfMCd8A`MNp9_fsAtiSuyQ9w2+ zKgjq7K1u8g+U&ET-8NC*9T(HN>Soy0hmM(E&MGmzl;)_DV6~eJW5fp5lS5VT-H~hH zi6`wM6j-s6C`8=;E_nYCFC@0~*RQ*?-j6QLC+HMT51~C4Y$>uo{bq80Rt8Mci;7Gp z#rxA0)8Ho{?59_NgV_rBZBGz-w@R1ezLy-w<|*M3)@k1yYmB0TQ+0D?8}>_$YTfid zeHJ4Ki)!d;uBaJvvP*Z>h3z54rD8=~vj<%?Pw|81om4PTG*& z=YXr@g?9fg9J(hE?^o<9_z}~qrw2ZWk(s7n?RR(nyv?gp-#=r$!73JZy9dHErROZg zci4Rv&a{2iApF*f6nemk7vpHntbpyo-z1qtBS;oeo2@1jp89$Nh6=TUW?`f5AiB^8 zJBz>ri1WkU3Hsa~tnN=r(CLXMdA8ycct*>KLpD(P==)Bo*KG^_8~iYSywZA-i90SE zbTLuTBZN(&vvS@iC(Hh`8;gKEi7fl)rD~2sU|%OzO(ajDu#o#bGKDiL7lZ8VD7ilN zb;6A0u(JgNSX-+gELxVcm;aSl!4pDoa?S5RRPsJD12A|Ri0^K-o+>Nwx>pKfv#K>u->Z*o)wM*xr*YeiZ_f*u0Q+@s1eY=fO7ku5j+-(Q8u!f~(< zWNo#?`{V?D4YnayPjp{(% zSd`?8x1oU30`ttZU&O=k{(ARI1TR#X^8I)!WSr zmOi3u<+hvgS1kuvUPoddyuZ$+UtM{>fTTzCb$?4cU6%~Hxg7^{1E-7_C>2O%A9=%G zs3m?Se(Sa9Yd3mv@UyRpOp`zK@{dnBcKdiP!i2y)IHCwHVq8&jEh27t*-gHS7lW7d zpe{o9t-;A%;Nik$#`paj-43scBUp;Q)55_TTQ+YYQa4imW??)KJy2n?U`b0ioGx ztn-Gp(yS1G*$Qsy{?pENi@A4bWZi0|Ao%9s(8+z|VRJkPU2c)15bmkAlVH@qr{fUZ zAL7x4Zf9fl+rMxe;w!1L`=wy5&)zlu=x8hH-k)~#50=4KQXtJY1xcxKWLpu04eIu& z*X+VQlNXX(^S3ftjA2Jp%0Dn^jZk)hoM+Yqt{+(M59i>h(d7!0Oi`O(`Ay}81y%#N zUNaKOgAj`c9NC!}FR|k$Z&^-Uor2`s?ueHjlcSQR85$73^~r3G9j-;$h=}-Q{DZf{ zCY9y6QuZ&EYUM51P{*#v<(8ff3(GP3?0|{e?~Nfu@SzEyBOj_q(tL$hLtv2_i?zx7 z7lEBRNJ6e9b{_l_r-_)IR*N_Zn^p65yii~gfYDqgLZ;KS zZufax4ZjeRI6KJ&;@sc6`4g@B++Bh!Ouf-at4JI&wdeS5ajxT~tJbICf017JF6sD(j`pSd-`sco-eXue|+A^Tv#a ziqL#_3>KvoH>5Wft_QhUr+JW7z^5eYmZw)c501vd+2{*NT8Lf?Q@qyeABvMCuLJoW zA|~6VbA&kxok>s^Rc)6&dA1*jRQ&LMkbqG~rihTowsH9@Y5Pz6B?ZS;wT{-Cy05pF zO!`>c^fDWNxD4Dyei6hNmU|tW+E34^<{m^me2;NManaW~6Nw0w*SLG3V(WpcM34cW z-7Bc5oP%?81~WxW_;>$*Lv!g(=a5zb?SiLN2<6!Pk#QaNi_6Qyz3N9F-gQK}xs30> ztxWp5d~!ydQ@Lc_FTE=&Q$*z22#`c_aaSAx+0>ipkw&ZjZW4cQT9bS9rP$qqs^7zl zr{t?eyn}#@SN4*D=rPybSM279YoSAdk84x$(lUq(Rj251F&t?{#~V87VG6=_pA#q zYz?C&te_thGJ4rFXR)t+afv97sgzX(_BiDXHCZ_(^!7c-x=7}7oCn8+TmLu9-lnQU zFP}uGo%57x0wh25bGwT#w_FbY{%-mlmC1wyl7TwNJUc#pkbpLc)X2p7V$ zb+whrFd#ek6RX6^aaDfmO)#3T7{Abaw4Fmj7goORsl^~7y6Gf6SAL(K0#i?%{%243 z*4=s_k*rpXV8N|7t@bib$v{EaRL{t8VXx4V7Jf@3ydLHgVbv*fX;LMi&h@R*^rZ2F zsWgzeATEb8PKfEl{eSYsI>FoE*ONc715@%gDA$c6G10QxVWV++rhKG!4FA{;lU|{_ zrxDlQ2G0BHo3hT2IxZ`0A2sEFT(3kq1m?%_LaX-y6d@MXVeJ57e8TnI zMDBu3;m)uj+%UXQc13O~MB4f`{JQ;w1&l7)^%fiQNpsn=R}r%AzQ(7JV;{v5znK{i z!p)^hB?_5MlTfEhp=R5yiZhG2Ij#)NyLCD4&yf-N zB=5<)rIQohr3tqCCYk82-`KCENhsWkE!|c}w#nq8tNl7;uN%SkyXQG|RuGv|J1zeo zyD{d)zTU^OpYQul9xMd-{B9q?FD4XLRE8DGuE(3rVRHK4n>s=*dqO;doj1X|DGlTk z*7DCf&9Aw>Nxz%q><|vZV=ABNZWgo@LTZ(+|86Ud zT}~x%bG;UX!AM=jS>)@}L(1{3SXrX5U7jU3N52Y$xMdOH65N}p7R8S zFr)BMUT5<$Jp(C#!EkVUb;-O{%|*inPk#JWijjFxoJ=Gzu)Ed{en-Sh5GmthJM8R6 zm9VhU?_v@Al;Y+|fw+avCZpISyvTXoEbzdzga@-DWMgZessozifB4(^>Sy`qPD{7u z7Q>)&a0o7M0gZ?fYiB?7-#|EaLRLP(R=vdbL8p85cNRE|=SbupSuvLh!}8jK$=b4v zG_J;MKVye$w_%2pt`1R1L>E;C2o%E?L8ncY%q2g*&Sl6(AG=PP-8*RgA#g&{hbAb5 z8lnukilY*0$E=IN129ccE>1-{PQ`%$uF%Jl``<^`wg+>S{zpAJnD&lce$N~r>-m-| zP0prcm*j>7A<_w@Y`2u;W@l-UBL9+<=*A{T`*AN+Qv+3`4EreV%A~+ zns3#L$jnkGD(L-}iK9?he3c{kwWKX?L3m3;Sm9bJfZ;u(ZIiFp~uEIyDk4-3=3iMDYIX^TwdB?&x@ zqXGa~?~(iVK`p5NA*1Z!gvaMZ_l}FD_Aaw?Z7k7&iHg|u9wDpywr6BzZS=B6>2yn? z(!AzdGxkaf>6KnsqR6a>(&yqBEN?|0OysQKPQ149Mj0cqfphVBQ|PGYU)kc{?u}dD zGnIJ-9Y39DBc6Wz3*{!sn4vF8Xdc%mVF+*bxf;53$z!oIxxZqu!cHYH`4&+Y2>Mc5 zDMTb?+Iq$EYSbBz`)n=9DSh<7OSa+Tb*_D`G@&U-HkksV{5(HgI20%ISIGGW>G_vd zCK>8cW8``WZ2UM9+qp09nsp}$gzgR!i9rZ77lMDdu04@%Qf^dGy=FRjN)HUY9Oza| zS~>PeuQVlL$xVUW4H_tfE6@>BJoOM0%SYo*)b>SBytLvHUH@|ajY!xNwgsa)KT1rl zhb|__#!wS=DMS%%!<_s$(?}ijt<9PgSvIf7mL~^C_|;zBC)Xen=HQoaFGZhos@5;m zHuvriGGQ#Dw1D}?7&kbbkG5OOV-pj;yN|1rRp?m) zn{eCp>XHdbf#Xo4%K$Sz4lYGsbV-?&SX2i6C1s%qTmSEyx{oU>wS~QAAj_C4hm9F7 z$}yv(PPv8T-zfM+)4-;iaZBaO)JWRX5mt#dRc7yQ$@f2D->2vR5~QbBXGpIK7a`}g zO-zr;VCasrPPnC~#1y!G%20oACFtvO%A|I%QrJno^1Fidz--*=tV&ljE3>_l$Gg`)(H`8oc;Tsu2}aa>f!9Kus# z#6e5MKzpuA@m$WqW(bHD#?CrBI-Xkex70}PmgFq5-Z4?_yK!gY46+jOn)quOl|4W4 zuR-%MwshIs#PFpL^OzC$mMF=(JAH81{ZG3dUd5SlKwtEmIS@|*V7|s(z)_rVg8cz8 zQufS+*Jl6j#P!qrhWG^nUa}j**p!Y|{_~MsqShzOKk+6|Y{v@{s|O8I1y{=!heCEX zSbverlbk|r$Q&?>79C$UtMIZ)-7^BMY{OhO=5j@mh8?Ci^GAN6;OlN`PVUe21QlW0s8M(I9l2f5uvV9vy1oA`hA@QFC5UquC* zf%6qw8oGB+Z)7^W_;(rlKWA6fLvSeA1>01g1xDiIYkIZ#xqd45|NDjL<#-k3*LL7K#wIaj?U6o7tlla`@ZM1|SuGWx6;6(Xa4AQv$t*TS|A|!WrjS;S%;j0lbku?^DSm`J z!Tfh%7G8c`P9_-t+x6AErJMmqaqcto+sI> z>{aHBoPn?e|L$9t@PcLC?ruwH<@YTe{WDzYwSTE^7S4S+tv_PH*U8e;>EO@|H_onv zRxg8ny#~!+~?un@ADJm<-&UBZo ztYw#euqI$YJv3@!$I|1?;~mG zp)Ca~NWv{E)$$SEJk#-a@vy6-2=U_);9Ypr%P!DgH1OGrPHy6mFmu!n3GBpz9?K&eZ|XzqpY#PmWgbU;h^}D%%yE`!k2-?=kiz0M$)kWa#(FrK;+4hgwIUV`Wn@ zqY6&ozzqEnZxkxx$!CU%v|uy9$I2HsK6%sT;v&~=66DE53-66&#=d3@%t8m4v(vsO zGB}#LIDPd6pycUDfWD5|-pq05HFxo%We^Y4ok8+Ee~xbL*O&8MQD?K8i0tj9fsA9A7+nFAf= zx$+B!B2E!VF)(ao)Vwq-v%d)%E8>*ikUgunoMJJAKWRB9bboo}K>(0pT^#GXB|*6S@x z_a&pImdOU4jiAjmLboKo&EchNz4eRImV3`5QBu@}PnMf@wBDs0TuZLk{71a)BlBJ* z!*|-`g_t0#(Dkl*V7!o9RCd9Szq@|*7#zCDWt^<8)J9efJVv)d z`Zqah>>%N2lKc+~j?o0D2DcbwC-J;?N;l~nxf&zg5bu@7t%F!I3&gEQ8a-%(d@v6&z5WPIMUZA3#gGOgbTAOrF_v+ zS?WDW+t_qeE_z?ED*bOlY>S&d&y0`CzCrAQC=0f*2;*Xp?px6(Dqp-K$&Dg#{Bff9 zFL9u%QT23glL5$7~5bfO%{P_AG7oOBf2GolsbVW%@} zRCqG?q=nT@_(!1uUjG{lO*ImW|XNprknBG;H%+H)2!~ z;89g^cX5nwUOFvgwvZiNTDm1+x$XYyXA1*GehaT~LT0Lr8&qZE8H>AfDDYqhUh*I2(9Zzx6&1 zCzoikiIc{XGK8XwSh@5ltYSVjGfDp|;Dk)*xdmW*^?v|S<{Br0OhKBI^tnKvB`uypiCp-$=**{+H1FsKz)e8O1 zhDY3DuS%$;y(S=#7e%U+wI+U<=eo-h741@?HKAJwr5)_o%w*qwR{Zwth>#+?rnxXm zlV&4rNXG#-XtBX6XVFzQgq)$R$vI3ac--9)YJ>A|0vKTV66YwKiw>6|R&VmgZQIRb z<<6k|w_(sp{9?zkzOwA*J>T`JbcwfD)DoHZLug5^TyGiVCp9$fpU~9Jv5$5(2zrC0 zh^Bi8BHfT&z8g#7BzN?~;Q8zK>jhdm?;|(dHzR6H0>S4xApjPi|IR2v*nz=)Rv>Xy zBG=u^t3O5OO51PA3Az~H;BRXvyVH8m zP?Dp{Fdo8hNtuU;Zx!kzqOXi4+n36EORfDD@9KS=jZ0rhu7^x}bwK4ancuDcCLGIf zciFN?1~F92qr|d*3$}%Q`0t{kj73oYhJ^M+s`Jo~kcM>}uav-e_2kdV^_qS9ow`ta zFCN;5-aI%co_I}xS#AW~&DzV=<+BE;UYc0Fnqm-wX3L@`&QZCG&A|b~1{j#xW%j~) z{SO|?rG|JjL`%BhQJyX_WtYX`RBav8fO|zmg8gm{&m#<^MUNkcxAP$Z{-UVHIwlKS zCKPd0A8C~vbhRz(;6p2WgUI%QlIZ*7oSYn(V83Gq0q40;Bj~C0?g!|~pXyG|B$+>+ z`%@hqMd}L)s&}l8E>9$g8CvbDR}Ab@Z?Sp%h@ zjG2~f@Tjm|Gt)u(Y)yk3_N!>&W$PCE;khMYQ8M^?uwGxm{etJ*9=~1*+6Tj5OM1r zZ#TkdMC?W~s)*wiOkb^l({p~v!xrokd2)G$@?v=bGN-*%-}4|o>7N8qpnnHqu7;kX zSG84@v3GZx!BODE*)fB8ExhrPn2T+!+v40@&Ynkj|z%AyVRcn9`P zmiANt!MWKLCZ9B*TATi`_da06D&Ddu8kZZxsLuGDh>Y|Adt&3kjRz+0eJ&{Md5<>8 z$tgNmPfCdT5=hhf?kk_Brno1O zij%%bNmE%VpZydpkCo~%70@&_HC60%m}+UZu3pnd($%SjNI2l;DW7W z#VMTa8B2SxAbMqKN%!5mDdQ<9Z8aDV$Bclzn}nyPoPUCbuxUJan1S#Hml~h8kfwoH zGM}Eujy;=Rt2eK@ky9nHFFdmK_yey?r!7fR@n9jL#Cn)m8woTUKF{#ug@dJB(lpWf z51B>7m+W$AGoOkNO|cJQJfBu#*^UK+(!u0z&0ue4qzj_6+oUF}e?Eq7a>kaZ3FoGH z<#3p0ke>ULe2lI@O8Mnt9jj;mo^}eM5t-|dder(2A?Z~N`!2v8R&ML^>JN#Mz}|nH zzvoddf1`JFd)g=tYK^Cp>Jf}6k!9fdYiD6-jP`d+FiU`|3-mwFwztJ5CFfi6gnuHQ zluWNzlMxJgZ2CGP^q4Q)?!`S#_fGf7D69P|BS&M+I}puROX|9?>~Ll6H&C&Px3QKkVR z`$sP(ivA{LK4g6)lKN!l;dkP~X`<3$4=N5%99o^DWLT)FTQ~TI)YyRh1o8IWFPb>N5*_4Y);Fe{>Bg9$id5~Y z1i|)$VParPcQH-P({U&AJJOx`o5m)*-y5-+!!d}#0Is%|to*wnJ>4dDZy}}1pI5vgT5&iFBV__ zPVJt8%QomT{u4(eVNeOY4}Z#>78%t&24GQ&bme;VeAyVEQ~4cNooz6~ z(rbu26UMsq)so&)?=&~ZyifpdYY-WV-BAV&Le%e*>;qAV{f=vDkw&`67LV@m-kX~5chO)|Za=B}B@e7`kvKJ*4 zFG_w9CXqfNPb`Y@CD+uHTzBt+-Ltz6y5W1qLpyM6(b8%`(H`hhHD2^ZM(ipoCCO3%msm}fWxzBo{f818tu9ZBd zP=2Y+c@uayMLiDMO^;Up7oRS*^adqHDh#_PwKZq%HzB)Q%J-1T$hJ4Dt!-OoXr25S z^w3PFmX|6fC4JNAi1fPJ{rTYgI!sJ!Bu|PW=ww$yu<;^7a&P$oR>hxtXWytJ2HOZi zFXLj0liX(gF+-cL;-X)HkyR+=y#J8CtJz_2bXY*BASxYx;u{9v`O!K7MBhKyQ2uYx z(aEWQ$9elWJ1iTVENY; }; +const DIFFICULTY_LABELS: Record = { + 1: '入门', + 2: '基础', + 3: '进阶', + 4: '挑战', +}; + const SIDEBAR_TABS: CategoryTab[] = CATEGORY_LIST_WITH_ALL.map( ({ id, name, icon }) => ({ id, @@ -33,9 +40,74 @@ const TAB_BAR_PATHS = new Set([ '/pages/profile/profile', ]); -// Page-level mutable state (shared across methods, set once on load) +// Page-level mutable state (shared across methods) let _categoryData: CategoryDataset | null = null; +/** 将云端 worksheet 原始数据转换为 CategoryItem */ +function toDisplayItem(raw: Record): CategoryItem { + const difficulty = (Number(raw.difficulty) || 2) as 1 | 2 | 3 | 4; + const ageMin = Number(raw.ageMin) || 0; + const ageMax = Number(raw.ageMax) || 0; + return { + id: String(raw._id || ''), + title: String(raw.title || ''), + subtitle: String(raw.subtitle || ''), + previewImg: String(raw.previewImg || ''), + ageBand: `${ageMin}-${ageMax}岁`, + ageMin, + ageMax, + difficulty, + difficultyLabel: DIFFICULTY_LABELS[difficulty] || '基础', + path: String(raw.path || ''), + available: true, + likes: Number(raw.likes) || 0, + downloads: Number(raw.downloads) || 0, + date: raw.updatedAt + ? new Date(raw.updatedAt).toISOString().slice(0, 10) + : new Date().toISOString().slice(0, 10), + updatedAt: raw.updatedAt ? String(raw.updatedAt) : '', + }; +} + +/** 排序:updatedAt 最新的 2 个置顶,其余按 downloads 降序 */ +function sortWorksheets(items: CategoryItem[]): CategoryItem[] { + const byTime = [...items].sort( + (a, b) => + new Date(b.updatedAt || '').getTime() - + new Date(a.updatedAt || '').getTime(), + ); + const recent = byTime.slice(0, 2); + const recentIds = new Set(recent.map((w) => w.id)); + const rest = byTime + .filter((w) => !recentIds.has(w.id)) + .sort((a, b) => (b.downloads || 0) - (a.downloads || 0)); + return [...recent, ...rest]; +} + +/** 将云端 worksheet 数组按分类分组,构建 CategoryDataset */ +function buildCategoryDataFromCloud( + rawList: Record[], +): CategoryDataset { + const itemsByCategory: Record = {}; + for (const raw of rawList) { + const item = toDisplayItem(raw); + const cat = String(raw.category || ''); + if (!itemsByCategory[cat]) itemsByCategory[cat] = []; + itemsByCategory[cat].push(item); + } + + const categories = CATEGORY_LIST_WITH_ALL.filter((c) => c.id !== 'all').map( + (cat) => ({ + id: cat.id, + name: cat.name, + icon: cat.icon, + items: sortWorksheets(itemsByCategory[cat.id] || []), + }), + ); + + return { searchPlaceholder: '搜索练习纸...', categories }; +} + function buildAllItems(): CategoryItem[] { if (!_categoryData) return []; const items: CategoryItem[] = []; @@ -44,7 +116,7 @@ function buildAllItems(): CategoryItem[] { items.push({ ...item }); } } - return items; + return sortWorksheets(items); } function getItemsByCategory(categoryId: string): CategoryItem[] { @@ -69,7 +141,7 @@ function filterByKeyword( } // Skeleton placeholder items for loading state -const SKELETON_ITEMS = Array.from({ length: 4 }, (_, i) => ({ +const SKELETON_ITEMS = Array.from({ length: 6 }, (_, i) => ({ id: `skeleton-${i}`, title: '\u00A0', subtitle: '\u00A0', @@ -87,45 +159,45 @@ Page({ loading: true, }, + _defaultCategoryId: 'all', + onLoad(options: Record) { - void this.initData(options); + if (options.id && options.id !== 'all') { + this._defaultCategoryId = options.id; + this.setData({ activeCategoryId: options.id }); + } }, - async initData(options: Record) { - wx.showLoading({ title: '加载中', mask: false }); + onShow() { + _categoryData = null; + this.setData({ displayItems: SKELETON_ITEMS, loading: true }); + void this.loadWorksheets(); + }, - const app = getApp(); - const config = await app.getPageConfig(); + async loadWorksheets() { + try { + const res = await wx.cloud.callFunction({ + name: 'worksheetsQuery', + data: { status: 'active' }, + }); + const result = (res?.result as any) || {}; + if (!result.success) throw new Error(result.message || '查询失败'); - // 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 = buildCategoryDataFromCloud(result.data || []); + } catch { _categoryData = CATEGORY_DATA; } - const activeId = options.id && options.id !== 'all' ? options.id : 'all'; + const activeId = this.data.activeCategoryId || this._defaultCategoryId; const items = getItemsByCategory(activeId); + const filtered = filterByKeyword(items, this.data.searchKeyword); this.setData({ loading: false, searchPlaceholder: _categoryData.searchPlaceholder, activeCategoryId: activeId, - displayItems: items, + displayItems: filtered, }); - - wx.hideLoading(); }, onTapCategory(e: WechatMiniprogram.TouchEvent) { diff --git a/miniprogram/pages/favorites/favorites.less b/miniprogram/pages/favorites/favorites.less index bfc88fc..2af907c 100644 --- a/miniprogram/pages/favorites/favorites.less +++ b/miniprogram/pages/favorites/favorites.less @@ -243,7 +243,7 @@ .fav-cta__btn { margin-top: 32rpx; - padding: 22rpx 64rpx; + padding: 24rpx 52rpx; border-radius: 999rpx; font-size: 26rpx; font-weight: 700; @@ -425,7 +425,7 @@ .fav-empty__sub { font-size: 26rpx; color: @text-secondary; - margin-bottom: 40rpx; + margin-bottom: 32rpx; text-align: center; } diff --git a/miniprogram/pages/favorites/favorites.wxml b/miniprogram/pages/favorites/favorites.wxml index 703d6b0..d5854cd 100644 --- a/miniprogram/pages/favorites/favorites.wxml +++ b/miniprogram/pages/favorites/favorites.wxml @@ -31,11 +31,15 @@ aria-hidden="true"> - 🦆 - 鸭丫还没找到收藏呢 + + 还没找到收藏呢 去发现好玩的练习纸吧~ - @@ -112,14 +116,15 @@ - 暂无下载记录 - diff --git a/miniprogram/pages/home/home.wxml b/miniprogram/pages/home/home.wxml index 61471be..1fb7424 100644 --- a/miniprogram/pages/home/home.wxml +++ b/miniprogram/pages/home/home.wxml @@ -24,8 +24,8 @@ + bindtap="onTapCard" /> diff --git a/miniprogram/pages/profile/profile.json b/miniprogram/pages/profile/profile.json index 988b185..c31fe25 100644 --- a/miniprogram/pages/profile/profile.json +++ b/miniprogram/pages/profile/profile.json @@ -6,6 +6,8 @@ "backgroundColor": "#F8F0E0", "usingComponents": { "van-icon": "@vant/weapp/icon/index", - "nav-bar": "../../components3.0/nav-bar/nav-bar" + "van-popup": "@vant/weapp/popup/index", + "nav-bar": "../../components3.0/nav-bar/nav-bar", + "toy-icon": "../../toy/icon/icon" } } diff --git a/miniprogram/pages/profile/profile.less b/miniprogram/pages/profile/profile.less index 2043622..375d58a 100644 --- a/miniprogram/pages/profile/profile.less +++ b/miniprogram/pages/profile/profile.less @@ -57,8 +57,8 @@ page { } .profile-hero__avatar { - width: 256rpx; - height: 256rpx; + width: 224rpx; + height: 224rpx; border-radius: 50%; border: 8rpx solid @bg-white; background: @bg-gray; @@ -98,7 +98,7 @@ page { .profile-hero__name { margin-top: 32rpx; - font-size: 40rpx; + font-size: 36rpx; font-weight: 700; color: @text-title; text-align: center; @@ -116,7 +116,7 @@ page { .profile-hero__stat { flex: 1; background: @bg-header; - border-radius: @radius; + border-radius: 48rpx; border: 1rpx solid rgba(124, 118, 106, 0.12); padding: 32rpx 24rpx; text-align: center; @@ -321,4 +321,144 @@ page { color: @text-secondary; letter-spacing: 0.2em; text-transform: uppercase; +} + +.profile-editor-popup { + border-radius: 40rpx 40rpx 0 0; + overflow: hidden; +} + +.profile-editor { + overflow-y: auto; + padding: 32rpx 40rpx; + padding-bottom: env(safe-area-inset-bottom); + background: @bg-white; + box-sizing: border-box; +} + +.profile-editor__header { + margin-bottom: 24rpx; +} + +.profile-editor__title { + display: block; + font-size: 36rpx; + font-weight: 700; + color: @text-title; +} + +.profile-editor__desc { + display: block; + margin-top: 12rpx; + font-size: 24rpx; + line-height: 1.5; + color: @text-secondary; +} + +.profile-editor__avatar-section { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + margin-bottom: 28rpx; +} + +.profile-editor__avatar-button { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + margin: 0; + background: transparent; + border: 0; + line-height: 1; +} + +.profile-editor__avatar-button::after { + border: 0; +} + +.profile-editor__avatar { + width: 180rpx; + height: 180rpx; + border-radius: 50%; + background: @bg-gray; + border: 2rpx dashed fade(@text-secondary, 30%); + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + box-sizing: border-box; +} + +.profile-editor__avatar-placeholder { + width: 120rpx; + font-size: 24rpx; + line-height: 1.4; + color: @text-secondary; + text-align: center; +} + +.profile-editor__avatar-img { + width: 100%; + height: 100%; +} + +.profile-editor__field { + margin-bottom: 28rpx; +} + +.profile-editor__label { + display: block; + margin-bottom: 16rpx; + font-size: 26rpx; + font-weight: 600; + color: @text-title; +} + +.profile-editor__input { + width: 100%; + height: 96rpx; + padding: 0 28rpx; + border-radius: 24rpx; + background: @bg-header; + border: 1rpx solid rgba(124, 118, 106, 0.12); + box-sizing: border-box; + font-size: 30rpx; + color: @text-title; +} + +.profile-editor__actions { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 24rpx; + padding-top: 20rpx; +} + +.profile-editor__action { + height: 92rpx; + border-radius: 24rpx; + display: flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + font-size: 30rpx; + font-weight: 700; +} + +.profile-editor__action--ghost { + background: @bg-header; + color: @text-title; + border: 1rpx solid rgba(124, 118, 106, 0.12); +} + +.profile-editor__action--primary { + background: @brand; + color: @text-selected-btn; + box-shadow: @shadow; +} + +.profile-editor__action--disabled { + opacity: 0.6; } \ No newline at end of file diff --git a/miniprogram/pages/profile/profile.ts b/miniprogram/pages/profile/profile.ts index 2c90016..a9abe6c 100644 --- a/miniprogram/pages/profile/profile.ts +++ b/miniprogram/pages/profile/profile.ts @@ -1,31 +1,154 @@ -import { getTodayDownloadCount } from '../../utils/downloadPrint'; +import { getDownloadLogCount } from '../../utils/downloadLogs'; +import { getFavoriteCount } from '../../utils/favorites'; +import { + getCachedUser, + getUser, + type UserInfo, + updateUserProfile, +} from '../../utils/auth'; Page({ + DEFAULT_AVATAR_URL: '/assets/imgs/doodle-head.png', data: { userName: '微信用户', avatarUrl: '', printCount: 0, favoriteCount: 0, - version: '', - isDevEnv: false, + version: '3.0.0', + showProfileEditor: false, + draftNickName: '', + draftAvatarUrl: '/assets/imgs/doodle-head.png', + isSavingProfile: false, }, onLoad() { - const info = wx.getAccountInfoSync(); - const version = info.miniProgram.version || '开发版'; - const isDevEnv = info.miniProgram.envVersion === 'develop'; - this.setData({ version, isDevEnv }); + this.syncUserInfoFromApp(); }, onShow() { - this.refreshStats(); + this.syncUserInfoFromApp(); + void this.refreshStats(); }, - refreshStats() { - const printCount = getTodayDownloadCount(); - // 收藏数待与收藏页数据源打通后接入 - const favoriteCount = 0; - this.setData({ printCount, favoriteCount }); + async syncUserInfoFromApp() { + const app = getApp(); + const cachedUser = app.globalData.user || getCachedUser(); + + if (cachedUser) { + this.applyUserInfo(cachedUser); + } + + try { + const latestUser = await getUser(); + this.applyUserInfo(latestUser); + } catch (error) { + console.error('获取用户信息失败', error); + } + }, + + applyUserInfo(user: UserInfo) { + const userName = user.nickName?.trim() || '微信用户'; + const avatarUrl = + user.avatarUrl || (this as any).DEFAULT_AVATAR_URL || ''; + this.setData({ + userName, + avatarUrl, + draftNickName: userName === '微信用户' ? '' : userName, + draftAvatarUrl: avatarUrl, + }); + }, + + async refreshStats() { + try { + const [printCount, favoriteCount] = await Promise.all([ + getDownloadLogCount(), + getFavoriteCount(), + ]); + this.setData({ printCount, favoriteCount }); + } catch (e) { + console.error('refreshStats failed', e); + } + }, + + onOpenProfileEditor() { + this.setData({ + showProfileEditor: true, + draftNickName: + this.data.userName === '微信用户' ? '' : this.data.userName, + draftAvatarUrl: this.data.avatarUrl, + }); + }, + + onCloseProfileEditor() { + if (this.data.isSavingProfile) return; + this.setData({ showProfileEditor: false }); + }, + + onChooseAvatar(e: WechatMiniprogram.CustomEvent<{ avatarUrl?: string }>) { + const avatarUrl = e.detail?.avatarUrl || ''; + if (!avatarUrl) return; + this.setData({ draftAvatarUrl: avatarUrl }); + }, + + onNicknameInput(e: WechatMiniprogram.CustomEvent<{ value: string }>) { + const nickName = (e.detail?.value || '').trim(); + this.setData({ draftNickName: nickName }); + }, + + async onSubmitProfile() { + if (this.data.isSavingProfile) return; + + const nickName = this.data.draftNickName.trim(); + if (!nickName) { + wx.showToast({ title: '请输入昵称', icon: 'none' }); + return; + } + + if (!this.data.draftAvatarUrl) { + wx.showToast({ title: '请选择头像', icon: 'none' }); + return; + } + + this.setData({ isSavingProfile: true }); + + try { + const avatarUrl = await this.persistAvatarIfNeeded( + this.data.draftAvatarUrl, + ); + const user = await updateUserProfile({ + nickName, + avatarUrl, + }); + + this.applyUserInfo(user); + this.setData({ showProfileEditor: false }); + wx.showToast({ title: '资料已更新', icon: 'success' }); + } catch (error) { + console.error('更新用户资料失败', error); + wx.showToast({ title: '更新失败,请稍后重试', icon: 'none' }); + } finally { + this.setData({ isSavingProfile: false }); + } + }, + + async persistAvatarIfNeeded(avatarUrl: string): Promise { + if (!avatarUrl) return avatarUrl; + if ( + avatarUrl.startsWith('cloud://') || + avatarUrl.startsWith('http://') || + avatarUrl.startsWith('https://') + ) { + return avatarUrl; + } + + const user = await getUser(); + const cloudPath = `user-avatars/${user._id}-${Date.now()}.png`; + const uploadRes = await wx.cloud.uploadFile({ + cloudPath, + filePath: avatarUrl, + }); + + return uploadRes.fileID; }, onGoToDownloads() { @@ -47,7 +170,7 @@ Page({ }, onGoToSettings() { - wx.showToast({ title: '功能开发中', icon: 'none' }); + wx.navigateTo({ url: '/supportPages/settings/settings' }); }, onGoToDebug() { diff --git a/miniprogram/pages/profile/profile.wxml b/miniprogram/pages/profile/profile.wxml index 7a1e326..c95c5df 100644 --- a/miniprogram/pages/profile/profile.wxml +++ b/miniprogram/pages/profile/profile.wxml @@ -8,19 +8,14 @@ - 🦆 @@ -73,9 +68,13 @@ 打印指南 - + - + @@ -97,7 +100,11 @@ 设置 - + Debug 工具 - + - @@ -123,12 +134,16 @@ 关于涂鸦丫 - - + + --> - + - Doodle Mini v{{version}} + 涂鸦丫 v{{version}} + + + + + 完善资料 + 选择头像并填写昵称后保存 + + + + + + + + 昵称 + + + + + + 取消 + + + {{isSavingProfile ? '保存中...' : '保存'}} + + + + diff --git a/miniprogram/supportPages/categoryContentManage/categoryContentManage.ts b/miniprogram/supportPages/categoryContentManage/categoryContentManage.ts index 3b60a3b..f541425 100644 --- a/miniprogram/supportPages/categoryContentManage/categoryContentManage.ts +++ b/miniprogram/supportPages/categoryContentManage/categoryContentManage.ts @@ -200,45 +200,7 @@ Page({ } }, - async onBuildTap() { - if (this.data.building) return; - - const confirmed = await new Promise((resolve) => { - wx.showModal({ - title: '更新分类页数据', - content: - '将根据当前线上 worksheet 重新生成分类页数据,确认继续?', - success: (res) => resolve(!!res.confirm), - fail: () => resolve(false), - }); - }); - if (!confirmed) return; - - this.setData({ building: true }); - - try { - const result = await callCloudFunction<{ - version: number; - stats: Record; - }>('pageContentBuild', { page: 'category' }); - - if (!result.success) { - throw new Error(result.message || '生成失败'); - } - - const version = result.data?.version || 0; - wx.showToast({ - title: `生成成功 v${version}`, - icon: 'success', - }); - } catch (error) { - wx.showToast({ - title: - error instanceof Error ? error.message : '生成失败', - icon: 'none', - }); - } finally { - this.setData({ building: false }); - } - }, + // [v3.0] 分类页已改为实时接口查询,不再需要手动更新分类数据 + // eslint-disable-next-line @typescript-eslint/no-empty-function + onBuildTap() {}, }); diff --git a/miniprogram/supportPages/categoryContentManage/categoryContentManage.wxml b/miniprogram/supportPages/categoryContentManage/categoryContentManage.wxml index db6a8ee..5d77afd 100644 --- a/miniprogram/supportPages/categoryContentManage/categoryContentManage.wxml +++ b/miniprogram/supportPages/categoryContentManage/categoryContentManage.wxml @@ -90,7 +90,7 @@ - + diff --git a/miniprogram/supportPages/guide/guide.json b/miniprogram/supportPages/guide/guide.json index 1e5d362..6bfa0dd 100644 --- a/miniprogram/supportPages/guide/guide.json +++ b/miniprogram/supportPages/guide/guide.json @@ -1,6 +1,11 @@ { + "navigationStyle": "custom", "navigationBarTitleText": "打印指南", "navigationBarTextStyle": "black", - "navigationBarBackgroundColor": "#f7ee47", - "backgroundColor": "#F6F6F6" + "navigationBarBackgroundColor": "#F8F0E0", + "backgroundColor": "#F8F0E0", + "usingComponents": { + "van-icon": "@vant/weapp/icon/index", + "nav-bar": "../../components3.0/nav-bar/nav-bar" + } } diff --git a/miniprogram/supportPages/guide/guide.less b/miniprogram/supportPages/guide/guide.less index a737437..dbecdc6 100644 --- a/miniprogram/supportPages/guide/guide.less +++ b/miniprogram/supportPages/guide/guide.less @@ -1,45 +1,105 @@ -.guide-page { - min-height: 100vh; - background: #f6f6f6; - padding-bottom: 48rpx; +@import '../../style/theme.less'; + +page { + min-height: 100%; + background: @bg-header; } +.guide-page { + min-height: 100vh; + background: @bg-header; + box-sizing: border-box; +} + +.guide-page__body { + padding: @section-gap-xs @page-padding-x; + padding-bottom: calc(48rpx + env(safe-area-inset-bottom)); + box-sizing: border-box; +} + +/* Hero 主卡(参考 profile-hero) */ .guide-page__hero { - background: linear-gradient(180deg, #f7ee47 0%, #e6d520 100%); - padding: 60rpx 40rpx; + position: relative; display: flex; flex-direction: column; align-items: center; + padding: 80rpx @page-padding-inner-x 56rpx; + background: @bg-white; + border-radius: @radius-xl; + border: 1rpx solid rgba(124, 118, 106, 0.12); + box-shadow: @shadow; + overflow: hidden; + margin-bottom: @section-gap-xs; +} + +.guide-page__hero-tint { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 192rpx; + background: @brand; + opacity: 0.1; + pointer-events: none; +} + +.guide-page__hero-icon-wrap { + position: relative; + width: 144rpx; + height: 144rpx; + border-radius: 50%; + background: @brand; + border: 8rpx solid @bg-white; + box-shadow: @shadow; + display: flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + margin-top: 16rpx; } .guide-page__hero-icon { - font-size: 80rpx; - margin-bottom: 24rpx; + font-size: 72rpx; + line-height: 1; } .guide-page__hero-title { + margin-top: 32rpx; font-size: 40rpx; - font-weight: 700; - color: #333; + font-weight: 800; + color: @text-title; + text-align: center; + line-height: 1.2; } +.guide-page__hero-sub { + margin-top: 16rpx; + font-size: 24rpx; + font-weight: 600; + color: @text-secondary; + text-align: center; + letter-spacing: 0.04em; +} + +/* 步骤时间线卡片 */ .guide-page__steps { position: relative; - padding: 40rpx 32rpx 24rpx 32rpx; - margin: 0 24rpx; - margin-top: -24rpx; - background: #fff; - border-radius: 24rpx; - box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.06); + padding: 56rpx 40rpx 16rpx; + background: @bg-white; + border-radius: @radius-xl; + border: 1rpx solid rgba(124, 118, 106, 0.12); + box-shadow: @shadow; + margin-bottom: @section-gap-xs; + box-sizing: border-box; } .guide-page__timeline-line { position: absolute; - left: 62rpx; - top: 100rpx; - bottom: 100rpx; + left: 70rpx; + top: 110rpx; + bottom: 110rpx; width: 4rpx; - background: #eee; + background: @bg-gray; z-index: 0; } @@ -48,62 +108,66 @@ z-index: 1; display: flex; flex-direction: row; - margin-bottom: 48rpx; + margin-bottom: 56rpx; } .guide-page__step--last { - margin-bottom: 0; + margin-bottom: 40rpx; } .guide-page__step-num-wrap { flex-shrink: 0; width: 64rpx; - margin-right: 24rpx; + margin-right: 32rpx; display: flex; justify-content: center; } .guide-page__step-num { - width: 60rpx; - height: 60rpx; + width: 64rpx; + height: 64rpx; border-radius: 50%; - background: #f7ee47; - font-size: 28rpx; - font-weight: 700; - color: #333; + background: @brand; + font-size: 30rpx; + font-weight: 800; + color: @text-selected-btn; display: flex; align-items: center; justify-content: center; + box-shadow: @shadow; } .guide-page__step-body { flex: 1; padding-top: 4rpx; + min-width: 0; } .guide-page__step-title { display: block; font-size: 32rpx; - font-weight: 600; - color: #333; - margin-bottom: 12rpx; + font-weight: 700; + color: @text-title; + margin-bottom: 16rpx; } .guide-page__step-desc { display: block; - font-size: 28rpx; - color: #666; - line-height: 1.55; - margin-bottom: 16rpx; + font-size: 26rpx; + color: @text-secondary; + line-height: 1.6; + margin-bottom: 20rpx; } +/* 通用 Tip(弱提示) */ .guide-page__tip { - background: #fff8e1; - border-radius: 16rpx; + background: @bg-header; + border-radius: @radius; padding: 20rpx 24rpx; - font-size: 26rpx; - color: #666; - line-height: 1.5; + font-size: 24rpx; + color: @text-secondary; + line-height: 1.55; + border: 1rpx solid rgba(124, 118, 106, 0.12); } .guide-page__tip-line { @@ -115,36 +179,114 @@ margin-bottom: 0; } -.guide-page__faq-title { - font-size: 32rpx; +/* 相册权限提示卡 */ +.guide-page__perm { + display: flex; + flex-direction: row; + align-items: flex-start; + padding: 24rpx 28rpx; + border-radius: @radius; + background: fade(@brand, 18%); + border: 1rpx solid fade(@brand, 36%); + box-sizing: border-box; +} + +.guide-page__perm--granted { + background: fade(@brand, 12%); + border-color: fade(@brand, 28%); +} + +.guide-page__perm--denied { + background: fade(@brand, 22%); + border-color: fade(@brand, 50%); +} + +.guide-page__perm-icon-wrap { + flex-shrink: 0; + width: 56rpx; + height: 56rpx; + border-radius: 50%; + background: @bg-white; + display: flex; + align-items: center; + justify-content: center; + margin-right: 20rpx; + box-shadow: @shadow; +} + +.guide-page__perm-body { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; +} + +.guide-page__perm-title { + display: block; + font-size: 26rpx; font-weight: 700; - color: #333; - padding: 40rpx 32rpx 16rpx; + color: @text-title; + line-height: 1.4; +} + +.guide-page__perm-desc { + display: block; + margin-top: 8rpx; + font-size: 24rpx; + color: @text-secondary; + line-height: 1.55; +} + +.guide-page__perm-btn { + align-self: flex-start; + margin-top: 20rpx; + padding: 14rpx 32rpx; + border-radius: 999rpx; + background: @brand; + color: @text-selected-btn; + font-size: 24rpx; + font-weight: 700; + box-shadow: @shadow; + line-height: 1; +} + +/* FAQ */ +.guide-page__faq-title { + font-size: @fs-section-head-title; + font-weight: 800; + color: @text-title; + padding: 16rpx 8rpx 24rpx; } .guide-page__faq { - padding: 0 24rpx; + display: flex; + flex-direction: column; } .guide-page__faq-item { - background: #fff; - border-radius: 16rpx; - padding: 28rpx 32rpx; - margin-bottom: 20rpx; - box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04); + background: @bg-white; + border-radius: @radius-lg; + border: 1rpx solid rgba(124, 118, 106, 0.12); + padding: 32rpx 36rpx; + margin-bottom: 24rpx; + box-sizing: border-box; +} + +.guide-page__faq-item--last { + margin-bottom: 0; } .guide-page__faq-q { display: block; font-size: 28rpx; font-weight: 700; - color: #333; + color: @text-title; margin-bottom: 12rpx; } .guide-page__faq-a { display: block; font-size: 26rpx; - color: #999; - line-height: 1.55; -} + color: @text-secondary; + line-height: 1.6; +} \ No newline at end of file diff --git a/miniprogram/supportPages/guide/guide.ts b/miniprogram/supportPages/guide/guide.ts index 7cf30bb..0a05e19 100644 --- a/miniprogram/supportPages/guide/guide.ts +++ b/miniprogram/supportPages/guide/guide.ts @@ -1,7 +1,75 @@ +type AlbumAuthState = 'unknown' | 'granted' | 'denied'; + +const ALBUM_SCOPE = 'scope.writePhotosAlbum'; + Page({ - data: {}, + data: { + albumAuthState: 'unknown' as AlbumAuthState, + }, onLoad() { - // 静态说明页,无额外逻辑 + this.refreshAlbumAuthState(); + }, + + onShow() { + this.refreshAlbumAuthState(); + }, + + refreshAlbumAuthState() { + wx.getSetting({ + success: (res) => { + const auth = res.authSetting?.[ALBUM_SCOPE]; + let state: AlbumAuthState = 'unknown'; + if (auth === true) { + state = 'granted'; + } else if (auth === false) { + state = 'denied'; + } + this.setData({ albumAuthState: state }); + }, + fail: () => { + this.setData({ albumAuthState: 'unknown' }); + }, + }); + }, + + onHandleAlbumPerm() { + const { albumAuthState } = this.data; + + // 已被拒绝过:只能去系统设置打开 + if (albumAuthState === 'denied') { + wx.openSetting({ + success: (res) => { + const granted = !!res.authSetting?.[ALBUM_SCOPE]; + this.setData({ + albumAuthState: granted ? 'granted' : 'denied', + }); + if (granted) { + wx.showToast({ + title: '已开启相册权限', + icon: 'success', + duration: 1500, + }); + } + }, + }); + return; + } + + // 未授权过:直接发起授权请求 + wx.authorize({ + scope: ALBUM_SCOPE, + success: () => { + this.setData({ albumAuthState: 'granted' }); + wx.showToast({ + title: '已开启相册权限', + icon: 'success', + duration: 1500, + }); + }, + fail: () => { + this.setData({ albumAuthState: 'denied' }); + }, + }); }, }); diff --git a/miniprogram/supportPages/guide/guide.wxml b/miniprogram/supportPages/guide/guide.wxml index 321ea7b..f879b27 100644 --- a/miniprogram/supportPages/guide/guide.wxml +++ b/miniprogram/supportPages/guide/guide.wxml @@ -1,63 +1,132 @@ - - 🖨️ - 3 步完成打印 - + - - - - - - 1 - - - 保存到相册 - 在练习纸预览页点击「保存」或「保存到相册」,授权后即可保存图片。 - 提示:建议先关闭深色模式截图,打印对比度更好。 + + + + + + 🖨️ + 3 步完成打印 + 保存图片 → 连接打印机 → 一键打印 - - - 2 - - - 连接打印机 - 确保打印机与手机处于同一 Wi-Fi,或按打印机说明完成蓝牙配对。 - 家用喷墨 / 激光打印机均可;A4 纸竖向打印即可。 - - + + + - - - 3 + + + 1 + + + 保存到相册 + + 在练习纸预览页点击「保存到相册」,授权后即可下载图片到手机相册。 + + + + + + + + + + + {{albumAuthState === 'granted' ? + '已开启相册权限' : albumAuthState === 'denied' ? + '相册权限未开启' : '检查相册权限'}} + {{albumAuthState === 'granted' ? + '可正常将练习纸保存到手机相册。' : + albumAuthState === 'denied' ? + '需要在系统设置中开启「保存到相册」权限,才能下载图片。' + : + '请确认是否已授权小程序保存图片到相册。'}} + {{albumAuthState === 'denied' ? '前往设置开启' + : '立即检查授权'}} + + + - - 从相册打印 - 打开手机相册,选中刚保存的练习纸图片,点击分享或打印。 - - iPhone:分享 → 打印 → 选择打印机。 - Android:更多 → 打印 或 使用系统相册自带的打印入口。 + + + + 2 + + + 连接打印机 + + 确保打印机与手机处于同一 + Wi-Fi,或按打印机说明完成蓝牙配对。 + + + 家用喷墨 / 激光打印机均可,A4 纸竖向打印即可。 + + + + + + + 3 + + + 从相册打印 + + 打开手机相册,选中刚保存的练习纸图片,点击分享或打印。 + + + + iPhone:分享 → 打印 → 选择打印机。 + + + Android:更多 → 打印,或使用系统相册自带的打印入口。 + + - - 常见问题 + + 常见问题 - - - 保存失败怎么办? - 请在小程序设置中开启相册写入权限,并检查手机存储空间是否充足。 - - - 打印出来太小/太大? - 在打印预览中选择「适应页面」或调整缩放比例,通常 100% 即可。 - - - 没有打印机? - 可将图片发到电脑或拷贝至 U 盘,在打印店完成打印。 + + + 保存失败怎么办? + + 请在小程序设置中开启相册写入权限,并检查手机存储空间是否充足。 + + + + 没有打印机? + + 可将图片发到电脑或拷贝至 U 盘,在打印店完成打印。 + + diff --git a/miniprogram/supportPages/settings/settings.json b/miniprogram/supportPages/settings/settings.json new file mode 100644 index 0000000..a120e8e --- /dev/null +++ b/miniprogram/supportPages/settings/settings.json @@ -0,0 +1,10 @@ +{ + "navigationStyle": "custom", + "navigationBarTitleText": "设置", + "navigationBarTextStyle": "black", + "navigationBarBackgroundColor": "#F8F0E0", + "backgroundColor": "#F8F0E0", + "usingComponents": { + "nav-bar": "../../components3.0/nav-bar/nav-bar" + } +} diff --git a/miniprogram/supportPages/settings/settings.less b/miniprogram/supportPages/settings/settings.less new file mode 100644 index 0000000..b53ce9c --- /dev/null +++ b/miniprogram/supportPages/settings/settings.less @@ -0,0 +1,59 @@ +@import '../../style/theme.less'; + +page { + min-height: 100%; + background: @bg-header; +} + +.settings-page { + min-height: 100vh; + background: @bg-header; + box-sizing: border-box; +} + +.settings-body { + padding: @section-gap-xs @page-padding-x; + box-sizing: border-box; +} + +.settings-section { + background: @bg-white; + border-radius: @radius-lg; + border: 1rpx solid rgba(124, 118, 106, 0.12); + box-shadow: @shadow; + overflow: hidden; +} + +.settings-action { + display: flex; + align-items: center; + padding: 36rpx @page-padding-inner-x; + position: relative; + + &::after { + content: ''; + position: absolute; + left: @page-padding-inner-x; + right: @page-padding-inner-x; + bottom: 0; + height: 1rpx; + background: rgba(50, 46, 37, 0.08); + } + + &:last-child::after { + display: none; + } + + &:active { + background: rgba(50, 46, 37, 0.04); + } +} + +.settings-action__text { + font-size: 30rpx; + color: @text-primary; +} + +.settings-action--danger .settings-action__text { + color: #e53e3e; +} diff --git a/miniprogram/supportPages/settings/settings.ts b/miniprogram/supportPages/settings/settings.ts new file mode 100644 index 0000000..1c27f3f --- /dev/null +++ b/miniprogram/supportPages/settings/settings.ts @@ -0,0 +1,74 @@ +import { clearUser } from '../../utils/auth'; + +Page({ + onClearProfile() { + wx.showModal({ + title: '确认删除', + content: '删除后头像和昵称将恢复为默认状态,是否继续?', + confirmColor: '#e53e3e', + success: (res) => { + if (res.confirm) { + this.doClearProfile(); + } + }, + }); + }, + + async doClearProfile() { + wx.showLoading({ title: '处理中...', mask: true }); + try { + const res = await wx.cloud.callFunction({ + name: 'userSettings', + data: { action: 'clearProfile' }, + }); + const result = (res?.result as any) || {}; + if (result.code !== 0) { + throw new Error(result.message || '操作失败'); + } + clearUser(); + wx.hideLoading(); + wx.showToast({ title: '已删除', icon: 'success' }); + } catch (e) { + wx.hideLoading(); + console.error('clearProfile failed', e); + wx.showToast({ title: '操作失败,请稍后重试', icon: 'none' }); + } + }, + + onDeleteAccount() { + wx.showModal({ + title: '注销账户', + content: + '注销后您的所有数据(包括收藏、下载记录等)将被永久删除且无法恢复,确认要注销吗?', + confirmText: '确认注销', + confirmColor: '#e53e3e', + success: (res) => { + if (res.confirm) { + this.doDeleteAccount(); + } + }, + }); + }, + + async doDeleteAccount() { + wx.showLoading({ title: '处理中...', mask: true }); + try { + const res = await wx.cloud.callFunction({ + name: 'userSettings', + data: { action: 'deleteAccount' }, + }); + const result = (res?.result as any) || {}; + if (result.code !== 0) { + throw new Error(result.message || '操作失败'); + } + clearUser(); + wx.hideLoading(); + + wx.reLaunch({ url: '/pages/home/home' }); + } catch (e) { + wx.hideLoading(); + console.error('deleteAccount failed', e); + wx.showToast({ title: '操作失败,请稍后重试', icon: 'none' }); + } + }, +}); diff --git a/miniprogram/supportPages/settings/settings.wxml b/miniprogram/supportPages/settings/settings.wxml new file mode 100644 index 0000000..631f5f7 --- /dev/null +++ b/miniprogram/supportPages/settings/settings.wxml @@ -0,0 +1,16 @@ + + + + + + + 删除头像和昵称 + + + 注销账户 + + + + diff --git a/miniprogram/utils/auth.ts b/miniprogram/utils/auth.ts index 5eed2b0..0bac1d7 100644 --- a/miniprogram/utils/auth.ts +++ b/miniprogram/utils/auth.ts @@ -19,18 +19,40 @@ let currentUser: UserInfo | null = null; /** 登录进行中的 Promise(防止并发重复调用) */ let loginPromise: Promise | null = null; +function syncAppGlobalUser(user: UserInfo | null): void { + try { + const app = getApp(); + if (app?.globalData) { + app.globalData.user = user; + } + } catch { + // App 尚未初始化时忽略 + } +} + +function syncUserState(user: UserInfo, persist = true): UserInfo { + currentUser = user; + if (persist) { + saveToStorage(user); + } + syncAppGlobalUser(user); + return user; +} + /** * 获取当前用户,优先内存 → Storage → 云函数登录 */ export async function getUser(): Promise { // 1. 内存中有,直接返回 - if (currentUser) return currentUser; + if (currentUser) { + syncAppGlobalUser(currentUser); + return currentUser; + } // 2. 尝试从 Storage 读取 const cached = loadFromStorage(); if (cached) { - currentUser = cached; - return cached; + return syncUserState(cached, false); } // 3. 静默登录(防止并发) @@ -56,9 +78,18 @@ async function silentLogin(): Promise { } const user = result.data as UserInfo; - currentUser = user; - saveToStorage(user); - return user; + return syncUserState(user); +} + +export function getCachedUser(): UserInfo | null { + if (currentUser) { + syncAppGlobalUser(currentUser); + return currentUser; + } + + const cached = loadFromStorage(); + if (!cached) return null; + return syncUserState(cached, false); } /** @@ -96,6 +127,7 @@ function saveToStorage(user: UserInfo): void { */ export function clearUser(): void { currentUser = null; + syncAppGlobalUser(null); wx.removeStorageSync(STORAGE_KEY); wx.removeStorageSync(STORAGE_EXPIRE_KEY); } @@ -108,4 +140,26 @@ export async function refreshUser(): Promise { return await silentLogin(); } +export async function updateUserProfile(payload: { + nickName?: string | null; + avatarUrl?: string | null; +}): Promise { + await getUser(); + + const res = await wx.cloud.callFunction({ + name: 'userLogin', + data: { + action: 'updateProfile', + ...payload, + }, + }); + const result = (res?.result as any) || {}; + + if (result.code !== 0 || !result.data) { + throw new Error(result.message || '更新用户资料失败'); + } + + return syncUserState(result.data as UserInfo); +} + export type { UserInfo }; diff --git a/miniprogram/utils/downloadLogs.ts b/miniprogram/utils/downloadLogs.ts index 7744e85..e62701e 100644 --- a/miniprogram/utils/downloadLogs.ts +++ b/miniprogram/utils/downloadLogs.ts @@ -54,3 +54,21 @@ export async function getDownloadLogs( return []; } } + +/** + * 获取当前用户下载(打印)总数 + */ +export async function getDownloadLogCount(): Promise { + try { + await getUser(); + const res = await wx.cloud.callFunction({ + name: 'userDownloadLogs', + data: { action: 'count' }, + }); + const result = (res?.result as any) || {}; + return result.code === 0 ? Number(result.data?.count || 0) : 0; + } catch (e) { + console.error('getDownloadLogCount failed', e); + return 0; + } +} diff --git a/miniprogram/utils/favorites.ts b/miniprogram/utils/favorites.ts index 4605d52..67ef595 100644 --- a/miniprogram/utils/favorites.ts +++ b/miniprogram/utils/favorites.ts @@ -76,6 +76,24 @@ export async function getFavoriteList( } } +/** + * 获取当前用户收藏总数 + */ +export async function getFavoriteCount(): Promise { + try { + await getUser(); + const res = await wx.cloud.callFunction({ + name: 'userFavorites', + data: { action: 'count' }, + }); + const result = (res?.result as any) || {}; + return result.code === 0 ? Number(result.data?.count || 0) : 0; + } catch (e) { + console.error('getFavoriteCount failed', e); + return 0; + } +} + /** * 检查是否已收藏 */ diff --git a/project.private.config.json b/project.private.config.json index 34b8aeb..9a98dc0 100644 --- a/project.private.config.json +++ b/project.private.config.json @@ -24,12 +24,26 @@ "miniprogram": { "list": [ { - "name": "pages/age/age", - "pathName": "pages/age/age", + "name": "pages/profile/profile", + "pathName": "pages/profile/profile", "query": "", "scene": null, "launchMode": "default" }, + { + "name": "supportPages/index/index", + "pathName": "supportPages/index/index", + "query": "", + "launchMode": "default", + "scene": null + }, + { + "name": "pages/age/age", + "pathName": "pages/age/age", + "query": "", + "launchMode": "default", + "scene": null + }, { "name": "supportPages/debug/debug", "pathName": "supportPages/debug/debug", diff --git a/typings/index.d.ts b/typings/index.d.ts index 66fba68..4bb4107 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -21,6 +21,7 @@ interface IAppOption { globalData: { uuid: string; userInfo?: WechatMiniprogram.UserInfo; + user?: import('../miniprogram/utils/auth').UserInfo | null; env: string; printConfig?: PrintConfig; pageConfig?: PageConfigData;