feat: 我的、设置、打印指南页面开发完成
This commit is contained in:
@@ -287,6 +287,10 @@ exports.main = async (event) => {
|
|||||||
if (page === 'home') {
|
if (page === 'home') {
|
||||||
const { homeData } = await buildHomeData(db, homeEvent);
|
const { homeData } = await buildHomeData(db, homeEvent);
|
||||||
config.home = homeData;
|
config.home = homeData;
|
||||||
|
// [v3.0] 清空 category/age 字段,分类页已改为实时接口查询
|
||||||
|
// 如需恢复静态配置,注释掉以下两行即可
|
||||||
|
config.category = null;
|
||||||
|
config.age = null;
|
||||||
config.version = (config.version || 0) + 1;
|
config.version = (config.version || 0) + 1;
|
||||||
config.updatedAt = new Date().toISOString();
|
config.updatedAt = new Date().toISOString();
|
||||||
|
|
||||||
@@ -307,7 +311,10 @@ exports.main = async (event) => {
|
|||||||
if (page === 'all') {
|
if (page === 'all') {
|
||||||
const { categoryData, stats } = await buildCategoryData(db);
|
const { categoryData, stats } = await buildCategoryData(db);
|
||||||
const { homeData } = await buildHomeData(db, homeEvent);
|
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.home = homeData;
|
||||||
config.version = (config.version || 0) + 1;
|
config.version = (config.version || 0) + 1;
|
||||||
config.updatedAt = new Date().toISOString();
|
config.updatedAt = new Date().toISOString();
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ exports.main = async (event) => {
|
|||||||
return await handleAdd(userId, event);
|
return await handleAdd(userId, event);
|
||||||
case 'list':
|
case 'list':
|
||||||
return await handleList(userId, event);
|
return await handleList(userId, event);
|
||||||
|
case 'count':
|
||||||
|
return await handleCount(userId);
|
||||||
default:
|
default:
|
||||||
return { code: -1, message: '未知操作' };
|
return { code: -1, message: '未知操作' };
|
||||||
}
|
}
|
||||||
@@ -96,3 +98,9 @@ async function handleList(userId, event) {
|
|||||||
|
|
||||||
return { code: 0, data: result };
|
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 } };
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ exports.main = async (event) => {
|
|||||||
return await handleRemove(userId, event);
|
return await handleRemove(userId, event);
|
||||||
case 'list':
|
case 'list':
|
||||||
return await handleList(userId, event);
|
return await handleList(userId, event);
|
||||||
|
case 'count':
|
||||||
|
return await handleCount(userId);
|
||||||
case 'check':
|
case 'check':
|
||||||
return await handleCheck(userId, event);
|
return await handleCheck(userId, event);
|
||||||
case 'batchCheck':
|
case 'batchCheck':
|
||||||
@@ -153,6 +155,12 @@ async function handleList(userId, event) {
|
|||||||
return { code: 0, data: result };
|
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) {
|
async function handleCheck(userId, event) {
|
||||||
const { worksheetId } = event;
|
const { worksheetId } = event;
|
||||||
if (!worksheetId) return { code: -1, message: 'worksheetId 不能为空' };
|
if (!worksheetId) return { code: -1, message: 'worksheetId 不能为空' };
|
||||||
|
|||||||
@@ -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) {
|
async function handleSilentLogin(openid, unionid) {
|
||||||
const usersCollection = db.collection('users');
|
const usersCollection = db.collection('users');
|
||||||
|
|
||||||
@@ -32,10 +46,13 @@ async function handleSilentLogin(openid, unionid) {
|
|||||||
updateData.unionid = unionid;
|
updateData.unionid = unionid;
|
||||||
}
|
}
|
||||||
await usersCollection.doc(user._id).update({ data: updateData });
|
await usersCollection.doc(user._id).update({ data: updateData });
|
||||||
|
const latest = (
|
||||||
|
await usersCollection.doc(user._id).get()
|
||||||
|
).data;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
code: 0,
|
code: 0,
|
||||||
data: { ...user, ...updateData, lastActiveAt: new Date() },
|
data: serializeUser(latest),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
const newUser = {
|
const newUser = {
|
||||||
@@ -48,15 +65,11 @@ async function handleSilentLogin(openid, unionid) {
|
|||||||
lastActiveAt: now,
|
lastActiveAt: now,
|
||||||
};
|
};
|
||||||
const { _id } = await usersCollection.add({ data: newUser });
|
const { _id } = await usersCollection.add({ data: newUser });
|
||||||
|
const latest = (await usersCollection.doc(_id).get()).data;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
code: 0,
|
code: 0,
|
||||||
data: {
|
data: serializeUser(latest),
|
||||||
_id,
|
|
||||||
...newUser,
|
|
||||||
createdAt: new Date(),
|
|
||||||
lastActiveAt: new Date(),
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,11 +87,13 @@ async function handleUpdateProfile(event, openid) {
|
|||||||
const updateData = {};
|
const updateData = {};
|
||||||
if (nickName !== undefined) updateData.nickName = nickName;
|
if (nickName !== undefined) updateData.nickName = nickName;
|
||||||
if (avatarUrl !== undefined) updateData.avatarUrl = avatarUrl;
|
if (avatarUrl !== undefined) updateData.avatarUrl = avatarUrl;
|
||||||
|
updateData.lastActiveAt = db.serverDate();
|
||||||
|
|
||||||
await usersCollection.doc(data[0]._id).update({ data: updateData });
|
await usersCollection.doc(data[0]._id).update({ data: updateData });
|
||||||
|
const latest = (await usersCollection.doc(data[0]._id).get()).data;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
code: 0,
|
code: 0,
|
||||||
data: { ...data[0], ...updateData },
|
data: serializeUser(latest),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: '账户已注销' };
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "user-settings",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"dependencies": {
|
||||||
|
"wx-server-sdk": "^3.0.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
+138
-104
@@ -1,6 +1,6 @@
|
|||||||
# 页面内容管理方案
|
# 页面内容管理方案
|
||||||
|
|
||||||
> 版本:v2.0 | 最后更新:2026-04-30
|
> 版本:v3.0 | 最后更新:2026-05-07
|
||||||
> 配套文档:[Worksheet 发布方案](./Worksheet发布方案.md) | [小程序云开发方案](./小程序云开发方案.md)
|
> 配套文档:[Worksheet 发布方案](./Worksheet发布方案.md) | [小程序云开发方案](./小程序云开发方案.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -15,7 +15,11 @@ worksheet 已可通过 Debug 发布进入云数据库,但三个展示页仍使
|
|||||||
| 分类页 | `pages/category/category.data.ts` | 新 worksheet 发布后不会自动出现 |
|
| 分类页 | `pages/category/category.data.ts` | 新 worksheet 发布后不会自动出现 |
|
||||||
| 分龄页 | `pages/age/age.ts` 内 Mock | 推荐内容和周路线无法运营配置 |
|
| 分龄页 | `pages/age/age.ts` 内 Mock | 推荐内容和周路线无法运营配置 |
|
||||||
|
|
||||||
**目标:** 通过 Debug 管理页配置三个页面的内容,生成统一配置写入云数据库并同步至云存储,小程序启动时通过**数据预拉取**获取并渲染。
|
**目标:**
|
||||||
|
|
||||||
|
- **首页**:通过 Debug 管理页配置内容,生成配置写入云数据库并同步至云存储,小程序启动时通过数据预拉取获取并渲染
|
||||||
|
- **分类页**:每次进入时通过云函数实时查询所有已上线 worksheet,前端分组排序展示(v3.0 调整,因需展示收藏/下载等实时数据)
|
||||||
|
- **分龄页**:独立方案(详见分龄页文档)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -23,18 +27,24 @@ worksheet 已可通过 Debug 发布进入云数据库,但三个展示页仍使
|
|||||||
|
|
||||||
### 2.1 数据存储与同步
|
### 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
|
```ts
|
||||||
// page_configs/current 文档结构
|
// page_configs/current 文档结构
|
||||||
type PageConfig = {
|
type PageConfig = {
|
||||||
home: HomePageData; // 首页配置
|
home: HomePageData; // 首页配置
|
||||||
category: CategoryPageData; // 分类页配置
|
// category: 已移除,改为实时接口查询
|
||||||
age: AgePageData; // 分龄页配置
|
// age: 已移除,改为独立方案
|
||||||
version: number; // 版本号,每次更新递增
|
version: number; // 版本号,每次更新递增
|
||||||
updatedAt: string; // 最后更新时间
|
updatedAt: string; // 最后更新时间
|
||||||
};
|
};
|
||||||
@@ -44,7 +54,7 @@ type PageConfig = {
|
|||||||
|
|
||||||
```
|
```
|
||||||
readCurrentConfig() → 从 page_configs/current 读取当前完整配置
|
readCurrentConfig() → 从 page_configs/current 读取当前完整配置
|
||||||
config[page] = data → 只覆盖对应页面字段(home / category / age)
|
config.home = data → 只覆盖首页字段
|
||||||
saveConfig(config) → 写回数据库 → 上传云存储 content/page-config.json
|
saveConfig(config) → 写回数据库 → 上传云存储 content/page-config.json
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -60,16 +70,18 @@ saveConfig(config) → 写回数据库 → 上传云存储 content/page-config
|
|||||||
│ │ Manage │ │ Manage │ │ Manage │ │
|
│ │ Manage │ │ Manage │ │ Manage │ │
|
||||||
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
|
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
|
||||||
│ │ │ │ │
|
│ │ │ │ │
|
||||||
│ └────────┬─────────┘─────────┬─────────┘ │
|
│ ▼ ▼ ▼ │
|
||||||
│ ▼ ▼ │
|
│ 点击「更新首页」→ 仅管理上下线状态 待开发 │
|
||||||
│ 点击「更新」按钮 → 调用云函数 pageContentBuild │
|
│ 调用 pageContentBuild (不再生成分类页配置) │
|
||||||
│ │ │
|
│ 只更新 home 配置 │
|
||||||
│ ▼ │
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
│ ┌─────────────────────────────────────────┐ │
|
│ ┌─────────────────────────────────────────┐ │
|
||||||
│ │ 1. 从 page_configs/current 读取当前配置 │ │
|
│ │ 1. 从 page_configs/current 读取当前配置 │ │
|
||||||
│ │ 2. 只更新对应页面字段(home/category/age) │ │
|
│ │ 2. 更新 home 字段 │ │
|
||||||
│ │ 3. version++ → 写回数据库 │ │
|
│ │ 3. 清空 category / age 字段(注释保留) │ │
|
||||||
│ │ 4. 同步上传至云存储 content/page-config.json│ │
|
│ │ 4. version++ → 写回数据库 │ │
|
||||||
|
│ │ 5. 同步上传至云存储 content/page-config.json│ │
|
||||||
│ └─────────────────────────────────────────┘ │
|
│ └─────────────────────────────────────────┘ │
|
||||||
└──────────────────────────────────────────────────────────────────┘
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
@@ -86,22 +98,22 @@ saveConfig(config) → 写回数据库 → 上传云存储 content/page-config
|
|||||||
│ └─ 未命中 → 调用 pageConfigFetch 云函数兜底 │
|
│ └─ 未命中 → 调用 pageConfigFetch 云函数兜底 │
|
||||||
│ └─ 成功 → 存入 globalData + storage │
|
│ └─ 成功 → 存入 globalData + storage │
|
||||||
│ │
|
│ │
|
||||||
│ TabBar 页面 onLoad(首页 / 分类页 / 分龄页) │
|
│ 首页 onLoad │
|
||||||
│ ├─ 配置已就绪 → 从 globalData.pageConfig 读取对应字段 → 渲染 │
|
│ └─ 从 globalData.pageConfig.home 读取 → 渲染 │
|
||||||
│ └─ 配置未就绪 → 显示加载态 → 监听配置就绪回调 → 渲染 │
|
|
||||||
│ │
|
│ │
|
||||||
│ TabBar 页面 onShow(后续切换) │
|
│ 分类页 onShow(每次进入都重新请求) │
|
||||||
│ └─ 直接从 globalData.pageConfig 读取,零延迟 │
|
│ └─ 调用 worksheetsQuery({ status: 'active' }) │
|
||||||
|
│ → 前端按分类分组 + 排序 → 渲染 │
|
||||||
└──────────────────────────────────────────────────────────────────┘
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2.3 数据预拉取
|
### 2.3 数据预拉取
|
||||||
|
|
||||||
小程序数据预拉取通过云函数 `pageConfigFetch` 实现:
|
小程序数据预拉取通过云函数 `pageConfigFetch` 实现(仅用于首页数据):
|
||||||
|
|
||||||
- 在 `app.json` 中配置 `fetchDataUrl` 指向 `pageConfigFetch` 云函数
|
- 在 `app.json` 中配置 `fetchDataUrl` 指向 `pageConfigFetch` 云函数
|
||||||
- 小程序冷启动时由微信客户端自动调用,不占用页面加载时间
|
- 小程序冷启动时由微信客户端自动调用,不占用页面加载时间
|
||||||
- 云函数从 `page_configs/current` 读取完整配置并返回
|
- 云函数从 `page_configs/current` 读取配置并返回(主要是 home 字段)
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// cloudfunctions/pageConfigFetch/index.js
|
// cloudfunctions/pageConfigFetch/index.js
|
||||||
@@ -114,39 +126,37 @@ exports.main = async () => {
|
|||||||
|
|
||||||
### 2.4 页面加载策略
|
### 2.4 页面加载策略
|
||||||
|
|
||||||
三个展示页(首页、分类页、分龄页)均为 TabBar 页面,不再使用本地 `.data.ts` 静态数据渲染,改为**统一从内存读取预拉取配置**,并通过 storage 缓存保证离线可用:
|
**首页**:通过预拉取 + storage 缓存加载(保持不变)
|
||||||
|
|
||||||
```
|
```
|
||||||
app.onLaunch
|
app.onLaunch
|
||||||
│
|
│
|
||||||
├─ Step 1: wx.getBackgroundFetchData('pre') 获取预拉取结果
|
├─ Step 1: wx.getBackgroundFetchData('pre') 获取预拉取结果
|
||||||
│ 成功 → 存入 globalData.pageConfig
|
│ 成功 → 存入 globalData.pageConfig → 写入 storage 缓存
|
||||||
│ → wx.setStorageSync('pageConfig', data) 写入缓存
|
|
||||||
│ → 通知页面就绪
|
|
||||||
│
|
│
|
||||||
├─ Step 2: 预拉取失败 → wx.getStorageSync('pageConfig') 读取缓存
|
├─ Step 2: 预拉取失败 → wx.getStorageSync('pageConfig') 读取缓存
|
||||||
│ 命中 → 存入 globalData.pageConfig → 通知页面就绪
|
│ 命中 → 存入 globalData.pageConfig
|
||||||
│
|
│
|
||||||
└─ Step 3: 缓存也为空 → 调用 pageConfigFetch 云函数兜底
|
└─ Step 3: 缓存也为空 → 调用 pageConfigFetch 云函数兜底
|
||||||
成功 → 存入 globalData.pageConfig
|
成功 → 存入 globalData.pageConfig + storage 缓存
|
||||||
→ wx.setStorageSync('pageConfig', data) 写入缓存
|
|
||||||
→ 通知页面就绪
|
|
||||||
|
|
||||||
TabBar 页面 onLoad(仅首次进入触发)
|
首页 onLoad
|
||||||
│
|
└─ 从 globalData.pageConfig.home 读取 → 渲染
|
||||||
├─ globalData.pageConfig 已就绪
|
|
||||||
│ → 读取对应字段(home / category / age)→ setData 渲染
|
|
||||||
│
|
|
||||||
└─ globalData.pageConfig 未就绪
|
|
||||||
→ 显示加载态(骨架屏 / loading)
|
|
||||||
→ 注册回调,配置就绪后自动渲染
|
|
||||||
|
|
||||||
TabBar 页面 onShow(后续每次切换触发)
|
|
||||||
│
|
|
||||||
└─ 直接从 globalData.pageConfig 读取,零延迟,无网络请求
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**三级降级策略:** 预拉取 → 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 | 预拉取失败,但之前成功过 |
|
| L2 | `wx.getStorageSync('pageConfig')` | ~1ms | 预拉取失败,但之前成功过 |
|
||||||
| L3 | `pageConfigFetch` 云函数 | 200-500ms | 首次使用或缓存被清理 |
|
| 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 云函数总览
|
### 2.5 云函数总览
|
||||||
|
|
||||||
| 云函数 | 职责 |
|
| 云函数 | 职责 |
|
||||||
| ---------------------- | ------------------------------------------------- |
|
| ----------------------- | ------------------------------------------------- |
|
||||||
| `pageContentBuild` | 生成指定页面配置(home / category),写入数据库 + 云存储 |
|
| `pageContentBuild` | 生成首页配置(仅 home),写入数据库 + 云存储 |
|
||||||
| `pageConfigFetch` | 数据预拉取接口,从数据库读取完整配置返回给客户端 |
|
| `pageConfigFetch` | 数据预拉取接口,从数据库读取配置返回给客户端(首页用) |
|
||||||
| `homeAutoRefresh` | 定时任务,每日 22:00 自动更新 featured 和 hot |
|
| `homeAutoRefresh` | 定时任务,每日 22:00 自动更新 featured 和 hot |
|
||||||
| `homeTimerControl` | 暂停/启动首页定时刷新任务(更新 settings 集合标记位) |
|
| `homeTimerControl` | 暂停/启动首页定时刷新任务(更新 settings 集合标记位) |
|
||||||
| `worksheetsQuery` | 查询 worksheet 列表(支持按分类、状态筛选) |
|
| `worksheetsQuery` | 查询 worksheet 列表(支持按分类、状态筛选,分类页实时调用)|
|
||||||
| `worksheetsUpdateStatus` | 更新单个 worksheet 的状态 |
|
| `worksheetsUpdateStatus`| 更新单个 worksheet 的状态 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -263,9 +265,15 @@ CATEGORY_LIST.map(category => {
|
|||||||
})
|
})
|
||||||
→ 云函数批量查询所有引用的 worksheet
|
→ 云函数批量查询所有引用的 worksheet
|
||||||
→ 构建 home 配置(categoryTabs + ageBands + featured + hot + sections)
|
→ 构建 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 数据结构
|
### 3.6 数据结构
|
||||||
|
|
||||||
```ts
|
```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`
|
`supportPages/categoryContentManage/categoryContentManage`
|
||||||
|
|
||||||
### 4.2 管理 UI
|
分类页内容管理页**仅保留 worksheet 上下线管理功能**,不再提供「更新分类数据」按钮:
|
||||||
|
|
||||||
分类页管理顶部展示分类选择器(来源 `CATEGORY_LIST`),默认选中 `math`。
|
|
||||||
|
|
||||||
分类头部统计:
|
分类头部统计:
|
||||||
|
|
||||||
@@ -326,7 +382,9 @@ type HomeDisplaySection = {
|
|||||||
| `active` | 灰色按钮 | 下架为 hidden |
|
| `active` | 灰色按钮 | 下架为 hidden |
|
||||||
| `hidden` | 黄色按钮 | 恢复为 draft 或直接激活 |
|
| `hidden` | 黄色按钮 | 恢复为 draft 或直接激活 |
|
||||||
|
|
||||||
### 4.3 分类页支持 id 参数
|
> **已移除:**「更新分类数据」按钮及相关代码已注释,分类页数据改为实时接口查询。
|
||||||
|
|
||||||
|
### 4.4 分类页支持 id 参数
|
||||||
|
|
||||||
分类页 `/pages/category/category` 支持通过 URL 参数 `id` 指定默认选中的分类:
|
分类页 `/pages/category/category` 支持通过 URL 参数 `id` 指定默认选中的分类:
|
||||||
|
|
||||||
@@ -338,32 +396,6 @@ type HomeDisplaySection = {
|
|||||||
|
|
||||||
首页 sections 的「查看更多」链接使用此参数跳转到对应分类。
|
首页 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` 云函数(数据预拉取接口)
|
2. 实现 `pageConfigFetch` 云函数(数据预拉取接口)
|
||||||
3. 实现 `worksheetsQuery` / `worksheetsUpdateStatus` 云函数
|
3. 实现 `worksheetsQuery` / `worksheetsUpdateStatus` 云函数
|
||||||
|
|
||||||
### Phase 2:分类页管理 ✅
|
### Phase 2:分类页管理 ✅(v3.0 调整)
|
||||||
|
|
||||||
1. 实现分类页管理页(worksheet 列表、状态管理)
|
1. 实现分类页管理页(worksheet 列表、状态管理)
|
||||||
2. 实现「更新分类数据」→ 生成 category 配置
|
2. ~~实现「更新分类数据」→ 生成 category 配置~~(已移除,改为实时接口)
|
||||||
3. 分类页支持 `?id=xxx` 参数默认选中分类
|
3. 分类页支持 `?id=xxx` 参数默认选中分类
|
||||||
|
4. 分类页改为 onShow 实时查询 worksheetsQuery + 前端排序
|
||||||
|
|
||||||
### Phase 3:首页管理 ✅
|
### Phase 3:首页管理 ✅
|
||||||
|
|
||||||
@@ -432,10 +465,11 @@ supportPages/debug/debug
|
|||||||
|
|
||||||
| 风险 | 处理方式 |
|
| 风险 | 处理方式 |
|
||||||
| ----------------- | ----------------------------------------- |
|
| ----------------- | ----------------------------------------- |
|
||||||
| 预拉取失败 | 依次降级:storage 缓存 → pageConfigFetch 云函数兜底 |
|
| 首页预拉取失败 | 依次降级:storage 缓存 → pageConfigFetch 云函数兜底 |
|
||||||
|
| 分类页接口请求失败 | 显示错误提示 + 重试按钮,或降级使用本地 category.data.ts |
|
||||||
| 配置生成失败 | 云函数返回错误,管理页提示重试,线上数据不受影响 |
|
| 配置生成失败 | 云函数返回错误,管理页提示重试,线上数据不受影响 |
|
||||||
| 首页配置引用了 hidden 内容 | sections 回显时通过 wsMap 匹配,已下架的自动过滤 |
|
| 首页配置引用了 hidden 内容 | sections 回显时通过 wsMap 匹配,已下架的自动过滤 |
|
||||||
| 更新某页数据清空其他页 | 云函数从数据库读取完整配置,只覆盖对应字段,其他页面数据不受影响 |
|
| 分类页每次请求性能 | 一次查询所有 active worksheet(通常 < 100 条),前端分组排序,耗时可控 |
|
||||||
| 分龄内容不适龄 | 选择器按年龄段默认过滤 |
|
| 分龄内容不适龄 | 选择器按年龄段默认过滤 |
|
||||||
| 并发更新冲突 | 云函数使用 version 乐观锁,冲突时提示重新加载后重试 |
|
| 并发更新冲突 | 云函数使用 version 乐观锁,冲突时提示重新加载后重试 |
|
||||||
|
|
||||||
@@ -446,20 +480,20 @@ supportPages/debug/debug
|
|||||||
| 文件 / 模块 | 职责 |
|
| 文件 / 模块 | 职责 |
|
||||||
| -------------------------------------------- | ------------------------------ |
|
| -------------------------------------------- | ------------------------------ |
|
||||||
| `supportPages/homeContentManage/` | 首页内容管理页 |
|
| `supportPages/homeContentManage/` | 首页内容管理页 |
|
||||||
| `supportPages/categoryContentManage/` | 分类页内容管理页 |
|
| `supportPages/categoryContentManage/` | 分类页内容管理页(仅上下线管理) |
|
||||||
| `cloudfunctions/pageContentBuild/` | 生成 page-config 中指定页面的配置 |
|
| `cloudfunctions/pageContentBuild/` | 生成首页配置,写入数据库 + 云存储 |
|
||||||
| `cloudfunctions/pageConfigFetch/` | 数据预拉取接口,返回完整页面配置 |
|
| `cloudfunctions/pageConfigFetch/` | 数据预拉取接口,返回首页配置 |
|
||||||
| `cloudfunctions/homeAutoRefresh/` | 定时任务,每日自动更新首页推荐位数据 |
|
| `cloudfunctions/homeAutoRefresh/` | 定时任务,每日自动更新首页推荐位数据 |
|
||||||
| `cloudfunctions/homeTimerControl/` | 暂停/启动首页定时刷新任务 |
|
| `cloudfunctions/homeTimerControl/` | 暂停/启动首页定时刷新任务 |
|
||||||
| `cloudfunctions/worksheetsQuery/` | 查询 worksheet 列表 |
|
| `cloudfunctions/worksheetsQuery/` | 查询 worksheet 列表(分类页实时调用) |
|
||||||
| `cloudfunctions/worksheetsUpdateStatus/` | 更新 worksheet 状态 |
|
| `cloudfunctions/worksheetsUpdateStatus/` | 更新 worksheet 状态 |
|
||||||
| `pages/category/category.ts` | 分类页,支持 `?id=xxx` 参数选中分类 |
|
| `pages/category/category.ts` | 分类页,onShow 实时查询 + 前端排序 |
|
||||||
| `pages/age/age.config.ts` | 分龄页配置(详见 [分龄页方案](./分龄页内容管理方案.md)) |
|
| `pages/age/age.config.ts` | 分龄页配置(详见 [分龄页方案](./分龄页内容管理方案.md)) |
|
||||||
| `skills/generate-age-abilities/SKILL.md` | AI Skill:生成分龄页能力目标 |
|
| `skills/generate-age-abilities/SKILL.md` | AI Skill:生成分龄页能力目标 |
|
||||||
| `skills/generate-age-weekly-plans/SKILL.md` | AI Skill:生成分龄页学习路线 |
|
| `skills/generate-age-weekly-plans/SKILL.md` | AI Skill:生成分龄页学习路线 |
|
||||||
| `pages/home/home.data.ts` | 首页兜底数据 |
|
| `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) |
|
| `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) |
|
| `page_configs/current`(云数据库) | 配置数据源(Source of Truth) |
|
||||||
| `settings/homeAutoRefresh`(云数据库) | 定时任务开关标记 |
|
| `settings/homeAutoRefresh`(云数据库) | 定时任务开关标记 |
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"name": "supportPages",
|
"name": "supportPages",
|
||||||
"pages": [
|
"pages": [
|
||||||
"guide/guide",
|
"guide/guide",
|
||||||
|
"settings/settings",
|
||||||
"debug/debug",
|
"debug/debug",
|
||||||
"worksheetSync/worksheetSync",
|
"worksheetSync/worksheetSync",
|
||||||
"categoryManage/categoryManage",
|
"categoryManage/categoryManage",
|
||||||
|
|||||||
+8
-3
@@ -12,6 +12,7 @@ App<IAppOption>({
|
|||||||
globalData: {
|
globalData: {
|
||||||
env: 'release',
|
env: 'release',
|
||||||
uuid: '',
|
uuid: '',
|
||||||
|
user: null,
|
||||||
printConfig: defaultPrintConfig,
|
printConfig: defaultPrintConfig,
|
||||||
pageConfig: undefined,
|
pageConfig: undefined,
|
||||||
pageConfigReady: false,
|
pageConfigReady: false,
|
||||||
@@ -41,9 +42,13 @@ App<IAppOption>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 静默登录(不阻塞页面渲染)
|
// 静默登录(不阻塞页面渲染)
|
||||||
getUser().catch((err) => {
|
getUser()
|
||||||
console.error('静默登录失败', err);
|
.then((user) => {
|
||||||
});
|
this.globalData.user = user;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('静默登录失败', err);
|
||||||
|
});
|
||||||
|
|
||||||
void this.loadPageConfig();
|
void this.loadPageConfig();
|
||||||
},
|
},
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,6 +5,13 @@
|
|||||||
"css_prefix_text": "icon-",
|
"css_prefix_text": "icon-",
|
||||||
"description": "",
|
"description": "",
|
||||||
"glyphs": [
|
"glyphs": [
|
||||||
|
{
|
||||||
|
"icon_id": "47510723",
|
||||||
|
"name": "iconify-tabler_mood-empty",
|
||||||
|
"font_class": "iconify-tabler_mood-empty",
|
||||||
|
"unicode": "e62f",
|
||||||
|
"unicode_decimal": 58927
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"icon_id": "47501281",
|
"icon_id": "47501281",
|
||||||
"name": "loading",
|
"name": "loading",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
@@ -18,6 +18,8 @@ export type CategoryItem = {
|
|||||||
downloads: number;
|
downloads: number;
|
||||||
/** 列表底部展示的日期(静态数据用 id 派生稳定值) */
|
/** 列表底部展示的日期(静态数据用 id 派生稳定值) */
|
||||||
date: string;
|
date: string;
|
||||||
|
/** 最后更新时间(云端数据用于排序) */
|
||||||
|
updatedAt?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CategoryGroup = {
|
export type CategoryGroup = {
|
||||||
|
|||||||
@@ -17,6 +17,13 @@ type CategoryDataset = {
|
|||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DIFFICULTY_LABELS: Record<number, string> = {
|
||||||
|
1: '入门',
|
||||||
|
2: '基础',
|
||||||
|
3: '进阶',
|
||||||
|
4: '挑战',
|
||||||
|
};
|
||||||
|
|
||||||
const SIDEBAR_TABS: CategoryTab[] = CATEGORY_LIST_WITH_ALL.map(
|
const SIDEBAR_TABS: CategoryTab[] = CATEGORY_LIST_WITH_ALL.map(
|
||||||
({ id, name, icon }) => ({
|
({ id, name, icon }) => ({
|
||||||
id,
|
id,
|
||||||
@@ -33,9 +40,74 @@ const TAB_BAR_PATHS = new Set([
|
|||||||
'/pages/profile/profile',
|
'/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;
|
let _categoryData: CategoryDataset | null = null;
|
||||||
|
|
||||||
|
/** 将云端 worksheet 原始数据转换为 CategoryItem */
|
||||||
|
function toDisplayItem(raw: Record<string, any>): 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<string, any>[],
|
||||||
|
): CategoryDataset {
|
||||||
|
const itemsByCategory: Record<string, CategoryItem[]> = {};
|
||||||
|
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[] {
|
function buildAllItems(): CategoryItem[] {
|
||||||
if (!_categoryData) return [];
|
if (!_categoryData) return [];
|
||||||
const items: CategoryItem[] = [];
|
const items: CategoryItem[] = [];
|
||||||
@@ -44,7 +116,7 @@ function buildAllItems(): CategoryItem[] {
|
|||||||
items.push({ ...item });
|
items.push({ ...item });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return items;
|
return sortWorksheets(items);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getItemsByCategory(categoryId: string): CategoryItem[] {
|
function getItemsByCategory(categoryId: string): CategoryItem[] {
|
||||||
@@ -69,7 +141,7 @@ function filterByKeyword(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Skeleton placeholder items for loading state
|
// 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}`,
|
id: `skeleton-${i}`,
|
||||||
title: '\u00A0',
|
title: '\u00A0',
|
||||||
subtitle: '\u00A0',
|
subtitle: '\u00A0',
|
||||||
@@ -87,45 +159,45 @@ Page({
|
|||||||
loading: true,
|
loading: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_defaultCategoryId: 'all',
|
||||||
|
|
||||||
onLoad(options: Record<string, string>) {
|
onLoad(options: Record<string, string>) {
|
||||||
void this.initData(options);
|
if (options.id && options.id !== 'all') {
|
||||||
|
this._defaultCategoryId = options.id;
|
||||||
|
this.setData({ activeCategoryId: options.id });
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async initData(options: Record<string, string>) {
|
onShow() {
|
||||||
wx.showLoading({ title: '加载中', mask: false });
|
_categoryData = null;
|
||||||
|
this.setData({ displayItems: SKELETON_ITEMS, loading: true });
|
||||||
|
void this.loadWorksheets();
|
||||||
|
},
|
||||||
|
|
||||||
const app = getApp<IAppOption>();
|
async loadWorksheets() {
|
||||||
const config = await app.getPageConfig();
|
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
|
_categoryData = buildCategoryDataFromCloud(result.data || []);
|
||||||
if (config?.category?.categories?.length) {
|
} catch {
|
||||||
_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;
|
_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 items = getItemsByCategory(activeId);
|
||||||
|
const filtered = filterByKeyword(items, this.data.searchKeyword);
|
||||||
|
|
||||||
this.setData({
|
this.setData({
|
||||||
loading: false,
|
loading: false,
|
||||||
searchPlaceholder: _categoryData.searchPlaceholder,
|
searchPlaceholder: _categoryData.searchPlaceholder,
|
||||||
activeCategoryId: activeId,
|
activeCategoryId: activeId,
|
||||||
displayItems: items,
|
displayItems: filtered,
|
||||||
});
|
});
|
||||||
|
|
||||||
wx.hideLoading();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onTapCategory(e: WechatMiniprogram.TouchEvent) {
|
onTapCategory(e: WechatMiniprogram.TouchEvent) {
|
||||||
|
|||||||
@@ -243,7 +243,7 @@
|
|||||||
|
|
||||||
.fav-cta__btn {
|
.fav-cta__btn {
|
||||||
margin-top: 32rpx;
|
margin-top: 32rpx;
|
||||||
padding: 22rpx 64rpx;
|
padding: 24rpx 52rpx;
|
||||||
border-radius: 999rpx;
|
border-radius: 999rpx;
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -425,7 +425,7 @@
|
|||||||
.fav-empty__sub {
|
.fav-empty__sub {
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
color: @text-secondary;
|
color: @text-secondary;
|
||||||
margin-bottom: 40rpx;
|
margin-bottom: 32rpx;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,11 +31,15 @@
|
|||||||
aria-hidden="true"></text>
|
aria-hidden="true"></text>
|
||||||
</view>
|
</view>
|
||||||
<view wx:elif="{{favoriteList.length === 0}}" class="fav-empty">
|
<view wx:elif="{{favoriteList.length === 0}}" class="fav-empty">
|
||||||
<text class="fav-empty__emoji">🦆</text>
|
<toy-icon
|
||||||
<text class="fav-empty__msg">鸭丫还没找到收藏呢</text>
|
name="mood-empty"
|
||||||
|
size="100rpx"
|
||||||
|
color="#b3ac9f"
|
||||||
|
class="fav-empty__icon" />
|
||||||
|
<text class="fav-empty__msg">还没找到收藏呢</text>
|
||||||
<text class="fav-empty__sub">去发现好玩的练习纸吧~</text>
|
<text class="fav-empty__sub">去发现好玩的练习纸吧~</text>
|
||||||
<button class="fav-empty__btn" bindtap="onDiscoverTap">
|
<button class="fav-cta__btn" bindtap="onDiscoverTap">
|
||||||
去发现
|
去发现页看看
|
||||||
</button>
|
</button>
|
||||||
</view>
|
</view>
|
||||||
<view wx:elif="{{favoriteList.length > 0}}" class="fav-list">
|
<view wx:elif="{{favoriteList.length > 0}}" class="fav-list">
|
||||||
@@ -112,14 +116,15 @@
|
|||||||
<view
|
<view
|
||||||
wx:elif="{{downloadSections.length === 0}}"
|
wx:elif="{{downloadSections.length === 0}}"
|
||||||
class="fav-empty">
|
class="fav-empty">
|
||||||
<van-icon
|
<toy-icon
|
||||||
name="clock"
|
name="mood-empty"
|
||||||
size="120rpx"
|
size="100rpx"
|
||||||
color="#b3ac9f"
|
color="#b3ac9f"
|
||||||
class="fav-empty__icon" />
|
class="fav-empty__icon" />
|
||||||
<text class="fav-empty__msg">暂无下载记录</text>
|
<text class="fav-empty__msg">暂无下载记录</text>
|
||||||
<button class="fav-empty__btn" bindtap="onDiscoverTap">
|
<text class="fav-empty__sub">去发现好玩的练习纸吧~</text>
|
||||||
去发现
|
<button class="fav-cta__btn" bindtap="onDiscoverTap">
|
||||||
|
去发现页看看
|
||||||
</button>
|
</button>
|
||||||
</view>
|
</view>
|
||||||
<view wx:elif="{{downloadSections.length > 0}}" class="fav-dl">
|
<view wx:elif="{{downloadSections.length > 0}}" class="fav-dl">
|
||||||
|
|||||||
@@ -24,8 +24,8 @@
|
|||||||
|
|
||||||
<swiper
|
<swiper
|
||||||
class="home-hero-swiper"
|
class="home-hero-swiper"
|
||||||
previous-margin="100rpx"
|
previous-margin="80rpx"
|
||||||
next-margin="100rpx"
|
next-margin="80rpx"
|
||||||
circular="{{featuredItems.length > 1 && !loading}}"
|
circular="{{featuredItems.length > 1 && !loading}}"
|
||||||
autoplay="{{!loading}}"
|
autoplay="{{!loading}}"
|
||||||
interval="{{4000}}"
|
interval="{{4000}}"
|
||||||
@@ -200,8 +200,7 @@
|
|||||||
path="{{item.path}}"
|
path="{{item.path}}"
|
||||||
data-title="{{item.title}}"
|
data-title="{{item.title}}"
|
||||||
data-path="{{item.path}}"
|
data-path="{{item.path}}"
|
||||||
bindtap="onTapCard"
|
bindtap="onTapCard" />
|
||||||
/>
|
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
"backgroundColor": "#F8F0E0",
|
"backgroundColor": "#F8F0E0",
|
||||||
"usingComponents": {
|
"usingComponents": {
|
||||||
"van-icon": "@vant/weapp/icon/index",
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,8 +57,8 @@ page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.profile-hero__avatar {
|
.profile-hero__avatar {
|
||||||
width: 256rpx;
|
width: 224rpx;
|
||||||
height: 256rpx;
|
height: 224rpx;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: 8rpx solid @bg-white;
|
border: 8rpx solid @bg-white;
|
||||||
background: @bg-gray;
|
background: @bg-gray;
|
||||||
@@ -98,7 +98,7 @@ page {
|
|||||||
|
|
||||||
.profile-hero__name {
|
.profile-hero__name {
|
||||||
margin-top: 32rpx;
|
margin-top: 32rpx;
|
||||||
font-size: 40rpx;
|
font-size: 36rpx;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: @text-title;
|
color: @text-title;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -116,7 +116,7 @@ page {
|
|||||||
.profile-hero__stat {
|
.profile-hero__stat {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background: @bg-header;
|
background: @bg-header;
|
||||||
border-radius: @radius;
|
border-radius: 48rpx;
|
||||||
border: 1rpx solid rgba(124, 118, 106, 0.12);
|
border: 1rpx solid rgba(124, 118, 106, 0.12);
|
||||||
padding: 32rpx 24rpx;
|
padding: 32rpx 24rpx;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -321,4 +321,144 @@ page {
|
|||||||
color: @text-secondary;
|
color: @text-secondary;
|
||||||
letter-spacing: 0.2em;
|
letter-spacing: 0.2em;
|
||||||
text-transform: uppercase;
|
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;
|
||||||
}
|
}
|
||||||
@@ -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({
|
Page({
|
||||||
|
DEFAULT_AVATAR_URL: '/assets/imgs/doodle-head.png',
|
||||||
data: {
|
data: {
|
||||||
userName: '微信用户',
|
userName: '微信用户',
|
||||||
avatarUrl: '',
|
avatarUrl: '',
|
||||||
printCount: 0,
|
printCount: 0,
|
||||||
favoriteCount: 0,
|
favoriteCount: 0,
|
||||||
version: '',
|
version: '3.0.0',
|
||||||
isDevEnv: false,
|
showProfileEditor: false,
|
||||||
|
draftNickName: '',
|
||||||
|
draftAvatarUrl: '/assets/imgs/doodle-head.png',
|
||||||
|
isSavingProfile: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
onLoad() {
|
onLoad() {
|
||||||
const info = wx.getAccountInfoSync();
|
this.syncUserInfoFromApp();
|
||||||
const version = info.miniProgram.version || '开发版';
|
|
||||||
const isDevEnv = info.miniProgram.envVersion === 'develop';
|
|
||||||
this.setData({ version, isDevEnv });
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onShow() {
|
onShow() {
|
||||||
this.refreshStats();
|
this.syncUserInfoFromApp();
|
||||||
|
void this.refreshStats();
|
||||||
},
|
},
|
||||||
|
|
||||||
refreshStats() {
|
async syncUserInfoFromApp() {
|
||||||
const printCount = getTodayDownloadCount();
|
const app = getApp<IAppOption>();
|
||||||
// 收藏数待与收藏页数据源打通后接入
|
const cachedUser = app.globalData.user || getCachedUser();
|
||||||
const favoriteCount = 0;
|
|
||||||
this.setData({ printCount, favoriteCount });
|
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<string> {
|
||||||
|
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() {
|
onGoToDownloads() {
|
||||||
@@ -47,7 +170,7 @@ Page({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onGoToSettings() {
|
onGoToSettings() {
|
||||||
wx.showToast({ title: '功能开发中', icon: 'none' });
|
wx.navigateTo({ url: '/supportPages/settings/settings' });
|
||||||
},
|
},
|
||||||
|
|
||||||
onGoToDebug() {
|
onGoToDebug() {
|
||||||
|
|||||||
@@ -8,19 +8,14 @@
|
|||||||
<view class="profile-hero__tint"></view>
|
<view class="profile-hero__tint"></view>
|
||||||
<view class="profile-hero__avatar-wrap">
|
<view class="profile-hero__avatar-wrap">
|
||||||
<view class="profile-hero__avatar">
|
<view class="profile-hero__avatar">
|
||||||
<text
|
|
||||||
class="profile-hero__avatar-emoji"
|
|
||||||
wx:if="{{!avatarUrl}}"
|
|
||||||
>🦆</text
|
|
||||||
>
|
|
||||||
<image
|
<image
|
||||||
wx:else
|
|
||||||
class="profile-hero__avatar-img"
|
class="profile-hero__avatar-img"
|
||||||
src="{{avatarUrl}}"
|
src="{{avatarUrl}}"
|
||||||
mode="aspectFill" />
|
mode="aspectFill" />
|
||||||
</view>
|
</view>
|
||||||
<view
|
<view
|
||||||
class="profile-hero__edit-badge"
|
class="profile-hero__edit-badge"
|
||||||
|
bindtap="onOpenProfileEditor"
|
||||||
aria-role="button"
|
aria-role="button"
|
||||||
aria-label="编辑头像">
|
aria-label="编辑头像">
|
||||||
<van-icon name="edit" size="28rpx" color="#6c5a00" />
|
<van-icon name="edit" size="28rpx" color="#6c5a00" />
|
||||||
@@ -73,9 +68,13 @@
|
|||||||
</view>
|
</view>
|
||||||
<text class="profile-row__label">打印指南</text>
|
<text class="profile-row__label">打印指南</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="profile-row__chevron">›</text>
|
<toy-icon
|
||||||
|
class="profile-row__chevron"
|
||||||
|
name="arrow-right"
|
||||||
|
size="40rpx"
|
||||||
|
color="#9a958b" />
|
||||||
</view>
|
</view>
|
||||||
<view class="profile-row" bindtap="onGoToFeedback">
|
<!-- <view class="profile-row" bindtap="onGoToFeedback">
|
||||||
<view class="profile-row__left">
|
<view class="profile-row__left">
|
||||||
<view class="profile-row__icon-bg">
|
<view class="profile-row__icon-bg">
|
||||||
<van-icon
|
<van-icon
|
||||||
@@ -85,8 +84,12 @@
|
|||||||
</view>
|
</view>
|
||||||
<text class="profile-row__label">提交反馈</text>
|
<text class="profile-row__label">提交反馈</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="profile-row__chevron">›</text>
|
<toy-icon
|
||||||
</view>
|
class="profile-row__chevron"
|
||||||
|
name="arrow-right"
|
||||||
|
size="40rpx"
|
||||||
|
color="#9a958b" />
|
||||||
|
</view> -->
|
||||||
<view class="profile-row" bindtap="onGoToSettings">
|
<view class="profile-row" bindtap="onGoToSettings">
|
||||||
<view class="profile-row__left">
|
<view class="profile-row__left">
|
||||||
<view class="profile-row__icon-bg">
|
<view class="profile-row__icon-bg">
|
||||||
@@ -97,7 +100,11 @@
|
|||||||
</view>
|
</view>
|
||||||
<text class="profile-row__label">设置</text>
|
<text class="profile-row__label">设置</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="profile-row__chevron">›</text>
|
<toy-icon
|
||||||
|
class="profile-row__chevron"
|
||||||
|
name="arrow-right"
|
||||||
|
size="40rpx"
|
||||||
|
color="#9a958b" />
|
||||||
</view>
|
</view>
|
||||||
<view
|
<view
|
||||||
wx:if="{{isDevEnv}}"
|
wx:if="{{isDevEnv}}"
|
||||||
@@ -109,9 +116,13 @@
|
|||||||
</view>
|
</view>
|
||||||
<text class="profile-row__label">Debug 工具</text>
|
<text class="profile-row__label">Debug 工具</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="profile-row__chevron">›</text>
|
<toy-icon
|
||||||
|
class="profile-row__chevron"
|
||||||
|
name="arrow-right"
|
||||||
|
size="40rpx"
|
||||||
|
color="#9a958b" />
|
||||||
</view>
|
</view>
|
||||||
<view
|
<!-- <view
|
||||||
class="profile-row profile-row--last"
|
class="profile-row profile-row--last"
|
||||||
bindtap="onGoToAbout">
|
bindtap="onGoToAbout">
|
||||||
<view class="profile-row__left">
|
<view class="profile-row__left">
|
||||||
@@ -123,12 +134,16 @@
|
|||||||
</view>
|
</view>
|
||||||
<text class="profile-row__label">关于涂鸦丫</text>
|
<text class="profile-row__label">关于涂鸦丫</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="profile-row__chevron">›</text>
|
<toy-icon
|
||||||
</view>
|
class="profile-row__chevron"
|
||||||
|
name="arrow-right"
|
||||||
|
size="40rpx"
|
||||||
|
color="#9a958b" />
|
||||||
|
</view> -->
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 专业版推广(Stitch 深色卡) -->
|
<!-- 专业版推广(Stitch 深色卡) -->
|
||||||
<view class="profile-upsell" bindtap="onGoToPremium">
|
<!-- <view class="profile-upsell" bindtap="onGoToPremium">
|
||||||
<van-icon
|
<van-icon
|
||||||
name="gem-o"
|
name="gem-o"
|
||||||
class="profile-upsell__deco"
|
class="profile-upsell__deco"
|
||||||
@@ -141,13 +156,73 @@
|
|||||||
>
|
>
|
||||||
<view class="profile-upsell__btn">立即升级</view>
|
<view class="profile-upsell__btn">立即升级</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view> -->
|
||||||
|
|
||||||
<view class="profile-version">
|
<view class="profile-version">
|
||||||
<text class="profile-version__text"
|
<text class="profile-version__text">涂鸦丫 v{{version}}</text>
|
||||||
>Doodle Mini v{{version}}</text
|
|
||||||
>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<van-popup
|
||||||
|
show="{{showProfileEditor}}"
|
||||||
|
position="bottom"
|
||||||
|
round
|
||||||
|
safe-area-inset-bottom="{{false}}"
|
||||||
|
custom-class="profile-editor-popup"
|
||||||
|
bind:close="onCloseProfileEditor">
|
||||||
|
<view class="profile-editor">
|
||||||
|
<view class="profile-editor__header">
|
||||||
|
<text class="profile-editor__title">完善资料</text>
|
||||||
|
<text class="profile-editor__desc"
|
||||||
|
>选择头像并填写昵称后保存</text
|
||||||
|
>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="profile-editor__avatar-section">
|
||||||
|
<button
|
||||||
|
class="profile-editor__avatar-button"
|
||||||
|
open-type="chooseAvatar"
|
||||||
|
bindchooseavatar="onChooseAvatar">
|
||||||
|
<view class="profile-editor__avatar">
|
||||||
|
<text
|
||||||
|
class="profile-editor__avatar-placeholder"
|
||||||
|
wx:if="{{!draftAvatarUrl}}"
|
||||||
|
>选择头像</text
|
||||||
|
>
|
||||||
|
<image
|
||||||
|
wx:else
|
||||||
|
class="profile-editor__avatar-img"
|
||||||
|
src="{{draftAvatarUrl}}"
|
||||||
|
mode="aspectFill" />
|
||||||
|
</view>
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="profile-editor__field">
|
||||||
|
<text class="profile-editor__label">昵称</text>
|
||||||
|
<input
|
||||||
|
class="profile-editor__input"
|
||||||
|
type="nickname"
|
||||||
|
placeholder="请输入昵称"
|
||||||
|
maxlength="20"
|
||||||
|
value="{{draftNickName}}"
|
||||||
|
bindblur="onNicknameInput"
|
||||||
|
bindinput="onNicknameInput" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="profile-editor__actions">
|
||||||
|
<view
|
||||||
|
class="profile-editor__action profile-editor__action--ghost"
|
||||||
|
bindtap="onCloseProfileEditor">
|
||||||
|
取消
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="profile-editor__action profile-editor__action--primary {{isSavingProfile ? 'profile-editor__action--disabled' : ''}}"
|
||||||
|
bindtap="onSubmitProfile">
|
||||||
|
{{isSavingProfile ? '保存中...' : '保存'}}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</van-popup>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -200,45 +200,7 @@ Page({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async onBuildTap() {
|
// [v3.0] 分类页已改为实时接口查询,不再需要手动更新分类数据
|
||||||
if (this.data.building) return;
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
onBuildTap() {},
|
||||||
const confirmed = await new Promise<boolean>((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<string, number>;
|
|
||||||
}>('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 });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -90,7 +90,7 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- Build Button -->
|
<!-- [v3.0] 分类页已改为实时接口查询,不再需要手动更新分类数据
|
||||||
<view class="ccm-page__build">
|
<view class="ccm-page__build">
|
||||||
<toy-button
|
<toy-button
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -101,4 +101,5 @@
|
|||||||
{{building ? '生成中...' : '更新分类页数据'}}
|
{{building ? '生成中...' : '更新分类页数据'}}
|
||||||
</toy-button>
|
</toy-button>
|
||||||
</view>
|
</view>
|
||||||
|
-->
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
{
|
{
|
||||||
|
"navigationStyle": "custom",
|
||||||
"navigationBarTitleText": "打印指南",
|
"navigationBarTitleText": "打印指南",
|
||||||
"navigationBarTextStyle": "black",
|
"navigationBarTextStyle": "black",
|
||||||
"navigationBarBackgroundColor": "#f7ee47",
|
"navigationBarBackgroundColor": "#F8F0E0",
|
||||||
"backgroundColor": "#F6F6F6"
|
"backgroundColor": "#F8F0E0",
|
||||||
|
"usingComponents": {
|
||||||
|
"van-icon": "@vant/weapp/icon/index",
|
||||||
|
"nav-bar": "../../components3.0/nav-bar/nav-bar"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +1,105 @@
|
|||||||
.guide-page {
|
@import '../../style/theme.less';
|
||||||
min-height: 100vh;
|
|
||||||
background: #f6f6f6;
|
page {
|
||||||
padding-bottom: 48rpx;
|
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 {
|
.guide-page__hero {
|
||||||
background: linear-gradient(180deg, #f7ee47 0%, #e6d520 100%);
|
position: relative;
|
||||||
padding: 60rpx 40rpx;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
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 {
|
.guide-page__hero-icon {
|
||||||
font-size: 80rpx;
|
font-size: 72rpx;
|
||||||
margin-bottom: 24rpx;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.guide-page__hero-title {
|
.guide-page__hero-title {
|
||||||
|
margin-top: 32rpx;
|
||||||
font-size: 40rpx;
|
font-size: 40rpx;
|
||||||
font-weight: 700;
|
font-weight: 800;
|
||||||
color: #333;
|
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 {
|
.guide-page__steps {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 40rpx 32rpx 24rpx 32rpx;
|
padding: 56rpx 40rpx 16rpx;
|
||||||
margin: 0 24rpx;
|
background: @bg-white;
|
||||||
margin-top: -24rpx;
|
border-radius: @radius-xl;
|
||||||
background: #fff;
|
border: 1rpx solid rgba(124, 118, 106, 0.12);
|
||||||
border-radius: 24rpx;
|
box-shadow: @shadow;
|
||||||
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.06);
|
margin-bottom: @section-gap-xs;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.guide-page__timeline-line {
|
.guide-page__timeline-line {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 62rpx;
|
left: 70rpx;
|
||||||
top: 100rpx;
|
top: 110rpx;
|
||||||
bottom: 100rpx;
|
bottom: 110rpx;
|
||||||
width: 4rpx;
|
width: 4rpx;
|
||||||
background: #eee;
|
background: @bg-gray;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,62 +108,66 @@
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
margin-bottom: 48rpx;
|
margin-bottom: 56rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.guide-page__step--last {
|
.guide-page__step--last {
|
||||||
margin-bottom: 0;
|
margin-bottom: 40rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.guide-page__step-num-wrap {
|
.guide-page__step-num-wrap {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 64rpx;
|
width: 64rpx;
|
||||||
margin-right: 24rpx;
|
margin-right: 32rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.guide-page__step-num {
|
.guide-page__step-num {
|
||||||
width: 60rpx;
|
width: 64rpx;
|
||||||
height: 60rpx;
|
height: 64rpx;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #f7ee47;
|
background: @brand;
|
||||||
font-size: 28rpx;
|
font-size: 30rpx;
|
||||||
font-weight: 700;
|
font-weight: 800;
|
||||||
color: #333;
|
color: @text-selected-btn;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
box-shadow: @shadow;
|
||||||
}
|
}
|
||||||
|
|
||||||
.guide-page__step-body {
|
.guide-page__step-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding-top: 4rpx;
|
padding-top: 4rpx;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.guide-page__step-title {
|
.guide-page__step-title {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 32rpx;
|
font-size: 32rpx;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
color: #333;
|
color: @text-title;
|
||||||
margin-bottom: 12rpx;
|
margin-bottom: 16rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.guide-page__step-desc {
|
.guide-page__step-desc {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 28rpx;
|
font-size: 26rpx;
|
||||||
color: #666;
|
color: @text-secondary;
|
||||||
line-height: 1.55;
|
line-height: 1.6;
|
||||||
margin-bottom: 16rpx;
|
margin-bottom: 20rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 通用 Tip(弱提示) */
|
||||||
.guide-page__tip {
|
.guide-page__tip {
|
||||||
background: #fff8e1;
|
background: @bg-header;
|
||||||
border-radius: 16rpx;
|
border-radius: @radius;
|
||||||
padding: 20rpx 24rpx;
|
padding: 20rpx 24rpx;
|
||||||
font-size: 26rpx;
|
font-size: 24rpx;
|
||||||
color: #666;
|
color: @text-secondary;
|
||||||
line-height: 1.5;
|
line-height: 1.55;
|
||||||
|
border: 1rpx solid rgba(124, 118, 106, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.guide-page__tip-line {
|
.guide-page__tip-line {
|
||||||
@@ -115,36 +179,114 @@
|
|||||||
margin-bottom: 0;
|
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;
|
font-weight: 700;
|
||||||
color: #333;
|
color: @text-title;
|
||||||
padding: 40rpx 32rpx 16rpx;
|
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 {
|
.guide-page__faq {
|
||||||
padding: 0 24rpx;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.guide-page__faq-item {
|
.guide-page__faq-item {
|
||||||
background: #fff;
|
background: @bg-white;
|
||||||
border-radius: 16rpx;
|
border-radius: @radius-lg;
|
||||||
padding: 28rpx 32rpx;
|
border: 1rpx solid rgba(124, 118, 106, 0.12);
|
||||||
margin-bottom: 20rpx;
|
padding: 32rpx 36rpx;
|
||||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
margin-bottom: 24rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-page__faq-item--last {
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.guide-page__faq-q {
|
.guide-page__faq-q {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #333;
|
color: @text-title;
|
||||||
margin-bottom: 12rpx;
|
margin-bottom: 12rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.guide-page__faq-a {
|
.guide-page__faq-a {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
color: #999;
|
color: @text-secondary;
|
||||||
line-height: 1.55;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,75 @@
|
|||||||
|
type AlbumAuthState = 'unknown' | 'granted' | 'denied';
|
||||||
|
|
||||||
|
const ALBUM_SCOPE = 'scope.writePhotosAlbum';
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
data: {},
|
data: {
|
||||||
|
albumAuthState: 'unknown' as AlbumAuthState,
|
||||||
|
},
|
||||||
|
|
||||||
onLoad() {
|
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' });
|
||||||
|
},
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,63 +1,132 @@
|
|||||||
<view class="guide-page">
|
<view class="guide-page">
|
||||||
<view class="guide-page__hero">
|
<nav-bar title="打印指南" />
|
||||||
<text class="guide-page__hero-icon">🖨️</text>
|
|
||||||
<text class="guide-page__hero-title">3 步完成打印</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="guide-page__steps">
|
<view class="guide-page__body">
|
||||||
<view class="guide-page__timeline-line" />
|
<!-- Hero 卡片:白底大圆角 + 顶条品牌色(参考 profile 主卡) -->
|
||||||
|
<view class="guide-page__hero">
|
||||||
<view class="guide-page__step">
|
<view class="guide-page__hero-tint"></view>
|
||||||
<view class="guide-page__step-num-wrap">
|
<view class="guide-page__hero-icon-wrap">
|
||||||
<view class="guide-page__step-num">1</view>
|
<text class="guide-page__hero-icon">🖨️</text>
|
||||||
</view>
|
|
||||||
<view class="guide-page__step-body">
|
|
||||||
<text class="guide-page__step-title">保存到相册</text>
|
|
||||||
<text class="guide-page__step-desc">在练习纸预览页点击「保存」或「保存到相册」,授权后即可保存图片。</text>
|
|
||||||
<view class="guide-page__tip">提示:建议先关闭深色模式截图,打印对比度更好。</view>
|
|
||||||
</view>
|
</view>
|
||||||
|
<text class="guide-page__hero-title">3 步完成打印</text>
|
||||||
|
<text class="guide-page__hero-sub"
|
||||||
|
>保存图片 → 连接打印机 → 一键打印</text
|
||||||
|
>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="guide-page__step">
|
<!-- 步骤时间线 -->
|
||||||
<view class="guide-page__step-num-wrap">
|
<view class="guide-page__steps">
|
||||||
<view class="guide-page__step-num">2</view>
|
<view class="guide-page__timeline-line" />
|
||||||
</view>
|
|
||||||
<view class="guide-page__step-body">
|
|
||||||
<text class="guide-page__step-title">连接打印机</text>
|
|
||||||
<text class="guide-page__step-desc">确保打印机与手机处于同一 Wi-Fi,或按打印机说明完成蓝牙配对。</text>
|
|
||||||
<view class="guide-page__tip">家用喷墨 / 激光打印机均可;A4 纸竖向打印即可。</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="guide-page__step guide-page__step--last">
|
<view class="guide-page__step">
|
||||||
<view class="guide-page__step-num-wrap">
|
<view class="guide-page__step-num-wrap">
|
||||||
<view class="guide-page__step-num">3</view>
|
<view class="guide-page__step-num">1</view>
|
||||||
|
</view>
|
||||||
|
<view class="guide-page__step-body">
|
||||||
|
<text class="guide-page__step-title">保存到相册</text>
|
||||||
|
<text class="guide-page__step-desc">
|
||||||
|
在练习纸预览页点击「保存到相册」,授权后即可下载图片到手机相册。
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<!-- 相册权限提示(动态状态) -->
|
||||||
|
<view
|
||||||
|
class="guide-page__perm guide-page__perm--{{albumAuthState}}">
|
||||||
|
<view class="guide-page__perm-icon-wrap">
|
||||||
|
<van-icon
|
||||||
|
wx:if="{{albumAuthState === 'granted'}}"
|
||||||
|
name="checked"
|
||||||
|
size="36rpx"
|
||||||
|
color="#1fe000" />
|
||||||
|
<van-icon
|
||||||
|
wx:elif="{{albumAuthState === 'denied'}}"
|
||||||
|
name="warning-o"
|
||||||
|
size="36rpx"
|
||||||
|
color="#e8793a" />
|
||||||
|
<van-icon
|
||||||
|
wx:else
|
||||||
|
name="info-o"
|
||||||
|
size="36rpx"
|
||||||
|
color="#605b50" />
|
||||||
|
</view>
|
||||||
|
<view class="guide-page__perm-body">
|
||||||
|
<text class="guide-page__perm-title"
|
||||||
|
>{{albumAuthState === 'granted' ?
|
||||||
|
'已开启相册权限' : albumAuthState === 'denied' ?
|
||||||
|
'相册权限未开启' : '检查相册权限'}}</text
|
||||||
|
>
|
||||||
|
<text class="guide-page__perm-desc"
|
||||||
|
>{{albumAuthState === 'granted' ?
|
||||||
|
'可正常将练习纸保存到手机相册。' :
|
||||||
|
albumAuthState === 'denied' ?
|
||||||
|
'需要在系统设置中开启「保存到相册」权限,才能下载图片。'
|
||||||
|
:
|
||||||
|
'请确认是否已授权小程序保存图片到相册。'}}</text
|
||||||
|
>
|
||||||
|
<view
|
||||||
|
wx:if="{{albumAuthState !== 'granted'}}"
|
||||||
|
class="guide-page__perm-btn"
|
||||||
|
bindtap="onHandleAlbumPerm"
|
||||||
|
>{{albumAuthState === 'denied' ? '前往设置开启'
|
||||||
|
: '立即检查授权'}}</view
|
||||||
|
>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="guide-page__step-body">
|
|
||||||
<text class="guide-page__step-title">从相册打印</text>
|
<view class="guide-page__step">
|
||||||
<text class="guide-page__step-desc">打开手机相册,选中刚保存的练习纸图片,点击分享或打印。</text>
|
<view class="guide-page__step-num-wrap">
|
||||||
<view class="guide-page__tip">
|
<view class="guide-page__step-num">2</view>
|
||||||
<text class="guide-page__tip-line">iPhone:分享 → 打印 → 选择打印机。</text>
|
</view>
|
||||||
<text class="guide-page__tip-line">Android:更多 → 打印 或 使用系统相册自带的打印入口。</text>
|
<view class="guide-page__step-body">
|
||||||
|
<text class="guide-page__step-title">连接打印机</text>
|
||||||
|
<text class="guide-page__step-desc">
|
||||||
|
确保打印机与手机处于同一
|
||||||
|
Wi-Fi,或按打印机说明完成蓝牙配对。
|
||||||
|
</text>
|
||||||
|
<view class="guide-page__tip">
|
||||||
|
家用喷墨 / 激光打印机均可,A4 纸竖向打印即可。
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="guide-page__step guide-page__step--last">
|
||||||
|
<view class="guide-page__step-num-wrap">
|
||||||
|
<view class="guide-page__step-num">3</view>
|
||||||
|
</view>
|
||||||
|
<view class="guide-page__step-body">
|
||||||
|
<text class="guide-page__step-title">从相册打印</text>
|
||||||
|
<text class="guide-page__step-desc">
|
||||||
|
打开手机相册,选中刚保存的练习纸图片,点击分享或打印。
|
||||||
|
</text>
|
||||||
|
<view class="guide-page__tip">
|
||||||
|
<text class="guide-page__tip-line">
|
||||||
|
iPhone:分享 → 打印 → 选择打印机。
|
||||||
|
</text>
|
||||||
|
<text class="guide-page__tip-line">
|
||||||
|
Android:更多 → 打印,或使用系统相册自带的打印入口。
|
||||||
|
</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="guide-page__faq-title">常见问题</view>
|
<!-- FAQ -->
|
||||||
|
<view class="guide-page__faq-title">常见问题</view>
|
||||||
|
|
||||||
<view class="guide-page__faq">
|
<view class="guide-page__faq">
|
||||||
<view class="guide-page__faq-item">
|
<view class="guide-page__faq-item">
|
||||||
<text class="guide-page__faq-q">保存失败怎么办?</text>
|
<text class="guide-page__faq-q">保存失败怎么办?</text>
|
||||||
<text class="guide-page__faq-a">请在小程序设置中开启相册写入权限,并检查手机存储空间是否充足。</text>
|
<text class="guide-page__faq-a">
|
||||||
</view>
|
请在小程序设置中开启相册写入权限,并检查手机存储空间是否充足。
|
||||||
<view class="guide-page__faq-item">
|
</text>
|
||||||
<text class="guide-page__faq-q">打印出来太小/太大?</text>
|
</view>
|
||||||
<text class="guide-page__faq-a">在打印预览中选择「适应页面」或调整缩放比例,通常 100% 即可。</text>
|
<view class="guide-page__faq-item guide-page__faq-item--last">
|
||||||
</view>
|
<text class="guide-page__faq-q">没有打印机?</text>
|
||||||
<view class="guide-page__faq-item">
|
<text class="guide-page__faq-a">
|
||||||
<text class="guide-page__faq-q">没有打印机?</text>
|
可将图片发到电脑或拷贝至 U 盘,在打印店完成打印。
|
||||||
<text class="guide-page__faq-a">可将图片发到电脑或拷贝至 U 盘,在打印店完成打印。</text>
|
</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"navigationBarTitleText": "设置",
|
||||||
|
"navigationBarTextStyle": "black",
|
||||||
|
"navigationBarBackgroundColor": "#F8F0E0",
|
||||||
|
"backgroundColor": "#F8F0E0",
|
||||||
|
"usingComponents": {
|
||||||
|
"nav-bar": "../../components3.0/nav-bar/nav-bar"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<view class="settings-page">
|
||||||
|
<nav-bar title="设置" />
|
||||||
|
|
||||||
|
<view class="settings-body">
|
||||||
|
<view class="settings-section">
|
||||||
|
<view class="settings-action" bindtap="onClearProfile">
|
||||||
|
<text class="settings-action__text">删除头像和昵称</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="settings-action settings-action--danger"
|
||||||
|
bindtap="onDeleteAccount">
|
||||||
|
<text class="settings-action__text">注销账户</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
@@ -19,18 +19,40 @@ let currentUser: UserInfo | null = null;
|
|||||||
/** 登录进行中的 Promise(防止并发重复调用) */
|
/** 登录进行中的 Promise(防止并发重复调用) */
|
||||||
let loginPromise: Promise<UserInfo> | null = null;
|
let loginPromise: Promise<UserInfo> | null = null;
|
||||||
|
|
||||||
|
function syncAppGlobalUser(user: UserInfo | null): void {
|
||||||
|
try {
|
||||||
|
const app = getApp<IAppOption>();
|
||||||
|
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 → 云函数登录
|
* 获取当前用户,优先内存 → Storage → 云函数登录
|
||||||
*/
|
*/
|
||||||
export async function getUser(): Promise<UserInfo> {
|
export async function getUser(): Promise<UserInfo> {
|
||||||
// 1. 内存中有,直接返回
|
// 1. 内存中有,直接返回
|
||||||
if (currentUser) return currentUser;
|
if (currentUser) {
|
||||||
|
syncAppGlobalUser(currentUser);
|
||||||
|
return currentUser;
|
||||||
|
}
|
||||||
|
|
||||||
// 2. 尝试从 Storage 读取
|
// 2. 尝试从 Storage 读取
|
||||||
const cached = loadFromStorage();
|
const cached = loadFromStorage();
|
||||||
if (cached) {
|
if (cached) {
|
||||||
currentUser = cached;
|
return syncUserState(cached, false);
|
||||||
return cached;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 静默登录(防止并发)
|
// 3. 静默登录(防止并发)
|
||||||
@@ -56,9 +78,18 @@ async function silentLogin(): Promise<UserInfo> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const user = result.data as UserInfo;
|
const user = result.data as UserInfo;
|
||||||
currentUser = user;
|
return syncUserState(user);
|
||||||
saveToStorage(user);
|
}
|
||||||
return 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 {
|
export function clearUser(): void {
|
||||||
currentUser = null;
|
currentUser = null;
|
||||||
|
syncAppGlobalUser(null);
|
||||||
wx.removeStorageSync(STORAGE_KEY);
|
wx.removeStorageSync(STORAGE_KEY);
|
||||||
wx.removeStorageSync(STORAGE_EXPIRE_KEY);
|
wx.removeStorageSync(STORAGE_EXPIRE_KEY);
|
||||||
}
|
}
|
||||||
@@ -108,4 +140,26 @@ export async function refreshUser(): Promise<UserInfo> {
|
|||||||
return await silentLogin();
|
return await silentLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateUserProfile(payload: {
|
||||||
|
nickName?: string | null;
|
||||||
|
avatarUrl?: string | null;
|
||||||
|
}): Promise<UserInfo> {
|
||||||
|
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 };
|
export type { UserInfo };
|
||||||
|
|||||||
@@ -54,3 +54,21 @@ export async function getDownloadLogs(
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前用户下载(打印)总数
|
||||||
|
*/
|
||||||
|
export async function getDownloadLogCount(): Promise<number> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -76,6 +76,24 @@ export async function getFavoriteList(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前用户收藏总数
|
||||||
|
*/
|
||||||
|
export async function getFavoriteCount(): Promise<number> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查是否已收藏
|
* 检查是否已收藏
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -24,12 +24,26 @@
|
|||||||
"miniprogram": {
|
"miniprogram": {
|
||||||
"list": [
|
"list": [
|
||||||
{
|
{
|
||||||
"name": "pages/age/age",
|
"name": "pages/profile/profile",
|
||||||
"pathName": "pages/age/age",
|
"pathName": "pages/profile/profile",
|
||||||
"query": "",
|
"query": "",
|
||||||
"scene": null,
|
"scene": null,
|
||||||
"launchMode": "default"
|
"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",
|
"name": "supportPages/debug/debug",
|
||||||
"pathName": "supportPages/debug/debug",
|
"pathName": "supportPages/debug/debug",
|
||||||
|
|||||||
Vendored
+1
@@ -21,6 +21,7 @@ interface IAppOption {
|
|||||||
globalData: {
|
globalData: {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
userInfo?: WechatMiniprogram.UserInfo;
|
userInfo?: WechatMiniprogram.UserInfo;
|
||||||
|
user?: import('../miniprogram/utils/auth').UserInfo | null;
|
||||||
env: string;
|
env: string;
|
||||||
printConfig?: PrintConfig;
|
printConfig?: PrintConfig;
|
||||||
pageConfig?: PageConfigData;
|
pageConfig?: PageConfigData;
|
||||||
|
|||||||
Reference in New Issue
Block a user