hookehuyr

feat(index): 首页网格导航动态化,支持后台配置

## 主要变更

### 优化
- 移除硬编码的 CATEGORY_IDS 和 loopNav 配置
- 新增 fetchHomeIcons 函数,从 API 动态获取导航数据
- 实现智能参数解析,自动提取 link 字段中的路由和查询参数
- 支持任意数量的查询参数(如 cid, category_id 等)
- API 失败时自动降级到默认配置,保证可用性

### 导航跳转逻辑优化
- 重构 handleGridNav 函数,使用通用的参数提取逻辑
- 自动添加 title 参数到目标页面
- 移除硬编码的路由判断,提升可维护性

### 代码质量提升
- 性能优化:使用 shallowRef 存储导航数据
- 错误处理:完善的 try-catch 和降级方案
- 代码注释:添加完整的 JSDoc 注释

## 影响文件
- src/pages/index/index.vue
- docs/CHANGELOG.md

## 技术细节
- link 字段格式:/pages/category-list/index?cid=3129684
- 自动解析路由和查询参数
- 参数值自动 URL 解码
- 灵活支持后台配置,无需发版

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
...@@ -5,6 +5,33 @@ ...@@ -5,6 +5,33 @@
5 5
6 --- 6 ---
7 7
8 +## [2026-02-05] - 首页网格导航动态化
9 +
10 +### 优化
11 +- **首页网格导航** (`src/pages/index/index.vue`)
12 + - 移除硬编码的 `CATEGORY_IDS` 配置和 `loopNav` 数组
13 + - 新增 `fetchHomeIcons` 函数,从 API 动态获取导航数据
14 + - 实现智能参数解析,自动提取 `link` 字段中的路由和查询参数
15 + - 支持任意数量的查询参数 (如 `cid`, `category_id` 等)
16 + - API 失败时自动降级到默认配置,保证可用性
17 +- **导航跳转逻辑优化**
18 + - 重构 `handleGridNav` 函数,使用通用的参数提取逻辑
19 + - 自动添加 `title` 参数到目标页面
20 + - 移除硬编码的路由判断,提升可维护性
21 +
22 +### 代码质量提升
23 +- **性能优化**: 使用 `shallowRef` 存储导航数据,避免深度响应式
24 +- **错误处理**: 完善的 try-catch 和降级方案
25 +- **代码注释**: 添加完整的 JSDoc 注释
26 +- **可维护性**: 支持后台配置,无需修改代码即可调整导航
27 +
28 +### 影响
29 +- **灵活性**: 可通过后台接口动态配置首页导航,无需发版
30 +- **可扩展性**: 支持任意数量的查询参数
31 +- **稳定性**: API 失败时有降级方案,不影响核心功能
32 +
33 +---
34 +
8 ## [2026-02-05] - 代码重构:收藏操作逻辑抽取 35 ## [2026-02-05] - 代码重构:收藏操作逻辑抽取
9 36
10 ### 重构 37 ### 重构
......
...@@ -191,6 +191,7 @@ import ListItemActions from '@/components/ListItemActions/index.vue'; ...@@ -191,6 +191,7 @@ import ListItemActions from '@/components/ListItemActions/index.vue';
191 import { listAPI } from '@/api/get_product'; 191 import { listAPI } from '@/api/get_product';
192 import { weekHotAPI } from '@/api/file'; 192 import { weekHotAPI } from '@/api/file';
193 import { useCollectOperation } from '@/composables/useCollectOperation'; 193 import { useCollectOperation } from '@/composables/useCollectOperation';
194 +import { homeIconAPI } from '@/api/home';
194 195
195 // User Store 196 // User Store
196 const userStore = useUserStore(); 197 const userStore = useUserStore();
...@@ -223,25 +224,55 @@ const handlePlanSubmit = (formData) => { ...@@ -223,25 +224,55 @@ const handlePlanSubmit = (formData) => {
223 }; 224 };
224 225
225 /** 226 /**
226 - * 分类 ID 配置 227 + * 首页网格导航数据
227 - * @description 各业务模块对应的分类 ID,需要根据后端实际返回的 ID 配置 228 + *
228 - * TODO: 将这些 CID 替换为实际的分类 ID 229 + * @description 从 API 动态获取,包含图标、名称、路由等信息
229 */ 230 */
230 -const CATEGORY_IDS = { 231 +const loopNav = shallowRef([]);
231 - onboarding: '3129684', // 入职相关分类 ID
232 - signing: '', // 签单相关分类 ID
233 - familyOffice: '', // 家办相关分类 ID
234 - customerService: '' // 客户服务分类 ID
235 -}
236 232
237 -const loopNav = shallowRef([ 233 +/**
238 - { id: 'plan', icon: 'order', name: '计划书', route: '/pages/plan/index' }, 234 + * 获取首页图标列表
239 - { id: 'onboarding', icon: 'my', name: '入职相关', route: '/pages/category-list/index', cid: CATEGORY_IDS.onboarding }, 235 + *
240 - { id: 'signing', icon: 'cart', name: '签单相关', route: '/pages/category-list/index', cid: CATEGORY_IDS.signing }, 236 + * @description 从 API 获取首页网格导航数据,并解析 link 字段
241 - { id: 'family-office', icon: 'home', name: '家办相关', route: '/pages/category-list/index', cid: CATEGORY_IDS.familyOffice }, 237 + */
242 - { id: 'knowledge-base', icon: 'category', name: '产品知识库', route: '/pages/knowledge-base/index' }, 238 +const fetchHomeIcons = async () => {
243 - { id: 'customer-service', icon: 'star', name: '客户服务', route: '/pages/category-list/index', cid: CATEGORY_IDS.customerService }, 239 + try {
244 -]); 240 + const res = await homeIconAPI();
241 +
242 + if (res.code === 1 && res.data) {
243 + // 将 API 数据映射为 loopNav 格式
244 + loopNav.value = res.data.map(item => {
245 + // 解析 link 字段,格式: "/pages/category-list/index?cid=3129684"
246 + const [route, queryStr] = item.link.split('?');
247 + const params = {};
248 +
249 + // 如果有查询参数,解析为对象
250 + if (queryStr) {
251 + queryStr.split('&').forEach(param => {
252 + const [key, value] = param.split('=');
253 + params[key] = decodeURIComponent(value);
254 + });
255 + }
256 +
257 + // 返回导航项对象
258 + return {
259 + id: String(item.id),
260 + icon: item.icon,
261 + name: item.name,
262 + route,
263 + ...params // 展开参数(如 cid)
264 + };
265 + });
266 + }
267 + } catch (err) {
268 + console.error('获取首页图标失败:', err);
269 + // 如果 API 调用失败,使用默认配置
270 + loopNav.value = [
271 + { id: 'plan', icon: 'order', name: '计划书', route: '/pages/plan/index' },
272 + { id: 'knowledge-base', icon: 'category', name: '产品知识库', route: '/pages/knowledge-base/index' }
273 + ];
274 + }
275 +};
245 276
246 /** 277 /**
247 * 热卖产品数据 278 * 热卖产品数据
...@@ -337,7 +368,15 @@ const { handleClick: onViewMaterial } = useListItemClick({ ...@@ -337,7 +368,15 @@ const { handleClick: onViewMaterial } = useListItemClick({
337 // 使用收藏操作 composable 368 // 使用收藏操作 composable
338 const { toggleCollect: toggleMaterialCollect } = useCollectOperation(); 369 const { toggleCollect: toggleMaterialCollect } = useCollectOperation();
339 370
340 -// Handle grid navigation click 371 +/**
372 + * 处理网格导航点击
373 + *
374 + * @description 根据导航项的路由和参数进行跳转
375 + * @param {Object} item - 导航项对象
376 + * @param {string} item.route - 目标路由
377 + * @param {string} item.name - 导航名称
378 + * @param {Object} [item.cid] - 可选的分类 ID 参数
379 + */
341 const handleGridNav = (item) => { 380 const handleGridNav = (item) => {
342 if (!item.route) { 381 if (!item.route) {
343 Taro.showToast({ 382 Taro.showToast({
...@@ -348,14 +387,21 @@ const handleGridNav = (item) => { ...@@ -348,14 +387,21 @@ const handleGridNav = (item) => {
348 return; 387 return;
349 } 388 }
350 389
351 - // 如果是分类列表页面,需要带参数跳转 390 + // 提取除 route 以外的所有参数(如 cid)
352 - if (item.route === '/pages/category-list/index') { 391 + const params = { ...item };
392 + delete params.id;
393 + delete params.icon;
394 + delete params.name;
395 + delete params.route;
396 +
397 + // 如果有参数(如 cid),则带参数跳转
398 + if (Object.keys(params).length > 0) {
353 go(item.route, { 399 go(item.route, {
354 - cid: item.cid, // 分类 ID 400 + ...params,
355 - title: item.name // 页面标题 401 + title: item.name // 将导航名称作为页面标题
356 }); 402 });
357 } else { 403 } else {
358 - // 其他页面直接跳转 404 + // 无参数,直接跳转
359 go(item.route); 405 go(item.route);
360 } 406 }
361 }; 407 };
...@@ -374,8 +420,9 @@ const openWebView = (url) => { ...@@ -374,8 +420,9 @@ const openWebView = (url) => {
374 }); 420 });
375 }; 421 };
376 422
377 -// 页面加载时获取热卖产品和热门资料 423 +// 页面加载时获取首页图标、热卖产品和热门资料
378 useLoad(() => { 424 useLoad(() => {
425 + fetchHomeIcons();
379 fetchHotProducts(); 426 fetchHotProducts();
380 fetchHotMaterials(); 427 fetchHotMaterials();
381 }); 428 });
......