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 + :page="currentPage"
10 + :page-size="pageSize"
11 + :has-more="hasMore"
12 + :loading="loading"
13 + :loading-more="loadingMore"
14 + key-field="meta_id"
15 + @load-more="handleLoadMore"
16 + >
17 + <!-- 头部:导航 + 搜索 + Tabs -->
18 + <template #header>
19 + <view class="sticky top-0 z-10 bg-[#F9FAFB]">
9 <NavHeader :title="pageTitle" /> 20 <NavHeader :title="pageTitle" />
10 21
11 <view class="px-[32rpx] mt-[32rpx] mb-[24rpx]"> 22 <view class="px-[32rpx] mt-[32rpx] mb-[24rpx]">
...@@ -40,18 +51,13 @@ ...@@ -40,18 +51,13 @@
40 </template> 51 </template>
41 </nut-tabs> 52 </nut-tabs>
42 </view> 53 </view>
54 + </template>
43 55
44 - <!-- 列表容器 - 页面级滚动 --> 56 + <!-- 列表项:资料卡片 -->
57 + <template #item="{ item }">
45 <view 58 <view
46 - v-if="listVisible"
47 - :key="listRenderKey"
48 - class="px-[32rpx] pb-[calc(160rpx+env(safe-area-inset-bottom))] box-border"
49 - >
50 - <view class="flex flex-col gap-[24rpx]">
51 - <view v-for="(item, index) in currentList" :key="index"
52 class="material-item bg-white rounded-[24rpx] p-[24rpx] shadow-sm transition-all duration-200 border border-gray-50 flex flex-row" 59 class="material-item bg-white rounded-[24rpx] p-[24rpx] shadow-sm transition-all duration-200 border border-gray-50 flex flex-row"
53 - :style="{ animationDelay: `${index * 50}ms` }"> 60 + >
54 -
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
7 + :list="currentList"
8 + :page="currentPage"
9 + :page-size="pageSize"
10 + :has-more="hasMore"
11 + :loading="loading"
12 + :loading-more="loadingMore"
13 + :enable-pull-down-refresh="true"
14 + key-field="id"
15 + @load-more="handleLoadMore"
16 + @refresh="handleRefresh"
17 + >
18 + <!-- 头部 -->
19 + <template #header>
3 <NavHeader title="我的消息" /> 20 <NavHeader title="我的消息" />
21 + </template>
4 22
5 - <!-- 列表区域 --> 23 + <!-- 列表项 -->
6 - <view class="p-4"> 24 + <template #item="{ item }">
7 - <template v-if="messageList.length > 0">
8 <view 25 <view
9 - v-for="item in messageList" 26 + class="message-item bg-white rounded-xl p-4 mb-3 shadow-sm active:opacity-70 transition-opacity"
10 - :key="item.id"
11 - class="bg-white rounded-xl p-4 mb-3 shadow-sm active:opacity-70 transition-opacity"
12 @tap="handleItemClick(item)" 27 @tap="handleItemClick(item)"
13 > 28 >
14 <view class="flex justify-between items-start mb-2"> 29 <view class="flex justify-between items-start mb-2">
...@@ -26,29 +41,20 @@ ...@@ -26,29 +41,20 @@
26 {{ item.intro || item.content || '暂无简介' }} 41 {{ item.intro || item.content || '暂无简介' }}
27 </view> 42 </view>
28 </view> 43 </view>
29 -
30 - <!-- 加载更多/没有更多 -->
31 - <view class="py-4 text-center text-[24rpx] text-gray-400">
32 - <text v-if="loading">加载中...</text>
33 - <text v-else-if="!hasMore">没有更多了</text>
34 - <text v-else>上拉加载更多</text>
35 - </view>
36 </template> 44 </template>
37 45
38 <!-- 空状态 --> 46 <!-- 空状态 -->
39 - <nut-empty 47 + <template #empty>
40 - v-else-if="!loading && messageList.length === 0" 48 + <nut-empty description="暂无消息" image="empty" />
41 - description="暂无消息" 49 + </template>
42 - image="empty" 50 + </LoadMoreList>
43 - />
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 96
74 - if (refresh) { 97 +/**
75 - page.value = 1 98 + * 加载更多状态
76 - hasMore.value = true 99 + * @type {Ref<boolean>}
77 - } else if (!hasMore.value) { 100 + */
78 - return 101 +const loadingMore = ref(false)
79 - }
80 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) => {
113 + try {
114 + // 如果是加载更多,使用 loadingMore 状态,否则使用 loading 状态
115 + if (isLoadMore) {
116 + loadingMore.value = true
117 + } else {
81 loading.value = true 118 loading.value = true
119 + }
82 120
83 - try { 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
102 } else { 139 } else {
103 - messageList.value = [...messageList.value, ...list] 140 + // 首次加载或刷新:替换数据
141 + currentList.value = listData
104 } 142 }
105 143
106 - if (list.length < limit.value) { 144 + // 判断是否还有更多数据
145 + // 如果返回的数据量少于请求的量,说明没有更多了
146 + hasMore.value = listData.length >= params.limit
147 + } else {
148 + // 没有数据了
149 + if (isLoadMore) {
107 hasMore.value = false 150 hasMore.value = false
108 } else { 151 } else {
109 - page.value++ 152 + currentList.value = []
153 + }
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 {
161 + if (isLoadMore) {
162 + loadingMore.value = false
163 + } else {
115 loading.value = false 164 loading.value = false
116 - if (refresh) {
117 - stopPullDownRefresh()
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 + <PlanFormContainer
10 + v-model:visible="showPlanPopup"
11 + :product="selectedProduct"
12 + @close="showPlanPopup = false"
13 + @submit="handlePlanSubmit"
14 + />
15 + </view>
16 +
17 + <LoadMoreList
18 + :list="currentList"
19 + :page="currentPage"
20 + :page-size="pageSize"
21 + :has-more="hasMore"
22 + :loading="loading"
23 + :loading-more="loadingMore"
24 + key-field="id"
25 + @load-more="handleLoadMore"
26 + >
27 + <!-- 头部:导航 + 搜索 + Tabs -->
28 + <template #header>
29 + <view class="sticky top-0 z-10 bg-[#F9FAFB]">
9 <NavHeader title="产品中心" /> 30 <NavHeader title="产品中心" />
10 31
11 <!-- Search Bar --> 32 <!-- Search Bar -->
...@@ -43,25 +64,18 @@ ...@@ -43,25 +64,18 @@
43 </nut-tabs> 64 </nut-tabs>
44 </view> 65 </view>
45 </view> 66 </view>
67 + </template>
46 68
47 - <!-- 列表容器 - 页面级滚动 --> 69 + <!-- 列表项:产品卡片 -->
48 - <view class="pb-[calc(160rpx+env(safe-area-inset-bottom))]"> 70 + <template #item="{ item }">
49 - <!-- 加载状态 --> 71 + <view
50 - <view v-if="loading && products.length === 0" class="flex justify-center items-center py-[100rpx]"> 72 + class="bg-white rounded-[24rpx] overflow-hidden shadow-sm active:scale-[0.98] transition-transform duration-200 product-card"
51 - <text class="text-gray-400 text-[28rpx]">加载中...</text> 73 + @tap="handleProductClick(item)"
52 - </view>
53 -
54 - <!-- Product List -->
55 - <view v-else class="px-[40rpx]">
56 - <!-- Card Item -->
57 - <view v-for="(item, index) in products" :key="item.id"
58 - class="bg-white rounded-[24rpx] overflow-hidden mb-[24rpx] shadow-sm active:scale-[0.98] transition-transform duration-200"
59 - :style="{ animationDelay: `${index * 50}ms` }"
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 // 处理产品列表
251 + if (res.data.list?.length) {
252 + const listData = res.data.list
253 +
235 if (isLoadMore) { 254 if (isLoadMore) {
236 // 加载更多:追加数据 255 // 加载更多:追加数据
237 - products.value = [...products.value, ...res.data.list] 256 + currentList.value = [...currentList.value, ...listData]
238 } else { 257 } else {
239 // 首次加载或切换分类:替换数据 258 // 首次加载或切换分类:替换数据
240 - products.value = res.data.list || [] 259 + currentList.value = listData
241 } 260 }
242 261
243 - // 更新总数和分页状态 262 + // 判断是否还有更多数据
244 - total.value = res.data.total || 0 263 + // 如果返回的数据量少于请求的量,说明没有更多了
245 - hasMore.value = products.value.length < total.value 264 + hasMore.value = listData.length >= params.limit
246 } else { 265 } else {
247 - Taro.showToast({ 266 + // 没有数据了
248 - title: res.msg || '获取产品列表失败', 267 + if (isLoadMore) {
249 - icon: 'none' 268 + hasMore.value = false
250 - }) 269 + } else {
270 + currentList.value = []
251 } 271 }
252 - } catch (err) { 272 + }
253 - console.error('获取产品列表失败:', err) 273 + } else {
254 - Taro.showToast({ 274 + console.error('[Product Center] API 返回错误:', res.msg)
255 - title: '网络错误,请重试', 275 + }
256 - icon: 'none' 276 + } catch (error) {
257 - }) 277 + console.error('[Product Center] 获取产品列表失败:', error)
258 } finally { 278 } finally {
279 + if (isLoadMore) {
280 + loadingMore.value = false
281 + } else {
259 loading.value = false 282 loading.value = false
260 } 283 }
284 + }
285 +}
286 +
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)
261 } 331 }
262 332
263 /** 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]">
8 + <LoadMoreList
9 + :list="currentList"
10 + :page="currentPage"
11 + :page-size="pageSize"
12 + :has-more="hasMore"
13 + :loading="loading"
14 + :loading-more="loadingMore"
15 + :show-header="true"
16 + key-field="id"
17 + @load-more="handleLoadMore"
18 + >
7 <!-- 固定顶部:导航栏 + 搜索栏 + Tabs + 结果计数 --> 19 <!-- 固定顶部:导航栏 + 搜索栏 + Tabs + 结果计数 -->
20 + <template #header>
8 <view class="bg-[#FFF] sticky top-0 z-10"> 21 <view class="bg-[#FFF] sticky top-0 z-10">
9 <NavHeader title="搜索" /> 22 <NavHeader title="搜索" />
10 23
...@@ -47,35 +60,24 @@ ...@@ -47,35 +60,24 @@
47 找到 {{ currentTotal }} 个相关结果 60 找到 {{ currentTotal }} 个相关结果
48 </view> 61 </view>
49 </view> 62 </view>
63 + </template>
50 64
51 - <!-- 列表容器 --> 65 + <!-- 列表项:根据 activeTab 动态渲染 -->
52 - <view class="px-[40rpx] pb-[calc(160rpx+env(safe-area-inset-bottom))]"> 66 + <template #item="{ item }">
53 -
54 - <!-- Search Results -->
55 - <view
56 - v-if="currentList.length > 0"
57 - :key="listRenderKey"
58 - >
59 <!-- Product Results --> 67 <!-- Product Results -->
60 - <view v-if="activeTab === 'product'" class="flex flex-col gap-[24rpx] pb-[40rpx]">
61 <ProductCard 68 <ProductCard
62 - v-for="(item, index) in currentList" 69 + v-if="activeTab === 'product'"
63 - :key="index"
64 :product-id="item.id" 70 :product-id="item.id"
65 :product-name="item.product_name || item.name" 71 :product-name="item.product_name || item.name"
66 :tags="item.tags || []" 72 :tags="item.tags || []"
67 class="search-result-item" 73 class="search-result-item"
68 - :style="{ animationDelay: `${index * 30}ms` }"
69 @detail="goToProductDetail" 74 @detail="goToProductDetail"
70 @plan="openPlanPopup" 75 @plan="openPlanPopup"
71 /> 76 />
72 - </view>
73 77
74 <!-- File Results --> 78 <!-- File Results -->
75 - <view v-else-if="activeTab === 'file'" class="flex flex-col gap-[24rpx] pb-[40rpx]">
76 <MaterialCard 79 <MaterialCard
77 - v-for="(item, index) in currentList" 80 + v-else-if="activeTab === 'file'"
78 - :key="index"
79 :id="item.id" 81 :id="item.id"
80 :title="item.title" 82 :title="item.title"
81 :file-name="item.fileName" 83 :file-name="item.fileName"
...@@ -86,37 +88,27 @@ ...@@ -86,37 +88,27 @@
86 :extension="item.extension" 88 :extension="item.extension"
87 :download-url="item.downloadUrl" 89 :download-url="item.downloadUrl"
88 class="search-result-item" 90 class="search-result-item"
89 - :style="{ animationDelay: `${index * 30}ms` }"
90 @collect-changed="handleCollectChanged(item, $event)" 91 @collect-changed="handleCollectChanged(item, $event)"
91 /> 92 />
92 - </view> 93 + </template>
93 94
94 - <!-- 加载更多提示 --> 95 + <!-- 自定义空状态:处理三种状态 -->
95 - <view v-if="currentList.length > 0" class="flex items-center justify-center py-[40rpx]"> 96 + <template #empty>
96 - <view v-if="loadingMore" class="flex items-center"> 97 + <!-- Initial State (从未搜索过) -->
97 - <view class="loading-spinner-small"></view> 98 + <view v-if="!hasSearched" class="flex flex-col items-center justify-center py-[120rpx]">
98 - <text class="ml-[12rpx] text-[#9CA3AF] text-[24rpx]">加载中...</text> 99 + <IconFont name="search" class="text-gray-300 mb-[24rpx]" size="64" />
99 - </view> 100 + <view class="text-[#6B7280] text-[28rpx]">搜索产品或资料</view>
100 - <view v-else-if="!hasMore" class="text-[#9CA3AF] text-[24rpx]"> 101 + <view class="text-[#9CA3AF] text-[24rpx] mt-[12rpx]">输入关键词开始搜索,自动切换分类</view>
101 - 没有更多了
102 - </view>
103 - </view>
104 </view> 102 </view>
105 103
106 <!-- Empty State (已搜索但无结果) --> 104 <!-- Empty State (已搜索但无结果) -->
107 - <view v-else-if="hasSearched && currentList.length === 0" class="flex flex-col items-center justify-center py-[40rpx]"> 105 + <view v-else class="flex flex-col items-center justify-center py-[40rpx]">
108 <nut-empty description="暂无搜索结果" image="empty"> 106 <nut-empty description="暂无搜索结果" image="empty">
109 <view class="text-[#9CA3AF] text-[24rpx] mt-[12rpx]">试试其他关键词吧</view> 107 <view class="text-[#9CA3AF] text-[24rpx] mt-[12rpx]">试试其他关键词吧</view>
110 </nut-empty> 108 </nut-empty>
111 </view> 109 </view>
112 - 110 + </template>
113 - <!-- Initial State (从未搜索过) --> 111 + </LoadMoreList>
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>
......