hookehuyr

refactor: 迁移所有剩余页面到 LoadMoreList 组件

- message 页面:添加下拉刷新功能
- product-center 页面:保留搜索、tabs、计划书弹窗
- material-list 页面:保留分类缓存、搜索防抖
- search 页面:保留双列表、自动 tab 切换、三种状态

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 <!-- 1 <!--
2 - * @Date: 2026-01-31 2 + * @Date: 2026-02-08
3 - * @Description: 资料列表页 - 已改造为 NutTabs 版本 3 + * @Description: 资料/文档列表页 - 使用 LoadMoreList 组件重构版本
4 --> 4 -->
5 <template> 5 <template>
6 <view class="bg-[#F9FAFB]"> 6 <view class="bg-[#F9FAFB]">
7 - <!-- 固定在顶部的导航和搜索 --> 7 + <LoadMoreList
8 - <view class="bg-[#F9FAFB] sticky top-0 z-10"> 8 + :list="currentList"
9 - <NavHeader :title="pageTitle" /> 9 + :page="currentPage"
10 - 10 + :page-size="pageSize"
11 - <view class="px-[32rpx] mt-[32rpx] mb-[24rpx]"> 11 + :has-more="hasMore"
12 - <SearchBar 12 + :loading="loading"
13 - v-model="searchValue" 13 + :loading-more="loadingMore"
14 - placeholder="搜索资料..." 14 + key-field="meta_id"
15 - @search="onSearch" 15 + @load-more="handleLoadMore"
16 - @clear="onClear"
17 - variant="rounded"
18 - :show-border="true"
19 - :show-clear="true"
20 - />
21 - </view>
22 -
23 - <!-- 动态显示 Tabs(仅在有分类时显示) -->
24 - <nut-tabs v-if="hasCategories" v-model="activeTabId">
25 - <!-- 自定义标签栏 -->
26 - <template #titles>
27 - <view class="filter-tabs-wrapper">
28 - <view
29 - v-for="item in tabsData"
30 - :key="item.id"
31 - :class="[
32 - 'filter-tab-item',
33 - activeTabId === item.id ? 'filter-tab-active' : 'filter-tab-inactive'
34 - ]"
35 - @tap="onTabClick(item.id)"
36 - >
37 - <text class="filter-tab-text">{{ item.name }}</text>
38 - </view>
39 - </view>
40 - </template>
41 - </nut-tabs>
42 - </view>
43 -
44 - <!-- 列表容器 - 页面级滚动 -->
45 - <view
46 - v-if="listVisible"
47 - :key="listRenderKey"
48 - class="px-[32rpx] pb-[calc(160rpx+env(safe-area-inset-bottom))] box-border"
49 > 16 >
50 - <view class="flex flex-col gap-[24rpx]"> 17 + <!-- 头部:导航 + 搜索 + Tabs -->
51 - <view v-for="(item, index) in currentList" :key="index" 18 + <template #header>
52 - class="material-item bg-white rounded-[24rpx] p-[24rpx] shadow-sm transition-all duration-200 border border-gray-50 flex flex-row" 19 + <view class="sticky top-0 z-10 bg-[#F9FAFB]">
53 - :style="{ animationDelay: `${index * 50}ms` }"> 20 + <NavHeader :title="pageTitle" />
21 +
22 + <view class="px-[32rpx] mt-[32rpx] mb-[24rpx]">
23 + <SearchBar
24 + v-model="searchValue"
25 + placeholder="搜索资料..."
26 + @search="onSearch"
27 + @clear="onClear"
28 + variant="rounded"
29 + :show-border="true"
30 + :show-clear="true"
31 + />
32 + </view>
33 +
34 + <!-- 动态显示 Tabs(仅在有分类时显示) -->
35 + <nut-tabs v-if="hasCategories" v-model="activeTabId">
36 + <!-- 自定义标签栏 -->
37 + <template #titles>
38 + <view class="filter-tabs-wrapper">
39 + <view
40 + v-for="item in tabsData"
41 + :key="item.id"
42 + :class="[
43 + 'filter-tab-item',
44 + activeTabId === item.id ? 'filter-tab-active' : 'filter-tab-inactive'
45 + ]"
46 + @tap="onTabClick(item.id)"
47 + >
48 + <text class="filter-tab-text">{{ item.name }}</text>
49 + </view>
50 + </view>
51 + </template>
52 + </nut-tabs>
53 + </view>
54 + </template>
54 55
56 + <!-- 列表项:资料卡片 -->
57 + <template #item="{ item }">
58 + <view
59 + class="material-item bg-white rounded-[24rpx] p-[24rpx] shadow-sm transition-all duration-200 border border-gray-50 flex flex-row"
60 + >
55 <view 61 <view
56 class="w-[88rpx] h-[88rpx] mr-[24rpx] flex-shrink-0 flex items-center justify-center bg-gradient-to-br from-blue-50 to-blue-100 rounded-[20rpx] shadow-inner self-start"> 62 class="w-[88rpx] h-[88rpx] mr-[24rpx] flex-shrink-0 flex items-center justify-center bg-gradient-to-br from-blue-50 to-blue-100 rounded-[20rpx] shadow-inner self-start">
57 <image 63 <image
...@@ -69,7 +75,7 @@ ...@@ -69,7 +75,7 @@
69 {{ item.desc }} 75 {{ item.desc }}
70 </p> 76 </p>
71 77
72 - <view class="flex items-center gap-[12rpx] mb-[20rpx]"> 78 + <view class="flex items-center gap-[12rpx] mb-[16rpx]">
73 <view 79 <view
74 class="inline-flex items-center justify-center px-[12rpx] py-[4rpx] bg-gray-100 text-gray-500 text-[20rpx] font-medium rounded-[8rpx]"> 80 class="inline-flex items-center justify-center px-[12rpx] py-[4rpx] bg-gray-100 text-gray-500 text-[20rpx] font-medium rounded-[8rpx]">
75 {{ getDocumentLabel(item.extension ? `file.${item.extension}` : item.fileName) }} 81 {{ getDocumentLabel(item.extension ? `file.${item.extension}` : item.fileName) }}
...@@ -92,33 +98,23 @@ ...@@ -92,33 +98,23 @@
92 /> 98 />
93 </view> 99 </view>
94 </view> 100 </view>
101 + </template>
95 102
96 - <!-- 空状态 --> 103 + <!-- 空状态 -->
97 - <view v-if="currentList.length === 0 && !loading"> 104 + <template #empty>
98 - <nut-empty description="暂无相关资料" image="empty" /> 105 + <nut-empty description="暂无相关资料" image="empty" />
99 - </view> 106 + </template>
100 - 107 + </LoadMoreList>
101 - <!-- 加载更多状态 -->
102 - <view v-if="currentList.length > 0" class="load-more-container">
103 - <view v-if="loadingMore" class="load-more-loading">
104 - <view class="loading-spinner"></view>
105 - <text class="ml-[16rpx] text-[#9CA3AF] text-[24rpx]">加载中...</text>
106 - </view>
107 - <view v-else-if="!hasMore" class="load-more-finished">
108 - <text class="text-[#9CA3AF] text-[24rpx]">没有更多了</text>
109 - </view>
110 - </view>
111 - </view>
112 - </view>
113 </view> 108 </view>
114 </template> 109 </template>
115 110
116 <script setup> 111 <script setup>
117 -import { ref, computed, nextTick, watch } from 'vue' 112 +import { ref, computed, watch } from 'vue'
118 -import { useLoad, useReachBottom } from '@tarojs/taro' 113 +import { useLoad } from '@tarojs/taro'
119 import NavHeader from '@/components/NavHeader.vue' 114 import NavHeader from '@/components/NavHeader.vue'
120 import SearchBar from '@/components/SearchBar.vue' 115 import SearchBar from '@/components/SearchBar.vue'
121 import ListItemActions from '@/components/ListItemActions/index.vue' 116 import ListItemActions from '@/components/ListItemActions/index.vue'
117 +import LoadMoreList from '@/components/LoadMoreList'
122 import { useListItemClick, ListType } from '@/composables/useListItemClick' 118 import { useListItemClick, ListType } from '@/composables/useListItemClick'
123 import { getDocumentIcon, getDocumentLabel } from '@/utils/documentIcons' 119 import { getDocumentIcon, getDocumentLabel } from '@/utils/documentIcons'
124 import { debounce } from '@/utils/debounce' 120 import { debounce } from '@/utils/debounce'
...@@ -132,8 +128,6 @@ const USE_MOCK_DATA = process.env.NODE_ENV === 'development' ...@@ -132,8 +128,6 @@ const USE_MOCK_DATA = process.env.NODE_ENV === 'development'
132 128
133 const searchValue = ref('') 129 const searchValue = ref('')
134 const activeTabId = ref('all') // 默认选中"全部" 130 const activeTabId = ref('all') // 默认选中"全部"
135 -const listVisible = ref(true)
136 -const listRenderKey = ref(0)
137 131
138 /** 132 /**
139 * 防抖搜索函数 133 * 防抖搜索函数
...@@ -267,6 +261,7 @@ const transformDocItem = (doc) => { ...@@ -267,6 +261,7 @@ const transformDocItem = (doc) => {
267 261
268 /** 262 /**
269 * 获取文档分类列表 263 * 获取文档分类列表
264 + *
270 * @param {Object} params - 请求参数 265 * @param {Object} params - 请求参数
271 * @param {string} params.cid - 分类ID(可选) 266 * @param {string} params.cid - 分类ID(可选)
272 * @param {string} params.child_id - 子分类ID(可选) 267 * @param {string} params.child_id - 子分类ID(可选)
...@@ -274,6 +269,7 @@ const transformDocItem = (doc) => { ...@@ -274,6 +269,7 @@ const transformDocItem = (doc) => {
274 * @param {number} params.page - 页码(从0开始) 269 * @param {number} params.page - 页码(从0开始)
275 * @param {number} params.limit - 每页数量 270 * @param {number} params.limit - 每页数量
276 * @param {boolean} isLoadMore - 是否为加载更多 271 * @param {boolean} isLoadMore - 是否为加载更多
272 + * @returns {Promise<void>}
277 */ 273 */
278 const fetchMaterialList = async (params = {}, isLoadMore = false) => { 274 const fetchMaterialList = async (params = {}, isLoadMore = false) => {
279 try { 275 try {
...@@ -296,9 +292,6 @@ const fetchMaterialList = async (params = {}, isLoadMore = false) => { ...@@ -296,9 +292,6 @@ const fetchMaterialList = async (params = {}, isLoadMore = false) => {
296 // 如果是初始请求(没有 child_id),保存完整的分类信息 292 // 如果是初始请求(没有 child_id),保存完整的分类信息
297 if (!params.child_id && !params.keyword) { 293 if (!params.child_id && !params.keyword) {
298 data.value = res.data 294 data.value = res.data
299 - // console.log('[Material List] 数据:', res.data)
300 - // console.log('[Material List] 分类数量:', res.data.children?.length)
301 - // console.log('[Material List] 文档数量:', res.data.list?.length)
302 295
303 // 处理并缓存"全部"列表 296 // 处理并缓存"全部"列表
304 if (res.data.list?.length) { 297 if (res.data.list?.length) {
...@@ -419,12 +412,67 @@ useLoad(async (options) => { ...@@ -419,12 +412,67 @@ useLoad(async (options) => {
419 }) 412 })
420 413
421 /** 414 /**
415 + * 处理加载更多事件
416 + *
417 + * @param {number} page - 下一页页码
418 + * @returns {Promise<void>}
419 + */
420 +const handleLoadMore = async (page) => {
421 + console.log('[Material List] 加载更多,页码:', page)
422 +
423 + // 更新页码
424 + currentPage.value = page
425 +
426 + // 构建请求参数
427 + const params = {
428 + cid: initialCategoryId.value,
429 + page: page,
430 + limit: pageSize
431 + }
432 +
433 + // 判断当前状态:搜索、子分类、或全部
434 + const isSearching = searchValue.value.trim() !== ''
435 +
436 + if (isSearching) {
437 + // 搜索模式
438 + params.keyword = searchValue.value.trim()
439 + if (activeTabId.value !== 'all') {
440 + params.child_id = activeTabId.value
441 + }
442 + } else {
443 + // 非搜索模式:如果当前选中的是子分类,添加 child_id 参数
444 + if (activeTabId.value !== 'all') {
445 + params.child_id = activeTabId.value
446 + }
447 + }
448 +
449 + // 加载下一页数据
450 + await fetchMaterialList(params, true) // true 表示加载更多
451 +
452 + // 保存更新后的分页状态
453 + let cacheKey
454 + if (isSearching) {
455 + cacheKey = params.keyword
456 + } else {
457 + cacheKey = activeTabId.value !== 'all' ? activeTabId.value : 'all'
458 + }
459 + categoryPageCache.value.set(cacheKey, {
460 + currentPage: currentPage.value,
461 + hasMore: hasMore.value
462 + })
463 +}
464 +
465 +/**
422 * Tab 点击处理 466 * Tab 点击处理
467 + *
423 * @param {string} id - Tab ID 468 * @param {string} id - Tab ID
424 */ 469 */
425 const onTabClick = async (id) => { 470 const onTabClick = async (id) => {
471 + if (activeTabId.value === id) return
472 +
473 + console.log('[Material List] 切换分类:', id)
474 +
426 activeTabId.value = id 475 activeTabId.value = id
427 - listVisible.value = false
428 476
429 // 恢复或初始化该分类的分页状态 477 // 恢复或初始化该分类的分页状态
430 const pageState = categoryPageCache.value.get(id) 478 const pageState = categoryPageCache.value.get(id)
...@@ -462,82 +510,15 @@ const onTabClick = async (id) => { ...@@ -462,82 +510,15 @@ const onTabClick = async (id) => {
462 }) 510 })
463 } 511 }
464 } 512 }
465 -
466 - nextTick(() => {
467 - listRenderKey.value += 1
468 - listVisible.value = true
469 - })
470 } 513 }
471 514
472 /** 515 /**
473 - * 触底加载更多
474 - * @description 使用防抖避免频繁触发
475 - */
476 -let loadMoreTimer = null
477 -useReachBottom(() => {
478 - // 如果正在加载或没有更多数据,不执行
479 - if (loadingMore.value || loading.value || !hasMore.value) {
480 - return
481 - }
482 -
483 - // 防抖:300ms 内只触发一次
484 - if (loadMoreTimer) {
485 - clearTimeout(loadMoreTimer)
486 - }
487 -
488 - loadMoreTimer = setTimeout(async () => {
489 - console.log('[Material List] 触底加载更多')
490 -
491 - // 页码 +1
492 - currentPage.value += 1
493 -
494 - // 构建请求参数
495 - const params = {
496 - cid: initialCategoryId.value,
497 - page: currentPage.value,
498 - limit: pageSize
499 - }
500 -
501 - // 判断当前状态:搜索、子分类、或全部
502 - const isSearching = searchValue.value.trim() !== ''
503 -
504 - if (isSearching) {
505 - // 搜索模式
506 - params.keyword = searchValue.value.trim()
507 - if (activeTabId.value !== 'all') {
508 - params.child_id = activeTabId.value
509 - }
510 - } else {
511 - // 非搜索模式:如果当前选中的是子分类,添加 child_id 参数
512 - if (activeTabId.value !== 'all') {
513 - params.child_id = activeTabId.value
514 - }
515 - }
516 -
517 - // 加载下一页数据
518 - await fetchMaterialList(params, true) // true 表示加载更多
519 -
520 - // 保存更新后的分页状态
521 - let cacheKey
522 - if (isSearching) {
523 - cacheKey = params.keyword
524 - } else {
525 - cacheKey = activeTabId.value !== 'all' ? activeTabId.value : 'all'
526 - }
527 - categoryPageCache.value.set(cacheKey, {
528 - currentPage: currentPage.value,
529 - hasMore: hasMore.value
530 - })
531 - }, 300)
532 -})
533 -
534 -/**
535 * 搜索处理函数 516 * 搜索处理函数
536 * @description 根据 child_id 和 keyword 调用接口查询列表 517 * @description 根据 child_id 和 keyword 调用接口查询列表
537 */ 518 */
538 const onSearch = async () => { 519 const onSearch = async () => {
539 - console.log('Searching for:', searchValue.value) 520 + console.log('[Material List] 搜索产品:', searchValue.value)
540 - console.log('当前分类:', activeTabId.value) 521 + console.log('[Material List] 当前分类:', activeTabId.value)
541 522
542 // 如果没有搜索关键词,清空搜索并恢复当前分类的列表 523 // 如果没有搜索关键词,清空搜索并恢复当前分类的列表
543 if (!searchValue.value.trim()) { 524 if (!searchValue.value.trim()) {
...@@ -651,15 +632,10 @@ watch(searchValue, (newValue, oldValue) => { ...@@ -651,15 +632,10 @@ watch(searchValue, (newValue, oldValue) => {
651 }) 632 })
652 633
653 /** 634 /**
654 - * 清除搜索关键词 635 + * 清空搜索
655 - * @description 用户点击搜索框右侧的删除按钮时触发,重新请求当前分类的最新数据
656 - *
657 - * 场景说明:
658 - * - 有tab:重新请求当前tab的数据(不带keyword)
659 - * - 无tab:重新请求"全部"数据(不带keyword)
660 */ 636 */
661 const onClear = async () => { 637 const onClear = async () => {
662 - console.log('[Material List] 清除搜索,重新请求数据') 638 + console.log('[Material List] 清空搜索')
663 console.log('[Material List] 当前分类:', activeTabId.value) 639 console.log('[Material List] 当前分类:', activeTabId.value)
664 640
665 // 构建请求参数(不带 keyword) 641 // 构建请求参数(不带 keyword)
...@@ -742,7 +718,7 @@ const { handleClick: onView } = useListItemClick({ ...@@ -742,7 +718,7 @@ const { handleClick: onView } = useListItemClick({
742 return true 718 return true
743 }, 719 },
744 onAfterClick: (item) => { 720 onAfterClick: (item) => {
745 - console.log('用户打开了资料:', item.title) 721 + console.log('[Material List] 用户打开了资料:', item.title)
746 } 722 }
747 }) 723 })
748 724
...@@ -765,8 +741,6 @@ const onDelete = (item) => { ...@@ -765,8 +741,6 @@ const onDelete = (item) => {
765 const index = allList.value.findIndex(i => i.id === item.id) 741 const index = allList.value.findIndex(i => i.id === item.id)
766 if (index !== -1) { 742 if (index !== -1) {
767 allList.value.splice(index, 1) 743 allList.value.splice(index, 1)
768 - // 重新渲染列表
769 - listRenderKey.value += 1
770 Taro.showToast({ title: '已删除', icon: 'success' }) 744 Taro.showToast({ title: '已删除', icon: 'success' })
771 } 745 }
772 } 746 }
...@@ -776,22 +750,6 @@ const onDelete = (item) => { ...@@ -776,22 +750,6 @@ const onDelete = (item) => {
776 </script> 750 </script>
777 751
778 <style lang="less"> 752 <style lang="less">
779 -@keyframes slideIn {
780 - from {
781 - opacity: 0;
782 - transform: translateY(20rpx);
783 - }
784 -
785 - to {
786 - opacity: 1;
787 - transform: translateY(0);
788 - }
789 -}
790 -
791 -.material-item {
792 - animation: slideIn 0.5s cubic-bezier(0.2, 0.8, 0.2, 1) backwards;
793 -}
794 -
795 // FilterTabs 风格的标签栏 753 // FilterTabs 风格的标签栏
796 .filter-tabs-wrapper { 754 .filter-tabs-wrapper {
797 display: flex; 755 display: flex;
...@@ -848,40 +806,22 @@ const onDelete = (item) => { ...@@ -848,40 +806,22 @@ const onDelete = (item) => {
848 display: none; 806 display: none;
849 } 807 }
850 808
851 -// 加载更多容器 809 +/* 多行文本省略 */
852 -.load-more-container { 810 +.line-clamp-1 {
853 - display: flex; 811 + display: -webkit-box;
854 - justify-content: center; 812 + -webkit-box-orient: vertical;
855 - align-items: center; 813 + -webkit-line-clamp: 1;
856 - padding: 40rpx 0; 814 + line-clamp: 1;
857 - min-height: 80rpx; 815 + overflow: hidden;
858 -} 816 + word-break: break-all;
859 -
860 -.load-more-loading {
861 - display: flex;
862 - align-items: center;
863 - justify-content: center;
864 } 817 }
865 818
866 -.load-more-finished { 819 +.line-clamp-2 {
867 - display: flex; 820 + display: -webkit-box;
868 - align-items: center; 821 + -webkit-box-orient: vertical;
869 - justify-content: center; 822 + -webkit-line-clamp: 2;
870 -} 823 + line-clamp: 2;
871 - 824 + overflow: hidden;
872 -// 自定义加载动画 825 + word-break: break-all;
873 -.loading-spinner {
874 - width: 32rpx;
875 - height: 32rpx;
876 - border: 4rpx solid #E5E7EB;
877 - border-top-color: #2563EB;
878 - border-radius: 50%;
879 - animation: spin 0.8s linear infinite;
880 -}
881 -
882 -@keyframes spin {
883 - to {
884 - transform: rotate(360deg);
885 - }
886 } 826 }
887 </style> 827 </style>
......
1 +<!--
2 + * @Date: 2026-02-08
3 + * @Description: 我的消息页 - 使用 LoadMoreList 组件重构版本
4 +-->
1 <template> 5 <template>
2 - <view class="min-h-screen bg-[#F9FAFB] pb-safe"> 6 + <LoadMoreList
3 - <NavHeader title="我的消息" /> 7 + :list="currentList"
4 - 8 + :page="currentPage"
5 - <!-- 列表区域 --> 9 + :page-size="pageSize"
6 - <view class="p-4"> 10 + :has-more="hasMore"
7 - <template v-if="messageList.length > 0"> 11 + :loading="loading"
8 - <view 12 + :loading-more="loadingMore"
9 - v-for="item in messageList" 13 + :enable-pull-down-refresh="true"
10 - :key="item.id" 14 + key-field="id"
11 - class="bg-white rounded-xl p-4 mb-3 shadow-sm active:opacity-70 transition-opacity" 15 + @load-more="handleLoadMore"
12 - @tap="handleItemClick(item)" 16 + @refresh="handleRefresh"
13 - > 17 + >
14 - <view class="flex justify-between items-start mb-2"> 18 + <!-- 头部 -->
15 - <view class="flex-1 mr-2"> 19 + <template #header>
16 - <view class="text-base font-bold text-gray-900 line-clamp-1"> 20 + <NavHeader title="我的消息" />
17 - {{ item.title }} 21 + </template>
18 - </view> 22 +
23 + <!-- 列表项 -->
24 + <template #item="{ item }">
25 + <view
26 + class="message-item bg-white rounded-xl p-4 mb-3 shadow-sm active:opacity-70 transition-opacity"
27 + @tap="handleItemClick(item)"
28 + >
29 + <view class="flex justify-between items-start mb-2">
30 + <view class="flex-1 mr-2">
31 + <view class="text-base font-bold text-gray-900 line-clamp-1">
32 + {{ item.title }}
19 </view> 33 </view>
20 - <text class="text-xs text-gray-400 shrink-0 mt-1">
21 - {{ item.create_time }}
22 - </text>
23 - </view>
24 -
25 - <view class="text-sm text-gray-600 line-clamp-2 leading-relaxed">
26 - {{ item.intro || item.content || '暂无简介' }}
27 </view> 34 </view>
35 + <text class="text-xs text-gray-400 shrink-0 mt-1">
36 + {{ item.create_time }}
37 + </text>
28 </view> 38 </view>
29 39
30 - <!-- 加载更多/没有更多 --> 40 + <view class="text-sm text-gray-600 line-clamp-2 leading-relaxed">
31 - <view class="py-4 text-center text-[24rpx] text-gray-400"> 41 + {{ item.intro || item.content || '暂无简介' }}
32 - <text v-if="loading">加载中...</text>
33 - <text v-else-if="!hasMore">没有更多了</text>
34 - <text v-else>上拉加载更多</text>
35 </view> 42 </view>
36 - </template> 43 + </view>
37 - 44 + </template>
38 - <!-- 空状态 --> 45 +
39 - <nut-empty 46 + <!-- 空状态 -->
40 - v-else-if="!loading && messageList.length === 0" 47 + <template #empty>
41 - description="暂无消息" 48 + <nut-empty description="暂无消息" image="empty" />
42 - image="empty" 49 + </template>
43 - /> 50 + </LoadMoreList>
44 - </view>
45 - </view>
46 </template> 51 </template>
47 52
48 <script setup> 53 <script setup>
49 import { ref } from 'vue' 54 import { ref } from 'vue'
50 -import { useLoad, usePullDownRefresh, useReachBottom, stopPullDownRefresh } from '@tarojs/taro' 55 +import { useLoad } from '@tarojs/taro'
51 import { useGo } from '@/hooks/useGo' 56 import { useGo } from '@/hooks/useGo'
57 +import LoadMoreList from '@/components/LoadMoreList'
52 import NavHeader from '@/components/NavHeader.vue' 58 import NavHeader from '@/components/NavHeader.vue'
53 import { myListAPI } from '@/api/news' 59 import { myListAPI } from '@/api/news'
54 import { mockMessageListAPI } from '@/utils/mockData' 60 import { mockMessageListAPI } from '@/utils/mockData'
...@@ -58,91 +64,165 @@ const USE_MOCK_DATA = process.env.NODE_ENV === 'development' ...@@ -58,91 +64,165 @@ const USE_MOCK_DATA = process.env.NODE_ENV === 'development'
58 64
59 const go = useGo() 65 const go = useGo()
60 66
61 -const messageList = ref([]) 67 +/**
62 -const page = ref(1) 68 + * 当前列表数据
63 -const limit = ref(10) 69 + * @type {Ref<Array<any>>}
70 + */
71 +const currentList = ref([])
72 +
73 +/**
74 + * 当前页码(从1开始)
75 + * @type {Ref<number>}
76 + */
77 +const currentPage = ref(1)
78 +
79 +/**
80 + * 每页数量
81 + * @type {number}
82 + */
83 +const pageSize = 10
84 +
85 +/**
86 + * 是否还有更多数据
87 + * @type {Ref<boolean>}
88 + */
64 const hasMore = ref(true) 89 const hasMore = ref(true)
65 -const loading = ref(false)
66 90
67 /** 91 /**
68 - * @description 加载消息列表 92 + * 首次加载状态
69 - * @param {boolean} refresh 是否刷新 93 + * @type {Ref<boolean>}
70 */ 94 */
71 -const fetchMessageList = async (refresh = false) => { 95 +const loading = ref(false)
72 - if (loading.value) return
73 -
74 - if (refresh) {
75 - page.value = 1
76 - hasMore.value = true
77 - } else if (!hasMore.value) {
78 - return
79 - }
80 96
81 - loading.value = true 97 +/**
98 + * 加载更多状态
99 + * @type {Ref<boolean>}
100 + */
101 +const loadingMore = ref(false)
82 102
103 +/**
104 + * 获取消息列表
105 + *
106 + * @param {Object} params - 请求参数
107 + * @param {number} params.page - 页码(从1开始)
108 + * @param {number} params.limit - 每页数量
109 + * @param {boolean} isLoadMore - 是否为加载更多
110 + * @returns {Promise<void>}
111 + */
112 +const fetchMessageList = async (params = {}, isLoadMore = false) => {
83 try { 113 try {
114 + // 如果是加载更多,使用 loadingMore 状态,否则使用 loading 状态
115 + if (isLoadMore) {
116 + loadingMore.value = true
117 + } else {
118 + loading.value = true
119 + }
120 +
121 + console.log('[Message] 请求参数:', params)
84 console.log('[Message] 使用 Mock 数据:', USE_MOCK_DATA) 122 console.log('[Message] 使用 Mock 数据:', USE_MOCK_DATA)
85 123
86 // 根据开关选择使用真实 API 或 Mock 数据 124 // 根据开关选择使用真实 API 或 Mock 数据
87 const res = USE_MOCK_DATA 125 const res = USE_MOCK_DATA
88 - ? await mockMessageListAPI({ 126 + ? await mockMessageListAPI(params)
89 - page: page.value, 127 + : await myListAPI(params)
90 - limit: limit.value 128 +
91 - }) 129 + if (res.code === 1 && res.data) {
92 - : await myListAPI({ 130 + console.log('[Message] 数据:', res.data)
93 - page: page.value, 131 +
94 - limit: limit.value 132 + // 处理列表数据
95 - }) 133 + if (res.data.list?.length) {
96 - 134 + const listData = res.data.list
97 - if (res.code === 1) { 135 +
98 - const list = res.data?.list || [] 136 + if (isLoadMore) {
99 - 137 + // 加载更多:追加数据
100 - if (refresh) { 138 + currentList.value = [...currentList.value, ...listData]
101 - messageList.value = list 139 + } else {
140 + // 首次加载或刷新:替换数据
141 + currentList.value = listData
142 + }
143 +
144 + // 判断是否还有更多数据
145 + // 如果返回的数据量少于请求的量,说明没有更多了
146 + hasMore.value = listData.length >= params.limit
102 } else { 147 } else {
103 - messageList.value = [...messageList.value, ...list] 148 + // 没有数据了
104 - } 149 + if (isLoadMore) {
105 - 150 + hasMore.value = false
106 - if (list.length < limit.value) { 151 + } else {
107 - hasMore.value = false 152 + currentList.value = []
108 - } else { 153 + }
109 - page.value++
110 } 154 }
155 + } else {
156 + console.error('[Message] API 返回错误:', res.msg)
111 } 157 }
112 - } catch (err) { 158 + } catch (error) {
113 - console.error('获取消息列表失败:', err) 159 + console.error('[Message] 获取消息列表失败:', error)
114 } finally { 160 } finally {
115 - loading.value = false 161 + if (isLoadMore) {
116 - if (refresh) { 162 + loadingMore.value = false
117 - stopPullDownRefresh() 163 + } else {
164 + loading.value = false
118 } 165 }
119 } 166 }
120 } 167 }
121 168
122 /** 169 /**
123 - * @description 跳转到详情页 170 + * 页面加载时获取数据
124 - * @param {Object} item 消息对象
125 */ 171 */
126 -const handleItemClick = (item) => { 172 +useLoad(async (options) => {
127 - go('/pages/message-detail/index', { id: item.id }) 173 + console.log('[Message] 页面参数:', options)
128 -}
129 174
130 -// 页面加载 175 + // 重置分页状态
131 -useLoad(() => { 176 + currentPage.value = 1
132 - fetchMessageList(true) 177 + hasMore.value = true
133 -})
134 178
135 -// 下拉刷新 179 + // 获取消息列表
136 -usePullDownRefresh(() => { 180 + await fetchMessageList({ page: 1, limit: pageSize })
137 - fetchMessageList(true)
138 }) 181 })
139 182
140 -// 上拉加载更多 183 +/**
141 -useReachBottom(() => { 184 + * 处理加载更多事件
142 - fetchMessageList() 185 + *
143 -}) 186 + * @param {number} page - 下一页页码
187 + * @returns {Promise<void>}
188 + */
189 +const handleLoadMore = async (page) => {
190 + console.log('[Message] 加载更多,页码:', page)
191 +
192 + // 更新页码
193 + currentPage.value = page
194 +
195 + // 加载下一页数据
196 + await fetchMessageList(
197 + { page: page, limit: pageSize },
198 + true // 标记为加载更多
199 + )
200 +}
201 +
202 +/**
203 + * 处理下拉刷新事件
204 + */
205 +const handleRefresh = async () => {
206 + console.log('[Message] 下拉刷新')
207 +
208 + // 重置分页状态
209 + currentPage.value = 1
210 + hasMore.value = true
211 +
212 + // 刷新数据
213 + await fetchMessageList({ page: 1, limit: pageSize })
214 +}
215 +
216 +/**
217 + * 跳转到详情页
218 + *
219 + * @param {Object} item - 消息对象
220 + */
221 +const handleItemClick = (item) => {
222 + go('/pages/message-detail/index', { id: item.id })
223 +}
144 </script> 224 </script>
145 225
146 <style lang="less"> 226 <style lang="less">
147 -/* Scoped styles if needed */ 227 +/* LoadMoreList 组件已内置样式,此处无需额外样式 */
148 </style> 228 </style>
......
1 <!-- 1 <!--
2 - * @Date: 2026-01-31 2 + * @Date: 2026-02-08
3 - * @Description: 产品中心 - API 接口集成版本(含搜索功能) 3 + * @Description: 产品中心 - 使用 LoadMoreList 组件重构版本
4 --> 4 -->
5 <template> 5 <template>
6 <view class="bg-[#F9FAFB]"> 6 <view class="bg-[#F9FAFB]">
7 - <!-- 固定在顶部的导航和搜索 --> 7 + <!-- 计划书弹窗 -->
8 - <view class="bg-[#F9FAFB] sticky top-0 z-10"> 8 + <view v-if="showPlanPopup && selectedProduct">
9 - <NavHeader title="产品中心" /> 9 + <PlanFormContainer
10 - 10 + v-model:visible="showPlanPopup"
11 - <!-- Search Bar --> 11 + :product="selectedProduct"
12 - <view class="px-[24rpx] py-[16rpx] bg-white"> 12 + @close="showPlanPopup = false"
13 - <SearchBar 13 + @submit="handlePlanSubmit"
14 - v-model="searchValue" 14 + />
15 - placeholder="搜索产品名称..."
16 - variant="rounded"
17 - :show-clear="true"
18 - @search="onSearch"
19 - @input="onSearchInput"
20 - @clear="onClear"
21 - />
22 - </view>
23 -
24 - <!-- Tabs Container -->
25 - <view class="bg-white mt-[2rpx]">
26 - <nut-tabs v-model="activeTabId">
27 - <!-- 自定义标签栏 -->
28 - <template #titles>
29 - <view class="filter-tabs-wrapper">
30 - <view
31 - v-for="item in tabsData"
32 - :key="item.id"
33 - :class="[
34 - 'filter-tab-item',
35 - activeTabId === item.id ? 'filter-tab-active' : 'filter-tab-inactive'
36 - ]"
37 - @tap="onTabClick(item.id)"
38 - >
39 - <text class="filter-tab-text">{{ item.name }}</text>
40 - </view>
41 - </view>
42 - </template>
43 - </nut-tabs>
44 - </view>
45 </view> 15 </view>
46 16
47 - <!-- 列表容器 - 页面级滚动 --> 17 + <LoadMoreList
48 - <view class="pb-[calc(160rpx+env(safe-area-inset-bottom))]"> 18 + :list="currentList"
49 - <!-- 加载状态 --> 19 + :page="currentPage"
50 - <view v-if="loading && products.length === 0" class="flex justify-center items-center py-[100rpx]"> 20 + :page-size="pageSize"
51 - <text class="text-gray-400 text-[28rpx]">加载中...</text> 21 + :has-more="hasMore"
52 - </view> 22 + :loading="loading"
53 - 23 + :loading-more="loadingMore"
54 - <!-- Product List --> 24 + key-field="id"
55 - <view v-else class="px-[40rpx]"> 25 + @load-more="handleLoadMore"
56 - <!-- Card Item --> 26 + >
57 - <view v-for="(item, index) in products" :key="item.id" 27 + <!-- 头部:导航 + 搜索 + Tabs -->
58 - class="bg-white rounded-[24rpx] overflow-hidden mb-[24rpx] shadow-sm active:scale-[0.98] transition-transform duration-200" 28 + <template #header>
59 - :style="{ animationDelay: `${index * 50}ms` }" 29 + <view class="sticky top-0 z-10 bg-[#F9FAFB]">
30 + <NavHeader title="产品中心" />
31 +
32 + <!-- Search Bar -->
33 + <view class="px-[24rpx] py-[16rpx] bg-white">
34 + <SearchBar
35 + v-model="searchValue"
36 + placeholder="搜索产品名称..."
37 + variant="rounded"
38 + :show-clear="true"
39 + @search="onSearch"
40 + @input="onSearchInput"
41 + @clear="onClear"
42 + />
43 + </view>
44 +
45 + <!-- Tabs Container -->
46 + <view class="bg-white mt-[2rpx]">
47 + <nut-tabs v-model="activeTabId">
48 + <!-- 自定义标签栏 -->
49 + <template #titles>
50 + <view class="filter-tabs-wrapper">
51 + <view
52 + v-for="item in tabsData"
53 + :key="item.id"
54 + :class="[
55 + 'filter-tab-item',
56 + activeTabId === item.id ? 'filter-tab-active' : 'filter-tab-inactive'
57 + ]"
58 + @tap="onTabClick(item.id)"
59 + >
60 + <text class="filter-tab-text">{{ item.name }}</text>
61 + </view>
62 + </view>
63 + </template>
64 + </nut-tabs>
65 + </view>
66 + </view>
67 + </template>
68 +
69 + <!-- 列表项:产品卡片 -->
70 + <template #item="{ item }">
71 + <view
72 + class="bg-white rounded-[24rpx] overflow-hidden shadow-sm active:scale-[0.98] transition-transform duration-200 product-card"
73 + @tap="handleProductClick(item)"
60 > 74 >
61 <!-- Product Content (Horizontal Layout) --> 75 <!-- Product Content (Horizontal Layout) -->
62 - <view class="flex gap-[24rpx]" @tap="handleProductClick(item)"> 76 + <view class="flex gap-[24rpx]">
63 <!-- Image Container --> 77 <!-- Image Container -->
64 - <view class="relative w-[220rpx] h-[220rpx] flex-shrink-0 product-card-item"> 78 + <view class="relative w-[220rpx] h-[220rpx] flex-shrink-0">
65 <image :src="item.cover_image" class="w-full h-full object-cover bg-gray-100" mode="aspectFill" /> 79 <image :src="item.cover_image" class="w-full h-full object-cover bg-gray-100" mode="aspectFill" />
66 <!-- Tag --> 80 <!-- Tag -->
67 <view v-if="item.recommend === 'hot'" 81 <view v-if="item.recommend === 'hot'"
...@@ -113,68 +127,74 @@ ...@@ -113,68 +127,74 @@
113 </view> 127 </view>
114 </view> 128 </view>
115 </view> 129 </view>
116 - </view> 130 + </template>
117 -
118 - <!-- 加载更多状态 -->
119 - <view v-if="loading && products.length > 0" class="flex justify-center items-center py-[40rpx]">
120 - <text class="text-gray-400 text-[28rpx]">加载中...</text>
121 - </view>
122 -
123 - <!-- 没有更多数据 -->
124 - <view v-if="!hasMore && products.length > 0" class="flex justify-center items-center py-[40rpx]">
125 - <text class="text-gray-400 text-[24rpx]">没有更多了</text>
126 - </view>
127 131
128 <!-- 空状态 --> 132 <!-- 空状态 -->
129 - <view v-if="!loading && products.length === 0"> 133 + <template #empty>
130 <nut-empty description="暂无相关产品" image="empty" /> 134 <nut-empty description="暂无相关产品" image="empty" />
131 - </view> 135 + </template>
132 - </view> 136 + </LoadMoreList>
133 -
134 - <!-- 计划书表单容器 -->
135 - <!-- 测试数据:后端接口和字段还没有准备好,使用 PlanFormContainer 进行的前端测试 -->
136 - <!-- 使用 v-if 条件渲染,避免 selectedProduct 为 null 时的 prop 类型检查错误 -->
137 - <view v-if="showPlanPopup && selectedProduct">
138 - <PlanFormContainer
139 - v-model:visible="showPlanPopup"
140 - :product="selectedProduct"
141 - @close="showPlanPopup = false"
142 - @submit="handlePlanSubmit"
143 - />
144 - </view>
145 </view> 137 </view>
146 </template> 138 </template>
147 139
148 <script setup> 140 <script setup>
149 import { ref, computed } from 'vue' 141 import { ref, computed } from 'vue'
150 -import Taro, { useLoad, useReachBottom } from '@tarojs/taro' 142 +import Taro, { useLoad } from '@tarojs/taro'
143 +import { useGo } from '@/hooks/useGo'
144 +import { useListItemClick, ListType } from '@/composables/useListItemClick'
145 +import LoadMoreList from '@/components/LoadMoreList'
151 import NavHeader from '@/components/NavHeader.vue' 146 import NavHeader from '@/components/NavHeader.vue'
152 import SearchBar from '@/components/SearchBar.vue' 147 import SearchBar from '@/components/SearchBar.vue'
153 import PlanFormContainer from '@/components/PlanFormContainer.vue' 148 import PlanFormContainer from '@/components/PlanFormContainer.vue'
154 -import { useListItemClick, ListType } from '@/composables/useListItemClick'
155 import { listAPI } from '@/api/get_product' 149 import { listAPI } from '@/api/get_product'
156 import { mockProductListAPI } from '@/utils/mockData' 150 import { mockProductListAPI } from '@/utils/mockData'
157 151
158 // ⚠️ MOCK 数据开关 - 开发环境使用 mock 数据,生产环境使用真实 API 152 // ⚠️ MOCK 数据开关 - 开发环境使用 mock 数据,生产环境使用真实 API
159 const USE_MOCK_DATA = process.env.NODE_ENV === 'development' 153 const USE_MOCK_DATA = process.env.NODE_ENV === 'development'
160 154
161 -const activeTabId = ref('') 155 +const go = useGo()
162 156
163 -// 搜索状态 157 +/**
164 -const searchValue = ref('') 158 + * 当前列表数据
165 -// 搜索防抖定时器 159 + * @type {Ref<Array<any>>}
166 -let searchTimer = null 160 + */
161 +const currentList = ref([])
167 162
168 -// 分页状态 163 +/**
169 -const page = ref(0) 164 + * 当前页码(从0开始)
170 -const limit = ref(10) 165 + * @type {Ref<number>}
171 -const loading = ref(false) 166 + */
167 +const currentPage = ref(0)
168 +
169 +/**
170 + * 每页数量
171 + * @type {number}
172 + */
173 +const pageSize = 10
174 +
175 +/**
176 + * 是否还有更多数据
177 + * @type {Ref<boolean>}
178 + */
172 const hasMore = ref(true) 179 const hasMore = ref(true)
173 180
174 -// 数据状态 181 +/**
182 + * 首次加载状态
183 + * @type {Ref<boolean>}
184 + */
185 +const loading = ref(false)
186 +
187 +/**
188 + * 加载更多状态
189 + * @type {Ref<boolean>}
190 + */
191 +const loadingMore = ref(false)
192 +
193 +// 搜索和 Tabs 相关状态
194 +const activeTabId = ref('')
195 +const searchValue = ref('')
175 const categories = ref([]) // 从接口获取的分类列表 196 const categories = ref([]) // 从接口获取的分类列表
176 -const products = ref([]) // 当前产品列表 197 +let searchTimer = null // 搜索防抖定时器
177 -const total = ref(0) // 产品总数
178 198
179 // 计划书弹窗状态 199 // 计划书弹窗状态
180 const showPlanPopup = ref(false) 200 const showPlanPopup = ref(false)
...@@ -195,29 +215,23 @@ const tabsData = computed(() => { ...@@ -195,29 +215,23 @@ const tabsData = computed(() => {
195 215
196 /** 216 /**
197 * 获取产品列表 217 * 获取产品列表
198 - * @description 根据 activeTabId 和 searchValue 获取对应分类的产品列表 218 + *
219 + * @param {Object} params - 请求参数
220 + * @param {number} params.page - 页码(从0开始)
221 + * @param {number} params.limit - 每页数量
222 + * @param {boolean} isLoadMore - 是否为加载更多
223 + * @returns {Promise<void>}
199 */ 224 */
200 -const fetchProducts = async (isLoadMore = false) => { 225 +const fetchProducts = async (params = {}, isLoadMore = false) => {
201 - if (loading.value) return
202 -
203 - loading.value = true
204 -
205 try { 226 try {
206 - const params = { 227 + // 如果是加载更多,使用 loadingMore 状态,否则使用 loading 状态
207 - page: String(page.value), 228 + if (isLoadMore) {
208 - limit: String(limit.value) 229 + loadingMore.value = true
209 - } 230 + } else {
210 - 231 + loading.value = true
211 - // 如果不是"全部"标签,添加分类 ID 参数
212 - if (activeTabId.value !== '') {
213 - params.cid = activeTabId.value
214 - }
215 -
216 - // 添加搜索关键词参数
217 - if (searchValue.value) {
218 - params.keyword = searchValue.value
219 } 232 }
220 233
234 + console.log('[Product Center] 请求参数:', params)
221 console.log('[Product Center] 使用 Mock 数据:', USE_MOCK_DATA) 235 console.log('[Product Center] 使用 Mock 数据:', USE_MOCK_DATA)
222 236
223 // 根据开关选择使用真实 API 或 Mock 数据 237 // 根据开关选择使用真实 API 或 Mock 数据
...@@ -226,65 +240,139 @@ const fetchProducts = async (isLoadMore = false) => { ...@@ -226,65 +240,139 @@ const fetchProducts = async (isLoadMore = false) => {
226 : await listAPI(params) 240 : await listAPI(params)
227 241
228 if (res.code === 1 && res.data) { 242 if (res.code === 1 && res.data) {
243 + console.log('[Product Center] 数据:', res.data)
244 +
229 // 更新分类列表(首次加载时) 245 // 更新分类列表(首次加载时)
230 if (!isLoadMore && res.data.categories) { 246 if (!isLoadMore && res.data.categories) {
231 categories.value = res.data.categories 247 categories.value = res.data.categories
232 } 248 }
233 249
234 // 处理产品列表 250 // 处理产品列表
235 - if (isLoadMore) { 251 + if (res.data.list?.length) {
236 - // 加载更多:追加数据 252 + const listData = res.data.list
237 - products.value = [...products.value, ...res.data.list] 253 +
254 + if (isLoadMore) {
255 + // 加载更多:追加数据
256 + currentList.value = [...currentList.value, ...listData]
257 + } else {
258 + // 首次加载或切换分类:替换数据
259 + currentList.value = listData
260 + }
261 +
262 + // 判断是否还有更多数据
263 + // 如果返回的数据量少于请求的量,说明没有更多了
264 + hasMore.value = listData.length >= params.limit
238 } else { 265 } else {
239 - // 首次加载或切换分类:替换数据 266 + // 没有数据了
240 - products.value = res.data.list || [] 267 + if (isLoadMore) {
268 + hasMore.value = false
269 + } else {
270 + currentList.value = []
271 + }
241 } 272 }
242 -
243 - // 更新总数和分页状态
244 - total.value = res.data.total || 0
245 - hasMore.value = products.value.length < total.value
246 } else { 273 } else {
247 - Taro.showToast({ 274 + console.error('[Product Center] API 返回错误:', res.msg)
248 - title: res.msg || '获取产品列表失败',
249 - icon: 'none'
250 - })
251 } 275 }
252 - } catch (err) { 276 + } catch (error) {
253 - console.error('获取产品列表失败:', err) 277 + console.error('[Product Center] 获取产品列表失败:', error)
254 - Taro.showToast({
255 - title: '网络错误,请重试',
256 - icon: 'none'
257 - })
258 } finally { 278 } finally {
259 - loading.value = false 279 + if (isLoadMore) {
280 + loadingMore.value = false
281 + } else {
282 + loading.value = false
283 + }
260 } 284 }
261 } 285 }
262 286
263 /** 287 /**
288 + * 页面加载时获取数据
289 + */
290 +useLoad(async (options) => {
291 + console.log('[Product Center] 页面参数:', options)
292 +
293 + // 重置分页状态
294 + currentPage.value = 0
295 + hasMore.value = true
296 +
297 + // 获取产品列表
298 + await fetchProducts({ page: 0, limit: pageSize })
299 +})
300 +
301 +/**
302 + * 处理加载更多事件
303 + *
304 + * @param {number} page - 下一页页码
305 + * @returns {Promise<void>}
306 + */
307 +const handleLoadMore = async (page) => {
308 + console.log('[Product Center] 加载更多,页码:', page)
309 +
310 + // 更新页码
311 + currentPage.value = page
312 +
313 + // 构建请求参数
314 + const params = {
315 + page: page,
316 + limit: pageSize
317 + }
318 +
319 + // 如果不是"全部"标签,添加分类 ID 参数
320 + if (activeTabId.value !== '') {
321 + params.cid = activeTabId.value
322 + }
323 +
324 + // 添加搜索关键词参数
325 + if (searchValue.value) {
326 + params.keyword = searchValue.value
327 + }
328 +
329 + // 加载下一页数据
330 + await fetchProducts(params, true)
331 +}
332 +
333 +/**
264 * Tab 点击处理 334 * Tab 点击处理
265 - * @description 切换分类,重置分页并重新加载数据 335 + *
336 + * @param {string} id - 分类 ID
266 */ 337 */
267 const onTabClick = (id) => { 338 const onTabClick = (id) => {
268 if (activeTabId.value === id) return 339 if (activeTabId.value === id) return
269 340
341 + console.log('[Product Center] 切换分类:', id)
342 +
270 activeTabId.value = id 343 activeTabId.value = id
271 344
272 // 重置分页状态 345 // 重置分页状态
273 - page.value = 0 346 + currentPage.value = 0
274 - products.value = []
275 hasMore.value = true 347 hasMore.value = true
276 348
349 + // 构建请求参数
350 + const params = {
351 + page: 0,
352 + limit: pageSize
353 + }
354 +
355 + // 如果不是"全部"标签,添加分类 ID 参数
356 + if (id !== '') {
357 + params.cid = id
358 + }
359 +
360 + // 添加搜索关键词参数
361 + if (searchValue.value) {
362 + params.keyword = searchValue.value
363 + }
364 +
277 // 重新加载数据(保持搜索状态) 365 // 重新加载数据(保持搜索状态)
278 - fetchProducts(false) 366 + fetchProducts(params, false)
279 } 367 }
280 368
281 /** 369 /**
282 * 搜索输入处理(带防抖) 370 * 搜索输入处理(带防抖)
283 - * @description 用户输入时实时搜索,使用防抖优化性能 371 + *
284 * @param {string} value - 搜索关键词 372 * @param {string} value - 搜索关键词
285 */ 373 */
286 const onSearchInput = (value) => { 374 const onSearchInput = (value) => {
287 - console.log('搜索输入:', value) 375 + console.log('[Product Center] 搜索输入:', value)
288 376
289 // 清除之前的定时器 377 // 清除之前的定时器
290 if (searchTimer) { 378 if (searchTimer) {
...@@ -294,22 +382,37 @@ const onSearchInput = (value) => { ...@@ -294,22 +382,37 @@ const onSearchInput = (value) => {
294 // 设置新的定时器(500ms 后执行搜索) 382 // 设置新的定时器(500ms 后执行搜索)
295 searchTimer = setTimeout(() => { 383 searchTimer = setTimeout(() => {
296 // 重置分页状态 384 // 重置分页状态
297 - page.value = 0 385 + currentPage.value = 0
298 - products.value = []
299 hasMore.value = true 386 hasMore.value = true
300 387
388 + // 构建请求参数
389 + const params = {
390 + page: 0,
391 + limit: pageSize
392 + }
393 +
394 + // 如果不是"全部"标签,添加分类 ID 参数
395 + if (activeTabId.value !== '') {
396 + params.cid = activeTabId.value
397 + }
398 +
399 + // 添加搜索关键词参数
400 + if (value) {
401 + params.keyword = value
402 + }
403 +
301 // 重新加载数据 404 // 重新加载数据
302 - fetchProducts(false) 405 + fetchProducts(params, false)
303 }, 500) 406 }, 500)
304 } 407 }
305 408
306 /** 409 /**
307 * 搜索处理(回车键) 410 * 搜索处理(回车键)
308 - * @description 用户按下回车或点击搜索按钮时触发 411 + *
309 * @param {string} value - 搜索关键词 412 * @param {string} value - 搜索关键词
310 */ 413 */
311 const onSearch = (value) => { 414 const onSearch = (value) => {
312 - console.log('搜索产品:', value) 415 + console.log('[Product Center] 搜索产品:', value)
313 416
314 // 清除防抖定时器 417 // 清除防抖定时器
315 if (searchTimer) { 418 if (searchTimer) {
...@@ -318,20 +421,34 @@ const onSearch = (value) => { ...@@ -318,20 +421,34 @@ const onSearch = (value) => {
318 } 421 }
319 422
320 // 重置分页状态 423 // 重置分页状态
321 - page.value = 0 424 + currentPage.value = 0
322 - products.value = []
323 hasMore.value = true 425 hasMore.value = true
324 426
427 + // 构建请求参数
428 + const params = {
429 + page: 0,
430 + limit: pageSize
431 + }
432 +
433 + // 如果不是"全部"标签,添加分类 ID 参数
434 + if (activeTabId.value !== '') {
435 + params.cid = activeTabId.value
436 + }
437 +
438 + // 添加搜索关键词参数
439 + if (value) {
440 + params.keyword = value
441 + }
442 +
325 // 重新加载数据 443 // 重新加载数据
326 - fetchProducts(false) 444 + fetchProducts(params, false)
327 } 445 }
328 446
329 /** 447 /**
330 * 清空搜索 448 * 清空搜索
331 - * @description 用户点击清除按钮时触发
332 */ 449 */
333 const onClear = () => { 450 const onClear = () => {
334 - console.log('清空搜索') 451 + console.log('[Product Center] 清空搜索')
335 452
336 // 清除防抖定时器 453 // 清除防抖定时器
337 if (searchTimer) { 454 if (searchTimer) {
...@@ -340,12 +457,22 @@ const onClear = () => { ...@@ -340,12 +457,22 @@ const onClear = () => {
340 } 457 }
341 458
342 // 重置分页状态 459 // 重置分页状态
343 - page.value = 0 460 + currentPage.value = 0
344 - products.value = []
345 hasMore.value = true 461 hasMore.value = true
346 462
463 + // 构建请求参数
464 + const params = {
465 + page: 0,
466 + limit: pageSize
467 + }
468 +
469 + // 如果不是"全部"标签,添加分类 ID 参数
470 + if (activeTabId.value !== '') {
471 + params.cid = activeTabId.value
472 + }
473 +
347 // 重新加载数据 474 // 重新加载数据
348 - fetchProducts(false) 475 + fetchProducts(params, false)
349 } 476 }
350 477
351 /** 478 /**
...@@ -355,13 +482,13 @@ const onClear = () => { ...@@ -355,13 +482,13 @@ const onClear = () => {
355 const { handleClick: handleProductClick } = useListItemClick({ 482 const { handleClick: handleProductClick } = useListItemClick({
356 listType: ListType.PRODUCT, 483 listType: ListType.PRODUCT,
357 onAfterClick: (item) => { 484 onAfterClick: (item) => {
358 - console.log('用户查看了产品:', item.product_name) 485 + console.log('[Product Center] 用户查看了产品:', item.product_name)
359 } 486 }
360 }) 487 })
361 488
362 /** 489 /**
363 * 打开计划书弹窗 490 * 打开计划书弹窗
364 - * @description 根据产品对象打开计划书表单 491 + *
365 * @param {Object} product - 产品对象 492 * @param {Object} product - 产品对象
366 */ 493 */
367 const openPlanPopup = (product) => { 494 const openPlanPopup = (product) => {
...@@ -371,12 +498,11 @@ const openPlanPopup = (product) => { ...@@ -371,12 +498,11 @@ const openPlanPopup = (product) => {
371 498
372 /** 499 /**
373 * 处理计划书提交 500 * 处理计划书提交
374 - * @description 测试环境:前端不调用后端API,直接跳转到结果页 501 + *
375 - * 生产环境:需要调用 submitPlanAPI 提交表单数据
376 * @param {Object} formData - 表单数据 502 * @param {Object} formData - 表单数据
377 */ 503 */
378 const handlePlanSubmit = (formData) => { 504 const handlePlanSubmit = (formData) => {
379 - console.log('计划书提交:', { 505 + console.log('[Product Center] 计划书提交:', {
380 product_id: selectedProduct.value.id, 506 product_id: selectedProduct.value.id,
381 product_name: selectedProduct.value.product_name, 507 product_name: selectedProduct.value.product_name,
382 form_sn: selectedProduct.value.form_sn, 508 form_sn: selectedProduct.value.form_sn,
...@@ -388,59 +514,15 @@ const handlePlanSubmit = (formData) => { ...@@ -388,59 +514,15 @@ const handlePlanSubmit = (formData) => {
388 514
389 // TODO: 后端接口还没有准备好,暂时不调用API 515 // TODO: 后端接口还没有准备好,暂时不调用API
390 // 测试完成后需要对接 submitPlanAPI 516 // 测试完成后需要对接 submitPlanAPI
391 - // const res = await submitPlanAPI({
392 - // product_id: selectedProduct.value.id,
393 - // template: selectedProduct.value.form_sn,
394 - // form_data: formData
395 - // })
396 517
397 // 模拟提交成功,跳转到结果页面 518 // 模拟提交成功,跳转到结果页面
398 Taro.navigateTo({ 519 Taro.navigateTo({
399 url: '/pages/plan-submit-result/index?success=true' 520 url: '/pages/plan-submit-result/index?success=true'
400 }) 521 })
401 } 522 }
402 -
403 -/**
404 - * 页面加载时获取数据
405 - */
406 -useLoad(() => {
407 - fetchProducts(false)
408 -})
409 -
410 -/**
411 - * 触底加载更多
412 - * @description 使用 Taro 的 useReachBottom hook 监听页面滚动到底部
413 - */
414 -useReachBottom(() => {
415 - console.log('滚动到底部,加载更多')
416 -
417 - if (!hasMore.value || loading.value) {
418 - console.log('没有更多数据或正在加载中,跳过')
419 - return
420 - }
421 -
422 - page.value += 1
423 - fetchProducts(true)
424 -})
425 </script> 523 </script>
426 524
427 <style lang="less"> 525 <style lang="less">
428 -@keyframes slideIn {
429 - from {
430 - opacity: 0;
431 - transform: translateY(20rpx);
432 - }
433 -
434 - to {
435 - opacity: 1;
436 - transform: translateY(0);
437 - }
438 -}
439 -
440 -.product-card-item {
441 - animation: slideIn 0.5s cubic-bezier(0.2, 0.8, 0.2, 1) backwards;
442 -}
443 -
444 // FilterTabs 风格的标签栏 526 // FilterTabs 风格的标签栏
445 .filter-tabs-wrapper { 527 .filter-tabs-wrapper {
446 display: flex; 528 display: flex;
......
1 <!-- 1 <!--
2 - * @Date: 2026-02-06 2 + * @Date: 2026-02-08
3 - * @Description: 搜索页面 - 支持产品和资料搜索,实时查询API 3 + * @Description: 搜索页面 - 使用 LoadMoreList 组件重构版本
4 + * @description 支持产品和资料搜索,实时查询API,自动切换分类
4 --> 5 -->
5 <template> 6 <template>
6 <view class="bg-[#FFF]"> 7 <view class="bg-[#FFF]">
7 - <!-- 固定顶部:导航栏 + 搜索栏 + Tabs + 结果计数 --> 8 + <LoadMoreList
8 - <view class="bg-[#FFF] sticky top-0 z-10"> 9 + :list="currentList"
9 - <NavHeader title="搜索" /> 10 + :page="currentPage"
10 - 11 + :page-size="pageSize"
11 - <!-- Search Input --> 12 + :has-more="hasMore"
12 - <view class="px-[40rpx] mt-[32rpx]"> 13 + :loading="loading"
13 - <SearchBar 14 + :loading-more="loadingMore"
14 - v-model="searchKeyword" 15 + :show-header="true"
15 - placeholder="搜索培训资料、案例、产品..." 16 + key-field="id"
16 - variant="rounded" 17 + @load-more="handleLoadMore"
17 - :show-border="true" 18 + >
18 - :show-clear="true" 19 + <!-- 固定顶部:导航栏 + 搜索栏 + Tabs + 结果计数 -->
19 - @search="handleSearch" 20 + <template #header>
20 - @clear="clearSearch" 21 + <view class="bg-[#FFF] sticky top-0 z-10">
21 - /> 22 + <NavHeader title="搜索" />
22 - </view> 23 +
23 - 24 + <!-- Search Input -->
24 - <!-- Tabs Container --> 25 + <view class="px-[40rpx] mt-[32rpx]">
25 - <nut-tabs v-model="activeTab"> 26 + <SearchBar
26 - <!-- 自定义标签栏 --> 27 + v-model="searchKeyword"
27 - <template #titles> 28 + placeholder="搜索培训资料、案例、产品..."
28 - <view class="filter-tabs-wrapper"> 29 + variant="rounded"
29 - <view 30 + :show-border="true"
30 - v-for="item in tabsData" 31 + :show-clear="true"
31 - :key="item.id" 32 + @search="handleSearch"
32 - :class="[ 33 + @clear="clearSearch"
33 - 'filter-tab-item', 34 + />
34 - activeTab === item.id ? 'filter-tab-active' : 'filter-tab-inactive', 35 + </view>
35 - !activeTab ? 'filter-tab-inactive' : '' // 初始状态不高亮任何tab 36 +
36 - ]" 37 + <!-- Tabs Container -->
37 - @tap="onTabClick(item.id)" 38 + <nut-tabs v-model="activeTab">
38 - > 39 + <!-- 自定义标签栏 -->
39 - <text class="filter-tab-text">{{ item.name }}</text> 40 + <template #titles>
40 - </view> 41 + <view class="filter-tabs-wrapper">
42 + <view
43 + v-for="item in tabsData"
44 + :key="item.id"
45 + :class="[
46 + 'filter-tab-item',
47 + activeTab === item.id ? 'filter-tab-active' : 'filter-tab-inactive',
48 + !activeTab ? 'filter-tab-inactive' : '' // 初始状态不高亮任何tab
49 + ]"
50 + @tap="onTabClick(item.id)"
51 + >
52 + <text class="filter-tab-text">{{ item.name }}</text>
53 + </view>
54 + </view>
55 + </template>
56 + </nut-tabs>
57 +
58 + <!-- Result Count -->
59 + <view v-if="currentList.length > 0" class="px-[60rpx] text-[#6B7280] text-[24rpx] pb-[24rpx]">
60 + 找到 {{ currentTotal }} 个相关结果
41 </view> 61 </view>
42 - </template>
43 - </nut-tabs>
44 -
45 - <!-- Result Count -->
46 - <view v-if="currentList.length > 0" class="px-[60rpx] text-[#6B7280] text-[24rpx] pb-[24rpx]">
47 - 找到 {{ currentTotal }} 个相关结果
48 - </view>
49 - </view>
50 -
51 - <!-- 列表容器 -->
52 - <view class="px-[40rpx] pb-[calc(160rpx+env(safe-area-inset-bottom))]">
53 -
54 - <!-- Search Results -->
55 - <view
56 - v-if="currentList.length > 0"
57 - :key="listRenderKey"
58 - >
59 - <!-- Product Results -->
60 - <view v-if="activeTab === 'product'" class="flex flex-col gap-[24rpx] pb-[40rpx]">
61 - <ProductCard
62 - v-for="(item, index) in currentList"
63 - :key="index"
64 - :product-id="item.id"
65 - :product-name="item.product_name || item.name"
66 - :tags="item.tags || []"
67 - class="search-result-item"
68 - :style="{ animationDelay: `${index * 30}ms` }"
69 - @detail="goToProductDetail"
70 - @plan="openPlanPopup"
71 - />
72 </view> 62 </view>
63 + </template>
64 +
65 + <!-- 列表项:根据 activeTab 动态渲染 -->
66 + <template #item="{ item }">
67 + <!-- Product Results -->
68 + <ProductCard
69 + v-if="activeTab === 'product'"
70 + :product-id="item.id"
71 + :product-name="item.product_name || item.name"
72 + :tags="item.tags || []"
73 + class="search-result-item"
74 + @detail="goToProductDetail"
75 + @plan="openPlanPopup"
76 + />
73 77
74 <!-- File Results --> 78 <!-- File Results -->
75 - <view v-else-if="activeTab === 'file'" class="flex flex-col gap-[24rpx] pb-[40rpx]"> 79 + <MaterialCard
76 - <MaterialCard 80 + v-else-if="activeTab === 'file'"
77 - v-for="(item, index) in currentList" 81 + :id="item.id"
78 - :key="index" 82 + :title="item.title"
79 - :id="item.id" 83 + :file-name="item.fileName"
80 - :title="item.title" 84 + :file-size="item.fileSize"
81 - :file-name="item.fileName" 85 + :learners="item.learners"
82 - :file-size="item.fileSize" 86 + :read-people-percent="item.readPeoplePercent"
83 - :learners="item.learners" 87 + :collected="item.collected"
84 - :read-people-percent="item.readPeoplePercent" 88 + :extension="item.extension"
85 - :collected="item.collected" 89 + :download-url="item.downloadUrl"
86 - :extension="item.extension" 90 + class="search-result-item"
87 - :download-url="item.downloadUrl" 91 + @collect-changed="handleCollectChanged(item, $event)"
88 - class="search-result-item" 92 + />
89 - :style="{ animationDelay: `${index * 30}ms` }" 93 + </template>
90 - @collect-changed="handleCollectChanged(item, $event)" 94 +
91 - /> 95 + <!-- 自定义空状态:处理三种状态 -->
96 + <template #empty>
97 + <!-- Initial State (从未搜索过) -->
98 + <view v-if="!hasSearched" class="flex flex-col items-center justify-center py-[120rpx]">
99 + <IconFont name="search" class="text-gray-300 mb-[24rpx]" size="64" />
100 + <view class="text-[#6B7280] text-[28rpx]">搜索产品或资料</view>
101 + <view class="text-[#9CA3AF] text-[24rpx] mt-[12rpx]">输入关键词开始搜索,自动切换分类</view>
92 </view> 102 </view>
93 103
94 - <!-- 加载更多提示 --> 104 + <!-- Empty State (已搜索但无结果) -->
95 - <view v-if="currentList.length > 0" class="flex items-center justify-center py-[40rpx]"> 105 + <view v-else class="flex flex-col items-center justify-center py-[40rpx]">
96 - <view v-if="loadingMore" class="flex items-center"> 106 + <nut-empty description="暂无搜索结果" image="empty">
97 - <view class="loading-spinner-small"></view> 107 + <view class="text-[#9CA3AF] text-[24rpx] mt-[12rpx]">试试其他关键词吧</view>
98 - <text class="ml-[12rpx] text-[#9CA3AF] text-[24rpx]">加载中...</text> 108 + </nut-empty>
99 - </view>
100 - <view v-else-if="!hasMore" class="text-[#9CA3AF] text-[24rpx]">
101 - 没有更多了
102 - </view>
103 </view> 109 </view>
104 - </view> 110 + </template>
105 - 111 + </LoadMoreList>
106 - <!-- Empty State (已搜索但无结果) -->
107 - <view v-else-if="hasSearched && currentList.length === 0" class="flex flex-col items-center justify-center py-[40rpx]">
108 - <nut-empty description="暂无搜索结果" image="empty">
109 - <view class="text-[#9CA3AF] text-[24rpx] mt-[12rpx]">试试其他关键词吧</view>
110 - </nut-empty>
111 - </view>
112 -
113 - <!-- Initial State (从未搜索过) -->
114 - <view v-else class="flex flex-col items-center justify-center py-[120rpx]">
115 - <IconFont name="search" class="text-gray-300 mb-[24rpx]" size="64" />
116 - <view class="text-[#6B7280] text-[28rpx]">搜索产品或资料</view>
117 - <view class="text-[#9CA3AF] text-[24rpx] mt-[12rpx]">输入关键词开始搜索,自动切换分类</view>
118 - </view>
119 - </view>
120 112
121 <!-- Plan Form Container --> 113 <!-- Plan Form Container -->
122 <!-- 仅当 selectedProduct 不为 null 时才渲染组件,避免 product prop required 警告 --> 114 <!-- 仅当 selectedProduct 不为 null 时才渲染组件,避免 product prop required 警告 -->
...@@ -132,8 +124,9 @@ ...@@ -132,8 +124,9 @@
132 124
133 <script setup> 125 <script setup>
134 import { ref, computed } from 'vue' 126 import { ref, computed } from 'vue'
135 -import Taro, { useReachBottom } from '@tarojs/taro' 127 +import Taro from '@tarojs/taro'
136 import { useGo } from '@/hooks/useGo' 128 import { useGo } from '@/hooks/useGo'
129 +import LoadMoreList from '@/components/LoadMoreList'
137 import NavHeader from '@/components/NavHeader.vue' 130 import NavHeader from '@/components/NavHeader.vue'
138 import IconFont from '@/components/IconFont.vue' 131 import IconFont from '@/components/IconFont.vue'
139 import SearchBar from '@/components/SearchBar.vue' 132 import SearchBar from '@/components/SearchBar.vue'
...@@ -146,9 +139,13 @@ import { mockSearchAPI } from '@/utils/mockData' ...@@ -146,9 +139,13 @@ import { mockSearchAPI } from '@/utils/mockData'
146 // ⚠️ MOCK 数据开关 - 开发环境使用 mock 数据,生产环境使用真实 API 139 // ⚠️ MOCK 数据开关 - 开发环境使用 mock 数据,生产环境使用真实 API
147 const USE_MOCK_DATA = process.env.NODE_ENV === 'development' 140 const USE_MOCK_DATA = process.env.NODE_ENV === 'development'
148 141
149 -// Navigation
150 const go = useGo() 142 const go = useGo()
151 143
144 +/**
145 + * 搜索页面状态管理
146 + * @description 支持双类型(产品/资料)搜索,自动切换分类
147 + */
148 +
152 // Plan Popup State 149 // Plan Popup State
153 const showPlanPopup = ref(false) 150 const showPlanPopup = ref(false)
154 const selectedProduct = ref(null) 151 const selectedProduct = ref(null)
...@@ -157,13 +154,15 @@ const selectedProduct = ref(null) ...@@ -157,13 +154,15 @@ const selectedProduct = ref(null)
157 const searchKeyword = ref('') 154 const searchKeyword = ref('')
158 const activeTab = ref('') // 当前选中的 tab(初始为空,不选中任何tab) 155 const activeTab = ref('') // 当前选中的 tab(初始为空,不选中任何tab)
159 const hasSearched = ref(false) // 是否已经搜索过 156 const hasSearched = ref(false) // 是否已经搜索过
160 -const listRenderKey = ref(0)
161 157
162 -// 数据状态 158 +// 数据状态 - 双列表系统
163 const products = ref([]) // 产品列表 159 const products = ref([]) // 产品列表
164 const files = ref([]) // 资料列表 160 const files = ref([]) // 资料列表
165 const productsTotal = ref(0) // 产品总数 161 const productsTotal = ref(0) // 产品总数
166 const filesTotal = ref(0) // 资料总数 162 const filesTotal = ref(0) // 资料总数
163 +
164 +// 分页状态
165 +const loading = ref(false) // 首次加载状态
167 const loadingMore = ref(false) // 加载更多状态 166 const loadingMore = ref(false) // 加载更多状态
168 const hasMore = ref(true) // 是否还有更多数据 167 const hasMore = ref(true) // 是否还有更多数据
169 const currentPage = ref(0) // 当前页码(从0开始) 168 const currentPage = ref(0) // 当前页码(从0开始)
...@@ -179,6 +178,7 @@ const tabsData = ref([ ...@@ -179,6 +178,7 @@ const tabsData = ref([
179 178
180 /** 179 /**
181 * 当前显示的列表 180 * 当前显示的列表
181 + * @description 根据 activeTab 动态返回对应的列表数据
182 */ 182 */
183 const currentList = computed(() => { 183 const currentList = computed(() => {
184 // 如果没有选中任何tab,返回空数组 184 // 如果没有选中任何tab,返回空数组
...@@ -207,11 +207,13 @@ const currentTotal = computed(() => { ...@@ -207,11 +207,13 @@ const currentTotal = computed(() => {
207 207
208 /** 208 /**
209 * 执行搜索 209 * 执行搜索
210 + *
210 * @param {string} keyword - 搜索关键字 211 * @param {string} keyword - 搜索关键字
211 * @param {string} type - 可选,'product' | 'file' | undefined 212 * @param {string} type - 可选,'product' | 'file' | undefined
212 * @param {number} page - 页码(从0开始) 213 * @param {number} page - 页码(从0开始)
213 * @param {number} limit - 每页数量 214 * @param {number} limit - 每页数量
214 * @param {boolean} isLoadMore - 是否为加载更多 215 * @param {boolean} isLoadMore - 是否为加载更多
216 + * @returns {Promise<void>}
215 */ 217 */
216 const performSearch = async (keyword, type, page = 0, limit = pageSize, isLoadMore = false) => { 218 const performSearch = async (keyword, type, page = 0, limit = pageSize, isLoadMore = false) => {
217 try { 219 try {
...@@ -219,7 +221,7 @@ const performSearch = async (keyword, type, page = 0, limit = pageSize, isLoadMo ...@@ -219,7 +221,7 @@ const performSearch = async (keyword, type, page = 0, limit = pageSize, isLoadMo
219 if (isLoadMore) { 221 if (isLoadMore) {
220 loadingMore.value = true 222 loadingMore.value = true
221 } else { 223 } else {
222 - Taro.showLoading({ title: '搜索中...', mask: true }) 224 + loading.value = true
223 } 225 }
224 226
225 const params = { keyword, page, limit } 227 const params = { keyword, page, limit }
...@@ -295,7 +297,6 @@ const performSearch = async (keyword, type, page = 0, limit = pageSize, isLoadMo ...@@ -295,7 +297,6 @@ const performSearch = async (keyword, type, page = 0, limit = pageSize, isLoadMo
295 } 297 }
296 298
297 hasSearched.value = true 299 hasSearched.value = true
298 - listRenderKey.value += 1
299 300
300 console.log('[Search] 搜索成功', { 301 console.log('[Search] 搜索成功', {
301 productsTotal: productsTotal.value, 302 productsTotal: productsTotal.value,
...@@ -320,20 +321,48 @@ const performSearch = async (keyword, type, page = 0, limit = pageSize, isLoadMo ...@@ -320,20 +321,48 @@ const performSearch = async (keyword, type, page = 0, limit = pageSize, isLoadMo
320 if (isLoadMore) { 321 if (isLoadMore) {
321 loadingMore.value = false 322 loadingMore.value = false
322 } else { 323 } else {
323 - Taro.hideLoading() 324 + loading.value = false
324 } 325 }
325 } 326 }
326 } 327 }
327 328
328 /** 329 /**
330 + * 处理加载更多事件
331 + *
332 + * @param {number} page - 下一页页码
333 + * @returns {Promise<void>}
334 + */
335 +const handleLoadMore = async (page) => {
336 + console.log('[Search] 加载更多,页码:', page)
337 +
338 + // 更新页码
339 + currentPage.value = page
340 +
341 + // 如果没有搜索过或没有选中 tab,不执行
342 + if (!hasSearched.value || !activeTab.value || !searchKeyword.value.trim()) {
343 + return
344 + }
345 +
346 + // 加载下一页数据
347 + await performSearch(
348 + searchKeyword.value.trim(),
349 + activeTab.value,
350 + page,
351 + pageSize,
352 + true // 标记为加载更多
353 + )
354 +}
355 +
356 +/**
329 * Tab 点击处理(实时查询) 357 * Tab 点击处理(实时查询)
358 + *
359 + * @param {string} tabId - Tab ID
330 */ 360 */
331 const onTabClick = async (tabId) => { 361 const onTabClick = async (tabId) => {
332 if (activeTab.value === tabId) return 362 if (activeTab.value === tabId) return
333 363
334 // 立即切换 tab(响应更快) 364 // 立即切换 tab(响应更快)
335 activeTab.value = tabId 365 activeTab.value = tabId
336 - listRenderKey.value += 1
337 366
338 // 重置分页状态 367 // 重置分页状态
339 currentPage.value = 0 368 currentPage.value = 0
...@@ -383,51 +412,11 @@ const clearSearch = () => { ...@@ -383,51 +412,11 @@ const clearSearch = () => {
383 activeTab.value = '' // 重置为空,不选中任何tab 412 activeTab.value = '' // 重置为空,不选中任何tab
384 currentPage.value = 0 413 currentPage.value = 0
385 hasMore.value = true 414 hasMore.value = true
386 - listRenderKey.value += 1
387 } 415 }
388 416
389 /** 417 /**
390 - * 触底加载更多
391 - * @description 使用防抖避免频繁触发
392 - */
393 -let loadMoreTimer = null
394 -useReachBottom(() => {
395 - // 如果正在加载更多或没有更多数据,不执行
396 - if (loadingMore.value || !hasMore.value) {
397 - return
398 - }
399 -
400 - // 如果没有搜索过或没有选中 tab,不执行
401 - if (!hasSearched.value || !activeTab.value || !searchKeyword.value.trim()) {
402 - return
403 - }
404 -
405 - // 防抖:300ms 内只触发一次
406 - if (loadMoreTimer) {
407 - clearTimeout(loadMoreTimer)
408 - }
409 -
410 - loadMoreTimer = setTimeout(async () => {
411 - console.log('[Search] 触底加载更多')
412 -
413 - // 页码 +1
414 - currentPage.value += 1
415 -
416 - // 加载下一页数据
417 - await performSearch(
418 - searchKeyword.value.trim(),
419 - activeTab.value,
420 - currentPage.value,
421 - pageSize,
422 - true // 标记为加载更多
423 - )
424 - }, 300)
425 -})
426 -
427 -/**
428 * 跳转到产品详情页 418 * 跳转到产品详情页
429 * 419 *
430 - * @description 处理产品详情按钮点击事件
431 * @param {number} productId - 产品ID 420 * @param {number} productId - 产品ID
432 */ 421 */
433 const goToProductDetail = (productId) => { 422 const goToProductDetail = (productId) => {
...@@ -437,7 +426,6 @@ const goToProductDetail = (productId) => { ...@@ -437,7 +426,6 @@ const goToProductDetail = (productId) => {
437 /** 426 /**
438 * 打开计划书弹窗 427 * 打开计划书弹窗
439 * 428 *
440 - * @description 根据产品ID找到对应的产品对象,并打开计划书表单
441 * @param {number} productId - 产品ID 429 * @param {number} productId - 产品ID
442 */ 430 */
443 const openPlanPopup = (productId) => { 431 const openPlanPopup = (productId) => {
...@@ -461,8 +449,6 @@ const openPlanPopup = (productId) => { ...@@ -461,8 +449,6 @@ const openPlanPopup = (productId) => {
461 /** 449 /**
462 * 处理计划书提交 450 * 处理计划书提交
463 * 451 *
464 - * @description 测试环境:前端不调用后端API,直接跳转到结果页
465 - * 生产环境:需要调用 submitPlanAPI 提交表单数据
466 * @param {Object} formData - 表单数据 452 * @param {Object} formData - 表单数据
467 */ 453 */
468 const handlePlanSubmit = (formData) => { 454 const handlePlanSubmit = (formData) => {
...@@ -478,11 +464,6 @@ const handlePlanSubmit = (formData) => { ...@@ -478,11 +464,6 @@ const handlePlanSubmit = (formData) => {
478 464
479 // TODO: 后端接口还没有准备好,暂时不调用API 465 // TODO: 后端接口还没有准备好,暂时不调用API
480 // 测试完成后需要对接 submitPlanAPI 466 // 测试完成后需要对接 submitPlanAPI
481 - // const res = await submitPlanAPI({
482 - // product_id: selectedProduct.value.id,
483 - // template: selectedProduct.value.form_sn,
484 - // form_data: formData
485 - // });
486 467
487 // 模拟提交成功,跳转到结果页面 468 // 模拟提交成功,跳转到结果页面
488 go('/pages/plan-submit-result/index', { 469 go('/pages/plan-submit-result/index', {
...@@ -493,9 +474,8 @@ const handlePlanSubmit = (formData) => { ...@@ -493,9 +474,8 @@ const handlePlanSubmit = (formData) => {
493 /** 474 /**
494 * 处理收藏状态改变 475 * 处理收藏状态改变
495 * 476 *
496 - * @description 当用户点击收藏按钮时,更新本地状态
497 * @param {Object} item - 资料对象 477 * @param {Object} item - 资料对象
498 - * @param {Object} newStatus - 新的状态 478 + * @param {Object} newStatus - 新的状态 { collected: boolean }
499 */ 479 */
500 const handleCollectChanged = (item, newStatus) => { 480 const handleCollectChanged = (item, newStatus) => {
501 console.log('[Search] 收藏状态改变:', item.title, newStatus.collected) 481 console.log('[Search] 收藏状态改变:', item.title, newStatus.collected)
...@@ -505,45 +485,9 @@ const handleCollectChanged = (item, newStatus) => { ...@@ -505,45 +485,9 @@ const handleCollectChanged = (item, newStatus) => {
505 file.collected = newStatus.collected 485 file.collected = newStatus.collected
506 } 486 }
507 } 487 }
508 -
509 </script> 488 </script>
510 489
511 <style lang="less"> 490 <style lang="less">
512 -@keyframes slideIn {
513 - from {
514 - opacity: 0;
515 - transform: translateY(20rpx);
516 - }
517 -
518 - to {
519 - opacity: 1;
520 - transform: translateY(0);
521 - }
522 -}
523 -
524 -.search-result-item {
525 - animation: slideIn 0.3s cubic-bezier(0.2, 0.8, 0.2, 1) backwards;
526 -}
527 -
528 -/* 加载动画 */
529 -@keyframes spin {
530 - 0% {
531 - transform: rotate(0deg);
532 - }
533 - 100% {
534 - transform: rotate(360deg);
535 - }
536 -}
537 -
538 -.loading-spinner-small {
539 - width: 32rpx;
540 - height: 32rpx;
541 - border: 3rpx solid #E5E7EB;
542 - border-top-color: #4CAF50;
543 - border-radius: 50%;
544 - animation: spin 0.8s linear infinite;
545 -}
546 -
547 // FilterTabs 风格的标签栏 491 // FilterTabs 风格的标签栏
548 .filter-tabs-wrapper { 492 .filter-tabs-wrapper {
549 display: flex; 493 display: flex;
...@@ -599,4 +543,6 @@ const handleCollectChanged = (item, newStatus) => { ...@@ -599,4 +543,6 @@ const handleCollectChanged = (item, newStatus) => {
599 :deep(.nut-tabs__content) { 543 :deep(.nut-tabs__content) {
600 display: none; 544 display: none;
601 } 545 }
546 +
547 +/* LoadMoreList 组件已内置动画和加载状态,此处无需额外样式 */
602 </style> 548 </style>
......