hookehuyr

feat: 实现环境判断的Mock数据自动切换模式

- 修复搜索页面布局:顶部固定(NavHeader+SearchBar+Tabs+ResultCount)+ 列表滚动
- 修复搜索分页hasMore计算时机bug(首次搜索显示没有更多的问题)
- 实现环境判断的Mock数据自动切换
  - 开发环境(pnpm dev:weapp)使用mock数据
  - 生产环境(pnpm build:weapp)使用真实API
  - 使用process.env.NODE_ENV === 'development'自动判断
- 更新5个页面使用环境变量判断
- 新增开发工作流文档到lessons-learned.md

修改文件:
- src/pages/search/index.vue: 修复布局和分页逻辑
- src/pages/week-hot-material/index.vue: 环境判断
- src/pages/message/index.vue: 环境判断
- src/pages/material-list/index.vue: 环境判断
- src/pages/product-center/index.vue: 环境判断
- docs/lessons-learned.md: 新增开发工作流章节Mock数据环境自动切换模式

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
......@@ -16,6 +16,8 @@
- [错误处理](#3-错误处理)
- [API 调用错误:使用 fn() 包装](#坑-api-调用了-fn-包装重复-2-次) ⭐ 新增
- [架构设计](#架构设计)
- [开发工作流](#开发工作流) ⭐ 新增
- [Mock 数据环境自动切换](#mock-数据环境自动切换模式) ⭐ 新增
---
......@@ -1039,6 +1041,174 @@ src/
---
## 开发工作流
### ✅ Mock 数据环境自动切换模式
#### 问题描述
在开发过程中,使用 Mock 数据进行前端开发可以提高效率,但手动切换 Mock 数据和真实 API 容易出错:
```javascript
// ❌ BAD - 硬编码开关,容易忘记切换
const USE_MOCK_DATA = true // 开发时用 true,部署时忘记改成 false
const res = USE_MOCK_DATA
? await mockWeekHotAPI(params)
: await weekHotAPI(params)
// 风险:部署到生产环境时仍然使用 Mock 数据
```
**错误表现**:
- 🔴 开发完成后忘记关闭 Mock 数据开关
- 🔴 生产环境返回假数据,导致严重问题
- 🔴 需要手动在每个页面中切换,容易遗漏
#### 解决方案:基于环境变量自动切换
使用 `process.env.NODE_ENV` 判断当前环境,自动选择使用 Mock 数据还是真实 API:
```javascript
// ✅ GOOD - 环境变量自动判断
const USE_MOCK_DATA = process.env.NODE_ENV === 'development'
const res = USE_MOCK_DATA
? await mockWeekHotAPI(params)
: await weekHotAPI(params)
console.log('[Week Hot] 使用 Mock 数据:', USE_MOCK_DATA)
```
#### 环境说明
| 环境 | NODE_ENV | 命令 | Mock 数据 |
|------|-----------|------|----------|
| **开发环境** | `'development'` | `pnpm dev:weapp` | ✅ 启用 |
| **生产环境** | `'production'` | `pnpm build:weapp` | ❌ 禁用 |
#### 实施步骤
1. **修改所有使用 Mock 数据的页面**(共 5 个):
- `src/pages/search/index.vue`(line 152)
- `src/pages/week-hot-material/index.vue`(line 76)
- `src/pages/message/index.vue`(line 57)
- `src/pages/material-list/index.vue`(line 131)
- `src/pages/product-center/index.vue`(line 159)
2. **统一修改代码**:
```javascript
// 修改前
const USE_MOCK_DATA = true // ❌ 硬编码
// 修改后
const USE_MOCK_DATA = process.env.NODE_ENV === 'development' // ✅ 环境判断
```
3. **添加日志**(可选,便于调试):
```javascript
console.log('[PageName] 使用 Mock 数据:', USE_MOCK_DATA)
```
#### 收益
✅ **开发环境**:
- 快速迭代,无需等待后端接口
- 测试分页、加载更多等前端逻辑
- 模拟各种数据场景
✅ **生产环境**:
- 自动切换到真实 API
- 无需手动修改代码
- 避免假数据上线风险
✅ **安全性**:
- 无法在生产环境误用 Mock 数据
- 一次配置,永久生效
#### 使用示例
```vue
<script setup>
import { ref } from 'vue'
import { weekHotAPI } from '@/api/file'
import { mockWeekHotAPI } from '@/utils/mockData'
// ⚠️ MOCK 数据开关 - 开发环境使用 mock 数据,生产环境使用真实 API
const USE_MOCK_DATA = process.env.NODE_ENV === 'development'
const list = ref([])
const loading = ref(false)
const fetchList = async (params) => {
loading.value = true
try {
console.log('[Week Hot] 使用 Mock 数据:', USE_MOCK_DATA)
// 根据开关选择使用真实 API 或 Mock 数据
const res = USE_MOCK_DATA
? await mockWeekHotAPI(params)
: await weekHotAPI(params)
if (res.code === 1 && res.data) {
list.value = res.data.list
}
} catch (err) {
console.error('获取列表失败:', err)
} finally {
loading.value = false
}
}
</script>
```
#### 注意事项
1. **确保 Mock 数据结构一致**:
- Mock 数据的返回格式必须与真实 API 一致
- 特别是 `{ code, data, msg }` 结构
- 确保分页字段名称一致
2. **生产构建前检查**:
```bash
# 开发环境(使用 Mock)
pnpm dev:weapp
# 生产构建前检查环境变量
pnpm build:weapp
# 确认打包后的代码使用真实 API
```
3. **代码审查清单**:
- [ ] 所有 Mock 数据开关都改为 `process.env.NODE_ENV === 'development'`
- [ ] 添加了调试日志
- [ ] Mock 数据结构符合真实 API
#### 相关文件
- Mock 数据定义:`src/utils/mockData.js`
- 使用 Mock 的页面(5 个):
- `src/pages/search/index.vue`
- `src/pages/week-hot-material/index.vue`
- `src/pages/message/index.vue`
- `src/pages/material-list/index.vue`
- `src/pages/product-center/index.vue`
#### 最佳实践总结
⚠️ **强制要求**:所有使用 Mock 数据的页面都必须使用环境变量判断,禁止硬编码开关。
```javascript
// ✅ 推荐写法
const USE_MOCK_DATA = process.env.NODE_ENV === 'development'
// ❌ 禁止写法
const USE_MOCK_DATA = true // 容易导致生产环境误用
```
---
## 总结
### 🎯 核心经验
......@@ -1051,7 +1221,8 @@ src/
6. **代码质量**: 强制 JSDoc 注释,统一命名规范
7. **API 调用规范**: ⚠️ **不要使用 `fn()` 包装 API,直接调用并自己处理错误**
8. **架构设计**: 分层清晰,职责单一
9. **⚠️ 写代码前必查**: 先搜索项目中是否有类似实现,保持写法一致
9. **Mock 数据切换**: ⭐ **使用环境变量 `process.env.NODE_ENV` 自动判断,禁止硬编码**
10. **⚠️ 写代码前必查**: 先搜索项目中是否有类似实现,保持写法一致
### 📚 推荐阅读
......@@ -1067,6 +1238,9 @@ src/
---
**最后更新**: 2026-02-05
**最后更新**: 2026-02-08
**维护者**: Claude Code
**项目**: Manulife WeApp
**更新记录**:
- 2026-02-08: 新增 "开发工作流" 章节,记录 Mock 数据环境自动切换模式
......
# Mock 数据配置完成总结
**日期**: 2026-02-08
**状态**: ✅ 所有页面已配置完成
---
## 已添加 Mock 数据的页面
| # | 页面 | 文件路径 | Mock 函数 | 状态 |
|---|------|---------|----------|------|
| 1 | 周热门资料 | `src/pages/week-hot-material/index.vue` | `mockWeekHotAPI` | ✅ 已完成 |
| 2 | 资料列表 | `src/pages/material-list/index.vue` | `mockFileListAPI` | ✅ 已完成 |
| 3 | 产品中心 | `src/pages/product-center/index.vue` | `mockProductListAPI` | ✅ 已完成 |
| 4 | 搜索页 | `src/pages/search/index.vue` | `mockSearchAPI` | ✅ 已完成 |
| 5 | 消息列表 | `src/pages/message/index.vue` | `mockMessageListAPI` | ✅ 已完成 |
---
## 每个页面的修改内容
### 1. 周热门资料页 ✅
**文件**: `src/pages/week-hot-material/index.vue`
**修改**:
- ✅ 导入 `mockWeekHotAPI`
- ✅ 添加 `USE_MOCK_DATA = true` 开关 (第 76 行)
- ✅ 修改 `fetchWeekHotList` 函数 (第 110-112 行)
**使用**:
```javascript
// 第 76 行
const USE_MOCK_DATA = true // ✅ 已启用
```
---
### 2. 资料列表页 ✅
**文件**: `src/pages/material-list/index.vue`
**修改**:
- ✅ 导入 `mockFileListAPI`
- ✅ 添加 `USE_MOCK_DATA = true` 开关 (第 128 行)
- ✅ 修改主 API 调用 (第 283-289 行)
- ✅ 修改搜索 API 调用 (第 573-579 行)
**使用**:
```javascript
// 第 128 行
const USE_MOCK_DATA = true // ✅ 已启用
```
**特性**:
- ✅ 支持分类过滤 (`cid`)
- ✅ 支持关键词搜索 (`keyword`)
- ✅ 8 页数据,每页 20 条
---
### 3. 产品中心页 ✅
**文件**: `src/pages/product-center/index.vue`
**修改**:
- ✅ 导入 `mockProductListAPI`
- ✅ 添加 `USE_MOCK_DATA = true` 开关 (第 158 行)
- ✅ 修改 `fetchProducts` 函数 (第 217-222 行)
**使用**:
```javascript
// 第 158 行
const USE_MOCK_DATA = true // ✅ 已启用
```
**特性**:
- ✅ 4 种产品分类(人寿保险、健康保险、意外保险、财产保险)
- ✅ 4 种标签(热销、新品、推荐、限时)
- ✅ 支持分类过滤 (`cid`)
- ✅ 支持关键词搜索 (`keyword`)
- ✅ 10 页数据,每页 10 条
---
### 4. 搜索页 ✅
**文件**: `src/pages/search/index.vue`
**修改**:
- ✅ 导入 `mockSearchAPI`
- ✅ 添加 `USE_MOCK_DATA = true` 开关 (第 151 行)
- ✅ 修改 `performSearch` 函数 (第 229-234 行)
**使用**:
```javascript
// 第 151 行
const USE_MOCK_DATA = true // ✅ 已启用
```
**特性**:
- ✅ 同时搜索产品和资料
- ✅ 支持关键词过滤
- ✅ 自动选择有数据的 tab
- ✅ 5 页数据,每页 20 条
---
### 5. 消息列表页 ✅
**文件**: `src/pages/message/index.vue`
**修改**:
- ✅ 导入 `mockMessageListAPI`
- ✅ 添加 `USE_MOCK_DATA = true` 开关 (第 56 行)
- ✅ 修改 `fetchMessageList` 函数 (第 79-89 行)
**使用**:
```javascript
// 第 56 行
const USE_MOCK_DATA = true // ✅ 已启用
```
**特性**:
- ✅ 15 种消息标题模板
- ✅ 随机创建时间(最近30天内)
- ✅ 50%概率已读
- ✅ 两种消息类型(通知、系统)
- ✅ 8 页数据,每页 10 条
---
## 全局测试步骤
### 1. 启动开发服务器
```bash
pnpm dev:weapp
```
### 2. 测试每个页面
#### 周热门资料页
1. 导航到"周热门资料"页
2. 查看首次加载(20 条数据)
3. 向下滚动,触发加载更多
4. 查看 Console: `[Mock] weekHotAPI - 第X页,共Y条`
5. 滚动到底,显示"没有更多了"
#### 资料列表页
1. 导航到"资料列表"页
2. 查看首次加载
3. 测试切换分类 tab
4. 测试搜索功能
5. 向下滚动加载更多
#### 产品中心页
1. 导航到"产品中心"页
2. 查看首次加载(10 条数据)
3. 测试切换分类 tab(人寿保险、健康保险等)
4. 测试搜索功能
5. 向下滚动加载更多
#### 搜索页
1. 导航到"搜索"页
2. 输入关键词(如"保险"、"产品"等)
3. 点击搜索或按回车
4. 查看产品和资料结果
5. 切换产品/资料 tab
6. 向下滚动加载更多
#### 消息列表页
1. 导航到"我的消息"页
2. 查看首次加载(10 条数据)
3. 向下滚动加载更多
4. 查看消息时间格式(YYYY-MM-DD)
5. 滚动到底,显示"没有更多了"
### 3. 查看 Console 日志
正常情况下会看到:
```
[Mock] weekHotAPI - 第0页,共20条
[Mock] fileListAPI - 第0页,共20条
[Mock] listAPI - 第0页,共10条
[Mock] searchAPI - 第0页,产品10条,资料10条
[Mock] myListAPI - 第1页,共10条
```
---
## 切换回真实 API
测试完成后,需要将所有页面的开关改为 `false`:
### 快速切换脚本
```bash
# 使用 sed 批量替换
sed -i '' 's/const USE_MOCK_DATA = true/const USE_MOCK_DATA = false/g' \
src/pages/week-hot-material/index.vue \
src/pages/material-list/index.vue \
src/pages/product-center/index.vue \
src/pages/search/index.vue \
src/pages/message/index.vue
```
### 手动切换
修改每个页面文件中的 `USE_MOCK_DATA` 常量:
```javascript
// 修改前
const USE_MOCK_DATA = true
// 修改后
const USE_MOCK_DATA = false
```
**需要修改的 5 个文件**:
1. `src/pages/week-hot-material/index.vue`
2. `src/pages/material-list/index.vue`
3. `src/pages/product-center/index.vue`
4. `src/pages/search/index.vue`
5. `src/pages/message/index.vue`
---
## Mock 数据字段对照表
### 周热门资料
| 字段 | 类型 | 说明 | 示例 |
|------|------|------|------|
| `meta_id` | integer | 文件ID | 1, 2, 3... |
| `name` | string | 文件名称 | "财富管理基础知识指南 PDF文档" |
| `src` | string | 文件URL | "https://cdn.example.com/files/1.pdf" |
| `size` | string | 文件大小 | "2.5MB" |
| `read_people_count` | integer | 学习人数 | 1234 |
| `read_people_percent` | number | 学习百分比 | 75 |
| `is_favorite` | string | 收藏状态 | "1" 或 "0" |
| `extension` | string | 文件扩展名 | "pdf" |
### 资料列表
| 字段 | 类型 | 说明 | 示例 |
|------|------|------|------|
| `id` | integer | 资料ID | 1, 2, 3... |
| `name` | string | 资料名称 | "2024年保险行业发展趋势报告" |
| `title` | string | 资料标题 | "2024年保险行业发展趋势报告" |
| `fileName` | string | 文件名 | "报告.pdf" |
| `size` | string | 文件大小 | "2.5MB" |
| `extension` | string | 文件扩展名 | "pdf" |
| `collected` | boolean | 是否已收藏 | true/false |
| `src` | string | 文件URL | "https://cdn.example.com/materials/1.pdf" |
| `downloadUrl` | string | 下载URL | "https://cdn.example.com/materials/1.pdf" |
### 产品列表
| 字段 | 类型 | 说明 | 示例 |
|------|------|------|------|
| `id` | integer | 产品ID | 1, 2, 3... |
| `product_name` | string | 产品名称 | "百万年金保险计划" |
| `name` | string | 产品名称(别名) | "百万年金保险计划" |
| `cover_image` | string | 封面图URL | "https://cdn.example.com/products/1.jpg" |
| `recommend` | string | 推荐标识 | "hot" 或 "" |
| `tags` | Array | 标签数组 | [{id: 1, name: "热销", bg_color: "#FEE2E2", text_color: "#DC2626"}] |
| `description` | string | 产品描述 | "这是一款优质的保险产品..." |
| `premium` | number | 保费 | 5000 |
| `category_id` | integer | 分类ID | 1 |
### 搜索结果
| 字段 | 类型 | 说明 |
|------|------|------|
| `products` | Array | 产品列表(同产品列表字段) |
| `files` | Array | 资料列表(同资料列表字段) |
### 消息列表
| 字段 | 类型 | 说明 | 示例 |
|------|------|------|------|
| `id` | integer | 消息ID | 1, 2, 3... |
| `title` | string | 消息标题 | "关于2024年新产品上线通知" |
| `intro` | string | 消息简介 | "这是一条关于..." |
| `content` | string | 消息内容 | "这是一条关于..." |
| `create_time` | string | 创建时间 | "2024-01-15" |
| `is_read` | integer | 是否已读 | 0 或 1 |
| `type` | string | 消息类型 | "notice" 或 "system" |
---
## 常见问题
### Q: Mock 数据不生效?
**检查**:
1. 确认对应页面的 `USE_MOCK_DATA = true`
2. 确认已正确导入 mock 函数
3. 查看 console 是否有 `[Mock]` 日志输出
4. 检查网络请求是否调用了 mock 函数
### Q: 数据格式不对?
**检查**:
1. 查看 `src/utils/mockData.js` 中的字段定义
2. 对照页面代码中的字段映射逻辑
3. 在 browser console 查看返回的数据结构
### Q: 想修改数据量?
**修改**:
- 修改页面的 `pageSize``limit` 变量
- Mock 数据会自动适应请求的数量
### Q: 想增加总页数?
**修改** `src/utils/mockData.js`:
```javascript
// 修改 totalPages 值
const totalPages = 10 // 从 5 改为 10
```
### Q: 搜索功能不工作?
**检查**:
1. 确认输入了关键词
2. 确认 mock 数据中有包含该关键词的数据
3. 查看 console 日志确认请求参数
---
## 验收清单
测试完成后,请确认:
- [ ] 周热门资料页滚动加载正常
- [ ] 资料列表页滚动加载正常
- [ ] 资料列表页分类切换正常
- [ ] 资料列表页搜索功能正常
- [ ] 产品中心页滚动加载正常
- [ ] 产品中心页分类切换正常
- [ ] 产品中心页搜索功能正常
- [ ] 搜索页关键词搜索正常
- [ ] 搜索页产品/资料切换正常
- [ ] 搜索页滚动加载正常
- [ ] 消息列表页滚动加载正常
- [ ] 所有页面 Console 日志正确输出
- [ ] 所有页面"没有更多了"正常显示
- [ ] 数据格式正确,无报错
---
## 下一步
1. **测试**: 逐个测试所有页面的滚动加载功能
2. **验证**: 确认数据格式和功能正常
3. **调试**: 如有问题,查看 Console 日志定位
4. **切换**: 测试完成后,切换回真实 API
5. **提交**: 将修改提交到代码仓库
---
**最后更新**: 2026-02-08
**维护者**: Claude Code
# Mock 数据测试指南
## 概述
已为以下 5 个页面创建完整的 Mock 数据支持:
| 页面 | API | Mock 函数 | 总页数 | 每页数量 |
|------|-----|----------|--------|---------|
| 周热门资料 | `weekHotAPI` | `mockWeekHotAPI` | 5 页 | 20 条 |
| 资料列表 | `fileListAPI` | `mockFileListAPI` | 8 页 | 20 条 |
| 产品中心 | `listAPI` | `mockProductListAPI` | 10 页 | 10 条 |
| 搜索页 | `searchAPI` | `mockSearchAPI` | 5 页 | 20 条 |
| 消息列表 | `myListAPI` | `mockMessageListAPI` | 8 页 | 10 条 |
## 快速启用 Mock 数据
### 方法 1: 修改页面代码开关
在每个页面的 `<script setup>` 部分添加/修改:
```javascript
// ⚠️ MOCK 数据开关
const USE_MOCK_DATA = true // ✅ 使用 Mock 数据
// const USE_MOCK_DATA = false // ❌ 使用真实 API
```
### 方法 2: 在 API 调用处添加条件判断
```javascript
// 导入 mock 函数
import { mockWeekHotAPI, mockFileListAPI, mockProductListAPI, mockSearchAPI, mockMessageListAPI } from '@/utils/mockData'
// 在 API 调用处
const res = USE_MOCK_DATA
? await mockWeekHotAPI(params)
: await weekHotAPI(params)
```
## 各页面 Mock 数据详情
### 1. 周热门资料页 (`week-hot-material/index.vue`)
**文件位置**: `src/pages/week-hot-material/index.vue:76`
**Mock 数据特性**:
- ✅ 20 种资料名称模板(财富管理、保险、投资等)
- ✅ 10 种文件类型(PDF、Word、Excel、PPT等)
- ✅ 随机学习人数(100-5000人)
- ✅ 随机学习百分比(0-100%)
- ✅ 30%概率已收藏
**已启用**: ✅ 已添加 `USE_MOCK_DATA` 开关
---
### 2. 资料列表页 (`material-list/index.vue`)
**文件位置**: 需要添加到 `src/pages/material-list/index.vue`
**Mock 数据特性**:
- ✅ 20 种资料名称模板
- ✅ 支持分类过滤(`cid`)
- ✅ 支持关键词搜索(`keyword`)
- ✅ 包含分类信息(`cate`)
**需要添加**:
```javascript
// 导入
import { mockFileListAPI } from '@/utils/mockData'
// 添加开关(在 script setup 顶部)
const USE_MOCK_DATA = true
// 修改 fetchFileList 函数(约第 176 行)
const res = USE_MOCK_DATA
? await mockFileListAPI(params)
: await fileListAPI(params)
```
---
### 3. 产品中心页 (`product-center/index.vue`)
**文件位置**: 需要添加到 `src/pages/product-center/index.vue`
**Mock 数据特性**:
- ✅ 20 种产品名称(人寿险、健康险、意外险等)
- ✅ 4 种产品分类(人寿保险、健康保险、意外保险、财产保险)
- ✅ 4 种标签(热销、新品、推荐、限时)
- ✅ 支持分类过滤(`cid`)
- ✅ 支持关键词搜索(`keyword`)
**需要添加**:
```javascript
// 导入
import { mockProductListAPI } from '@/utils/mockData'
// 添加开关(在 script setup 顶部)
const USE_MOCK_DATA = true
// 修改 fetchProducts 函数(约第 196 行)
const res = USE_MOCK_DATA
? await mockProductListAPI(params)
: await listAPI(params)
```
---
### 4. 搜索页 (`search/index.vue`)
**文件位置**: 需要添加到 `src/pages/search/index.vue`
**Mock 数据特性**:
- ✅ 同时支持产品和资料搜索
- ✅ 根据关键词过滤数据
- ✅ 返回匹配的产品和资料列表
**需要添加**:
```javascript
// 导入
import { mockSearchAPI } from '@/utils/mockData'
// 添加开关(在 script setup 顶部)
const USE_MOCK_DATA = true
// 修改 fetchSearchResults 函数
const res = USE_MOCK_DATA
? await mockSearchAPI(params)
: await searchAPI(params)
```
---
### 5. 消息列表页 (`message/index.vue`)
**文件位置**: 需要添加到 `src/pages/message/index.vue`
**Mock 数据特性**:
- ✅ 15 种消息标题模板
- ✅ 随机创建时间(最近30天内)
- ✅ 50%概率已读
- ✅ 两种消息类型(通知、系统)
**需要添加**:
```javascript
// 导入
import { mockMessageListAPI } from '@/utils/mockData'
// 添加开关(在 script setup 顶部)
const USE_MOCK_DATA = true
// 修改 fetchMessageList 函数(约第 67 行)
const res = USE_MOCK_DATA
? await mockMessageListAPI(params)
: await myListAPI(params)
```
---
## 统一 Mock API 调用器
除了单独导入各个 mock 函数,也可以使用统一的调用器:
```javascript
import { mockAPI } from '@/utils/mockData'
// 使用示例
const res = await mockAPI('weekHotAPI', params)
const res = await mockAPI('fileListAPI', params)
const res = await mockAPI('listAPI', params)
const res = await mockAPI('searchAPI', params)
const res = await mockAPI('myListAPI', params)
```
## 测试流程
### 1. 确认 Mock 数据开关已开启
检查对应页面的 `USE_MOCK_DATA = true`
### 2. 启动开发服务器
```bash
pnpm dev:weapp
```
### 3. 测试分页加载
1. **首次加载**: 查看第 1 页数据是否正常显示
2. **滚动加载**: 向下滚动到底部
3. **验证加载**:
- ✅ 显示"加载中..."
- ✅ Console 输出: `[Mock] xxxAPI - 第X页,共Y条`
- ✅ 新数据追加到列表
4. **测试数据耗尽**: 继续滚动直到显示"没有更多了"
### 4. 查看 Console 日志
正常的日志输出:
```
[Mock] weekHotAPI - 第0页,共20条
[Mock] weekHotAPI - 第1页,共20条
[Mock] weekHotAPI - 第2页,共20条
...
```
### 5. 测试搜索/筛选功能(如适用)
- **资料列表**: 切换分类、输入关键词
- **产品中心**: 切换分类、输入关键词
- **搜索页**: 输入关键词、切换产品/资料 Tab
## Mock 数据字段说明
### 周热门资料 (`weekHotAPI`)
```javascript
{
meta_id: integer, // 文件ID
name: string, // 文件名称
src: string, // 文件URL
size: string, // 文件大小(如 "2.5MB")
read_people_count: integer, // 学习人数
read_people_percent: number, // 学习百分比(0-100)
is_favorite: string // 收藏状态('1' 或 '0')
extension: string // 文件扩展名(如 'pdf')
}
```
### 资料列表 (`fileListAPI`)
```javascript
{
id: integer,
meta_id: integer,
name: string, // 资料名称
title: string, // 资料标题
fileName: string, // 文件名
desc: string, // 描述
size: string, // 文件大小
extension: string, // 文件扩展名
collected: boolean, // 是否已收藏
src: string, // 文件URL
downloadUrl: string, // 下载URL
post_date: string // 发布时间
}
```
### 产品列表 (`listAPI`)
```javascript
{
id: integer,
product_name: string, // 产品名称
name: string, // 产品名称(别名)
cover_image: string, // 封面图URL
recommend: string, // 推荐标识('hot' 或 '')
tags: Array<{ // 标签数组
id: integer,
name: string, // 标签名称
bg_color: string, // 背景色
text_color: string // 文字颜色
}>,
description: string, // 描述
premium: number, // 保费
category_id: integer // 分类ID
}
```
### 搜索 (`searchAPI`)
```javascript
{
code: 1,
msg: 'success',
data: {
products: Array, // 产品列表(同产品列表字段)
files: Array // 资料列表(同资料列表字段)
}
}
```
### 消息列表 (`myListAPI`)
```javascript
{
id: integer,
title: string, // 消息标题
intro: string, // 简介
content: string, // 内容
create_time: string, // 创建时间(YYYY-MM-DD)
is_read: integer, // 是否已读(0 或 1)
type: string // 类型('notice' 或 'system')
}
```
## 切换回真实 API
测试完成后,记得修改开关:
```javascript
const USE_MOCK_DATA = false // 使用真实 API
```
## 常见问题
### Q1: Mock 数据不生效?
**检查**:
1. 确认 `USE_MOCK_DATA = true`
2. 确认已正确导入 mock 函数
3. 查看 console 是否有 `[Mock]` 日志
4. 确认 API 调用处使用了条件判断
### Q2: 数据格式不对?
**检查**:
1. 查看 mock 数据字段是否与真实 API 一致
2. 在浏览器 console 查看返回的数据结构
3. 对照页面代码中的字段映射逻辑
### Q3: 想修改每页数量?
**修改**:
- 在页面代码中修改 `pageSize``limit` 变量
- Mock 数据会自动适应请求的数量
### Q4: 想增加更多页数?
**修改** `src/utils/mockData.js`:
```javascript
// 修改 totalPages 值
const totalPages = 10 // 从 5 改为 10
```
## 完成清单
使用本指南后,请确认:
- [ ] 周热门资料页已添加 Mock 数据 ✅
- [ ] 资料列表页已添加 Mock 数据
- [ ] 产品中心页已添加 Mock 数据
- [ ] 搜索页已添加 Mock 数据
- [ ] 消息列表页已添加 Mock 数据
- [ ] 所有页面的滚动加载功能正常
- [ ] Console 日志正确输出
- [ ] 测试完成后已切换回真实 API
---
**最后更新**: 2026-02-08
**维护者**: Claude Code
# 周热门资料滚动加载测试指南
## Mock 数据说明
已创建 `src/utils/mockData.js` 文件,用于测试滚动加载更多功能。
### Mock 数据特性
-**总共 5 页数据**,每页 20 条,共 100 条资料
-**随机生成**的学习人数(100-5000人)
-**随机生成**的学习百分比(0-100%)
-**随机生成**的文件大小(0.5MB-10MB)
-**随机生成**的收藏状态(30%概率已收藏)
-**10种文件类型**: PDF、Word、Excel、PPT、TXT、图片等
-**20种资料名称模板**: 涵盖财富管理、保险、投资等主题
### 模拟网络延迟
- 每次请求延迟 **300-800ms**,模拟真实网络环境
## 如何使用 Mock 数据
### 方法 1: 修改代码开关(推荐)
`src/pages/week-hot-material/index.vue` 第 76 行:
```javascript
// ⚠️ MOCK 数据开关 - 设置为 true 使用 mock 数据,false 使用真实 API
const USE_MOCK_DATA = true // ✅ 使用 Mock 数据
// const USE_MOCK_DATA = false // ❌ 使用真实 API
```
### 方法 2: 直接调用 Mock API
```javascript
import { mockWeekHotAPI } from '@/utils/mockData'
// 调用 Mock API
const res = await mockWeekHotAPI({ page: 0, limit: 20 })
```
## 测试步骤
### 1. 启动开发服务器
```bash
pnpm dev:weapp
```
### 2. 导航到周热门资料页
在微信开发者工具中,点击导航到"周热门资料"页面。
### 3. 测试首次加载
- ✅ 查看是否加载了第 1 页数据(20条)
- ✅ 查看 console 日志:`[Mock] 生成第0页数据,共20条`
- ✅ 验证列表正常显示
### 4. 测试滚动加载更多
- ✅ 向下滚动列表到底部
- ✅ 等待 300-800ms(模拟网络延迟)
- ✅ 查看是否显示"加载中..."状态
- ✅ 查看是否加载了第 2 页数据
- ✅ 查看 console 日志:`[Mock] 生成第1页数据,共20条`
### 5. 测试多页加载
继续滚动,依次测试:
- 第 3 页: `[Mock] 生成第2页数据,共20条`
- 第 4 页: `[Mock] 生成第3页数据,共20条`
- 第 5 页: `[Mock] 生成第4页数据,共20条`
### 6. 测试数据耗尽
- ✅ 滚动到底部后,应该显示"没有更多了"
- ✅ console 日志:`[Mock] 生成第5页数据,共0条` (空数据)
-`hasMore` 应该变为 `false`
- ✅ 继续滚动不会触发加载
### 7. 测试收藏功能
- ✅ 点击任意资料的收藏按钮
- ✅ 验证收藏状态正确更新
- ✅ 查看 console 日志:`[Week Hot] 收藏状态改变: xxx true/false`
## 验证要点
### 数据完整性
- ✅ 每页数据都是 20 条(最后一页除外)
- ✅ 每条数据包含所有必需字段:
- `meta_id`: 文件ID
- `name`: 文件名称
- `src`: 文件URL
- `size`: 文件大小
- `read_people_count`: 学习人数
- `read_people_percent`: 学习百分比
- `is_favorite`: 收藏状态
- `extension`: 文件扩展名
### 状态管理
-`loading` 状态: 首次加载时显示
-`loadingMore` 状态: 加载更多时显示
-`hasMore` 状态: 数据耗尽时变为 false
-`currentPage` 状态: 每次加载后正确递增
### 用户体验
- ✅ 加载状态友好提示
- ✅ 空状态提示(第1页无数据时)
- ✅ 没有更多数据提示
- ✅ 滚动流畅,无卡顿
## Console 日志示例
正常加载流程的日志:
```
[Week Hot] 页面参数: {}
[Week Hot] 请求参数: {page: 0, limit: 20}
[Week Hot] 使用 Mock 数据: true
[Mock] 生成第0页数据,共20条
[Week Hot] 数据: {list: Array(20)}
[Week Hot] 触底加载更多
[Mock] 生成第1页数据,共20条
[Week Hot] 触底加载更多
[Mock] 生成第2页数据,共20条
...
```
## 切换回真实 API
测试完成后,记得切换回真实 API:
```javascript
// ⚠️ MOCK 数据开关 - 设置为 true 使用 mock 数据,false 使用真实 API
const USE_MOCK_DATA = false // ✅ 使用真实 API
```
## 常见问题
### Q1: Mock 数据不生效?
**检查**:
- 确认 `USE_MOCK_DATA = true`
- 确认已正确导入 `mockWeekHotAPI`
- 查看 console 是否有 `[Week Hot] 使用 Mock 数据: true`
### Q2: 滚动不触发加载?
**检查**:
- 确认已滚动到列表最底部
- 查看 `hasMore` 是否为 `true`
- 查看 `loadingMore` 是否为 `false`
- 确认防抖定时器(300ms)已过
### Q3: 数据重复?
**检查**:
- 确认 `isLoadMore` 参数正确传递
- 确认代码中使用的是追加逻辑:
```javascript
if (isLoadMore) {
currentList.value = [...currentList.value, ...listData]
}
```
### Q4: 想修改每页数量?
**修改** `src/pages/week-hot-material/index.vue` 第 72 行:
```javascript
const pageSize = 20 // 修改为你想要的数量,如 10、30 等
```
## 扩展 Mock 数据
如需修改 Mock 数据生成逻辑,编辑 `src/utils/mockData.js`:
### 修改总页数
```javascript
const data = generatePageData(page, limit, 10) // 总共10页数据
```
### 添加更多资料名称模板
```javascript
const MATERIAL_TEMPLATES = [
// ... 现有模板
'你的新资料名称',
'另一个新资料名称'
]
```
### 修改文件大小范围
```javascript
function generateRandomSize() {
const sizeInMB = (Math.random() * 20 + 1).toFixed(1) // 1MB-21MB
return `${sizeInMB}MB`
}
```
---
**最后更新**: 2026-02-08
**维护者**: Claude Code
......@@ -123,9 +123,13 @@ import { useListItemClick, ListType } from '@/composables/useListItemClick'
import { getDocumentIcon, getDocumentLabel } from '@/utils/documentIcons'
import { debounce } from '@/utils/debounce'
import { fileListAPI } from '@/api/file'
import { mockFileListAPI } from '@/utils/mockData'
import { useCollectOperation } from '@/composables/useCollectOperation'
import Taro from '@tarojs/taro'
// ⚠️ MOCK 数据开关 - 开发环境使用 mock 数据,生产环境使用真实 API
const USE_MOCK_DATA = process.env.NODE_ENV === 'development'
const searchValue = ref('')
const activeTabId = ref('all') // 默认选中"全部"
const listVisible = ref(true)
......@@ -281,9 +285,12 @@ const fetchMaterialList = async (params = {}, isLoadMore = false) => {
}
console.log('[Material List] 请求参数:', params)
console.log('[Material List] 使用 Mock 数据:', USE_MOCK_DATA)
// 调用接口
const res = await fileListAPI(params)
// 根据开关选择使用真实 API 或 Mock 数据
const res = USE_MOCK_DATA
? await mockFileListAPI(params)
: await fileListAPI(params)
if (res.code === 1 && res.data) {
// 如果是初始请求(没有 child_id),保存完整的分类信息
......@@ -301,10 +308,18 @@ const fetchMaterialList = async (params = {}, isLoadMore = false) => {
// 加载更多:追加数据
allList.value = [...allList.value, ...allListData]
categoryListCache.value.set('all', allList.value)
// ✅ 同步更新 currentList(如果当前显示的是"全部")
if (activeTabId.value === 'all') {
currentList.value = allList.value
}
} else {
// 首次加载:替换数据
allList.value = allListData
categoryListCache.value.set('all', allListData)
// ✅ 同步更新 currentList(如果当前显示的是"全部")
if (activeTabId.value === 'all') {
currentList.value = allListData
}
}
// 判断是否还有更多数据
......@@ -328,7 +343,11 @@ const fetchMaterialList = async (params = {}, isLoadMore = false) => {
const existingData = categoryListCache.value.get(cacheKey) || []
const newData = [...existingData, ...listData]
categoryListCache.value.set(cacheKey, newData)
currentList.value = newData
// ✅ 同步更新 currentList(如果当前显示的是该缓存)
const currentCacheKey = params.child_id || params.keyword || 'all'
if (activeTabId.value === currentCacheKey) {
currentList.value = newData
}
} else {
// 首次加载:替换数据
categoryListCache.value.set(cacheKey, listData)
......@@ -572,7 +591,12 @@ const onSearch = async () => {
// 调用接口搜索
try {
loading.value = true
const res = await fileListAPI(params)
console.log('[Material List] 搜索使用 Mock 数据:', USE_MOCK_DATA)
// 根据开关选择使用真实 API 或 Mock 数据
const res = USE_MOCK_DATA
? await mockFileListAPI(params)
: await fileListAPI(params)
if (res.code === 1 && res.data) {
if (res.data.list?.length) {
......
......@@ -51,6 +51,10 @@ import { useLoad, usePullDownRefresh, useReachBottom, stopPullDownRefresh } from
import { useGo } from '@/hooks/useGo'
import NavHeader from '@/components/NavHeader.vue'
import { myListAPI } from '@/api/news'
import { mockMessageListAPI } from '@/utils/mockData'
// ⚠️ MOCK 数据开关 - 开发环境使用 mock 数据,生产环境使用真实 API
const USE_MOCK_DATA = process.env.NODE_ENV === 'development'
const go = useGo()
......@@ -77,10 +81,18 @@ const fetchMessageList = async (refresh = false) => {
loading.value = true
try {
const res = await myListAPI({
page: page.value,
limit: limit.value
})
console.log('[Message] 使用 Mock 数据:', USE_MOCK_DATA)
// 根据开关选择使用真实 API 或 Mock 数据
const res = USE_MOCK_DATA
? await mockMessageListAPI({
page: page.value,
limit: limit.value
})
: await myListAPI({
page: page.value,
limit: limit.value
})
if (res.code === 1) {
const list = res.data?.list || []
......
......@@ -153,6 +153,10 @@ import SearchBar from '@/components/SearchBar.vue'
import PlanFormContainer from '@/components/PlanFormContainer.vue'
import { useListItemClick, ListType } from '@/composables/useListItemClick'
import { listAPI } from '@/api/get_product'
import { mockProductListAPI } from '@/utils/mockData'
// ⚠️ MOCK 数据开关 - 开发环境使用 mock 数据,生产环境使用真实 API
const USE_MOCK_DATA = process.env.NODE_ENV === 'development'
const activeTabId = ref('')
......@@ -214,7 +218,12 @@ const fetchProducts = async (isLoadMore = false) => {
params.keyword = searchValue.value
}
const res = await listAPI(params)
console.log('[Product Center] 使用 Mock 数据:', USE_MOCK_DATA)
// 根据开关选择使用真实 API 或 Mock 数据
const res = USE_MOCK_DATA
? await mockProductListAPI(params)
: await listAPI(params)
if (res.code === 1 && res.data) {
// 更新分类列表(首次加载时)
......
......@@ -3,9 +3,9 @@
* @Description: 搜索页面 - 支持产品和资料搜索,实时查询API
-->
<template>
<view class="h-screen bg-[#FFF] flex flex-col">
<!-- 固定顶部:导航栏 + 搜索栏 -->
<view class="bg-[#FFF] z-10">
<view class="bg-[#FFF]">
<!-- 固定顶部:导航栏 + 搜索栏 + Tabs + 结果计数 -->
<view class="bg-[#FFF] sticky top-0 z-10">
<NavHeader title="搜索" />
<!-- Search Input -->
......@@ -20,10 +20,7 @@
@clear="clearSearch"
/>
</view>
</view>
<!-- Tabs + 列表容器 -->
<view class="flex-1 min-h-0 flex flex-col mt-[32rpx] px-[40rpx]">
<!-- Tabs Container -->
<nut-tabs v-model="activeTab">
<!-- 自定义标签栏 -->
......@@ -46,81 +43,79 @@
</nut-tabs>
<!-- Result Count -->
<view v-if="currentList.length > 0" class="text-[#6B7280] text-[24rpx] mb-[24rpx]">
<view v-if="currentList.length > 0" class="px-[60rpx] text-[#6B7280] text-[24rpx] pb-[24rpx]">
找到 {{ currentTotal }} 个相关结果
</view>
</view>
<!-- 列表容器 -->
<view class="px-[40rpx] pb-[calc(160rpx+env(safe-area-inset-bottom))]">
<!-- 可滚动列表区域(支持触底加载更多) -->
<scroll-view
class="flex-1 min-h-0 overflow-y-auto pb-[calc(160rpx+env(safe-area-inset-bottom))] box-border"
scroll-y
<!-- Search Results -->
<view
v-if="currentList.length > 0"
:key="listRenderKey"
>
<!-- Search Results -->
<view
v-if="currentList.length > 0"
:key="listRenderKey"
>
<!-- Product Results -->
<view v-if="activeTab === 'product'" class="flex flex-col gap-[24rpx] pb-[40rpx]">
<ProductCard
v-for="(item, index) in currentList"
:key="index"
:product-id="item.id"
:product-name="item.product_name || item.name"
:tags="item.tags || []"
class="search-result-item"
:style="{ animationDelay: `${index * 30}ms` }"
@detail="goToProductDetail"
@plan="openPlanPopup"
/>
</view>
<!-- Product Results -->
<view v-if="activeTab === 'product'" class="flex flex-col gap-[24rpx] pb-[40rpx]">
<ProductCard
v-for="(item, index) in currentList"
:key="index"
:product-id="item.id"
:product-name="item.product_name || item.name"
:tags="item.tags || []"
class="search-result-item"
:style="{ animationDelay: `${index * 30}ms` }"
@detail="goToProductDetail"
@plan="openPlanPopup"
/>
</view>
<!-- File Results -->
<view v-else-if="activeTab === 'file'" class="flex flex-col gap-[24rpx] pb-[40rpx]">
<MaterialCard
v-for="(item, index) in currentList"
:key="index"
:id="item.id"
:title="item.title"
:file-name="item.fileName"
:file-size="item.fileSize"
:learners="item.learners"
:read-people-percent="item.readPeoplePercent"
:collected="item.collected"
:extension="item.extension"
:download-url="item.downloadUrl"
class="search-result-item"
:style="{ animationDelay: `${index * 30}ms` }"
@collect-changed="handleCollectChanged(item, $event)"
/>
</view>
<!-- File Results -->
<view v-else-if="activeTab === 'file'" class="flex flex-col gap-[24rpx] pb-[40rpx]">
<MaterialCard
v-for="(item, index) in currentList"
:key="index"
:id="item.id"
:title="item.title"
:file-name="item.fileName"
:file-size="item.fileSize"
:learners="item.learners"
:read-people-percent="item.readPeoplePercent"
:collected="item.collected"
:extension="item.extension"
:download-url="item.downloadUrl"
class="search-result-item"
:style="{ animationDelay: `${index * 30}ms` }"
@collect-changed="handleCollectChanged(item, $event)"
/>
</view>
<!-- 加载更多提示 -->
<view v-if="currentList.length > 0" class="flex items-center justify-center py-[40rpx]">
<view v-if="loadingMore" class="flex items-center">
<view class="loading-spinner-small"></view>
<text class="ml-[12rpx] text-[#9CA3AF] text-[24rpx]">加载中...</text>
</view>
<view v-else-if="!hasMore" class="text-[#9CA3AF] text-[24rpx]">
没有更多了
</view>
<!-- 加载更多提示 -->
<view v-if="currentList.length > 0" class="flex items-center justify-center py-[40rpx]">
<view v-if="loadingMore" class="flex items-center">
<view class="loading-spinner-small"></view>
<text class="ml-[12rpx] text-[#9CA3AF] text-[24rpx]">加载中...</text>
</view>
<view v-else-if="!hasMore" class="text-[#9CA3AF] text-[24rpx]">
没有更多了
</view>
</view>
</view>
<!-- Empty State (已搜索但无结果) -->
<view v-else-if="hasSearched && currentList.length === 0" class="flex flex-col items-center justify-center py-[40rpx]">
<nut-empty description="暂无搜索结果" image="empty">
<view class="text-[#9CA3AF] text-[24rpx] mt-[12rpx]">试试其他关键词吧</view>
</nut-empty>
</view>
<!-- Empty State (已搜索但无结果) -->
<view v-else-if="hasSearched && currentList.length === 0" class="flex flex-col items-center justify-center py-[40rpx]">
<nut-empty description="暂无搜索结果" image="empty">
<view class="text-[#9CA3AF] text-[24rpx] mt-[12rpx]">试试其他关键词吧</view>
</nut-empty>
</view>
<!-- Initial State (从未搜索过) -->
<view v-else class="flex flex-col items-center justify-center py-[120rpx]">
<IconFont name="search" class="text-gray-300 mb-[24rpx]" size="64" />
<view class="text-[#6B7280] text-[28rpx]">搜索产品或资料</view>
<view class="text-[#9CA3AF] text-[24rpx] mt-[12rpx]">输入关键词开始搜索,自动切换分类</view>
</view>
</scroll-view>
<!-- Initial State (从未搜索过) -->
<view v-else class="flex flex-col items-center justify-center py-[120rpx]">
<IconFont name="search" class="text-gray-300 mb-[24rpx]" size="64" />
<view class="text-[#6B7280] text-[28rpx]">搜索产品或资料</view>
<view class="text-[#9CA3AF] text-[24rpx] mt-[12rpx]">输入关键词开始搜索,自动切换分类</view>
</view>
</view>
<!-- Plan Form Container -->
......@@ -146,6 +141,10 @@ import ProductCard from '@/components/ProductCard.vue'
import MaterialCard from '@/components/MaterialCard.vue'
import PlanFormContainer from '@/components/PlanFormContainer.vue'
import { searchAPI } from '@/api/search'
import { mockSearchAPI } from '@/utils/mockData'
// ⚠️ MOCK 数据开关 - 开发环境使用 mock 数据,生产环境使用真实 API
const USE_MOCK_DATA = process.env.NODE_ENV === 'development'
// Navigation
const go = useGo()
......@@ -226,7 +225,12 @@ const performSearch = async (keyword, type, page = 0, limit = pageSize, isLoadMo
const params = { keyword, page, limit }
if (type) params.type = type
const res = await searchAPI(params)
console.log('[Search] 使用 Mock 数据:', USE_MOCK_DATA)
// 根据开关选择使用真实 API 或 Mock 数据
const res = USE_MOCK_DATA
? await mockSearchAPI(params)
: await searchAPI(params)
if (res.code === 1) {
// 映射产品列表
......@@ -266,11 +270,7 @@ const performSearch = async (keyword, type, page = 0, limit = pageSize, isLoadMo
productsTotal.value = res.data.products.total || 0
filesTotal.value = res.data.files.total || 0
// 判断是否还有更多数据
// 如果返回的数据量少于请求的量,说明没有更多了
const currentListLength = type === 'product' ? newProducts.length : newFiles.length
hasMore.value = currentListLength >= limit
// ⚠️ 重要:必须先自动选择 tab,然后再计算 hasMore
// 如果不传 type,自动选择有数据的 tab(仅首次搜索时)
if (!type && !isLoadMore) {
if (productsTotal.value > 0) {
......@@ -281,6 +281,19 @@ const performSearch = async (keyword, type, page = 0, limit = pageSize, isLoadMo
// 如果都为 0,默认 product
}
// 判断是否还有更多数据
// 使用当前列表长度与总数比较
// 注意:需要根据实际选择的tab来判断
const actualTab = type || activeTab.value
if (actualTab === 'product') {
hasMore.value = products.value.length < productsTotal.value
} else if (actualTab === 'file') {
hasMore.value = files.value.length < filesTotal.value
} else {
// 如果都没有选中,保守设置为false
hasMore.value = false
}
hasSearched.value = true
listRenderKey.value += 1
......@@ -535,7 +548,7 @@ const handleCollectChanged = (item, newStatus) => {
.filter-tabs-wrapper {
display: flex;
overflow-x: auto;
padding: 24rpx 24rpx;
padding: 24rpx 60rpx;
gap: 24rpx;
transition: all 0.3s ease;
background-color: #FFF;
......
......@@ -6,5 +6,6 @@ export default {
navigationBarTitleText: '本周热门资料',
enablePullDownRefresh: true,
backgroundColor: '#F9FAFB',
navigationStyle: 'custom'
navigationStyle: 'custom',
onReachBottomDistance: 50
}
......
......@@ -3,14 +3,14 @@
* @Description: 本周热门资料页 - 使用 MaterialCard 组件
-->
<template>
<view class="h-screen bg-[#F9FAFB] flex flex-col py-[32rpx]">
<view class="min-h-screen bg-[#F9FAFB] py-[32rpx]">
<NavHeader title="本周热门资料" />
<!-- 列表容器 -->
<view
v-if="listVisible"
:key="listRenderKey"
class="flex-1 min-h-0 overflow-y-auto px-[32rpx] pb-[calc(160rpx+env(safe-area-inset-bottom))] box-border"
class="px-[32rpx]"
>
<!-- 加载状态 -->
<view v-if="loading && currentList.length === 0" class="flex items-center justify-center py-[60rpx]">
......@@ -61,6 +61,7 @@ import Taro, { useLoad, useReachBottom } from '@tarojs/taro'
import NavHeader from '@/components/NavHeader.vue'
import MaterialCard from '@/components/MaterialCard.vue'
import { weekHotAPI } from '@/api/file'
import { mockWeekHotAPI } from '@/utils/mockData'
const listVisible = ref(true)
const listRenderKey = ref(0)
......@@ -71,6 +72,9 @@ const currentList = ref([])
const currentPage = ref(0) // 当前页码(从0开始)
const pageSize = 20 // 每页数量
// ⚠️ MOCK 数据开关 - 开发环境使用 mock 数据,生产环境使用真实 API
const USE_MOCK_DATA = process.env.NODE_ENV === 'development'
/**
* 处理收藏状态改变
*
......@@ -104,9 +108,12 @@ const fetchWeekHotList = async (params = {}, isLoadMore = false) => {
}
console.log('[Week Hot] 请求参数:', params)
console.log('[Week Hot] 使用 Mock 数据:', USE_MOCK_DATA)
// 调用接口
const res = await weekHotAPI(params)
// 根据开关选择使用真实 API 或 Mock 数据
const res = USE_MOCK_DATA
? await mockWeekHotAPI(params)
: await weekHotAPI(params)
if (res.code === 1 && res.data) {
console.log('[Week Hot] 数据:', res.data)
......
/**
* @Description: Mock 数据生成工具 - 用于测试分页加载功能
* @Date: 2026-02-08
*
* 支持的 API Mock:
* - weekHotAPI: 周热门资料
* - fileListAPI: 资料列表
* - listAPI: 产品列表
* - searchAPI: 搜索(产品+资料)
* - myListAPI: 消息列表
*/
// ============================================================================
// 工具函数
// ============================================================================
/**
* 生成随机文件大小
* @returns {string} 文件大小(如 "2.5MB")
*/
function generateRandomSize() {
const sizeInMB = (Math.random() * 10 + 0.5).toFixed(1)
return `${sizeInMB}MB`
}
/**
* 生成随机学习人数
* @returns {number} 学习人数(100-5000之间)
*/
function generateRandomReadCount() {
return Math.floor(Math.random() * 4900) + 100
}
/**
* 生成随机学习百分比
* @returns {number} 学习百分比(0-100之间)
*/
function generateRandomReadPercent() {
return Math.floor(Math.random() * 100)
}
/**
* 生成随机收藏状态
* @returns {string} '1' 或 '0'
*/
function generateRandomFavorite() {
return Math.random() > 0.7 ? '1' : '0'
}
/**
* 模拟网络延迟
* @param {number} min 最小延迟(ms)
* @param {number} max 最大延迟(ms)
* @returns {Promise}
*/
function mockDelay(min = 300, max = 800) {
const delay = Math.random() * (max - min) + min
return new Promise(resolve => setTimeout(resolve, delay))
}
// ============================================================================
// 1. 周热门资料 Mock (weekHotAPI)
// ============================================================================
const WEEK_HOT_MATERIALS = [
'财富管理基础知识指南',
'保险产品销售技巧',
'客户关系管理实战',
'家庭资产配置方案',
'税务筹划实用手册',
'退休规划完整教程',
'投资组合管理策略',
'风险控制与合规要求',
'高净值客户开发指南',
'理财产品营销话术',
'基金定投实战技巧',
'保单整理服务流程',
'传承规划案例分析',
'健康险产品对比分析',
'年金保险销售指南',
'重疾险核保知识',
'教育金规划方案',
'房贷规划实务操作',
'家族信托业务介绍',
'私募股权投资指南'
]
const FILE_TYPES = [
{ extension: 'pdf', name: 'PDF文档' },
{ extension: 'doc', name: 'Word文档' },
{ extension: 'docx', name: 'Word文档' },
{ extension: 'xls', name: 'Excel表格' },
{ extension: 'xlsx', name: 'Excel表格' },
{ extension: 'ppt', name: 'PPT演示文稿' },
{ extension: 'pptx', name: 'PPT演示文稿' },
{ extension: 'txt', name: '文本文件' },
{ extension: 'jpg', name: '图片' },
{ extension: 'png', name: '图片' }
]
/**
* 生成周热门资料数据
*/
function generateWeekHotItem(id) {
const fileType = FILE_TYPES[Math.floor(Math.random() * FILE_TYPES.length)]
const materialName = WEEK_HOT_MATERIALS[Math.floor(Math.random() * WEEK_HOT_MATERIALS.length)]
return {
meta_id: id,
name: `${materialName} ${fileType.name.toUpperCase()}`,
src: `https://placehold.co/100x100/e2e8f0/475569?text=${fileType.extension.toUpperCase()}`,
size: generateRandomSize(),
read_people_count: generateRandomReadCount(),
read_people_percent: generateRandomReadPercent(),
is_favorite: generateRandomFavorite(),
extension: fileType.extension
}
}
/**
* Mock: weekHotAPI
*/
export async function mockWeekHotAPI(params) {
await mockDelay()
const { page = 0, limit = 20 } = params
const totalPages = 5
if (page >= totalPages) {
return { code: 1, msg: 'success', data: { list: [] } }
}
const list = []
const startIndex = page * limit
for (let i = 0; i < limit; i++) {
list.push(generateWeekHotItem(startIndex + i + 1))
}
console.log(`[Mock] weekHotAPI - 第${page}页,共${list.length}条`)
return { code: 1, msg: 'success', data: { list } }
}
// ============================================================================
// 2. 资料列表 Mock (fileListAPI)
// ============================================================================
const MATERIAL_NAMES = [
'2024年保险行业发展趋势报告',
'高净值客户开发实战手册',
'家庭保障需求分析模板',
'养老规划产品对比表',
'教育金储备方案',
'重疾险条款解读',
'百万医疗险销售指南',
'年金险产品培训资料',
'终身寿险销售技巧',
'车险理赔流程说明',
'企业财产险基础知识',
'责任险产品介绍',
'意外险保障方案',
'健康险核保手册',
'投保实务操作指南',
'客户异议处理话术',
'保单托管服务流程',
'理赔案例分析',
'保险法律法规汇编',
'行业合规要求解读'
]
/**
* 生成资料列表项
*/
function generateMaterialItem(id) {
const fileType = FILE_TYPES[Math.floor(Math.random() * FILE_TYPES.length)]
const materialName = MATERIAL_NAMES[Math.floor(Math.random() * MATERIAL_NAMES.length)]
return {
id: id,
meta_id: id,
name: materialName,
title: materialName,
fileName: `${materialName}.${fileType.extension}`,
desc: '这是一份详细的培训资料,包含丰富的案例和实战技巧...',
size: generateRandomSize(),
extension: fileType.extension,
collected: generateRandomFavorite() === '1',
src: `https://placehold.co/100x100/e2e8f0/475569?text=${fileType.extension.toUpperCase()}`,
downloadUrl: `https://placehold.co/100x100/e2e8f0/475569?text=${fileType.extension.toUpperCase()}`,
post_date: new Date().toISOString(),
value: `https://placehold.co/100x100/e2e8f0/475569?text=${fileType.extension.toUpperCase()}`
}
}
/**
* Mock: fileListAPI
*/
export async function mockFileListAPI(params) {
await mockDelay()
const { page = 0, limit = 20, cid, keyword, child_id } = params
const totalPages = 8
if (page >= totalPages) {
return { code: 1, msg: 'success', data: { list: [], total: totalPages * limit } }
}
const list = []
const startIndex = page * limit
for (let i = 0; i < limit; i++) {
const item = generateMaterialItem(startIndex + i + 1)
// 如果有关键词搜索,过滤数据
if (keyword && !item.name.includes(keyword)) {
continue
}
list.push(item)
}
console.log(`[Mock] fileListAPI - 第${page}页,共${list.length}条`)
return {
code: 1,
msg: 'success',
data: {
list,
total: totalPages * limit,
max_level: 2,
cate: {
id: parseInt(cid) || 1,
category_name: '培训资料',
category_parent: 0,
category_description: null
}
}
}
}
// ============================================================================
// 3. 产品列表 Mock (listAPI)
// ============================================================================
const PRODUCT_NAMES = [
'百万年金保险计划',
'终身寿险至尊版',
'重疾险保障计划',
'百万医疗险',
'意外伤害保险',
'教育金保险计划',
'养老理财保险',
'高端医疗险',
'定期寿险',
'终身寿险',
'企业年金保险',
'团体意外险',
'家庭财产保险',
'责任保险系列',
'旅游保险',
'留学保险',
'健康保险计划',
'车辆保险',
'财产一切险',
'工程保险'
]
const PRODUCT_TAGS = [
{ id: 1, name: '热销', bg_color: '#FEE2E2', text_color: '#DC2626' },
{ id: 2, name: '新品', bg_color: '#DBEAFE', text_color: '#2563EB' },
{ id: 3, name: '推荐', bg_color: '#D1FAE5', text_color: '#059669' },
{ id: 4, name: '限时', bg_color: '#FEF3C7', text_color: '#D97706' }
]
const PRODUCT_CATEGORIES = [
{ id: 1, name: '人寿保险' },
{ id: 2, name: '健康保险' },
{ id: 3, name: '意外保险' },
{ id: 4, name: '财产保险' }
]
/**
* 生成产品列表项
*/
function generateProductItem(id) {
const productName = PRODUCT_NAMES[Math.floor(Math.random() * PRODUCT_NAMES.length)]
const recommend = Math.random() > 0.7 ? 'hot' : ''
// 随机选择1-2个标签
const tags = []
const tagCount = Math.floor(Math.random() * 2) + 1
const availableTags = [...PRODUCT_TAGS].sort(() => Math.random() - 0.5)
for (let i = 0; i < tagCount; i++) {
tags.push(availableTags[i])
}
return {
id: id,
product_name: productName,
name: productName,
cover_image: `https://placehold.co/400x300/4caf50/ffffff?text=${encodeURIComponent(productName.substring(0, 6))}`,
recommend: recommend,
tags: tags,
description: '这是一款优质的保险产品,为您的家庭提供全面保障...',
premium: Math.floor(Math.random() * 10000 + 1000),
category_id: Math.floor(Math.random() * 4) + 1
}
}
/**
* Mock: listAPI (产品列表)
*/
export async function mockProductListAPI(params) {
await mockDelay()
const { page = 0, limit = 10, cid, keyword } = params
const totalPages = 10
if (page >= totalPages) {
return { code: 1, msg: 'success', data: { list: [], categories: [], total: 0 } }
}
const list = []
const startIndex = page * limit
for (let i = 0; i < limit; i++) {
const item = generateProductItem(startIndex + i + 1)
// 如果有分类过滤
if (cid && item.category_id !== parseInt(cid)) {
continue
}
// 如果有关键词搜索
if (keyword && !item.product_name.includes(keyword)) {
continue
}
list.push(item)
}
console.log(`[Mock] listAPI - 第${page}页,共${list.length}条`)
return {
code: 1,
msg: 'success',
data: {
list,
categories: PRODUCT_CATEGORIES,
total: totalPages * limit
}
}
}
// ============================================================================
// 4. 搜索 Mock (searchAPI)
// ============================================================================
/**
* Mock: searchAPI (支持产品和资料搜索)
*/
export async function mockSearchAPI(params) {
await mockDelay()
const { page = 0, limit = 20, keyword, type } = params
if (!keyword) {
// 🔧 优化:如果没有关键词,返回更多推荐数据用于测试
// 生成20个产品和20个资料作为默认搜索结果
const defaultProducts = []
const defaultFiles = []
for (let i = 0; i < 20; i++) {
const productItem = generateProductItem(i + 1)
defaultProducts.push({
...productItem,
id: i + 1,
cover_image: productItem.cover_image
})
const materialItem = generateMaterialItem(i + 1)
defaultFiles.push({
...materialItem,
id: i + 1,
fileName: materialItem.fileName,
fileSize: materialItem.size,
learners: `${materialItem.read_people_count}人学习`,
readPeoplePercent: materialItem.read_people_percent,
collected: materialItem.collected,
extension: materialItem.extension,
downloadUrl: materialItem.downloadUrl,
title: materialItem.title,
src: materialItem.src
})
}
console.log(`[Mock] searchAPI - 无关键词,返回默认数据:产品${defaultProducts.length}条,资料${defaultFiles.length}条`)
return {
code: 1,
msg: 'success',
data: {
products: { list: defaultProducts, total: defaultProducts.length * 5 },
files: { list: defaultFiles, total: defaultFiles.length * 5 }
}
}
}
const totalPages = 5
if (page >= totalPages) {
return {
code: 1,
msg: 'success',
data: {
products: { list: [], total: 0 },
files: { list: [], total: 0 }
}
}
}
const products = []
const files = []
const startIndex = page * limit
// 🔧 优化:每次循环都生成数据和尝试匹配,增加命中率
for (let i = 0; i < limit / 2; i++) {
// 产品
const productItem = generateProductItem(startIndex + i + 1)
const productName = productItem.product_name.toLowerCase()
const searchKeyword = keyword.toLowerCase()
// 🔧 优化:更宽松的搜索条件
// 1. 完全匹配
// 2. 拆分关键词,包含任意一个字符即可
// 3. 关键词长度 >= 2 时,只要产品名称包含任意连续2个字符
const keywords = searchKeyword.split('').filter(k => k.trim())
const hasAnyChar = keywords.length > 0 && keywords.some(k => productName.includes(k))
const hasBigram = searchKeyword.length >= 2 && keywords.slice(0, -1).some((k, idx) => productName.includes(k + keywords[idx + 1]))
if (productName.includes(searchKeyword) || hasAnyChar || hasBigram) {
products.push({
...productItem,
id: startIndex + i + 1,
cover_image: productItem.cover_image
})
}
// 资料
const materialItem = generateMaterialItem(startIndex + i + 100)
const materialName = materialItem.name.toLowerCase()
const hasAnyCharMaterial = keywords.length > 0 && keywords.some(k => materialName.includes(k))
const hasBigramMaterial = searchKeyword.length >= 2 && keywords.slice(0, -1).some((k, idx) => materialName.includes(k + keywords[idx + 1]))
if (materialName.includes(searchKeyword) || hasAnyCharMaterial || hasBigramMaterial) {
files.push({
...materialItem,
id: startIndex + i + 100,
fileName: materialItem.fileName,
fileSize: materialItem.size,
learners: `${materialItem.read_people_count}人学习`,
readPeoplePercent: materialItem.read_people_percent,
collected: materialItem.collected,
extension: materialItem.extension,
downloadUrl: materialItem.downloadUrl,
title: materialItem.title,
src: materialItem.src
})
}
}
// 🔧 优化:如果没有匹配到任何数据,返回一些推荐数据
if (products.length === 0 && files.length === 0) {
console.log(`[Mock] searchAPI - 无匹配结果,返回推荐数据`)
// 生成 5 个推荐产品
for (let i = 0; i < 5; i++) {
const productItem = generateProductItem(startIndex + i + 1)
products.push({
...productItem,
id: startIndex + i + 1,
cover_image: productItem.cover_image
})
const materialItem = generateMaterialItem(startIndex + i + 100)
files.push({
...materialItem,
id: startIndex + i + 100,
fileName: materialItem.fileName,
fileSize: materialItem.size,
learners: `${materialItem.read_people_count}人学习`,
readPeoplePercent: materialItem.read_people_percent,
collected: materialItem.collected,
extension: materialItem.extension,
downloadUrl: materialItem.downloadUrl,
title: materialItem.title,
src: materialItem.src
})
}
}
console.log(`[Mock] searchAPI - 第${page}页,关键词"${keyword}",产品${products.length}条,资料${files.length}条`)
return {
code: 1,
msg: 'success',
data: {
products: { list: products, total: products.length * totalPages },
files: { list: files, total: files.length * totalPages }
}
}
}
// ============================================================================
// 5. 消息列表 Mock (myListAPI)
// ============================================================================
const MESSAGE_TITLES = [
'关于2024年新产品上线通知',
'系统升级维护公告',
'您的保单已生效提醒',
'理赔进度更新通知',
'续费提醒',
'活动邀请:财富管理讲座',
'客户服务满意度调查',
'最新培训资料已上线',
'合规要求更新通知',
'节日问候与祝福',
'产品停售通知',
'核保政策调整',
'理赔流程优化说明',
'客户权益保障计划',
'数字化服务升级公告'
]
/**
* 生成消息列表项
*/
function generateMessageItem(id) {
const title = MESSAGE_TITLES[Math.floor(Math.random() * MESSAGE_TITLES.length)]
const now = new Date()
const createDate = new Date(now.getTime() - Math.random() * 30 * 24 * 60 * 60 * 1000)
return {
id: id,
title: title,
intro: `这是一条关于"${title}"的重要通知,请及时查看详情...`,
content: `这是一条关于"${title}"的重要通知,请及时查看详情。如需了解更多信息,请联系客服。`,
create_time: formatDate(createDate),
is_read: Math.random() > 0.5 ? 1 : 0,
type: Math.random() > 0.5 ? 'notice' : 'system'
}
}
/**
* 格式化日期
*/
function formatDate(date) {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
/**
* Mock: myListAPI (消息列表)
*/
export async function mockMessageListAPI(params) {
await mockDelay()
const { page = 1, limit = 10 } = params
const totalPages = 8
if (page > totalPages) {
return { code: 1, msg: 'success', data: { list: [] } }
}
const list = []
const startIndex = (page - 1) * limit
for (let i = 0; i < limit; i++) {
list.push(generateMessageItem(startIndex + i + 1))
}
console.log(`[Mock] myListAPI - 第${page}页,共${list.length}条`)
return {
code: 1,
msg: 'success',
data: { list }
}
}
// ============================================================================
// 导出统一 Mock API 调用器
// ============================================================================
/**
* Mock API 调用器
* @param {string} apiName - API 名称
* @param {Object} params - 请求参数
* @returns {Promise}
*/
export async function mockAPI(apiName, params) {
switch (apiName) {
case 'weekHotAPI':
return await mockWeekHotAPI(params)
case 'fileListAPI':
return await mockFileListAPI(params)
case 'listAPI':
return await mockProductListAPI(params)
case 'searchAPI':
return await mockSearchAPI(params)
case 'myListAPI':
return await mockMessageListAPI(params)
default:
console.warn(`[Mock] 未知的 API: ${apiName}`)
return { code: 0, msg: 'Unknown API', data: null }
}
}