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>
Showing
11 changed files
with
1821 additions
and
32 deletions
| ... | @@ -16,6 +16,8 @@ | ... | @@ -16,6 +16,8 @@ |
| 16 | - [错误处理](#3-错误处理) | 16 | - [错误处理](#3-错误处理) |
| 17 | - [API 调用错误:使用 fn() 包装](#坑-api-调用了-fn-包装重复-2-次) ⭐ 新增 | 17 | - [API 调用错误:使用 fn() 包装](#坑-api-调用了-fn-包装重复-2-次) ⭐ 新增 |
| 18 | - [架构设计](#架构设计) | 18 | - [架构设计](#架构设计) |
| 19 | +- [开发工作流](#开发工作流) ⭐ 新增 | ||
| 20 | + - [Mock 数据环境自动切换](#mock-数据环境自动切换模式) ⭐ 新增 | ||
| 19 | 21 | ||
| 20 | --- | 22 | --- |
| 21 | 23 | ||
| ... | @@ -1039,6 +1041,174 @@ src/ | ... | @@ -1039,6 +1041,174 @@ src/ |
| 1039 | 1041 | ||
| 1040 | --- | 1042 | --- |
| 1041 | 1043 | ||
| 1044 | +## 开发工作流 | ||
| 1045 | + | ||
| 1046 | +### ✅ Mock 数据环境自动切换模式 | ||
| 1047 | + | ||
| 1048 | +#### 问题描述 | ||
| 1049 | + | ||
| 1050 | +在开发过程中,使用 Mock 数据进行前端开发可以提高效率,但手动切换 Mock 数据和真实 API 容易出错: | ||
| 1051 | + | ||
| 1052 | +```javascript | ||
| 1053 | +// ❌ BAD - 硬编码开关,容易忘记切换 | ||
| 1054 | +const USE_MOCK_DATA = true // 开发时用 true,部署时忘记改成 false | ||
| 1055 | + | ||
| 1056 | +const res = USE_MOCK_DATA | ||
| 1057 | + ? await mockWeekHotAPI(params) | ||
| 1058 | + : await weekHotAPI(params) | ||
| 1059 | + | ||
| 1060 | +// 风险:部署到生产环境时仍然使用 Mock 数据 | ||
| 1061 | +``` | ||
| 1062 | + | ||
| 1063 | +**错误表现**: | ||
| 1064 | +- 🔴 开发完成后忘记关闭 Mock 数据开关 | ||
| 1065 | +- 🔴 生产环境返回假数据,导致严重问题 | ||
| 1066 | +- 🔴 需要手动在每个页面中切换,容易遗漏 | ||
| 1067 | + | ||
| 1068 | +#### 解决方案:基于环境变量自动切换 | ||
| 1069 | + | ||
| 1070 | +使用 `process.env.NODE_ENV` 判断当前环境,自动选择使用 Mock 数据还是真实 API: | ||
| 1071 | + | ||
| 1072 | +```javascript | ||
| 1073 | +// ✅ GOOD - 环境变量自动判断 | ||
| 1074 | +const USE_MOCK_DATA = process.env.NODE_ENV === 'development' | ||
| 1075 | + | ||
| 1076 | +const res = USE_MOCK_DATA | ||
| 1077 | + ? await mockWeekHotAPI(params) | ||
| 1078 | + : await weekHotAPI(params) | ||
| 1079 | + | ||
| 1080 | +console.log('[Week Hot] 使用 Mock 数据:', USE_MOCK_DATA) | ||
| 1081 | +``` | ||
| 1082 | + | ||
| 1083 | +#### 环境说明 | ||
| 1084 | + | ||
| 1085 | +| 环境 | NODE_ENV | 命令 | Mock 数据 | | ||
| 1086 | +|------|-----------|------|----------| | ||
| 1087 | +| **开发环境** | `'development'` | `pnpm dev:weapp` | ✅ 启用 | | ||
| 1088 | +| **生产环境** | `'production'` | `pnpm build:weapp` | ❌ 禁用 | | ||
| 1089 | + | ||
| 1090 | +#### 实施步骤 | ||
| 1091 | + | ||
| 1092 | +1. **修改所有使用 Mock 数据的页面**(共 5 个): | ||
| 1093 | + - `src/pages/search/index.vue`(line 152) | ||
| 1094 | + - `src/pages/week-hot-material/index.vue`(line 76) | ||
| 1095 | + - `src/pages/message/index.vue`(line 57) | ||
| 1096 | + - `src/pages/material-list/index.vue`(line 131) | ||
| 1097 | + - `src/pages/product-center/index.vue`(line 159) | ||
| 1098 | + | ||
| 1099 | +2. **统一修改代码**: | ||
| 1100 | + ```javascript | ||
| 1101 | + // 修改前 | ||
| 1102 | + const USE_MOCK_DATA = true // ❌ 硬编码 | ||
| 1103 | + | ||
| 1104 | + // 修改后 | ||
| 1105 | + const USE_MOCK_DATA = process.env.NODE_ENV === 'development' // ✅ 环境判断 | ||
| 1106 | + ``` | ||
| 1107 | + | ||
| 1108 | +3. **添加日志**(可选,便于调试): | ||
| 1109 | + ```javascript | ||
| 1110 | + console.log('[PageName] 使用 Mock 数据:', USE_MOCK_DATA) | ||
| 1111 | + ``` | ||
| 1112 | + | ||
| 1113 | +#### 收益 | ||
| 1114 | + | ||
| 1115 | +✅ **开发环境**: | ||
| 1116 | +- 快速迭代,无需等待后端接口 | ||
| 1117 | +- 测试分页、加载更多等前端逻辑 | ||
| 1118 | +- 模拟各种数据场景 | ||
| 1119 | + | ||
| 1120 | +✅ **生产环境**: | ||
| 1121 | +- 自动切换到真实 API | ||
| 1122 | +- 无需手动修改代码 | ||
| 1123 | +- 避免假数据上线风险 | ||
| 1124 | + | ||
| 1125 | +✅ **安全性**: | ||
| 1126 | +- 无法在生产环境误用 Mock 数据 | ||
| 1127 | +- 一次配置,永久生效 | ||
| 1128 | + | ||
| 1129 | +#### 使用示例 | ||
| 1130 | + | ||
| 1131 | +```vue | ||
| 1132 | +<script setup> | ||
| 1133 | +import { ref } from 'vue' | ||
| 1134 | +import { weekHotAPI } from '@/api/file' | ||
| 1135 | +import { mockWeekHotAPI } from '@/utils/mockData' | ||
| 1136 | + | ||
| 1137 | +// ⚠️ MOCK 数据开关 - 开发环境使用 mock 数据,生产环境使用真实 API | ||
| 1138 | +const USE_MOCK_DATA = process.env.NODE_ENV === 'development' | ||
| 1139 | + | ||
| 1140 | +const list = ref([]) | ||
| 1141 | +const loading = ref(false) | ||
| 1142 | + | ||
| 1143 | +const fetchList = async (params) => { | ||
| 1144 | + loading.value = true | ||
| 1145 | + | ||
| 1146 | + try { | ||
| 1147 | + console.log('[Week Hot] 使用 Mock 数据:', USE_MOCK_DATA) | ||
| 1148 | + | ||
| 1149 | + // 根据开关选择使用真实 API 或 Mock 数据 | ||
| 1150 | + const res = USE_MOCK_DATA | ||
| 1151 | + ? await mockWeekHotAPI(params) | ||
| 1152 | + : await weekHotAPI(params) | ||
| 1153 | + | ||
| 1154 | + if (res.code === 1 && res.data) { | ||
| 1155 | + list.value = res.data.list | ||
| 1156 | + } | ||
| 1157 | + } catch (err) { | ||
| 1158 | + console.error('获取列表失败:', err) | ||
| 1159 | + } finally { | ||
| 1160 | + loading.value = false | ||
| 1161 | + } | ||
| 1162 | +} | ||
| 1163 | +</script> | ||
| 1164 | +``` | ||
| 1165 | + | ||
| 1166 | +#### 注意事项 | ||
| 1167 | + | ||
| 1168 | +1. **确保 Mock 数据结构一致**: | ||
| 1169 | + - Mock 数据的返回格式必须与真实 API 一致 | ||
| 1170 | + - 特别是 `{ code, data, msg }` 结构 | ||
| 1171 | + - 确保分页字段名称一致 | ||
| 1172 | + | ||
| 1173 | +2. **生产构建前检查**: | ||
| 1174 | + ```bash | ||
| 1175 | + # 开发环境(使用 Mock) | ||
| 1176 | + pnpm dev:weapp | ||
| 1177 | + | ||
| 1178 | + # 生产构建前检查环境变量 | ||
| 1179 | + pnpm build:weapp | ||
| 1180 | + # 确认打包后的代码使用真实 API | ||
| 1181 | + ``` | ||
| 1182 | + | ||
| 1183 | +3. **代码审查清单**: | ||
| 1184 | + - [ ] 所有 Mock 数据开关都改为 `process.env.NODE_ENV === 'development'` | ||
| 1185 | + - [ ] 添加了调试日志 | ||
| 1186 | + - [ ] Mock 数据结构符合真实 API | ||
| 1187 | + | ||
| 1188 | +#### 相关文件 | ||
| 1189 | + | ||
| 1190 | +- Mock 数据定义:`src/utils/mockData.js` | ||
| 1191 | +- 使用 Mock 的页面(5 个): | ||
| 1192 | + - `src/pages/search/index.vue` | ||
| 1193 | + - `src/pages/week-hot-material/index.vue` | ||
| 1194 | + - `src/pages/message/index.vue` | ||
| 1195 | + - `src/pages/material-list/index.vue` | ||
| 1196 | + - `src/pages/product-center/index.vue` | ||
| 1197 | + | ||
| 1198 | +#### 最佳实践总结 | ||
| 1199 | + | ||
| 1200 | +⚠️ **强制要求**:所有使用 Mock 数据的页面都必须使用环境变量判断,禁止硬编码开关。 | ||
| 1201 | + | ||
| 1202 | +```javascript | ||
| 1203 | +// ✅ 推荐写法 | ||
| 1204 | +const USE_MOCK_DATA = process.env.NODE_ENV === 'development' | ||
| 1205 | + | ||
| 1206 | +// ❌ 禁止写法 | ||
| 1207 | +const USE_MOCK_DATA = true // 容易导致生产环境误用 | ||
| 1208 | +``` | ||
| 1209 | + | ||
| 1210 | +--- | ||
| 1211 | + | ||
| 1042 | ## 总结 | 1212 | ## 总结 |
| 1043 | 1213 | ||
| 1044 | ### 🎯 核心经验 | 1214 | ### 🎯 核心经验 |
| ... | @@ -1051,7 +1221,8 @@ src/ | ... | @@ -1051,7 +1221,8 @@ src/ |
| 1051 | 6. **代码质量**: 强制 JSDoc 注释,统一命名规范 | 1221 | 6. **代码质量**: 强制 JSDoc 注释,统一命名规范 |
| 1052 | 7. **API 调用规范**: ⚠️ **不要使用 `fn()` 包装 API,直接调用并自己处理错误** | 1222 | 7. **API 调用规范**: ⚠️ **不要使用 `fn()` 包装 API,直接调用并自己处理错误** |
| 1053 | 8. **架构设计**: 分层清晰,职责单一 | 1223 | 8. **架构设计**: 分层清晰,职责单一 |
| 1054 | -9. **⚠️ 写代码前必查**: 先搜索项目中是否有类似实现,保持写法一致 | 1224 | +9. **Mock 数据切换**: ⭐ **使用环境变量 `process.env.NODE_ENV` 自动判断,禁止硬编码** |
| 1225 | +10. **⚠️ 写代码前必查**: 先搜索项目中是否有类似实现,保持写法一致 | ||
| 1055 | 1226 | ||
| 1056 | ### 📚 推荐阅读 | 1227 | ### 📚 推荐阅读 |
| 1057 | 1228 | ||
| ... | @@ -1067,6 +1238,9 @@ src/ | ... | @@ -1067,6 +1238,9 @@ src/ |
| 1067 | 1238 | ||
| 1068 | --- | 1239 | --- |
| 1069 | 1240 | ||
| 1070 | -**最后更新**: 2026-02-05 | 1241 | +**最后更新**: 2026-02-08 |
| 1071 | **维护者**: Claude Code | 1242 | **维护者**: Claude Code |
| 1072 | **项目**: Manulife WeApp | 1243 | **项目**: Manulife WeApp |
| 1244 | + | ||
| 1245 | +**更新记录**: | ||
| 1246 | +- 2026-02-08: 新增 "开发工作流" 章节,记录 Mock 数据环境自动切换模式 | ... | ... |
docs/mock-data-complete-summary.md
0 → 100644
| 1 | +# Mock 数据配置完成总结 | ||
| 2 | + | ||
| 3 | +**日期**: 2026-02-08 | ||
| 4 | +**状态**: ✅ 所有页面已配置完成 | ||
| 5 | + | ||
| 6 | +--- | ||
| 7 | + | ||
| 8 | +## 已添加 Mock 数据的页面 | ||
| 9 | + | ||
| 10 | +| # | 页面 | 文件路径 | Mock 函数 | 状态 | | ||
| 11 | +|---|------|---------|----------|------| | ||
| 12 | +| 1 | 周热门资料 | `src/pages/week-hot-material/index.vue` | `mockWeekHotAPI` | ✅ 已完成 | | ||
| 13 | +| 2 | 资料列表 | `src/pages/material-list/index.vue` | `mockFileListAPI` | ✅ 已完成 | | ||
| 14 | +| 3 | 产品中心 | `src/pages/product-center/index.vue` | `mockProductListAPI` | ✅ 已完成 | | ||
| 15 | +| 4 | 搜索页 | `src/pages/search/index.vue` | `mockSearchAPI` | ✅ 已完成 | | ||
| 16 | +| 5 | 消息列表 | `src/pages/message/index.vue` | `mockMessageListAPI` | ✅ 已完成 | | ||
| 17 | + | ||
| 18 | +--- | ||
| 19 | + | ||
| 20 | +## 每个页面的修改内容 | ||
| 21 | + | ||
| 22 | +### 1. 周热门资料页 ✅ | ||
| 23 | + | ||
| 24 | +**文件**: `src/pages/week-hot-material/index.vue` | ||
| 25 | + | ||
| 26 | +**修改**: | ||
| 27 | +- ✅ 导入 `mockWeekHotAPI` | ||
| 28 | +- ✅ 添加 `USE_MOCK_DATA = true` 开关 (第 76 行) | ||
| 29 | +- ✅ 修改 `fetchWeekHotList` 函数 (第 110-112 行) | ||
| 30 | + | ||
| 31 | +**使用**: | ||
| 32 | +```javascript | ||
| 33 | +// 第 76 行 | ||
| 34 | +const USE_MOCK_DATA = true // ✅ 已启用 | ||
| 35 | +``` | ||
| 36 | + | ||
| 37 | +--- | ||
| 38 | + | ||
| 39 | +### 2. 资料列表页 ✅ | ||
| 40 | + | ||
| 41 | +**文件**: `src/pages/material-list/index.vue` | ||
| 42 | + | ||
| 43 | +**修改**: | ||
| 44 | +- ✅ 导入 `mockFileListAPI` | ||
| 45 | +- ✅ 添加 `USE_MOCK_DATA = true` 开关 (第 128 行) | ||
| 46 | +- ✅ 修改主 API 调用 (第 283-289 行) | ||
| 47 | +- ✅ 修改搜索 API 调用 (第 573-579 行) | ||
| 48 | + | ||
| 49 | +**使用**: | ||
| 50 | +```javascript | ||
| 51 | +// 第 128 行 | ||
| 52 | +const USE_MOCK_DATA = true // ✅ 已启用 | ||
| 53 | +``` | ||
| 54 | + | ||
| 55 | +**特性**: | ||
| 56 | +- ✅ 支持分类过滤 (`cid`) | ||
| 57 | +- ✅ 支持关键词搜索 (`keyword`) | ||
| 58 | +- ✅ 8 页数据,每页 20 条 | ||
| 59 | + | ||
| 60 | +--- | ||
| 61 | + | ||
| 62 | +### 3. 产品中心页 ✅ | ||
| 63 | + | ||
| 64 | +**文件**: `src/pages/product-center/index.vue` | ||
| 65 | + | ||
| 66 | +**修改**: | ||
| 67 | +- ✅ 导入 `mockProductListAPI` | ||
| 68 | +- ✅ 添加 `USE_MOCK_DATA = true` 开关 (第 158 行) | ||
| 69 | +- ✅ 修改 `fetchProducts` 函数 (第 217-222 行) | ||
| 70 | + | ||
| 71 | +**使用**: | ||
| 72 | +```javascript | ||
| 73 | +// 第 158 行 | ||
| 74 | +const USE_MOCK_DATA = true // ✅ 已启用 | ||
| 75 | +``` | ||
| 76 | + | ||
| 77 | +**特性**: | ||
| 78 | +- ✅ 4 种产品分类(人寿保险、健康保险、意外保险、财产保险) | ||
| 79 | +- ✅ 4 种标签(热销、新品、推荐、限时) | ||
| 80 | +- ✅ 支持分类过滤 (`cid`) | ||
| 81 | +- ✅ 支持关键词搜索 (`keyword`) | ||
| 82 | +- ✅ 10 页数据,每页 10 条 | ||
| 83 | + | ||
| 84 | +--- | ||
| 85 | + | ||
| 86 | +### 4. 搜索页 ✅ | ||
| 87 | + | ||
| 88 | +**文件**: `src/pages/search/index.vue` | ||
| 89 | + | ||
| 90 | +**修改**: | ||
| 91 | +- ✅ 导入 `mockSearchAPI` | ||
| 92 | +- ✅ 添加 `USE_MOCK_DATA = true` 开关 (第 151 行) | ||
| 93 | +- ✅ 修改 `performSearch` 函数 (第 229-234 行) | ||
| 94 | + | ||
| 95 | +**使用**: | ||
| 96 | +```javascript | ||
| 97 | +// 第 151 行 | ||
| 98 | +const USE_MOCK_DATA = true // ✅ 已启用 | ||
| 99 | +``` | ||
| 100 | + | ||
| 101 | +**特性**: | ||
| 102 | +- ✅ 同时搜索产品和资料 | ||
| 103 | +- ✅ 支持关键词过滤 | ||
| 104 | +- ✅ 自动选择有数据的 tab | ||
| 105 | +- ✅ 5 页数据,每页 20 条 | ||
| 106 | + | ||
| 107 | +--- | ||
| 108 | + | ||
| 109 | +### 5. 消息列表页 ✅ | ||
| 110 | + | ||
| 111 | +**文件**: `src/pages/message/index.vue` | ||
| 112 | + | ||
| 113 | +**修改**: | ||
| 114 | +- ✅ 导入 `mockMessageListAPI` | ||
| 115 | +- ✅ 添加 `USE_MOCK_DATA = true` 开关 (第 56 行) | ||
| 116 | +- ✅ 修改 `fetchMessageList` 函数 (第 79-89 行) | ||
| 117 | + | ||
| 118 | +**使用**: | ||
| 119 | +```javascript | ||
| 120 | +// 第 56 行 | ||
| 121 | +const USE_MOCK_DATA = true // ✅ 已启用 | ||
| 122 | +``` | ||
| 123 | + | ||
| 124 | +**特性**: | ||
| 125 | +- ✅ 15 种消息标题模板 | ||
| 126 | +- ✅ 随机创建时间(最近30天内) | ||
| 127 | +- ✅ 50%概率已读 | ||
| 128 | +- ✅ 两种消息类型(通知、系统) | ||
| 129 | +- ✅ 8 页数据,每页 10 条 | ||
| 130 | + | ||
| 131 | +--- | ||
| 132 | + | ||
| 133 | +## 全局测试步骤 | ||
| 134 | + | ||
| 135 | +### 1. 启动开发服务器 | ||
| 136 | + | ||
| 137 | +```bash | ||
| 138 | +pnpm dev:weapp | ||
| 139 | +``` | ||
| 140 | + | ||
| 141 | +### 2. 测试每个页面 | ||
| 142 | + | ||
| 143 | +#### 周热门资料页 | ||
| 144 | +1. 导航到"周热门资料"页 | ||
| 145 | +2. 查看首次加载(20 条数据) | ||
| 146 | +3. 向下滚动,触发加载更多 | ||
| 147 | +4. 查看 Console: `[Mock] weekHotAPI - 第X页,共Y条` | ||
| 148 | +5. 滚动到底,显示"没有更多了" | ||
| 149 | + | ||
| 150 | +#### 资料列表页 | ||
| 151 | +1. 导航到"资料列表"页 | ||
| 152 | +2. 查看首次加载 | ||
| 153 | +3. 测试切换分类 tab | ||
| 154 | +4. 测试搜索功能 | ||
| 155 | +5. 向下滚动加载更多 | ||
| 156 | + | ||
| 157 | +#### 产品中心页 | ||
| 158 | +1. 导航到"产品中心"页 | ||
| 159 | +2. 查看首次加载(10 条数据) | ||
| 160 | +3. 测试切换分类 tab(人寿保险、健康保险等) | ||
| 161 | +4. 测试搜索功能 | ||
| 162 | +5. 向下滚动加载更多 | ||
| 163 | + | ||
| 164 | +#### 搜索页 | ||
| 165 | +1. 导航到"搜索"页 | ||
| 166 | +2. 输入关键词(如"保险"、"产品"等) | ||
| 167 | +3. 点击搜索或按回车 | ||
| 168 | +4. 查看产品和资料结果 | ||
| 169 | +5. 切换产品/资料 tab | ||
| 170 | +6. 向下滚动加载更多 | ||
| 171 | + | ||
| 172 | +#### 消息列表页 | ||
| 173 | +1. 导航到"我的消息"页 | ||
| 174 | +2. 查看首次加载(10 条数据) | ||
| 175 | +3. 向下滚动加载更多 | ||
| 176 | +4. 查看消息时间格式(YYYY-MM-DD) | ||
| 177 | +5. 滚动到底,显示"没有更多了" | ||
| 178 | + | ||
| 179 | +### 3. 查看 Console 日志 | ||
| 180 | + | ||
| 181 | +正常情况下会看到: | ||
| 182 | +``` | ||
| 183 | +[Mock] weekHotAPI - 第0页,共20条 | ||
| 184 | +[Mock] fileListAPI - 第0页,共20条 | ||
| 185 | +[Mock] listAPI - 第0页,共10条 | ||
| 186 | +[Mock] searchAPI - 第0页,产品10条,资料10条 | ||
| 187 | +[Mock] myListAPI - 第1页,共10条 | ||
| 188 | +``` | ||
| 189 | + | ||
| 190 | +--- | ||
| 191 | + | ||
| 192 | +## 切换回真实 API | ||
| 193 | + | ||
| 194 | +测试完成后,需要将所有页面的开关改为 `false`: | ||
| 195 | + | ||
| 196 | +### 快速切换脚本 | ||
| 197 | + | ||
| 198 | +```bash | ||
| 199 | +# 使用 sed 批量替换 | ||
| 200 | +sed -i '' 's/const USE_MOCK_DATA = true/const USE_MOCK_DATA = false/g' \ | ||
| 201 | + src/pages/week-hot-material/index.vue \ | ||
| 202 | + src/pages/material-list/index.vue \ | ||
| 203 | + src/pages/product-center/index.vue \ | ||
| 204 | + src/pages/search/index.vue \ | ||
| 205 | + src/pages/message/index.vue | ||
| 206 | +``` | ||
| 207 | + | ||
| 208 | +### 手动切换 | ||
| 209 | + | ||
| 210 | +修改每个页面文件中的 `USE_MOCK_DATA` 常量: | ||
| 211 | + | ||
| 212 | +```javascript | ||
| 213 | +// 修改前 | ||
| 214 | +const USE_MOCK_DATA = true | ||
| 215 | + | ||
| 216 | +// 修改后 | ||
| 217 | +const USE_MOCK_DATA = false | ||
| 218 | +``` | ||
| 219 | + | ||
| 220 | +**需要修改的 5 个文件**: | ||
| 221 | +1. `src/pages/week-hot-material/index.vue` | ||
| 222 | +2. `src/pages/material-list/index.vue` | ||
| 223 | +3. `src/pages/product-center/index.vue` | ||
| 224 | +4. `src/pages/search/index.vue` | ||
| 225 | +5. `src/pages/message/index.vue` | ||
| 226 | + | ||
| 227 | +--- | ||
| 228 | + | ||
| 229 | +## Mock 数据字段对照表 | ||
| 230 | + | ||
| 231 | +### 周热门资料 | ||
| 232 | + | ||
| 233 | +| 字段 | 类型 | 说明 | 示例 | | ||
| 234 | +|------|------|------|------| | ||
| 235 | +| `meta_id` | integer | 文件ID | 1, 2, 3... | | ||
| 236 | +| `name` | string | 文件名称 | "财富管理基础知识指南 PDF文档" | | ||
| 237 | +| `src` | string | 文件URL | "https://cdn.example.com/files/1.pdf" | | ||
| 238 | +| `size` | string | 文件大小 | "2.5MB" | | ||
| 239 | +| `read_people_count` | integer | 学习人数 | 1234 | | ||
| 240 | +| `read_people_percent` | number | 学习百分比 | 75 | | ||
| 241 | +| `is_favorite` | string | 收藏状态 | "1" 或 "0" | | ||
| 242 | +| `extension` | string | 文件扩展名 | "pdf" | | ||
| 243 | + | ||
| 244 | +### 资料列表 | ||
| 245 | + | ||
| 246 | +| 字段 | 类型 | 说明 | 示例 | | ||
| 247 | +|------|------|------|------| | ||
| 248 | +| `id` | integer | 资料ID | 1, 2, 3... | | ||
| 249 | +| `name` | string | 资料名称 | "2024年保险行业发展趋势报告" | | ||
| 250 | +| `title` | string | 资料标题 | "2024年保险行业发展趋势报告" | | ||
| 251 | +| `fileName` | string | 文件名 | "报告.pdf" | | ||
| 252 | +| `size` | string | 文件大小 | "2.5MB" | | ||
| 253 | +| `extension` | string | 文件扩展名 | "pdf" | | ||
| 254 | +| `collected` | boolean | 是否已收藏 | true/false | | ||
| 255 | +| `src` | string | 文件URL | "https://cdn.example.com/materials/1.pdf" | | ||
| 256 | +| `downloadUrl` | string | 下载URL | "https://cdn.example.com/materials/1.pdf" | | ||
| 257 | + | ||
| 258 | +### 产品列表 | ||
| 259 | + | ||
| 260 | +| 字段 | 类型 | 说明 | 示例 | | ||
| 261 | +|------|------|------|------| | ||
| 262 | +| `id` | integer | 产品ID | 1, 2, 3... | | ||
| 263 | +| `product_name` | string | 产品名称 | "百万年金保险计划" | | ||
| 264 | +| `name` | string | 产品名称(别名) | "百万年金保险计划" | | ||
| 265 | +| `cover_image` | string | 封面图URL | "https://cdn.example.com/products/1.jpg" | | ||
| 266 | +| `recommend` | string | 推荐标识 | "hot" 或 "" | | ||
| 267 | +| `tags` | Array | 标签数组 | [{id: 1, name: "热销", bg_color: "#FEE2E2", text_color: "#DC2626"}] | | ||
| 268 | +| `description` | string | 产品描述 | "这是一款优质的保险产品..." | | ||
| 269 | +| `premium` | number | 保费 | 5000 | | ||
| 270 | +| `category_id` | integer | 分类ID | 1 | | ||
| 271 | + | ||
| 272 | +### 搜索结果 | ||
| 273 | + | ||
| 274 | +| 字段 | 类型 | 说明 | | ||
| 275 | +|------|------|------| | ||
| 276 | +| `products` | Array | 产品列表(同产品列表字段) | | ||
| 277 | +| `files` | Array | 资料列表(同资料列表字段) | | ||
| 278 | + | ||
| 279 | +### 消息列表 | ||
| 280 | + | ||
| 281 | +| 字段 | 类型 | 说明 | 示例 | | ||
| 282 | +|------|------|------|------| | ||
| 283 | +| `id` | integer | 消息ID | 1, 2, 3... | | ||
| 284 | +| `title` | string | 消息标题 | "关于2024年新产品上线通知" | | ||
| 285 | +| `intro` | string | 消息简介 | "这是一条关于..." | | ||
| 286 | +| `content` | string | 消息内容 | "这是一条关于..." | | ||
| 287 | +| `create_time` | string | 创建时间 | "2024-01-15" | | ||
| 288 | +| `is_read` | integer | 是否已读 | 0 或 1 | | ||
| 289 | +| `type` | string | 消息类型 | "notice" 或 "system" | | ||
| 290 | + | ||
| 291 | +--- | ||
| 292 | + | ||
| 293 | +## 常见问题 | ||
| 294 | + | ||
| 295 | +### Q: Mock 数据不生效? | ||
| 296 | + | ||
| 297 | +**检查**: | ||
| 298 | +1. 确认对应页面的 `USE_MOCK_DATA = true` | ||
| 299 | +2. 确认已正确导入 mock 函数 | ||
| 300 | +3. 查看 console 是否有 `[Mock]` 日志输出 | ||
| 301 | +4. 检查网络请求是否调用了 mock 函数 | ||
| 302 | + | ||
| 303 | +### Q: 数据格式不对? | ||
| 304 | + | ||
| 305 | +**检查**: | ||
| 306 | +1. 查看 `src/utils/mockData.js` 中的字段定义 | ||
| 307 | +2. 对照页面代码中的字段映射逻辑 | ||
| 308 | +3. 在 browser console 查看返回的数据结构 | ||
| 309 | + | ||
| 310 | +### Q: 想修改数据量? | ||
| 311 | + | ||
| 312 | +**修改**: | ||
| 313 | +- 修改页面的 `pageSize` 或 `limit` 变量 | ||
| 314 | +- Mock 数据会自动适应请求的数量 | ||
| 315 | + | ||
| 316 | +### Q: 想增加总页数? | ||
| 317 | + | ||
| 318 | +**修改** `src/utils/mockData.js`: | ||
| 319 | +```javascript | ||
| 320 | +// 修改 totalPages 值 | ||
| 321 | +const totalPages = 10 // 从 5 改为 10 | ||
| 322 | +``` | ||
| 323 | + | ||
| 324 | +### Q: 搜索功能不工作? | ||
| 325 | + | ||
| 326 | +**检查**: | ||
| 327 | +1. 确认输入了关键词 | ||
| 328 | +2. 确认 mock 数据中有包含该关键词的数据 | ||
| 329 | +3. 查看 console 日志确认请求参数 | ||
| 330 | + | ||
| 331 | +--- | ||
| 332 | + | ||
| 333 | +## 验收清单 | ||
| 334 | + | ||
| 335 | +测试完成后,请确认: | ||
| 336 | + | ||
| 337 | +- [ ] 周热门资料页滚动加载正常 | ||
| 338 | +- [ ] 资料列表页滚动加载正常 | ||
| 339 | +- [ ] 资料列表页分类切换正常 | ||
| 340 | +- [ ] 资料列表页搜索功能正常 | ||
| 341 | +- [ ] 产品中心页滚动加载正常 | ||
| 342 | +- [ ] 产品中心页分类切换正常 | ||
| 343 | +- [ ] 产品中心页搜索功能正常 | ||
| 344 | +- [ ] 搜索页关键词搜索正常 | ||
| 345 | +- [ ] 搜索页产品/资料切换正常 | ||
| 346 | +- [ ] 搜索页滚动加载正常 | ||
| 347 | +- [ ] 消息列表页滚动加载正常 | ||
| 348 | +- [ ] 所有页面 Console 日志正确输出 | ||
| 349 | +- [ ] 所有页面"没有更多了"正常显示 | ||
| 350 | +- [ ] 数据格式正确,无报错 | ||
| 351 | + | ||
| 352 | +--- | ||
| 353 | + | ||
| 354 | +## 下一步 | ||
| 355 | + | ||
| 356 | +1. **测试**: 逐个测试所有页面的滚动加载功能 | ||
| 357 | +2. **验证**: 确认数据格式和功能正常 | ||
| 358 | +3. **调试**: 如有问题,查看 Console 日志定位 | ||
| 359 | +4. **切换**: 测试完成后,切换回真实 API | ||
| 360 | +5. **提交**: 将修改提交到代码仓库 | ||
| 361 | + | ||
| 362 | +--- | ||
| 363 | + | ||
| 364 | +**最后更新**: 2026-02-08 | ||
| 365 | +**维护者**: Claude Code |
docs/mock-data-setup-guide.md
0 → 100644
| 1 | +# Mock 数据测试指南 | ||
| 2 | + | ||
| 3 | +## 概述 | ||
| 4 | + | ||
| 5 | +已为以下 5 个页面创建完整的 Mock 数据支持: | ||
| 6 | + | ||
| 7 | +| 页面 | API | Mock 函数 | 总页数 | 每页数量 | | ||
| 8 | +|------|-----|----------|--------|---------| | ||
| 9 | +| 周热门资料 | `weekHotAPI` | `mockWeekHotAPI` | 5 页 | 20 条 | | ||
| 10 | +| 资料列表 | `fileListAPI` | `mockFileListAPI` | 8 页 | 20 条 | | ||
| 11 | +| 产品中心 | `listAPI` | `mockProductListAPI` | 10 页 | 10 条 | | ||
| 12 | +| 搜索页 | `searchAPI` | `mockSearchAPI` | 5 页 | 20 条 | | ||
| 13 | +| 消息列表 | `myListAPI` | `mockMessageListAPI` | 8 页 | 10 条 | | ||
| 14 | + | ||
| 15 | +## 快速启用 Mock 数据 | ||
| 16 | + | ||
| 17 | +### 方法 1: 修改页面代码开关 | ||
| 18 | + | ||
| 19 | +在每个页面的 `<script setup>` 部分添加/修改: | ||
| 20 | + | ||
| 21 | +```javascript | ||
| 22 | +// ⚠️ MOCK 数据开关 | ||
| 23 | +const USE_MOCK_DATA = true // ✅ 使用 Mock 数据 | ||
| 24 | +// const USE_MOCK_DATA = false // ❌ 使用真实 API | ||
| 25 | +``` | ||
| 26 | + | ||
| 27 | +### 方法 2: 在 API 调用处添加条件判断 | ||
| 28 | + | ||
| 29 | +```javascript | ||
| 30 | +// 导入 mock 函数 | ||
| 31 | +import { mockWeekHotAPI, mockFileListAPI, mockProductListAPI, mockSearchAPI, mockMessageListAPI } from '@/utils/mockData' | ||
| 32 | + | ||
| 33 | +// 在 API 调用处 | ||
| 34 | +const res = USE_MOCK_DATA | ||
| 35 | + ? await mockWeekHotAPI(params) | ||
| 36 | + : await weekHotAPI(params) | ||
| 37 | +``` | ||
| 38 | + | ||
| 39 | +## 各页面 Mock 数据详情 | ||
| 40 | + | ||
| 41 | +### 1. 周热门资料页 (`week-hot-material/index.vue`) | ||
| 42 | + | ||
| 43 | +**文件位置**: `src/pages/week-hot-material/index.vue:76` | ||
| 44 | + | ||
| 45 | +**Mock 数据特性**: | ||
| 46 | +- ✅ 20 种资料名称模板(财富管理、保险、投资等) | ||
| 47 | +- ✅ 10 种文件类型(PDF、Word、Excel、PPT等) | ||
| 48 | +- ✅ 随机学习人数(100-5000人) | ||
| 49 | +- ✅ 随机学习百分比(0-100%) | ||
| 50 | +- ✅ 30%概率已收藏 | ||
| 51 | + | ||
| 52 | +**已启用**: ✅ 已添加 `USE_MOCK_DATA` 开关 | ||
| 53 | + | ||
| 54 | +--- | ||
| 55 | + | ||
| 56 | +### 2. 资料列表页 (`material-list/index.vue`) | ||
| 57 | + | ||
| 58 | +**文件位置**: 需要添加到 `src/pages/material-list/index.vue` | ||
| 59 | + | ||
| 60 | +**Mock 数据特性**: | ||
| 61 | +- ✅ 20 种资料名称模板 | ||
| 62 | +- ✅ 支持分类过滤(`cid`) | ||
| 63 | +- ✅ 支持关键词搜索(`keyword`) | ||
| 64 | +- ✅ 包含分类信息(`cate`) | ||
| 65 | + | ||
| 66 | +**需要添加**: | ||
| 67 | +```javascript | ||
| 68 | +// 导入 | ||
| 69 | +import { mockFileListAPI } from '@/utils/mockData' | ||
| 70 | + | ||
| 71 | +// 添加开关(在 script setup 顶部) | ||
| 72 | +const USE_MOCK_DATA = true | ||
| 73 | + | ||
| 74 | +// 修改 fetchFileList 函数(约第 176 行) | ||
| 75 | +const res = USE_MOCK_DATA | ||
| 76 | + ? await mockFileListAPI(params) | ||
| 77 | + : await fileListAPI(params) | ||
| 78 | +``` | ||
| 79 | + | ||
| 80 | +--- | ||
| 81 | + | ||
| 82 | +### 3. 产品中心页 (`product-center/index.vue`) | ||
| 83 | + | ||
| 84 | +**文件位置**: 需要添加到 `src/pages/product-center/index.vue` | ||
| 85 | + | ||
| 86 | +**Mock 数据特性**: | ||
| 87 | +- ✅ 20 种产品名称(人寿险、健康险、意外险等) | ||
| 88 | +- ✅ 4 种产品分类(人寿保险、健康保险、意外保险、财产保险) | ||
| 89 | +- ✅ 4 种标签(热销、新品、推荐、限时) | ||
| 90 | +- ✅ 支持分类过滤(`cid`) | ||
| 91 | +- ✅ 支持关键词搜索(`keyword`) | ||
| 92 | + | ||
| 93 | +**需要添加**: | ||
| 94 | +```javascript | ||
| 95 | +// 导入 | ||
| 96 | +import { mockProductListAPI } from '@/utils/mockData' | ||
| 97 | + | ||
| 98 | +// 添加开关(在 script setup 顶部) | ||
| 99 | +const USE_MOCK_DATA = true | ||
| 100 | + | ||
| 101 | +// 修改 fetchProducts 函数(约第 196 行) | ||
| 102 | +const res = USE_MOCK_DATA | ||
| 103 | + ? await mockProductListAPI(params) | ||
| 104 | + : await listAPI(params) | ||
| 105 | +``` | ||
| 106 | + | ||
| 107 | +--- | ||
| 108 | + | ||
| 109 | +### 4. 搜索页 (`search/index.vue`) | ||
| 110 | + | ||
| 111 | +**文件位置**: 需要添加到 `src/pages/search/index.vue` | ||
| 112 | + | ||
| 113 | +**Mock 数据特性**: | ||
| 114 | +- ✅ 同时支持产品和资料搜索 | ||
| 115 | +- ✅ 根据关键词过滤数据 | ||
| 116 | +- ✅ 返回匹配的产品和资料列表 | ||
| 117 | + | ||
| 118 | +**需要添加**: | ||
| 119 | +```javascript | ||
| 120 | +// 导入 | ||
| 121 | +import { mockSearchAPI } from '@/utils/mockData' | ||
| 122 | + | ||
| 123 | +// 添加开关(在 script setup 顶部) | ||
| 124 | +const USE_MOCK_DATA = true | ||
| 125 | + | ||
| 126 | +// 修改 fetchSearchResults 函数 | ||
| 127 | +const res = USE_MOCK_DATA | ||
| 128 | + ? await mockSearchAPI(params) | ||
| 129 | + : await searchAPI(params) | ||
| 130 | +``` | ||
| 131 | + | ||
| 132 | +--- | ||
| 133 | + | ||
| 134 | +### 5. 消息列表页 (`message/index.vue`) | ||
| 135 | + | ||
| 136 | +**文件位置**: 需要添加到 `src/pages/message/index.vue` | ||
| 137 | + | ||
| 138 | +**Mock 数据特性**: | ||
| 139 | +- ✅ 15 种消息标题模板 | ||
| 140 | +- ✅ 随机创建时间(最近30天内) | ||
| 141 | +- ✅ 50%概率已读 | ||
| 142 | +- ✅ 两种消息类型(通知、系统) | ||
| 143 | + | ||
| 144 | +**需要添加**: | ||
| 145 | +```javascript | ||
| 146 | +// 导入 | ||
| 147 | +import { mockMessageListAPI } from '@/utils/mockData' | ||
| 148 | + | ||
| 149 | +// 添加开关(在 script setup 顶部) | ||
| 150 | +const USE_MOCK_DATA = true | ||
| 151 | + | ||
| 152 | +// 修改 fetchMessageList 函数(约第 67 行) | ||
| 153 | +const res = USE_MOCK_DATA | ||
| 154 | + ? await mockMessageListAPI(params) | ||
| 155 | + : await myListAPI(params) | ||
| 156 | +``` | ||
| 157 | + | ||
| 158 | +--- | ||
| 159 | + | ||
| 160 | +## 统一 Mock API 调用器 | ||
| 161 | + | ||
| 162 | +除了单独导入各个 mock 函数,也可以使用统一的调用器: | ||
| 163 | + | ||
| 164 | +```javascript | ||
| 165 | +import { mockAPI } from '@/utils/mockData' | ||
| 166 | + | ||
| 167 | +// 使用示例 | ||
| 168 | +const res = await mockAPI('weekHotAPI', params) | ||
| 169 | +const res = await mockAPI('fileListAPI', params) | ||
| 170 | +const res = await mockAPI('listAPI', params) | ||
| 171 | +const res = await mockAPI('searchAPI', params) | ||
| 172 | +const res = await mockAPI('myListAPI', params) | ||
| 173 | +``` | ||
| 174 | + | ||
| 175 | +## 测试流程 | ||
| 176 | + | ||
| 177 | +### 1. 确认 Mock 数据开关已开启 | ||
| 178 | + | ||
| 179 | +检查对应页面的 `USE_MOCK_DATA = true` | ||
| 180 | + | ||
| 181 | +### 2. 启动开发服务器 | ||
| 182 | + | ||
| 183 | +```bash | ||
| 184 | +pnpm dev:weapp | ||
| 185 | +``` | ||
| 186 | + | ||
| 187 | +### 3. 测试分页加载 | ||
| 188 | + | ||
| 189 | +1. **首次加载**: 查看第 1 页数据是否正常显示 | ||
| 190 | +2. **滚动加载**: 向下滚动到底部 | ||
| 191 | +3. **验证加载**: | ||
| 192 | + - ✅ 显示"加载中..." | ||
| 193 | + - ✅ Console 输出: `[Mock] xxxAPI - 第X页,共Y条` | ||
| 194 | + - ✅ 新数据追加到列表 | ||
| 195 | +4. **测试数据耗尽**: 继续滚动直到显示"没有更多了" | ||
| 196 | + | ||
| 197 | +### 4. 查看 Console 日志 | ||
| 198 | + | ||
| 199 | +正常的日志输出: | ||
| 200 | +``` | ||
| 201 | +[Mock] weekHotAPI - 第0页,共20条 | ||
| 202 | +[Mock] weekHotAPI - 第1页,共20条 | ||
| 203 | +[Mock] weekHotAPI - 第2页,共20条 | ||
| 204 | +... | ||
| 205 | +``` | ||
| 206 | + | ||
| 207 | +### 5. 测试搜索/筛选功能(如适用) | ||
| 208 | + | ||
| 209 | +- **资料列表**: 切换分类、输入关键词 | ||
| 210 | +- **产品中心**: 切换分类、输入关键词 | ||
| 211 | +- **搜索页**: 输入关键词、切换产品/资料 Tab | ||
| 212 | + | ||
| 213 | +## Mock 数据字段说明 | ||
| 214 | + | ||
| 215 | +### 周热门资料 (`weekHotAPI`) | ||
| 216 | + | ||
| 217 | +```javascript | ||
| 218 | +{ | ||
| 219 | + meta_id: integer, // 文件ID | ||
| 220 | + name: string, // 文件名称 | ||
| 221 | + src: string, // 文件URL | ||
| 222 | + size: string, // 文件大小(如 "2.5MB") | ||
| 223 | + read_people_count: integer, // 学习人数 | ||
| 224 | + read_people_percent: number, // 学习百分比(0-100) | ||
| 225 | + is_favorite: string // 收藏状态('1' 或 '0') | ||
| 226 | + extension: string // 文件扩展名(如 'pdf') | ||
| 227 | +} | ||
| 228 | +``` | ||
| 229 | + | ||
| 230 | +### 资料列表 (`fileListAPI`) | ||
| 231 | + | ||
| 232 | +```javascript | ||
| 233 | +{ | ||
| 234 | + id: integer, | ||
| 235 | + meta_id: integer, | ||
| 236 | + name: string, // 资料名称 | ||
| 237 | + title: string, // 资料标题 | ||
| 238 | + fileName: string, // 文件名 | ||
| 239 | + desc: string, // 描述 | ||
| 240 | + size: string, // 文件大小 | ||
| 241 | + extension: string, // 文件扩展名 | ||
| 242 | + collected: boolean, // 是否已收藏 | ||
| 243 | + src: string, // 文件URL | ||
| 244 | + downloadUrl: string, // 下载URL | ||
| 245 | + post_date: string // 发布时间 | ||
| 246 | +} | ||
| 247 | +``` | ||
| 248 | + | ||
| 249 | +### 产品列表 (`listAPI`) | ||
| 250 | + | ||
| 251 | +```javascript | ||
| 252 | +{ | ||
| 253 | + id: integer, | ||
| 254 | + product_name: string, // 产品名称 | ||
| 255 | + name: string, // 产品名称(别名) | ||
| 256 | + cover_image: string, // 封面图URL | ||
| 257 | + recommend: string, // 推荐标识('hot' 或 '') | ||
| 258 | + tags: Array<{ // 标签数组 | ||
| 259 | + id: integer, | ||
| 260 | + name: string, // 标签名称 | ||
| 261 | + bg_color: string, // 背景色 | ||
| 262 | + text_color: string // 文字颜色 | ||
| 263 | + }>, | ||
| 264 | + description: string, // 描述 | ||
| 265 | + premium: number, // 保费 | ||
| 266 | + category_id: integer // 分类ID | ||
| 267 | +} | ||
| 268 | +``` | ||
| 269 | + | ||
| 270 | +### 搜索 (`searchAPI`) | ||
| 271 | + | ||
| 272 | +```javascript | ||
| 273 | +{ | ||
| 274 | + code: 1, | ||
| 275 | + msg: 'success', | ||
| 276 | + data: { | ||
| 277 | + products: Array, // 产品列表(同产品列表字段) | ||
| 278 | + files: Array // 资料列表(同资料列表字段) | ||
| 279 | + } | ||
| 280 | +} | ||
| 281 | +``` | ||
| 282 | + | ||
| 283 | +### 消息列表 (`myListAPI`) | ||
| 284 | + | ||
| 285 | +```javascript | ||
| 286 | +{ | ||
| 287 | + id: integer, | ||
| 288 | + title: string, // 消息标题 | ||
| 289 | + intro: string, // 简介 | ||
| 290 | + content: string, // 内容 | ||
| 291 | + create_time: string, // 创建时间(YYYY-MM-DD) | ||
| 292 | + is_read: integer, // 是否已读(0 或 1) | ||
| 293 | + type: string // 类型('notice' 或 'system') | ||
| 294 | +} | ||
| 295 | +``` | ||
| 296 | + | ||
| 297 | +## 切换回真实 API | ||
| 298 | + | ||
| 299 | +测试完成后,记得修改开关: | ||
| 300 | + | ||
| 301 | +```javascript | ||
| 302 | +const USE_MOCK_DATA = false // 使用真实 API | ||
| 303 | +``` | ||
| 304 | + | ||
| 305 | +## 常见问题 | ||
| 306 | + | ||
| 307 | +### Q1: Mock 数据不生效? | ||
| 308 | + | ||
| 309 | +**检查**: | ||
| 310 | +1. 确认 `USE_MOCK_DATA = true` | ||
| 311 | +2. 确认已正确导入 mock 函数 | ||
| 312 | +3. 查看 console 是否有 `[Mock]` 日志 | ||
| 313 | +4. 确认 API 调用处使用了条件判断 | ||
| 314 | + | ||
| 315 | +### Q2: 数据格式不对? | ||
| 316 | + | ||
| 317 | +**检查**: | ||
| 318 | +1. 查看 mock 数据字段是否与真实 API 一致 | ||
| 319 | +2. 在浏览器 console 查看返回的数据结构 | ||
| 320 | +3. 对照页面代码中的字段映射逻辑 | ||
| 321 | + | ||
| 322 | +### Q3: 想修改每页数量? | ||
| 323 | + | ||
| 324 | +**修改**: | ||
| 325 | +- 在页面代码中修改 `pageSize` 或 `limit` 变量 | ||
| 326 | +- Mock 数据会自动适应请求的数量 | ||
| 327 | + | ||
| 328 | +### Q4: 想增加更多页数? | ||
| 329 | + | ||
| 330 | +**修改** `src/utils/mockData.js`: | ||
| 331 | +```javascript | ||
| 332 | +// 修改 totalPages 值 | ||
| 333 | +const totalPages = 10 // 从 5 改为 10 | ||
| 334 | +``` | ||
| 335 | + | ||
| 336 | +## 完成清单 | ||
| 337 | + | ||
| 338 | +使用本指南后,请确认: | ||
| 339 | + | ||
| 340 | +- [ ] 周热门资料页已添加 Mock 数据 ✅ | ||
| 341 | +- [ ] 资料列表页已添加 Mock 数据 | ||
| 342 | +- [ ] 产品中心页已添加 Mock 数据 | ||
| 343 | +- [ ] 搜索页已添加 Mock 数据 | ||
| 344 | +- [ ] 消息列表页已添加 Mock 数据 | ||
| 345 | +- [ ] 所有页面的滚动加载功能正常 | ||
| 346 | +- [ ] Console 日志正确输出 | ||
| 347 | +- [ ] 测试完成后已切换回真实 API | ||
| 348 | + | ||
| 349 | +--- | ||
| 350 | + | ||
| 351 | +**最后更新**: 2026-02-08 | ||
| 352 | +**维护者**: Claude Code |
docs/scroll-load-test-guide.md
0 → 100644
| 1 | +# 周热门资料滚动加载测试指南 | ||
| 2 | + | ||
| 3 | +## Mock 数据说明 | ||
| 4 | + | ||
| 5 | +已创建 `src/utils/mockData.js` 文件,用于测试滚动加载更多功能。 | ||
| 6 | + | ||
| 7 | +### Mock 数据特性 | ||
| 8 | + | ||
| 9 | +- ✅ **总共 5 页数据**,每页 20 条,共 100 条资料 | ||
| 10 | +- ✅ **随机生成**的学习人数(100-5000人) | ||
| 11 | +- ✅ **随机生成**的学习百分比(0-100%) | ||
| 12 | +- ✅ **随机生成**的文件大小(0.5MB-10MB) | ||
| 13 | +- ✅ **随机生成**的收藏状态(30%概率已收藏) | ||
| 14 | +- ✅ **10种文件类型**: PDF、Word、Excel、PPT、TXT、图片等 | ||
| 15 | +- ✅ **20种资料名称模板**: 涵盖财富管理、保险、投资等主题 | ||
| 16 | + | ||
| 17 | +### 模拟网络延迟 | ||
| 18 | + | ||
| 19 | +- 每次请求延迟 **300-800ms**,模拟真实网络环境 | ||
| 20 | + | ||
| 21 | +## 如何使用 Mock 数据 | ||
| 22 | + | ||
| 23 | +### 方法 1: 修改代码开关(推荐) | ||
| 24 | + | ||
| 25 | +在 `src/pages/week-hot-material/index.vue` 第 76 行: | ||
| 26 | + | ||
| 27 | +```javascript | ||
| 28 | +// ⚠️ MOCK 数据开关 - 设置为 true 使用 mock 数据,false 使用真实 API | ||
| 29 | +const USE_MOCK_DATA = true // ✅ 使用 Mock 数据 | ||
| 30 | +// const USE_MOCK_DATA = false // ❌ 使用真实 API | ||
| 31 | +``` | ||
| 32 | + | ||
| 33 | +### 方法 2: 直接调用 Mock API | ||
| 34 | + | ||
| 35 | +```javascript | ||
| 36 | +import { mockWeekHotAPI } from '@/utils/mockData' | ||
| 37 | + | ||
| 38 | +// 调用 Mock API | ||
| 39 | +const res = await mockWeekHotAPI({ page: 0, limit: 20 }) | ||
| 40 | +``` | ||
| 41 | + | ||
| 42 | +## 测试步骤 | ||
| 43 | + | ||
| 44 | +### 1. 启动开发服务器 | ||
| 45 | + | ||
| 46 | +```bash | ||
| 47 | +pnpm dev:weapp | ||
| 48 | +``` | ||
| 49 | + | ||
| 50 | +### 2. 导航到周热门资料页 | ||
| 51 | + | ||
| 52 | +在微信开发者工具中,点击导航到"周热门资料"页面。 | ||
| 53 | + | ||
| 54 | +### 3. 测试首次加载 | ||
| 55 | + | ||
| 56 | +- ✅ 查看是否加载了第 1 页数据(20条) | ||
| 57 | +- ✅ 查看 console 日志:`[Mock] 生成第0页数据,共20条` | ||
| 58 | +- ✅ 验证列表正常显示 | ||
| 59 | + | ||
| 60 | +### 4. 测试滚动加载更多 | ||
| 61 | + | ||
| 62 | +- ✅ 向下滚动列表到底部 | ||
| 63 | +- ✅ 等待 300-800ms(模拟网络延迟) | ||
| 64 | +- ✅ 查看是否显示"加载中..."状态 | ||
| 65 | +- ✅ 查看是否加载了第 2 页数据 | ||
| 66 | +- ✅ 查看 console 日志:`[Mock] 生成第1页数据,共20条` | ||
| 67 | + | ||
| 68 | +### 5. 测试多页加载 | ||
| 69 | + | ||
| 70 | +继续滚动,依次测试: | ||
| 71 | +- 第 3 页: `[Mock] 生成第2页数据,共20条` | ||
| 72 | +- 第 4 页: `[Mock] 生成第3页数据,共20条` | ||
| 73 | +- 第 5 页: `[Mock] 生成第4页数据,共20条` | ||
| 74 | + | ||
| 75 | +### 6. 测试数据耗尽 | ||
| 76 | + | ||
| 77 | +- ✅ 滚动到底部后,应该显示"没有更多了" | ||
| 78 | +- ✅ console 日志:`[Mock] 生成第5页数据,共0条` (空数据) | ||
| 79 | +- ✅ `hasMore` 应该变为 `false` | ||
| 80 | +- ✅ 继续滚动不会触发加载 | ||
| 81 | + | ||
| 82 | +### 7. 测试收藏功能 | ||
| 83 | + | ||
| 84 | +- ✅ 点击任意资料的收藏按钮 | ||
| 85 | +- ✅ 验证收藏状态正确更新 | ||
| 86 | +- ✅ 查看 console 日志:`[Week Hot] 收藏状态改变: xxx true/false` | ||
| 87 | + | ||
| 88 | +## 验证要点 | ||
| 89 | + | ||
| 90 | +### 数据完整性 | ||
| 91 | + | ||
| 92 | +- ✅ 每页数据都是 20 条(最后一页除外) | ||
| 93 | +- ✅ 每条数据包含所有必需字段: | ||
| 94 | + - `meta_id`: 文件ID | ||
| 95 | + - `name`: 文件名称 | ||
| 96 | + - `src`: 文件URL | ||
| 97 | + - `size`: 文件大小 | ||
| 98 | + - `read_people_count`: 学习人数 | ||
| 99 | + - `read_people_percent`: 学习百分比 | ||
| 100 | + - `is_favorite`: 收藏状态 | ||
| 101 | + - `extension`: 文件扩展名 | ||
| 102 | + | ||
| 103 | +### 状态管理 | ||
| 104 | + | ||
| 105 | +- ✅ `loading` 状态: 首次加载时显示 | ||
| 106 | +- ✅ `loadingMore` 状态: 加载更多时显示 | ||
| 107 | +- ✅ `hasMore` 状态: 数据耗尽时变为 false | ||
| 108 | +- ✅ `currentPage` 状态: 每次加载后正确递增 | ||
| 109 | + | ||
| 110 | +### 用户体验 | ||
| 111 | + | ||
| 112 | +- ✅ 加载状态友好提示 | ||
| 113 | +- ✅ 空状态提示(第1页无数据时) | ||
| 114 | +- ✅ 没有更多数据提示 | ||
| 115 | +- ✅ 滚动流畅,无卡顿 | ||
| 116 | + | ||
| 117 | +## Console 日志示例 | ||
| 118 | + | ||
| 119 | +正常加载流程的日志: | ||
| 120 | + | ||
| 121 | +``` | ||
| 122 | +[Week Hot] 页面参数: {} | ||
| 123 | +[Week Hot] 请求参数: {page: 0, limit: 20} | ||
| 124 | +[Week Hot] 使用 Mock 数据: true | ||
| 125 | +[Mock] 生成第0页数据,共20条 | ||
| 126 | +[Week Hot] 数据: {list: Array(20)} | ||
| 127 | +[Week Hot] 触底加载更多 | ||
| 128 | +[Mock] 生成第1页数据,共20条 | ||
| 129 | +[Week Hot] 触底加载更多 | ||
| 130 | +[Mock] 生成第2页数据,共20条 | ||
| 131 | +... | ||
| 132 | +``` | ||
| 133 | + | ||
| 134 | +## 切换回真实 API | ||
| 135 | + | ||
| 136 | +测试完成后,记得切换回真实 API: | ||
| 137 | + | ||
| 138 | +```javascript | ||
| 139 | +// ⚠️ MOCK 数据开关 - 设置为 true 使用 mock 数据,false 使用真实 API | ||
| 140 | +const USE_MOCK_DATA = false // ✅ 使用真实 API | ||
| 141 | +``` | ||
| 142 | + | ||
| 143 | +## 常见问题 | ||
| 144 | + | ||
| 145 | +### Q1: Mock 数据不生效? | ||
| 146 | + | ||
| 147 | +**检查**: | ||
| 148 | +- 确认 `USE_MOCK_DATA = true` | ||
| 149 | +- 确认已正确导入 `mockWeekHotAPI` | ||
| 150 | +- 查看 console 是否有 `[Week Hot] 使用 Mock 数据: true` | ||
| 151 | + | ||
| 152 | +### Q2: 滚动不触发加载? | ||
| 153 | + | ||
| 154 | +**检查**: | ||
| 155 | +- 确认已滚动到列表最底部 | ||
| 156 | +- 查看 `hasMore` 是否为 `true` | ||
| 157 | +- 查看 `loadingMore` 是否为 `false` | ||
| 158 | +- 确认防抖定时器(300ms)已过 | ||
| 159 | + | ||
| 160 | +### Q3: 数据重复? | ||
| 161 | + | ||
| 162 | +**检查**: | ||
| 163 | +- 确认 `isLoadMore` 参数正确传递 | ||
| 164 | +- 确认代码中使用的是追加逻辑: | ||
| 165 | + ```javascript | ||
| 166 | + if (isLoadMore) { | ||
| 167 | + currentList.value = [...currentList.value, ...listData] | ||
| 168 | + } | ||
| 169 | + ``` | ||
| 170 | + | ||
| 171 | +### Q4: 想修改每页数量? | ||
| 172 | + | ||
| 173 | +**修改** `src/pages/week-hot-material/index.vue` 第 72 行: | ||
| 174 | + | ||
| 175 | +```javascript | ||
| 176 | +const pageSize = 20 // 修改为你想要的数量,如 10、30 等 | ||
| 177 | +``` | ||
| 178 | + | ||
| 179 | +## 扩展 Mock 数据 | ||
| 180 | + | ||
| 181 | +如需修改 Mock 数据生成逻辑,编辑 `src/utils/mockData.js`: | ||
| 182 | + | ||
| 183 | +### 修改总页数 | ||
| 184 | + | ||
| 185 | +```javascript | ||
| 186 | +const data = generatePageData(page, limit, 10) // 总共10页数据 | ||
| 187 | +``` | ||
| 188 | + | ||
| 189 | +### 添加更多资料名称模板 | ||
| 190 | + | ||
| 191 | +```javascript | ||
| 192 | +const MATERIAL_TEMPLATES = [ | ||
| 193 | + // ... 现有模板 | ||
| 194 | + '你的新资料名称', | ||
| 195 | + '另一个新资料名称' | ||
| 196 | +] | ||
| 197 | +``` | ||
| 198 | + | ||
| 199 | +### 修改文件大小范围 | ||
| 200 | + | ||
| 201 | +```javascript | ||
| 202 | +function generateRandomSize() { | ||
| 203 | + const sizeInMB = (Math.random() * 20 + 1).toFixed(1) // 1MB-21MB | ||
| 204 | + return `${sizeInMB}MB` | ||
| 205 | +} | ||
| 206 | +``` | ||
| 207 | + | ||
| 208 | +--- | ||
| 209 | + | ||
| 210 | +**最后更新**: 2026-02-08 | ||
| 211 | +**维护者**: Claude Code |
| ... | @@ -123,9 +123,13 @@ import { useListItemClick, ListType } from '@/composables/useListItemClick' | ... | @@ -123,9 +123,13 @@ import { useListItemClick, ListType } from '@/composables/useListItemClick' |
| 123 | import { getDocumentIcon, getDocumentLabel } from '@/utils/documentIcons' | 123 | import { getDocumentIcon, getDocumentLabel } from '@/utils/documentIcons' |
| 124 | import { debounce } from '@/utils/debounce' | 124 | import { debounce } from '@/utils/debounce' |
| 125 | import { fileListAPI } from '@/api/file' | 125 | import { fileListAPI } from '@/api/file' |
| 126 | +import { mockFileListAPI } from '@/utils/mockData' | ||
| 126 | import { useCollectOperation } from '@/composables/useCollectOperation' | 127 | import { useCollectOperation } from '@/composables/useCollectOperation' |
| 127 | import Taro from '@tarojs/taro' | 128 | import Taro from '@tarojs/taro' |
| 128 | 129 | ||
| 130 | +// ⚠️ MOCK 数据开关 - 开发环境使用 mock 数据,生产环境使用真实 API | ||
| 131 | +const USE_MOCK_DATA = process.env.NODE_ENV === 'development' | ||
| 132 | + | ||
| 129 | const searchValue = ref('') | 133 | const searchValue = ref('') |
| 130 | const activeTabId = ref('all') // 默认选中"全部" | 134 | const activeTabId = ref('all') // 默认选中"全部" |
| 131 | const listVisible = ref(true) | 135 | const listVisible = ref(true) |
| ... | @@ -281,9 +285,12 @@ const fetchMaterialList = async (params = {}, isLoadMore = false) => { | ... | @@ -281,9 +285,12 @@ const fetchMaterialList = async (params = {}, isLoadMore = false) => { |
| 281 | } | 285 | } |
| 282 | 286 | ||
| 283 | console.log('[Material List] 请求参数:', params) | 287 | console.log('[Material List] 请求参数:', params) |
| 288 | + console.log('[Material List] 使用 Mock 数据:', USE_MOCK_DATA) | ||
| 284 | 289 | ||
| 285 | - // 调用接口 | 290 | + // 根据开关选择使用真实 API 或 Mock 数据 |
| 286 | - const res = await fileListAPI(params) | 291 | + const res = USE_MOCK_DATA |
| 292 | + ? await mockFileListAPI(params) | ||
| 293 | + : await fileListAPI(params) | ||
| 287 | 294 | ||
| 288 | if (res.code === 1 && res.data) { | 295 | if (res.code === 1 && res.data) { |
| 289 | // 如果是初始请求(没有 child_id),保存完整的分类信息 | 296 | // 如果是初始请求(没有 child_id),保存完整的分类信息 |
| ... | @@ -301,10 +308,18 @@ const fetchMaterialList = async (params = {}, isLoadMore = false) => { | ... | @@ -301,10 +308,18 @@ const fetchMaterialList = async (params = {}, isLoadMore = false) => { |
| 301 | // 加载更多:追加数据 | 308 | // 加载更多:追加数据 |
| 302 | allList.value = [...allList.value, ...allListData] | 309 | allList.value = [...allList.value, ...allListData] |
| 303 | categoryListCache.value.set('all', allList.value) | 310 | categoryListCache.value.set('all', allList.value) |
| 311 | + // ✅ 同步更新 currentList(如果当前显示的是"全部") | ||
| 312 | + if (activeTabId.value === 'all') { | ||
| 313 | + currentList.value = allList.value | ||
| 314 | + } | ||
| 304 | } else { | 315 | } else { |
| 305 | // 首次加载:替换数据 | 316 | // 首次加载:替换数据 |
| 306 | allList.value = allListData | 317 | allList.value = allListData |
| 307 | categoryListCache.value.set('all', allListData) | 318 | categoryListCache.value.set('all', allListData) |
| 319 | + // ✅ 同步更新 currentList(如果当前显示的是"全部") | ||
| 320 | + if (activeTabId.value === 'all') { | ||
| 321 | + currentList.value = allListData | ||
| 322 | + } | ||
| 308 | } | 323 | } |
| 309 | 324 | ||
| 310 | // 判断是否还有更多数据 | 325 | // 判断是否还有更多数据 |
| ... | @@ -328,7 +343,11 @@ const fetchMaterialList = async (params = {}, isLoadMore = false) => { | ... | @@ -328,7 +343,11 @@ const fetchMaterialList = async (params = {}, isLoadMore = false) => { |
| 328 | const existingData = categoryListCache.value.get(cacheKey) || [] | 343 | const existingData = categoryListCache.value.get(cacheKey) || [] |
| 329 | const newData = [...existingData, ...listData] | 344 | const newData = [...existingData, ...listData] |
| 330 | categoryListCache.value.set(cacheKey, newData) | 345 | categoryListCache.value.set(cacheKey, newData) |
| 346 | + // ✅ 同步更新 currentList(如果当前显示的是该缓存) | ||
| 347 | + const currentCacheKey = params.child_id || params.keyword || 'all' | ||
| 348 | + if (activeTabId.value === currentCacheKey) { | ||
| 331 | currentList.value = newData | 349 | currentList.value = newData |
| 350 | + } | ||
| 332 | } else { | 351 | } else { |
| 333 | // 首次加载:替换数据 | 352 | // 首次加载:替换数据 |
| 334 | categoryListCache.value.set(cacheKey, listData) | 353 | categoryListCache.value.set(cacheKey, listData) |
| ... | @@ -572,7 +591,12 @@ const onSearch = async () => { | ... | @@ -572,7 +591,12 @@ const onSearch = async () => { |
| 572 | // 调用接口搜索 | 591 | // 调用接口搜索 |
| 573 | try { | 592 | try { |
| 574 | loading.value = true | 593 | loading.value = true |
| 575 | - const res = await fileListAPI(params) | 594 | + console.log('[Material List] 搜索使用 Mock 数据:', USE_MOCK_DATA) |
| 595 | + | ||
| 596 | + // 根据开关选择使用真实 API 或 Mock 数据 | ||
| 597 | + const res = USE_MOCK_DATA | ||
| 598 | + ? await mockFileListAPI(params) | ||
| 599 | + : await fileListAPI(params) | ||
| 576 | 600 | ||
| 577 | if (res.code === 1 && res.data) { | 601 | if (res.code === 1 && res.data) { |
| 578 | if (res.data.list?.length) { | 602 | if (res.data.list?.length) { | ... | ... |
| ... | @@ -51,6 +51,10 @@ import { useLoad, usePullDownRefresh, useReachBottom, stopPullDownRefresh } from | ... | @@ -51,6 +51,10 @@ import { useLoad, usePullDownRefresh, useReachBottom, stopPullDownRefresh } from |
| 51 | import { useGo } from '@/hooks/useGo' | 51 | import { useGo } from '@/hooks/useGo' |
| 52 | import NavHeader from '@/components/NavHeader.vue' | 52 | import NavHeader from '@/components/NavHeader.vue' |
| 53 | import { myListAPI } from '@/api/news' | 53 | import { myListAPI } from '@/api/news' |
| 54 | +import { mockMessageListAPI } from '@/utils/mockData' | ||
| 55 | + | ||
| 56 | +// ⚠️ MOCK 数据开关 - 开发环境使用 mock 数据,生产环境使用真实 API | ||
| 57 | +const USE_MOCK_DATA = process.env.NODE_ENV === 'development' | ||
| 54 | 58 | ||
| 55 | const go = useGo() | 59 | const go = useGo() |
| 56 | 60 | ||
| ... | @@ -77,7 +81,15 @@ const fetchMessageList = async (refresh = false) => { | ... | @@ -77,7 +81,15 @@ const fetchMessageList = async (refresh = false) => { |
| 77 | loading.value = true | 81 | loading.value = true |
| 78 | 82 | ||
| 79 | try { | 83 | try { |
| 80 | - const res = await myListAPI({ | 84 | + console.log('[Message] 使用 Mock 数据:', USE_MOCK_DATA) |
| 85 | + | ||
| 86 | + // 根据开关选择使用真实 API 或 Mock 数据 | ||
| 87 | + const res = USE_MOCK_DATA | ||
| 88 | + ? await mockMessageListAPI({ | ||
| 89 | + page: page.value, | ||
| 90 | + limit: limit.value | ||
| 91 | + }) | ||
| 92 | + : await myListAPI({ | ||
| 81 | page: page.value, | 93 | page: page.value, |
| 82 | limit: limit.value | 94 | limit: limit.value |
| 83 | }) | 95 | }) | ... | ... |
| ... | @@ -153,6 +153,10 @@ import SearchBar from '@/components/SearchBar.vue' | ... | @@ -153,6 +153,10 @@ import SearchBar from '@/components/SearchBar.vue' |
| 153 | import PlanFormContainer from '@/components/PlanFormContainer.vue' | 153 | import PlanFormContainer from '@/components/PlanFormContainer.vue' |
| 154 | import { useListItemClick, ListType } from '@/composables/useListItemClick' | 154 | import { useListItemClick, ListType } from '@/composables/useListItemClick' |
| 155 | import { listAPI } from '@/api/get_product' | 155 | import { listAPI } from '@/api/get_product' |
| 156 | +import { mockProductListAPI } from '@/utils/mockData' | ||
| 157 | + | ||
| 158 | +// ⚠️ MOCK 数据开关 - 开发环境使用 mock 数据,生产环境使用真实 API | ||
| 159 | +const USE_MOCK_DATA = process.env.NODE_ENV === 'development' | ||
| 156 | 160 | ||
| 157 | const activeTabId = ref('') | 161 | const activeTabId = ref('') |
| 158 | 162 | ||
| ... | @@ -214,7 +218,12 @@ const fetchProducts = async (isLoadMore = false) => { | ... | @@ -214,7 +218,12 @@ const fetchProducts = async (isLoadMore = false) => { |
| 214 | params.keyword = searchValue.value | 218 | params.keyword = searchValue.value |
| 215 | } | 219 | } |
| 216 | 220 | ||
| 217 | - const res = await listAPI(params) | 221 | + console.log('[Product Center] 使用 Mock 数据:', USE_MOCK_DATA) |
| 222 | + | ||
| 223 | + // 根据开关选择使用真实 API 或 Mock 数据 | ||
| 224 | + const res = USE_MOCK_DATA | ||
| 225 | + ? await mockProductListAPI(params) | ||
| 226 | + : await listAPI(params) | ||
| 218 | 227 | ||
| 219 | if (res.code === 1 && res.data) { | 228 | if (res.code === 1 && res.data) { |
| 220 | // 更新分类列表(首次加载时) | 229 | // 更新分类列表(首次加载时) | ... | ... |
| ... | @@ -3,9 +3,9 @@ | ... | @@ -3,9 +3,9 @@ |
| 3 | * @Description: 搜索页面 - 支持产品和资料搜索,实时查询API | 3 | * @Description: 搜索页面 - 支持产品和资料搜索,实时查询API |
| 4 | --> | 4 | --> |
| 5 | <template> | 5 | <template> |
| 6 | - <view class="h-screen bg-[#FFF] flex flex-col"> | 6 | + <view class="bg-[#FFF]"> |
| 7 | - <!-- 固定顶部:导航栏 + 搜索栏 --> | 7 | + <!-- 固定顶部:导航栏 + 搜索栏 + Tabs + 结果计数 --> |
| 8 | - <view class="bg-[#FFF] z-10"> | 8 | + <view class="bg-[#FFF] sticky top-0 z-10"> |
| 9 | <NavHeader title="搜索" /> | 9 | <NavHeader title="搜索" /> |
| 10 | 10 | ||
| 11 | <!-- Search Input --> | 11 | <!-- Search Input --> |
| ... | @@ -20,10 +20,7 @@ | ... | @@ -20,10 +20,7 @@ |
| 20 | @clear="clearSearch" | 20 | @clear="clearSearch" |
| 21 | /> | 21 | /> |
| 22 | </view> | 22 | </view> |
| 23 | - </view> | ||
| 24 | 23 | ||
| 25 | - <!-- Tabs + 列表容器 --> | ||
| 26 | - <view class="flex-1 min-h-0 flex flex-col mt-[32rpx] px-[40rpx]"> | ||
| 27 | <!-- Tabs Container --> | 24 | <!-- Tabs Container --> |
| 28 | <nut-tabs v-model="activeTab"> | 25 | <nut-tabs v-model="activeTab"> |
| 29 | <!-- 自定义标签栏 --> | 26 | <!-- 自定义标签栏 --> |
| ... | @@ -46,15 +43,14 @@ | ... | @@ -46,15 +43,14 @@ |
| 46 | </nut-tabs> | 43 | </nut-tabs> |
| 47 | 44 | ||
| 48 | <!-- Result Count --> | 45 | <!-- Result Count --> |
| 49 | - <view v-if="currentList.length > 0" class="text-[#6B7280] text-[24rpx] mb-[24rpx]"> | 46 | + <view v-if="currentList.length > 0" class="px-[60rpx] text-[#6B7280] text-[24rpx] pb-[24rpx]"> |
| 50 | 找到 {{ currentTotal }} 个相关结果 | 47 | 找到 {{ currentTotal }} 个相关结果 |
| 51 | </view> | 48 | </view> |
| 49 | + </view> | ||
| 50 | + | ||
| 51 | + <!-- 列表容器 --> | ||
| 52 | + <view class="px-[40rpx] pb-[calc(160rpx+env(safe-area-inset-bottom))]"> | ||
| 52 | 53 | ||
| 53 | - <!-- 可滚动列表区域(支持触底加载更多) --> | ||
| 54 | - <scroll-view | ||
| 55 | - class="flex-1 min-h-0 overflow-y-auto pb-[calc(160rpx+env(safe-area-inset-bottom))] box-border" | ||
| 56 | - scroll-y | ||
| 57 | - > | ||
| 58 | <!-- Search Results --> | 54 | <!-- Search Results --> |
| 59 | <view | 55 | <view |
| 60 | v-if="currentList.length > 0" | 56 | v-if="currentList.length > 0" |
| ... | @@ -120,7 +116,6 @@ | ... | @@ -120,7 +116,6 @@ |
| 120 | <view class="text-[#6B7280] text-[28rpx]">搜索产品或资料</view> | 116 | <view class="text-[#6B7280] text-[28rpx]">搜索产品或资料</view> |
| 121 | <view class="text-[#9CA3AF] text-[24rpx] mt-[12rpx]">输入关键词开始搜索,自动切换分类</view> | 117 | <view class="text-[#9CA3AF] text-[24rpx] mt-[12rpx]">输入关键词开始搜索,自动切换分类</view> |
| 122 | </view> | 118 | </view> |
| 123 | - </scroll-view> | ||
| 124 | </view> | 119 | </view> |
| 125 | 120 | ||
| 126 | <!-- Plan Form Container --> | 121 | <!-- Plan Form Container --> |
| ... | @@ -146,6 +141,10 @@ import ProductCard from '@/components/ProductCard.vue' | ... | @@ -146,6 +141,10 @@ import ProductCard from '@/components/ProductCard.vue' |
| 146 | import MaterialCard from '@/components/MaterialCard.vue' | 141 | import MaterialCard from '@/components/MaterialCard.vue' |
| 147 | import PlanFormContainer from '@/components/PlanFormContainer.vue' | 142 | import PlanFormContainer from '@/components/PlanFormContainer.vue' |
| 148 | import { searchAPI } from '@/api/search' | 143 | import { searchAPI } from '@/api/search' |
| 144 | +import { mockSearchAPI } from '@/utils/mockData' | ||
| 145 | + | ||
| 146 | +// ⚠️ MOCK 数据开关 - 开发环境使用 mock 数据,生产环境使用真实 API | ||
| 147 | +const USE_MOCK_DATA = process.env.NODE_ENV === 'development' | ||
| 149 | 148 | ||
| 150 | // Navigation | 149 | // Navigation |
| 151 | const go = useGo() | 150 | const go = useGo() |
| ... | @@ -226,7 +225,12 @@ const performSearch = async (keyword, type, page = 0, limit = pageSize, isLoadMo | ... | @@ -226,7 +225,12 @@ const performSearch = async (keyword, type, page = 0, limit = pageSize, isLoadMo |
| 226 | const params = { keyword, page, limit } | 225 | const params = { keyword, page, limit } |
| 227 | if (type) params.type = type | 226 | if (type) params.type = type |
| 228 | 227 | ||
| 229 | - const res = await searchAPI(params) | 228 | + console.log('[Search] 使用 Mock 数据:', USE_MOCK_DATA) |
| 229 | + | ||
| 230 | + // 根据开关选择使用真实 API 或 Mock 数据 | ||
| 231 | + const res = USE_MOCK_DATA | ||
| 232 | + ? await mockSearchAPI(params) | ||
| 233 | + : await searchAPI(params) | ||
| 230 | 234 | ||
| 231 | if (res.code === 1) { | 235 | if (res.code === 1) { |
| 232 | // 映射产品列表 | 236 | // 映射产品列表 |
| ... | @@ -266,11 +270,7 @@ const performSearch = async (keyword, type, page = 0, limit = pageSize, isLoadMo | ... | @@ -266,11 +270,7 @@ const performSearch = async (keyword, type, page = 0, limit = pageSize, isLoadMo |
| 266 | productsTotal.value = res.data.products.total || 0 | 270 | productsTotal.value = res.data.products.total || 0 |
| 267 | filesTotal.value = res.data.files.total || 0 | 271 | filesTotal.value = res.data.files.total || 0 |
| 268 | 272 | ||
| 269 | - // 判断是否还有更多数据 | 273 | + // ⚠️ 重要:必须先自动选择 tab,然后再计算 hasMore |
| 270 | - // 如果返回的数据量少于请求的量,说明没有更多了 | ||
| 271 | - const currentListLength = type === 'product' ? newProducts.length : newFiles.length | ||
| 272 | - hasMore.value = currentListLength >= limit | ||
| 273 | - | ||
| 274 | // 如果不传 type,自动选择有数据的 tab(仅首次搜索时) | 274 | // 如果不传 type,自动选择有数据的 tab(仅首次搜索时) |
| 275 | if (!type && !isLoadMore) { | 275 | if (!type && !isLoadMore) { |
| 276 | if (productsTotal.value > 0) { | 276 | if (productsTotal.value > 0) { |
| ... | @@ -281,6 +281,19 @@ const performSearch = async (keyword, type, page = 0, limit = pageSize, isLoadMo | ... | @@ -281,6 +281,19 @@ const performSearch = async (keyword, type, page = 0, limit = pageSize, isLoadMo |
| 281 | // 如果都为 0,默认 product | 281 | // 如果都为 0,默认 product |
| 282 | } | 282 | } |
| 283 | 283 | ||
| 284 | + // 判断是否还有更多数据 | ||
| 285 | + // 使用当前列表长度与总数比较 | ||
| 286 | + // 注意:需要根据实际选择的tab来判断 | ||
| 287 | + const actualTab = type || activeTab.value | ||
| 288 | + if (actualTab === 'product') { | ||
| 289 | + hasMore.value = products.value.length < productsTotal.value | ||
| 290 | + } else if (actualTab === 'file') { | ||
| 291 | + hasMore.value = files.value.length < filesTotal.value | ||
| 292 | + } else { | ||
| 293 | + // 如果都没有选中,保守设置为false | ||
| 294 | + hasMore.value = false | ||
| 295 | + } | ||
| 296 | + | ||
| 284 | hasSearched.value = true | 297 | hasSearched.value = true |
| 285 | listRenderKey.value += 1 | 298 | listRenderKey.value += 1 |
| 286 | 299 | ||
| ... | @@ -535,7 +548,7 @@ const handleCollectChanged = (item, newStatus) => { | ... | @@ -535,7 +548,7 @@ const handleCollectChanged = (item, newStatus) => { |
| 535 | .filter-tabs-wrapper { | 548 | .filter-tabs-wrapper { |
| 536 | display: flex; | 549 | display: flex; |
| 537 | overflow-x: auto; | 550 | overflow-x: auto; |
| 538 | - padding: 24rpx 24rpx; | 551 | + padding: 24rpx 60rpx; |
| 539 | gap: 24rpx; | 552 | gap: 24rpx; |
| 540 | transition: all 0.3s ease; | 553 | transition: all 0.3s ease; |
| 541 | background-color: #FFF; | 554 | background-color: #FFF; | ... | ... |
| ... | @@ -6,5 +6,6 @@ export default { | ... | @@ -6,5 +6,6 @@ export default { |
| 6 | navigationBarTitleText: '本周热门资料', | 6 | navigationBarTitleText: '本周热门资料', |
| 7 | enablePullDownRefresh: true, | 7 | enablePullDownRefresh: true, |
| 8 | backgroundColor: '#F9FAFB', | 8 | backgroundColor: '#F9FAFB', |
| 9 | - navigationStyle: 'custom' | 9 | + navigationStyle: 'custom', |
| 10 | + onReachBottomDistance: 50 | ||
| 10 | } | 11 | } | ... | ... |
| ... | @@ -3,14 +3,14 @@ | ... | @@ -3,14 +3,14 @@ |
| 3 | * @Description: 本周热门资料页 - 使用 MaterialCard 组件 | 3 | * @Description: 本周热门资料页 - 使用 MaterialCard 组件 |
| 4 | --> | 4 | --> |
| 5 | <template> | 5 | <template> |
| 6 | - <view class="h-screen bg-[#F9FAFB] flex flex-col py-[32rpx]"> | 6 | + <view class="min-h-screen bg-[#F9FAFB] py-[32rpx]"> |
| 7 | <NavHeader title="本周热门资料" /> | 7 | <NavHeader title="本周热门资料" /> |
| 8 | 8 | ||
| 9 | <!-- 列表容器 --> | 9 | <!-- 列表容器 --> |
| 10 | <view | 10 | <view |
| 11 | v-if="listVisible" | 11 | v-if="listVisible" |
| 12 | :key="listRenderKey" | 12 | :key="listRenderKey" |
| 13 | - class="flex-1 min-h-0 overflow-y-auto px-[32rpx] pb-[calc(160rpx+env(safe-area-inset-bottom))] box-border" | 13 | + class="px-[32rpx]" |
| 14 | > | 14 | > |
| 15 | <!-- 加载状态 --> | 15 | <!-- 加载状态 --> |
| 16 | <view v-if="loading && currentList.length === 0" class="flex items-center justify-center py-[60rpx]"> | 16 | <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' | ... | @@ -61,6 +61,7 @@ import Taro, { useLoad, useReachBottom } from '@tarojs/taro' |
| 61 | import NavHeader from '@/components/NavHeader.vue' | 61 | import NavHeader from '@/components/NavHeader.vue' |
| 62 | import MaterialCard from '@/components/MaterialCard.vue' | 62 | import MaterialCard from '@/components/MaterialCard.vue' |
| 63 | import { weekHotAPI } from '@/api/file' | 63 | import { weekHotAPI } from '@/api/file' |
| 64 | +import { mockWeekHotAPI } from '@/utils/mockData' | ||
| 64 | 65 | ||
| 65 | const listVisible = ref(true) | 66 | const listVisible = ref(true) |
| 66 | const listRenderKey = ref(0) | 67 | const listRenderKey = ref(0) |
| ... | @@ -71,6 +72,9 @@ const currentList = ref([]) | ... | @@ -71,6 +72,9 @@ const currentList = ref([]) |
| 71 | const currentPage = ref(0) // 当前页码(从0开始) | 72 | const currentPage = ref(0) // 当前页码(从0开始) |
| 72 | const pageSize = 20 // 每页数量 | 73 | const pageSize = 20 // 每页数量 |
| 73 | 74 | ||
| 75 | +// ⚠️ MOCK 数据开关 - 开发环境使用 mock 数据,生产环境使用真实 API | ||
| 76 | +const USE_MOCK_DATA = process.env.NODE_ENV === 'development' | ||
| 77 | + | ||
| 74 | /** | 78 | /** |
| 75 | * 处理收藏状态改变 | 79 | * 处理收藏状态改变 |
| 76 | * | 80 | * |
| ... | @@ -104,9 +108,12 @@ const fetchWeekHotList = async (params = {}, isLoadMore = false) => { | ... | @@ -104,9 +108,12 @@ const fetchWeekHotList = async (params = {}, isLoadMore = false) => { |
| 104 | } | 108 | } |
| 105 | 109 | ||
| 106 | console.log('[Week Hot] 请求参数:', params) | 110 | console.log('[Week Hot] 请求参数:', params) |
| 111 | + console.log('[Week Hot] 使用 Mock 数据:', USE_MOCK_DATA) | ||
| 107 | 112 | ||
| 108 | - // 调用接口 | 113 | + // 根据开关选择使用真实 API 或 Mock 数据 |
| 109 | - const res = await weekHotAPI(params) | 114 | + const res = USE_MOCK_DATA |
| 115 | + ? await mockWeekHotAPI(params) | ||
| 116 | + : await weekHotAPI(params) | ||
| 110 | 117 | ||
| 111 | if (res.code === 1 && res.data) { | 118 | if (res.code === 1 && res.data) { |
| 112 | console.log('[Week Hot] 数据:', res.data) | 119 | console.log('[Week Hot] 数据:', res.data) | ... | ... |
src/utils/mockData.js
0 → 100644
| 1 | +/** | ||
| 2 | + * @Description: Mock 数据生成工具 - 用于测试分页加载功能 | ||
| 3 | + * @Date: 2026-02-08 | ||
| 4 | + * | ||
| 5 | + * 支持的 API Mock: | ||
| 6 | + * - weekHotAPI: 周热门资料 | ||
| 7 | + * - fileListAPI: 资料列表 | ||
| 8 | + * - listAPI: 产品列表 | ||
| 9 | + * - searchAPI: 搜索(产品+资料) | ||
| 10 | + * - myListAPI: 消息列表 | ||
| 11 | + */ | ||
| 12 | + | ||
| 13 | +// ============================================================================ | ||
| 14 | +// 工具函数 | ||
| 15 | +// ============================================================================ | ||
| 16 | + | ||
| 17 | +/** | ||
| 18 | + * 生成随机文件大小 | ||
| 19 | + * @returns {string} 文件大小(如 "2.5MB") | ||
| 20 | + */ | ||
| 21 | +function generateRandomSize() { | ||
| 22 | + const sizeInMB = (Math.random() * 10 + 0.5).toFixed(1) | ||
| 23 | + return `${sizeInMB}MB` | ||
| 24 | +} | ||
| 25 | + | ||
| 26 | +/** | ||
| 27 | + * 生成随机学习人数 | ||
| 28 | + * @returns {number} 学习人数(100-5000之间) | ||
| 29 | + */ | ||
| 30 | +function generateRandomReadCount() { | ||
| 31 | + return Math.floor(Math.random() * 4900) + 100 | ||
| 32 | +} | ||
| 33 | + | ||
| 34 | +/** | ||
| 35 | + * 生成随机学习百分比 | ||
| 36 | + * @returns {number} 学习百分比(0-100之间) | ||
| 37 | + */ | ||
| 38 | +function generateRandomReadPercent() { | ||
| 39 | + return Math.floor(Math.random() * 100) | ||
| 40 | +} | ||
| 41 | + | ||
| 42 | +/** | ||
| 43 | + * 生成随机收藏状态 | ||
| 44 | + * @returns {string} '1' 或 '0' | ||
| 45 | + */ | ||
| 46 | +function generateRandomFavorite() { | ||
| 47 | + return Math.random() > 0.7 ? '1' : '0' | ||
| 48 | +} | ||
| 49 | + | ||
| 50 | +/** | ||
| 51 | + * 模拟网络延迟 | ||
| 52 | + * @param {number} min 最小延迟(ms) | ||
| 53 | + * @param {number} max 最大延迟(ms) | ||
| 54 | + * @returns {Promise} | ||
| 55 | + */ | ||
| 56 | +function mockDelay(min = 300, max = 800) { | ||
| 57 | + const delay = Math.random() * (max - min) + min | ||
| 58 | + return new Promise(resolve => setTimeout(resolve, delay)) | ||
| 59 | +} | ||
| 60 | + | ||
| 61 | +// ============================================================================ | ||
| 62 | +// 1. 周热门资料 Mock (weekHotAPI) | ||
| 63 | +// ============================================================================ | ||
| 64 | + | ||
| 65 | +const WEEK_HOT_MATERIALS = [ | ||
| 66 | + '财富管理基础知识指南', | ||
| 67 | + '保险产品销售技巧', | ||
| 68 | + '客户关系管理实战', | ||
| 69 | + '家庭资产配置方案', | ||
| 70 | + '税务筹划实用手册', | ||
| 71 | + '退休规划完整教程', | ||
| 72 | + '投资组合管理策略', | ||
| 73 | + '风险控制与合规要求', | ||
| 74 | + '高净值客户开发指南', | ||
| 75 | + '理财产品营销话术', | ||
| 76 | + '基金定投实战技巧', | ||
| 77 | + '保单整理服务流程', | ||
| 78 | + '传承规划案例分析', | ||
| 79 | + '健康险产品对比分析', | ||
| 80 | + '年金保险销售指南', | ||
| 81 | + '重疾险核保知识', | ||
| 82 | + '教育金规划方案', | ||
| 83 | + '房贷规划实务操作', | ||
| 84 | + '家族信托业务介绍', | ||
| 85 | + '私募股权投资指南' | ||
| 86 | +] | ||
| 87 | + | ||
| 88 | +const FILE_TYPES = [ | ||
| 89 | + { extension: 'pdf', name: 'PDF文档' }, | ||
| 90 | + { extension: 'doc', name: 'Word文档' }, | ||
| 91 | + { extension: 'docx', name: 'Word文档' }, | ||
| 92 | + { extension: 'xls', name: 'Excel表格' }, | ||
| 93 | + { extension: 'xlsx', name: 'Excel表格' }, | ||
| 94 | + { extension: 'ppt', name: 'PPT演示文稿' }, | ||
| 95 | + { extension: 'pptx', name: 'PPT演示文稿' }, | ||
| 96 | + { extension: 'txt', name: '文本文件' }, | ||
| 97 | + { extension: 'jpg', name: '图片' }, | ||
| 98 | + { extension: 'png', name: '图片' } | ||
| 99 | +] | ||
| 100 | + | ||
| 101 | +/** | ||
| 102 | + * 生成周热门资料数据 | ||
| 103 | + */ | ||
| 104 | +function generateWeekHotItem(id) { | ||
| 105 | + const fileType = FILE_TYPES[Math.floor(Math.random() * FILE_TYPES.length)] | ||
| 106 | + const materialName = WEEK_HOT_MATERIALS[Math.floor(Math.random() * WEEK_HOT_MATERIALS.length)] | ||
| 107 | + | ||
| 108 | + return { | ||
| 109 | + meta_id: id, | ||
| 110 | + name: `${materialName} ${fileType.name.toUpperCase()}`, | ||
| 111 | + src: `https://placehold.co/100x100/e2e8f0/475569?text=${fileType.extension.toUpperCase()}`, | ||
| 112 | + size: generateRandomSize(), | ||
| 113 | + read_people_count: generateRandomReadCount(), | ||
| 114 | + read_people_percent: generateRandomReadPercent(), | ||
| 115 | + is_favorite: generateRandomFavorite(), | ||
| 116 | + extension: fileType.extension | ||
| 117 | + } | ||
| 118 | +} | ||
| 119 | + | ||
| 120 | +/** | ||
| 121 | + * Mock: weekHotAPI | ||
| 122 | + */ | ||
| 123 | +export async function mockWeekHotAPI(params) { | ||
| 124 | + await mockDelay() | ||
| 125 | + | ||
| 126 | + const { page = 0, limit = 20 } = params | ||
| 127 | + const totalPages = 5 | ||
| 128 | + | ||
| 129 | + if (page >= totalPages) { | ||
| 130 | + return { code: 1, msg: 'success', data: { list: [] } } | ||
| 131 | + } | ||
| 132 | + | ||
| 133 | + const list = [] | ||
| 134 | + const startIndex = page * limit | ||
| 135 | + | ||
| 136 | + for (let i = 0; i < limit; i++) { | ||
| 137 | + list.push(generateWeekHotItem(startIndex + i + 1)) | ||
| 138 | + } | ||
| 139 | + | ||
| 140 | + console.log(`[Mock] weekHotAPI - 第${page}页,共${list.length}条`) | ||
| 141 | + | ||
| 142 | + return { code: 1, msg: 'success', data: { list } } | ||
| 143 | +} | ||
| 144 | + | ||
| 145 | +// ============================================================================ | ||
| 146 | +// 2. 资料列表 Mock (fileListAPI) | ||
| 147 | +// ============================================================================ | ||
| 148 | + | ||
| 149 | +const MATERIAL_NAMES = [ | ||
| 150 | + '2024年保险行业发展趋势报告', | ||
| 151 | + '高净值客户开发实战手册', | ||
| 152 | + '家庭保障需求分析模板', | ||
| 153 | + '养老规划产品对比表', | ||
| 154 | + '教育金储备方案', | ||
| 155 | + '重疾险条款解读', | ||
| 156 | + '百万医疗险销售指南', | ||
| 157 | + '年金险产品培训资料', | ||
| 158 | + '终身寿险销售技巧', | ||
| 159 | + '车险理赔流程说明', | ||
| 160 | + '企业财产险基础知识', | ||
| 161 | + '责任险产品介绍', | ||
| 162 | + '意外险保障方案', | ||
| 163 | + '健康险核保手册', | ||
| 164 | + '投保实务操作指南', | ||
| 165 | + '客户异议处理话术', | ||
| 166 | + '保单托管服务流程', | ||
| 167 | + '理赔案例分析', | ||
| 168 | + '保险法律法规汇编', | ||
| 169 | + '行业合规要求解读' | ||
| 170 | +] | ||
| 171 | + | ||
| 172 | +/** | ||
| 173 | + * 生成资料列表项 | ||
| 174 | + */ | ||
| 175 | +function generateMaterialItem(id) { | ||
| 176 | + const fileType = FILE_TYPES[Math.floor(Math.random() * FILE_TYPES.length)] | ||
| 177 | + const materialName = MATERIAL_NAMES[Math.floor(Math.random() * MATERIAL_NAMES.length)] | ||
| 178 | + | ||
| 179 | + return { | ||
| 180 | + id: id, | ||
| 181 | + meta_id: id, | ||
| 182 | + name: materialName, | ||
| 183 | + title: materialName, | ||
| 184 | + fileName: `${materialName}.${fileType.extension}`, | ||
| 185 | + desc: '这是一份详细的培训资料,包含丰富的案例和实战技巧...', | ||
| 186 | + size: generateRandomSize(), | ||
| 187 | + extension: fileType.extension, | ||
| 188 | + collected: generateRandomFavorite() === '1', | ||
| 189 | + src: `https://placehold.co/100x100/e2e8f0/475569?text=${fileType.extension.toUpperCase()}`, | ||
| 190 | + downloadUrl: `https://placehold.co/100x100/e2e8f0/475569?text=${fileType.extension.toUpperCase()}`, | ||
| 191 | + post_date: new Date().toISOString(), | ||
| 192 | + value: `https://placehold.co/100x100/e2e8f0/475569?text=${fileType.extension.toUpperCase()}` | ||
| 193 | + } | ||
| 194 | +} | ||
| 195 | + | ||
| 196 | +/** | ||
| 197 | + * Mock: fileListAPI | ||
| 198 | + */ | ||
| 199 | +export async function mockFileListAPI(params) { | ||
| 200 | + await mockDelay() | ||
| 201 | + | ||
| 202 | + const { page = 0, limit = 20, cid, keyword, child_id } = params | ||
| 203 | + const totalPages = 8 | ||
| 204 | + | ||
| 205 | + if (page >= totalPages) { | ||
| 206 | + return { code: 1, msg: 'success', data: { list: [], total: totalPages * limit } } | ||
| 207 | + } | ||
| 208 | + | ||
| 209 | + const list = [] | ||
| 210 | + const startIndex = page * limit | ||
| 211 | + | ||
| 212 | + for (let i = 0; i < limit; i++) { | ||
| 213 | + const item = generateMaterialItem(startIndex + i + 1) | ||
| 214 | + | ||
| 215 | + // 如果有关键词搜索,过滤数据 | ||
| 216 | + if (keyword && !item.name.includes(keyword)) { | ||
| 217 | + continue | ||
| 218 | + } | ||
| 219 | + | ||
| 220 | + list.push(item) | ||
| 221 | + } | ||
| 222 | + | ||
| 223 | + console.log(`[Mock] fileListAPI - 第${page}页,共${list.length}条`) | ||
| 224 | + | ||
| 225 | + return { | ||
| 226 | + code: 1, | ||
| 227 | + msg: 'success', | ||
| 228 | + data: { | ||
| 229 | + list, | ||
| 230 | + total: totalPages * limit, | ||
| 231 | + max_level: 2, | ||
| 232 | + cate: { | ||
| 233 | + id: parseInt(cid) || 1, | ||
| 234 | + category_name: '培训资料', | ||
| 235 | + category_parent: 0, | ||
| 236 | + category_description: null | ||
| 237 | + } | ||
| 238 | + } | ||
| 239 | + } | ||
| 240 | +} | ||
| 241 | + | ||
| 242 | +// ============================================================================ | ||
| 243 | +// 3. 产品列表 Mock (listAPI) | ||
| 244 | +// ============================================================================ | ||
| 245 | + | ||
| 246 | +const PRODUCT_NAMES = [ | ||
| 247 | + '百万年金保险计划', | ||
| 248 | + '终身寿险至尊版', | ||
| 249 | + '重疾险保障计划', | ||
| 250 | + '百万医疗险', | ||
| 251 | + '意外伤害保险', | ||
| 252 | + '教育金保险计划', | ||
| 253 | + '养老理财保险', | ||
| 254 | + '高端医疗险', | ||
| 255 | + '定期寿险', | ||
| 256 | + '终身寿险', | ||
| 257 | + '企业年金保险', | ||
| 258 | + '团体意外险', | ||
| 259 | + '家庭财产保险', | ||
| 260 | + '责任保险系列', | ||
| 261 | + '旅游保险', | ||
| 262 | + '留学保险', | ||
| 263 | + '健康保险计划', | ||
| 264 | + '车辆保险', | ||
| 265 | + '财产一切险', | ||
| 266 | + '工程保险' | ||
| 267 | +] | ||
| 268 | + | ||
| 269 | +const PRODUCT_TAGS = [ | ||
| 270 | + { id: 1, name: '热销', bg_color: '#FEE2E2', text_color: '#DC2626' }, | ||
| 271 | + { id: 2, name: '新品', bg_color: '#DBEAFE', text_color: '#2563EB' }, | ||
| 272 | + { id: 3, name: '推荐', bg_color: '#D1FAE5', text_color: '#059669' }, | ||
| 273 | + { id: 4, name: '限时', bg_color: '#FEF3C7', text_color: '#D97706' } | ||
| 274 | +] | ||
| 275 | + | ||
| 276 | +const PRODUCT_CATEGORIES = [ | ||
| 277 | + { id: 1, name: '人寿保险' }, | ||
| 278 | + { id: 2, name: '健康保险' }, | ||
| 279 | + { id: 3, name: '意外保险' }, | ||
| 280 | + { id: 4, name: '财产保险' } | ||
| 281 | +] | ||
| 282 | + | ||
| 283 | +/** | ||
| 284 | + * 生成产品列表项 | ||
| 285 | + */ | ||
| 286 | +function generateProductItem(id) { | ||
| 287 | + const productName = PRODUCT_NAMES[Math.floor(Math.random() * PRODUCT_NAMES.length)] | ||
| 288 | + const recommend = Math.random() > 0.7 ? 'hot' : '' | ||
| 289 | + | ||
| 290 | + // 随机选择1-2个标签 | ||
| 291 | + const tags = [] | ||
| 292 | + const tagCount = Math.floor(Math.random() * 2) + 1 | ||
| 293 | + const availableTags = [...PRODUCT_TAGS].sort(() => Math.random() - 0.5) | ||
| 294 | + | ||
| 295 | + for (let i = 0; i < tagCount; i++) { | ||
| 296 | + tags.push(availableTags[i]) | ||
| 297 | + } | ||
| 298 | + | ||
| 299 | + return { | ||
| 300 | + id: id, | ||
| 301 | + product_name: productName, | ||
| 302 | + name: productName, | ||
| 303 | + cover_image: `https://placehold.co/400x300/4caf50/ffffff?text=${encodeURIComponent(productName.substring(0, 6))}`, | ||
| 304 | + recommend: recommend, | ||
| 305 | + tags: tags, | ||
| 306 | + description: '这是一款优质的保险产品,为您的家庭提供全面保障...', | ||
| 307 | + premium: Math.floor(Math.random() * 10000 + 1000), | ||
| 308 | + category_id: Math.floor(Math.random() * 4) + 1 | ||
| 309 | + } | ||
| 310 | +} | ||
| 311 | + | ||
| 312 | +/** | ||
| 313 | + * Mock: listAPI (产品列表) | ||
| 314 | + */ | ||
| 315 | +export async function mockProductListAPI(params) { | ||
| 316 | + await mockDelay() | ||
| 317 | + | ||
| 318 | + const { page = 0, limit = 10, cid, keyword } = params | ||
| 319 | + const totalPages = 10 | ||
| 320 | + | ||
| 321 | + if (page >= totalPages) { | ||
| 322 | + return { code: 1, msg: 'success', data: { list: [], categories: [], total: 0 } } | ||
| 323 | + } | ||
| 324 | + | ||
| 325 | + const list = [] | ||
| 326 | + const startIndex = page * limit | ||
| 327 | + | ||
| 328 | + for (let i = 0; i < limit; i++) { | ||
| 329 | + const item = generateProductItem(startIndex + i + 1) | ||
| 330 | + | ||
| 331 | + // 如果有分类过滤 | ||
| 332 | + if (cid && item.category_id !== parseInt(cid)) { | ||
| 333 | + continue | ||
| 334 | + } | ||
| 335 | + | ||
| 336 | + // 如果有关键词搜索 | ||
| 337 | + if (keyword && !item.product_name.includes(keyword)) { | ||
| 338 | + continue | ||
| 339 | + } | ||
| 340 | + | ||
| 341 | + list.push(item) | ||
| 342 | + } | ||
| 343 | + | ||
| 344 | + console.log(`[Mock] listAPI - 第${page}页,共${list.length}条`) | ||
| 345 | + | ||
| 346 | + return { | ||
| 347 | + code: 1, | ||
| 348 | + msg: 'success', | ||
| 349 | + data: { | ||
| 350 | + list, | ||
| 351 | + categories: PRODUCT_CATEGORIES, | ||
| 352 | + total: totalPages * limit | ||
| 353 | + } | ||
| 354 | + } | ||
| 355 | +} | ||
| 356 | + | ||
| 357 | +// ============================================================================ | ||
| 358 | +// 4. 搜索 Mock (searchAPI) | ||
| 359 | +// ============================================================================ | ||
| 360 | + | ||
| 361 | +/** | ||
| 362 | + * Mock: searchAPI (支持产品和资料搜索) | ||
| 363 | + */ | ||
| 364 | +export async function mockSearchAPI(params) { | ||
| 365 | + await mockDelay() | ||
| 366 | + | ||
| 367 | + const { page = 0, limit = 20, keyword, type } = params | ||
| 368 | + | ||
| 369 | + if (!keyword) { | ||
| 370 | + // 🔧 优化:如果没有关键词,返回更多推荐数据用于测试 | ||
| 371 | + // 生成20个产品和20个资料作为默认搜索结果 | ||
| 372 | + const defaultProducts = [] | ||
| 373 | + const defaultFiles = [] | ||
| 374 | + | ||
| 375 | + for (let i = 0; i < 20; i++) { | ||
| 376 | + const productItem = generateProductItem(i + 1) | ||
| 377 | + defaultProducts.push({ | ||
| 378 | + ...productItem, | ||
| 379 | + id: i + 1, | ||
| 380 | + cover_image: productItem.cover_image | ||
| 381 | + }) | ||
| 382 | + | ||
| 383 | + const materialItem = generateMaterialItem(i + 1) | ||
| 384 | + defaultFiles.push({ | ||
| 385 | + ...materialItem, | ||
| 386 | + id: i + 1, | ||
| 387 | + fileName: materialItem.fileName, | ||
| 388 | + fileSize: materialItem.size, | ||
| 389 | + learners: `${materialItem.read_people_count}人学习`, | ||
| 390 | + readPeoplePercent: materialItem.read_people_percent, | ||
| 391 | + collected: materialItem.collected, | ||
| 392 | + extension: materialItem.extension, | ||
| 393 | + downloadUrl: materialItem.downloadUrl, | ||
| 394 | + title: materialItem.title, | ||
| 395 | + src: materialItem.src | ||
| 396 | + }) | ||
| 397 | + } | ||
| 398 | + | ||
| 399 | + console.log(`[Mock] searchAPI - 无关键词,返回默认数据:产品${defaultProducts.length}条,资料${defaultFiles.length}条`) | ||
| 400 | + return { | ||
| 401 | + code: 1, | ||
| 402 | + msg: 'success', | ||
| 403 | + data: { | ||
| 404 | + products: { list: defaultProducts, total: defaultProducts.length * 5 }, | ||
| 405 | + files: { list: defaultFiles, total: defaultFiles.length * 5 } | ||
| 406 | + } | ||
| 407 | + } | ||
| 408 | + } | ||
| 409 | + | ||
| 410 | + const totalPages = 5 | ||
| 411 | + | ||
| 412 | + if (page >= totalPages) { | ||
| 413 | + return { | ||
| 414 | + code: 1, | ||
| 415 | + msg: 'success', | ||
| 416 | + data: { | ||
| 417 | + products: { list: [], total: 0 }, | ||
| 418 | + files: { list: [], total: 0 } | ||
| 419 | + } | ||
| 420 | + } | ||
| 421 | + } | ||
| 422 | + | ||
| 423 | + const products = [] | ||
| 424 | + const files = [] | ||
| 425 | + const startIndex = page * limit | ||
| 426 | + | ||
| 427 | + // 🔧 优化:每次循环都生成数据和尝试匹配,增加命中率 | ||
| 428 | + for (let i = 0; i < limit / 2; i++) { | ||
| 429 | + // 产品 | ||
| 430 | + const productItem = generateProductItem(startIndex + i + 1) | ||
| 431 | + const productName = productItem.product_name.toLowerCase() | ||
| 432 | + const searchKeyword = keyword.toLowerCase() | ||
| 433 | + | ||
| 434 | + // 🔧 优化:更宽松的搜索条件 | ||
| 435 | + // 1. 完全匹配 | ||
| 436 | + // 2. 拆分关键词,包含任意一个字符即可 | ||
| 437 | + // 3. 关键词长度 >= 2 时,只要产品名称包含任意连续2个字符 | ||
| 438 | + const keywords = searchKeyword.split('').filter(k => k.trim()) | ||
| 439 | + const hasAnyChar = keywords.length > 0 && keywords.some(k => productName.includes(k)) | ||
| 440 | + const hasBigram = searchKeyword.length >= 2 && keywords.slice(0, -1).some((k, idx) => productName.includes(k + keywords[idx + 1])) | ||
| 441 | + | ||
| 442 | + if (productName.includes(searchKeyword) || hasAnyChar || hasBigram) { | ||
| 443 | + products.push({ | ||
| 444 | + ...productItem, | ||
| 445 | + id: startIndex + i + 1, | ||
| 446 | + cover_image: productItem.cover_image | ||
| 447 | + }) | ||
| 448 | + } | ||
| 449 | + | ||
| 450 | + // 资料 | ||
| 451 | + const materialItem = generateMaterialItem(startIndex + i + 100) | ||
| 452 | + const materialName = materialItem.name.toLowerCase() | ||
| 453 | + | ||
| 454 | + const hasAnyCharMaterial = keywords.length > 0 && keywords.some(k => materialName.includes(k)) | ||
| 455 | + const hasBigramMaterial = searchKeyword.length >= 2 && keywords.slice(0, -1).some((k, idx) => materialName.includes(k + keywords[idx + 1])) | ||
| 456 | + | ||
| 457 | + if (materialName.includes(searchKeyword) || hasAnyCharMaterial || hasBigramMaterial) { | ||
| 458 | + files.push({ | ||
| 459 | + ...materialItem, | ||
| 460 | + id: startIndex + i + 100, | ||
| 461 | + fileName: materialItem.fileName, | ||
| 462 | + fileSize: materialItem.size, | ||
| 463 | + learners: `${materialItem.read_people_count}人学习`, | ||
| 464 | + readPeoplePercent: materialItem.read_people_percent, | ||
| 465 | + collected: materialItem.collected, | ||
| 466 | + extension: materialItem.extension, | ||
| 467 | + downloadUrl: materialItem.downloadUrl, | ||
| 468 | + title: materialItem.title, | ||
| 469 | + src: materialItem.src | ||
| 470 | + }) | ||
| 471 | + } | ||
| 472 | + } | ||
| 473 | + | ||
| 474 | + // 🔧 优化:如果没有匹配到任何数据,返回一些推荐数据 | ||
| 475 | + if (products.length === 0 && files.length === 0) { | ||
| 476 | + console.log(`[Mock] searchAPI - 无匹配结果,返回推荐数据`) | ||
| 477 | + // 生成 5 个推荐产品 | ||
| 478 | + for (let i = 0; i < 5; i++) { | ||
| 479 | + const productItem = generateProductItem(startIndex + i + 1) | ||
| 480 | + products.push({ | ||
| 481 | + ...productItem, | ||
| 482 | + id: startIndex + i + 1, | ||
| 483 | + cover_image: productItem.cover_image | ||
| 484 | + }) | ||
| 485 | + | ||
| 486 | + const materialItem = generateMaterialItem(startIndex + i + 100) | ||
| 487 | + files.push({ | ||
| 488 | + ...materialItem, | ||
| 489 | + id: startIndex + i + 100, | ||
| 490 | + fileName: materialItem.fileName, | ||
| 491 | + fileSize: materialItem.size, | ||
| 492 | + learners: `${materialItem.read_people_count}人学习`, | ||
| 493 | + readPeoplePercent: materialItem.read_people_percent, | ||
| 494 | + collected: materialItem.collected, | ||
| 495 | + extension: materialItem.extension, | ||
| 496 | + downloadUrl: materialItem.downloadUrl, | ||
| 497 | + title: materialItem.title, | ||
| 498 | + src: materialItem.src | ||
| 499 | + }) | ||
| 500 | + } | ||
| 501 | + } | ||
| 502 | + | ||
| 503 | + console.log(`[Mock] searchAPI - 第${page}页,关键词"${keyword}",产品${products.length}条,资料${files.length}条`) | ||
| 504 | + | ||
| 505 | + return { | ||
| 506 | + code: 1, | ||
| 507 | + msg: 'success', | ||
| 508 | + data: { | ||
| 509 | + products: { list: products, total: products.length * totalPages }, | ||
| 510 | + files: { list: files, total: files.length * totalPages } | ||
| 511 | + } | ||
| 512 | + } | ||
| 513 | +} | ||
| 514 | + | ||
| 515 | +// ============================================================================ | ||
| 516 | +// 5. 消息列表 Mock (myListAPI) | ||
| 517 | +// ============================================================================ | ||
| 518 | + | ||
| 519 | +const MESSAGE_TITLES = [ | ||
| 520 | + '关于2024年新产品上线通知', | ||
| 521 | + '系统升级维护公告', | ||
| 522 | + '您的保单已生效提醒', | ||
| 523 | + '理赔进度更新通知', | ||
| 524 | + '续费提醒', | ||
| 525 | + '活动邀请:财富管理讲座', | ||
| 526 | + '客户服务满意度调查', | ||
| 527 | + '最新培训资料已上线', | ||
| 528 | + '合规要求更新通知', | ||
| 529 | + '节日问候与祝福', | ||
| 530 | + '产品停售通知', | ||
| 531 | + '核保政策调整', | ||
| 532 | + '理赔流程优化说明', | ||
| 533 | + '客户权益保障计划', | ||
| 534 | + '数字化服务升级公告' | ||
| 535 | +] | ||
| 536 | + | ||
| 537 | +/** | ||
| 538 | + * 生成消息列表项 | ||
| 539 | + */ | ||
| 540 | +function generateMessageItem(id) { | ||
| 541 | + const title = MESSAGE_TITLES[Math.floor(Math.random() * MESSAGE_TITLES.length)] | ||
| 542 | + const now = new Date() | ||
| 543 | + const createDate = new Date(now.getTime() - Math.random() * 30 * 24 * 60 * 60 * 1000) | ||
| 544 | + | ||
| 545 | + return { | ||
| 546 | + id: id, | ||
| 547 | + title: title, | ||
| 548 | + intro: `这是一条关于"${title}"的重要通知,请及时查看详情...`, | ||
| 549 | + content: `这是一条关于"${title}"的重要通知,请及时查看详情。如需了解更多信息,请联系客服。`, | ||
| 550 | + create_time: formatDate(createDate), | ||
| 551 | + is_read: Math.random() > 0.5 ? 1 : 0, | ||
| 552 | + type: Math.random() > 0.5 ? 'notice' : 'system' | ||
| 553 | + } | ||
| 554 | +} | ||
| 555 | + | ||
| 556 | +/** | ||
| 557 | + * 格式化日期 | ||
| 558 | + */ | ||
| 559 | +function formatDate(date) { | ||
| 560 | + const year = date.getFullYear() | ||
| 561 | + const month = String(date.getMonth() + 1).padStart(2, '0') | ||
| 562 | + const day = String(date.getDate()).padStart(2, '0') | ||
| 563 | + return `${year}-${month}-${day}` | ||
| 564 | +} | ||
| 565 | + | ||
| 566 | +/** | ||
| 567 | + * Mock: myListAPI (消息列表) | ||
| 568 | + */ | ||
| 569 | +export async function mockMessageListAPI(params) { | ||
| 570 | + await mockDelay() | ||
| 571 | + | ||
| 572 | + const { page = 1, limit = 10 } = params | ||
| 573 | + const totalPages = 8 | ||
| 574 | + | ||
| 575 | + if (page > totalPages) { | ||
| 576 | + return { code: 1, msg: 'success', data: { list: [] } } | ||
| 577 | + } | ||
| 578 | + | ||
| 579 | + const list = [] | ||
| 580 | + const startIndex = (page - 1) * limit | ||
| 581 | + | ||
| 582 | + for (let i = 0; i < limit; i++) { | ||
| 583 | + list.push(generateMessageItem(startIndex + i + 1)) | ||
| 584 | + } | ||
| 585 | + | ||
| 586 | + console.log(`[Mock] myListAPI - 第${page}页,共${list.length}条`) | ||
| 587 | + | ||
| 588 | + return { | ||
| 589 | + code: 1, | ||
| 590 | + msg: 'success', | ||
| 591 | + data: { list } | ||
| 592 | + } | ||
| 593 | +} | ||
| 594 | + | ||
| 595 | +// ============================================================================ | ||
| 596 | +// 导出统一 Mock API 调用器 | ||
| 597 | +// ============================================================================ | ||
| 598 | + | ||
| 599 | +/** | ||
| 600 | + * Mock API 调用器 | ||
| 601 | + * @param {string} apiName - API 名称 | ||
| 602 | + * @param {Object} params - 请求参数 | ||
| 603 | + * @returns {Promise} | ||
| 604 | + */ | ||
| 605 | +export async function mockAPI(apiName, params) { | ||
| 606 | + switch (apiName) { | ||
| 607 | + case 'weekHotAPI': | ||
| 608 | + return await mockWeekHotAPI(params) | ||
| 609 | + case 'fileListAPI': | ||
| 610 | + return await mockFileListAPI(params) | ||
| 611 | + case 'listAPI': | ||
| 612 | + return await mockProductListAPI(params) | ||
| 613 | + case 'searchAPI': | ||
| 614 | + return await mockSearchAPI(params) | ||
| 615 | + case 'myListAPI': | ||
| 616 | + return await mockMessageListAPI(params) | ||
| 617 | + default: | ||
| 618 | + console.warn(`[Mock] 未知的 API: ${apiName}`) | ||
| 619 | + return { code: 0, msg: 'Unknown API', data: null } | ||
| 620 | + } | ||
| 621 | +} |
-
Please register or login to post a comment