hookehuyr

feat: 提取 LoadMoreList 可复用组件并迁移页面

- 创建通用加载更多列表组件 LoadMoreList
  * 支持自定义头部、列表项、空状态插槽
  * 内置触底加载、加载状态、动画效果
  * 优化动画延迟策略(前10项逐个显示,其余立即显示)
- 迁移 week-hot-material 页面使用新组件
  * 代码量减少 ~18%
  * 移除重复的分页逻辑和样式
- 修复样式问题
  * 修复列表项黑色圆点(list-style: none)
  * 使用原生 Less 替代 TailwindCSS(兼容性更好)
- 创建迁移指南文档
- 减少 mock 数据延迟(100-300ms)用于开发测试

技术栈: Vue 3 + Composition API + Taro
收益: 提高代码复用性,降低维护成本,统一用户体验
...@@ -18,6 +18,7 @@ declare module 'vue' { ...@@ -18,6 +18,7 @@ declare module 'vue' {
18 IndexNav: typeof import('./src/components/indexNav.vue')['default'] 18 IndexNav: typeof import('./src/components/indexNav.vue')['default']
19 LifeInsuranceTemplate: typeof import('./src/components/PlanTemplates/LifeInsuranceTemplate.vue')['default'] 19 LifeInsuranceTemplate: typeof import('./src/components/PlanTemplates/LifeInsuranceTemplate.vue')['default']
20 ListItemActions: typeof import('./src/components/ListItemActions/index.vue')['default'] 20 ListItemActions: typeof import('./src/components/ListItemActions/index.vue')['default']
21 + LoadMoreList: typeof import('./src/components/LoadMoreList/index.vue')['default']
21 MaterialCard: typeof import('./src/components/MaterialCard.vue')['default'] 22 MaterialCard: typeof import('./src/components/MaterialCard.vue')['default']
22 NavHeader: typeof import('./src/components/NavHeader.vue')['default'] 23 NavHeader: typeof import('./src/components/NavHeader.vue')['default']
23 NutAvatar: typeof import('@nutui/nutui-taro')['Avatar'] 24 NutAvatar: typeof import('@nutui/nutui-taro')['Avatar']
...@@ -35,7 +36,7 @@ declare module 'vue' { ...@@ -35,7 +36,7 @@ declare module 'vue' {
35 PdfPreview: typeof import('./src/components/PdfPreview.vue')['default'] 36 PdfPreview: typeof import('./src/components/PdfPreview.vue')['default']
36 Picker: typeof import('./src/components/time-picker-data/picker.vue')['default'] 37 Picker: typeof import('./src/components/time-picker-data/picker.vue')['default']
37 PlanFormContainer: typeof import('./src/components/PlanFormContainer.vue')['default'] 38 PlanFormContainer: typeof import('./src/components/PlanFormContainer.vue')['default']
38 - PlanPopup: typeof import('./src/components/PlanPopup/index.vue')['default'] 39 + PlanPopup: typeof import('./src/components/PlanSchemes/PlanPopup.vue')['default']
39 PosterBuilder: typeof import('./src/components/PosterBuilder/index.vue')['default'] 40 PosterBuilder: typeof import('./src/components/PosterBuilder/index.vue')['default']
40 ProductCard: typeof import('./src/components/ProductCard.vue')['default'] 41 ProductCard: typeof import('./src/components/ProductCard.vue')['default']
41 QrCode: typeof import('./src/components/qrCode.vue')['default'] 42 QrCode: typeof import('./src/components/qrCode.vue')['default']
......
...@@ -5,6 +5,39 @@ ...@@ -5,6 +5,39 @@
5 5
6 --- 6 ---
7 7
8 +## [2026-02-08] - 提取通用加载更多列表组件
9 +
10 +### 新增
11 +- 创建 `LoadMoreList` 通用加载更多列表组件
12 + - 封装分页状态管理(page、pageSize、hasMore)
13 + - 支持触底加载更多(内置300ms防抖)
14 + - 支持下拉刷新(可选)
15 + - 支持自定义头部(通过slot)
16 + - 支持自定义列表项(通过slot)
17 + - 内置空状态和加载提示UI
18 + - 内置列表项动画效果(slideIn + 延迟)
19 +- 组件配置文件 `src/components/LoadMoreList/index.config.js`
20 +- 迁移指南文档 `docs/LoadMoreList-迁移指南.md`
21 +
22 +### 重构
23 +- 重构 `src/pages/week-hot-material/index.vue` 使用 `LoadMoreList` 组件
24 + - 代码行数减少 18%(283行 → 230行)
25 + - 模板代码减少 34%(55行 → 36行)
26 + - 样式代码减少 96%(68行 → 3行)
27 + - 分页逻辑集中在组件内部
28 + - 逻辑可复用,易于维护
29 +
30 +---
31 +
32 +## [2026-02-08] - 修复 LoadMoreList 组件导入路径
33 +
34 +### 修复
35 +- 修复 `week-hot-material` 页面加载 `LoadMoreList` 组件报错 "Cannot find module '@/components/LoadMoreList.vue'"
36 +- 修正导入路径:从 `@/components/LoadMoreList.vue` 改为 `@/components/LoadMoreList`
37 +- 原因:组件使用目录结构 `LoadMoreList/index.vue`,导入时应指向目录而非文件
38 +
39 +---
40 +
8 ## [2026-02-06] - 修复搜索栏清空按钮点击无效 41 ## [2026-02-06] - 修复搜索栏清空按钮点击无效
9 42
10 ### 修复 43 ### 修复
......
This diff is collapsed. Click to expand it.
1 +/**
2 + * LoadMoreList 组件配置
3 + *
4 + * @description 通用加载更多列表组件
5 + */
6 +export default {
7 + component: true,
8 + usingComponents: {}
9 +}
1 +<!--
2 + @description 通用加载更多列表组件
3 + @features
4 + - 支持自定义头部(通过slot)
5 + - 支持下拉刷新
6 + - 支持触底加载更多(带防抖)
7 + - 支持多种列表项渲染(通过slot)
8 + - 内置空状态和加载提示
9 + - 内置列表项动画效果
10 +
11 + @example
12 + <LoadMoreList
13 + :list="products"
14 + :page="page"
15 + :page-size="pageSize"
16 + :has-more="hasMore"
17 + :loading="loading"
18 + :loading-more="loadingMore"
19 + key-field="id"
20 + @load-more="handleLoadMore"
21 + @refresh="handleRefresh"
22 + >
23 + <template #header>
24 + <NavHeader title="产品中心" />
25 + </template>
26 + <template #item="{ item }">
27 + <ProductCard :product="item" />
28 + </template>
29 + </LoadMoreList>
30 +-->
31 +<template>
32 + <view class="load-more-list" :class="{ 'has-header': showHeader }">
33 + <!-- 可选固定头部 -->
34 + <view v-if="showHeader" class="load-more-header sticky top-0 z-10">
35 + <slot name="header"></slot>
36 + </view>
37 +
38 + <!-- 列表容器 -->
39 + <view
40 + class="load-more-content"
41 + :class="{ 'no-padding': noPadding }"
42 + >
43 + <!-- 首次加载状态 -->
44 + <view v-if="loading && list.length === 0" class="flex justify-center items-center py-[100rpx]">
45 + <slot name="loading">
46 + <view class="loading-spinner"></view>
47 + <text class="ml-[16rpx] text-[#9CA3AF] text-[28rpx]">加载中...</text>
48 + </slot>
49 + </view>
50 +
51 + <!-- 列表内容 -->
52 + <view v-else class="list-container">
53 + <view
54 + v-for="(item, index) in displayList"
55 + :key="item[keyField] || index"
56 + class="list-item"
57 + :style="getAnimationDelay(index)"
58 + >
59 + <!-- 使用slot渲染每个列表项 -->
60 + <slot name="item" :item="item" :index="index"></slot>
61 + </view>
62 +
63 + <!-- 空状态 -->
64 + <view v-if="list.length === 0 && !loading && !loadingMore">
65 + <slot name="empty">
66 + <nut-empty description="暂无数据" image="empty" />
67 + </slot>
68 + </view>
69 +
70 + <!-- 加载更多提示 -->
71 + <view v-if="list.length > 0" class="load-more-container">
72 + <view v-if="loadingMore" class="load-more-loading">
73 + <slot name="loading-more">
74 + <view class="loading-spinner-small"></view>
75 + <text class="ml-[16rpx] text-[#9CA3AF] text-[24rpx]">加载中...</text>
76 + </slot>
77 + </view>
78 + <view v-else-if="!hasMore" class="load-more-finished">
79 + <slot name="no-more">
80 + <text class="text-[#9CA3AF] text-[24rpx]">没有更多了</text>
81 + </slot>
82 + </view>
83 + </view>
84 + </view>
85 + </view>
86 + </view>
87 +</template>
88 +
89 +<script setup>
90 +import { computed } from 'vue'
91 +import { useReachBottom, usePullDownRefresh, stopPullDownRefresh } from '@tarojs/taro'
92 +
93 +/**
94 + * 通用加载更多列表组件
95 + *
96 + * @description 封装分页加载逻辑,支持自定义头部、列表项、空状态。
97 + * 页面只需提供数据源和事件处理,组件内部处理触底加载、状态显示等。
98 + *
99 + * @component LoadMoreList
100 + *
101 + * @example
102 + * <!-- 简单列表(无头部) -->
103 + * <LoadMoreList
104 + * :list="products"
105 + * :page="page"
106 + * :page-size="10"
107 + * :has-more="hasMore"
108 + * :loading="loading"
109 + * :loading-more="loadingMore"
110 + * key-field="id"
111 + * @load-more="handleLoadMore"
112 + * >
113 + * <template #item="{ item }">
114 + * <ProductCard :product="item" />
115 + * </template>
116 + * </LoadMoreList>
117 + *
118 + * @example
119 + * <!-- 带头部和搜索的列表 -->
120 + * <LoadMoreList
121 + * :list="products"
122 + * :page="page"
123 + * :page-size="10"
124 + * :has-more="hasMore"
125 + * :loading="loading"
126 + * :loading-more="loadingMore"
127 + * :enable-pull-down-refresh="true"
128 + * @load-more="handleLoadMore"
129 + * @refresh="handleRefresh"
130 + * >
131 + * <template #header>
132 + * <NavHeader title="产品中心" />
133 + * <SearchBar v-model="searchValue" @search="onSearch" />
134 + * </template>
135 + * <template #item="{ item }">
136 + * <ProductCard :product="item" />
137 + * </template>
138 + * <template #empty>
139 + * <nut-empty description="暂无相关产品" image="empty" />
140 + * </template>
141 + * </LoadMoreList>
142 + */
143 +const props = defineProps({
144 + /**
145 + * 列表数据源
146 + * @type {Array<any>}
147 + * @description 需要渲染的列表数据数组
148 + */
149 + list: {
150 + type: Array,
151 + default: () => []
152 + },
153 +
154 + /**
155 + * 当前页码
156 + * @type {number}
157 + * @description 当前页码(从0或1开始,根据API要求)
158 + */
159 + page: {
160 + type: Number,
161 + required: true
162 + },
163 +
164 + /**
165 + * 每页数量
166 + * @type {number}
167 + * @default 10
168 + * @description 每页显示的数据条数
169 + */
170 + pageSize: {
171 + type: Number,
172 + default: 10
173 + },
174 +
175 + /**
176 + * 是否还有更多数据
177 + * @type {boolean}
178 + * @default true
179 + * @description 用于控制是否显示"没有更多了"提示
180 + */
181 + hasMore: {
182 + type: Boolean,
183 + default: true
184 + },
185 +
186 + /**
187 + * 首次加载状态
188 + * @type {boolean}
189 + * @default false
190 + * @description 首次加载时显示loading,隐藏列表
191 + */
192 + loading: {
193 + type: Boolean,
194 + default: false
195 + },
196 +
197 + /**
198 + * 加载更多状态
199 + * @type {boolean}
200 + * @default false
201 + * @description 加载更多时在列表底部显示loading
202 + */
203 + loadingMore: {
204 + type: Boolean,
205 + default: false
206 + },
207 +
208 + /**
209 + * 唯一标识字段名
210 + * @type {string}
211 + * @default 'id'
212 + * @description 用于v-for的key字段名,确保列表更新正确
213 + */
214 + keyField: {
215 + type: String,
216 + default: 'id'
217 + },
218 +
219 + /**
220 + * 是否显示固定头部
221 + * @type {boolean}
222 + * @default true
223 + * @description 是否在顶部显示固定的头部区域
224 + */
225 + showHeader: {
226 + type: Boolean,
227 + default: true
228 + },
229 +
230 + /**
231 + * 是否启用下拉刷新
232 + * @type {boolean}
233 + * @default false
234 + * @description 启用后,用户下拉会触发refresh事件
235 + */
236 + enablePullDownRefresh: {
237 + type: Boolean,
238 + default: false
239 + },
240 +
241 + /**
242 + * 列表容器是否不需要padding
243 + * @type {boolean}
244 + * @default false
245 + * @description 设为true时,列表容器不添加默认的左右padding
246 + */
247 + noPadding: {
248 + type: Boolean,
249 + default: false
250 + }
251 +})
252 +
253 +const emit = defineEmits({
254 + /**
255 + * 加载更多事件
256 + * @event {number} page - 下一页页码
257 + * @description 当用户滚动到底部时触发,页码自动+1
258 + */
259 + 'load-more': (page) => typeof page === 'number',
260 +
261 + /**
262 + * 下拉刷新事件
263 + * @description 当用户下拉刷新时触发,仅当enablePullDownRefresh为true时有效
264 + */
265 + 'refresh': null
266 +})
267 +
268 +/**
269 + * 显示列表(用于渲染)
270 + * @description 计算属性,返回非空的列表数组
271 + */
272 +const displayList = computed(() => props.list || [])
273 +
274 +/**
275 + * 获取动画延迟
276 + * @description 只为每批的前10项使用动画延迟,避免累积延迟
277 + * @param {number} index - 列表项索引
278 + * @returns {string} 动画延迟样式
279 + */
280 +function getAnimationDelay(index) {
281 + // 只为前10项使用动画延迟,其余立即显示
282 + if (index < 10) {
283 + return { animationDelay: `${index * 20}ms` }
284 + }
285 + // 第10项以后立即显示(无延迟)
286 + return {}
287 +}
288 +
289 +/**
290 + * 触底加载更多(使用防抖)
291 + * @description 当滚动到底部时触发,300ms防抖避免频繁触发
292 + */
293 +let loadMoreTimer = null
294 +useReachBottom(() => {
295 + // 如果正在加载或没有更多数据,不执行
296 + if (props.loadingMore || props.loading || !props.hasMore) {
297 + return
298 + }
299 +
300 + // 防抖:300ms 内只触发一次
301 + if (loadMoreTimer) {
302 + clearTimeout(loadMoreTimer)
303 + }
304 +
305 + loadMoreTimer = setTimeout(() => {
306 + console.log('[LoadMoreList] 触底加载更多,当前页:', props.page, '下一页:', props.page + 1)
307 + const nextPage = props.page + 1
308 + emit('load-more', nextPage)
309 + }, 300)
310 +})
311 +
312 +/**
313 + * 下拉刷新
314 + * @description 用户下拉时触发刷新事件,仅当enablePullDownRefresh为true时有效
315 + */
316 +if (props.enablePullDownRefresh) {
317 + usePullDownRefresh(() => {
318 + console.log('[LoadMoreList] 下拉刷新')
319 + emit('refresh')
320 + stopPullDownRefresh()
321 + })
322 +}
323 +</script>
324 +
325 +<style lang="less">
326 +.load-more-list {
327 + min-height: 100vh;
328 + background-color: #F9FAFB;
329 +
330 + &.has-header {
331 + padding-bottom: calc(160rpx + env(safe-area-inset-bottom));
332 + }
333 +}
334 +
335 +.load-more-content {
336 + // 列表容器样式
337 + min-height: calc(100vh - 200rpx);
338 + padding: 32rpx;
339 +
340 + &.no-padding {
341 + padding: 0;
342 + }
343 +}
344 +
345 +// 列表容器
346 +.list-container {
347 + display: flex;
348 + flex-direction: column;
349 + gap: 24rpx;
350 +
351 + // 去除列表项的黑点
352 + view {
353 + list-style: none;
354 + }
355 +}
356 +
357 +// 列表项进入动画
358 +@keyframes slideIn {
359 + from {
360 + opacity: 0;
361 + transform: translateY(20rpx);
362 + }
363 +
364 + to {
365 + opacity: 1;
366 + transform: translateY(0);
367 + }
368 +}
369 +
370 +.list-item {
371 + animation: slideIn 0.5s cubic-bezier(0.2, 0.8, 0.2, 1) backwards;
372 +
373 + // 确保去除列表样式
374 + list-style: none;
375 +}
376 +
377 +// 加载更多容器
378 +.load-more-container {
379 + display: flex;
380 + justify-content: center;
381 + align-items: center;
382 + padding: 40rpx 0;
383 + min-height: 80rpx;
384 +}
385 +
386 +.load-more-loading {
387 + display: flex;
388 + align-items: center;
389 + justify-content: center;
390 +}
391 +
392 +.load-more-finished {
393 + display: flex;
394 + align-items: center;
395 + justify-content: center;
396 +}
397 +
398 +// 自定义加载动画
399 +.loading-spinner {
400 + width: 40rpx;
401 + height: 40rpx;
402 + border: 4rpx solid #E5E7EB;
403 + border-top-color: #4CAF50;
404 + border-radius: 50%;
405 + animation: spin 0.8s linear infinite;
406 +}
407 +
408 +.loading-spinner-small {
409 + width: 32rpx;
410 + height: 32rpx;
411 + border: 3rpx solid #E5E7EB;
412 + border-top-color: #4CAF50;
413 + border-radius: 50%;
414 + animation: spin 0.8s linear infinite;
415 +}
416 +
417 +@keyframes spin {
418 + to {
419 + transform: rotate(360deg);
420 + }
421 +}
422 +</style>
1 <!-- 1 <!--
2 - * @Date: 2026-02-06 2 + * @Date: 2026-02-08
3 - * @Description: 本周热门资料页 - 使用 MaterialCard 组件 3 + * @Description: 本周热门资料页 - 使用 LoadMoreList 组件重构版本
4 --> 4 -->
5 <template> 5 <template>
6 - <view class="min-h-screen bg-[#F9FAFB] py-[32rpx]"> 6 + <LoadMoreList
7 - <NavHeader title="本周热门资料" /> 7 + :list="currentList"
8 - 8 + :page="currentPage"
9 - <!-- 列表容器 --> 9 + :page-size="pageSize"
10 - <view 10 + :has-more="hasMore"
11 - v-if="listVisible" 11 + :loading="loading"
12 - :key="listRenderKey" 12 + :loading-more="loadingMore"
13 - class="px-[32rpx]" 13 + key-field="meta_id"
14 + @load-more="handleLoadMore"
14 > 15 >
15 - <!-- 加载状态 --> 16 + <!-- 头部 -->
16 - <view v-if="loading && currentList.length === 0" class="flex items-center justify-center py-[60rpx]"> 17 + <template #header>
17 - <view class="loading-spinner"></view> 18 + <NavHeader title="本周热门资料" />
18 - <text class="ml-[16rpx] text-[#9CA3AF] text-[28rpx]">加载中...</text> 19 + </template>
19 - </view>
20 20
21 - <view v-else class="flex flex-col gap-[24rpx]"> 21 + <!-- 列表项 -->
22 + <template #item="{ item }">
22 <MaterialCard 23 <MaterialCard
23 - v-for="(item, index) in currentList"
24 - :key="item.meta_id"
25 :id="item.meta_id" 24 :id="item.meta_id"
26 :title="item.name" 25 :title="item.name"
27 :file-name="item.name" 26 :file-name="item.name"
28 :file-size="item.size" 27 :file-size="item.size"
29 - :learners="item.read_people_count ? `${item.read_people_count}人学习` : ''" 28 + :learners="item.learners"
30 :read-people-percent="item.read_people_percent" 29 :read-people-percent="item.read_people_percent"
31 :collected="item.collected" 30 :collected="item.collected"
32 :extension="item.extension" 31 :extension="item.extension"
33 :download-url="item.downloadUrl" 32 :download-url="item.downloadUrl"
34 - :style="{ animationDelay: `${index * 50}ms` }"
35 @collect-changed="handleCollectChanged(item, $event)" 33 @collect-changed="handleCollectChanged(item, $event)"
36 /> 34 />
37 - 35 + </template>
38 - <!-- 空状态 --> 36 + </LoadMoreList>
39 - <view v-if="currentList.length === 0 && !loading && !loadingMore">
40 - <nut-empty description="暂无热门资料" image="empty" />
41 - </view>
42 -
43 - <!-- 加载更多提示 -->
44 - <view v-if="currentList.length > 0" class="flex items-center justify-center py-[40rpx]">
45 - <view v-if="loadingMore" class="flex items-center">
46 - <view class="loading-spinner-small"></view>
47 - <text class="ml-[12rpx] text-[#9CA3AF] text-[24rpx]">加载中...</text>
48 - </view>
49 - <view v-else-if="!hasMore" class="text-[#9CA3AF] text-[24rpx]">
50 - 没有更多了
51 - </view>
52 - </view>
53 - </view>
54 - </view>
55 - </view>
56 </template> 37 </template>
57 38
58 <script setup> 39 <script setup>
59 import { ref } from 'vue' 40 import { ref } from 'vue'
60 -import Taro, { useLoad, useReachBottom } from '@tarojs/taro' 41 +import Taro, { useLoad } from '@tarojs/taro'
42 +import LoadMoreList from '@/components/LoadMoreList'
61 import NavHeader from '@/components/NavHeader.vue' 43 import NavHeader from '@/components/NavHeader.vue'
62 import MaterialCard from '@/components/MaterialCard.vue' 44 import MaterialCard from '@/components/MaterialCard.vue'
63 import { weekHotAPI } from '@/api/file' 45 import { weekHotAPI } from '@/api/file'
64 import { mockWeekHotAPI } from '@/utils/mockData' 46 import { mockWeekHotAPI } from '@/utils/mockData'
65 47
66 -const listVisible = ref(true)
67 -const listRenderKey = ref(0)
68 -const loading = ref(false)
69 -const loadingMore = ref(false) // 加载更多状态
70 -const hasMore = ref(true) // 是否还有更多数据
71 -const currentList = ref([])
72 -const currentPage = ref(0) // 当前页码(从0开始)
73 -const pageSize = 20 // 每页数量
74 -
75 // ⚠️ MOCK 数据开关 - 开发环境使用 mock 数据,生产环境使用真实 API 48 // ⚠️ MOCK 数据开关 - 开发环境使用 mock 数据,生产环境使用真实 API
76 const USE_MOCK_DATA = process.env.NODE_ENV === 'development' 49 const USE_MOCK_DATA = process.env.NODE_ENV === 'development'
77 50
78 /** 51 /**
52 + * 当前列表数据
53 + * @type {Ref<Array<any>>}
54 + */
55 +const currentList = ref([])
56 +
57 +/**
58 + * 当前页码(从0开始)
59 + * @type {Ref<number>}
60 + */
61 +const currentPage = ref(0)
62 +
63 +/**
64 + * 每页数量
65 + * @type {number}
66 + */
67 +const pageSize = 20
68 +
69 +/**
70 + * 是否还有更多数据
71 + * @type {Ref<boolean>}
72 + */
73 +const hasMore = ref(true)
74 +
75 +/**
76 + * 首次加载状态
77 + * @type {Ref<boolean>}
78 + */
79 +const loading = ref(false)
80 +
81 +/**
82 + * 加载更多状态
83 + * @type {Ref<boolean>}
84 + */
85 +const loadingMore = ref(false)
86 +
87 +/**
79 * 处理收藏状态改变 88 * 处理收藏状态改变
80 * 89 *
81 * @description 当用户点击收藏按钮时,更新本地状态 90 * @description 当用户点击收藏按钮时,更新本地状态
82 * @param {Object} item - 资料对象 91 * @param {Object} item - 资料对象
83 - * @param {Object} newStatus - 新的状态 92 + * @param {Object} newStatus - 新的状态 { collected: boolean }
84 */ 93 */
85 const handleCollectChanged = (item, newStatus) => { 94 const handleCollectChanged = (item, newStatus) => {
86 console.log('[Week Hot] 收藏状态改变:', item.name, newStatus.collected) 95 console.log('[Week Hot] 收藏状态改变:', item.name, newStatus.collected)
...@@ -93,10 +102,12 @@ const handleCollectChanged = (item, newStatus) => { ...@@ -93,10 +102,12 @@ const handleCollectChanged = (item, newStatus) => {
93 102
94 /** 103 /**
95 * 获取本周热门资料列表 104 * 获取本周热门资料列表
105 + *
96 * @param {Object} params - 请求参数 106 * @param {Object} params - 请求参数
97 * @param {number} params.page - 页码(从0开始) 107 * @param {number} params.page - 页码(从0开始)
98 * @param {number} params.limit - 每页数量 108 * @param {number} params.limit - 每页数量
99 - * @param {boolean} params.isLoadMore - 是否为加载更多 109 + * @param {boolean} isLoadMore - 是否为加载更多
110 + * @returns {Promise<void>}
100 */ 111 */
101 const fetchWeekHotList = async (params = {}, isLoadMore = false) => { 112 const fetchWeekHotList = async (params = {}, isLoadMore = false) => {
102 try { 113 try {
...@@ -194,89 +205,25 @@ useLoad(async (options) => { ...@@ -194,89 +205,25 @@ useLoad(async (options) => {
194 }) 205 })
195 206
196 /** 207 /**
197 - * 触底加载更多 208 + * 处理加载更多事件
198 - * @description 使用防抖避免频繁触发 209 + *
210 + * @param {number} page - 下一页页码
211 + * @returns {Promise<void>}
199 */ 212 */
200 -let loadMoreTimer = null 213 +const handleLoadMore = async (page) => {
201 -useReachBottom(() => { 214 + console.log('[Week Hot] 加载更多,页码:', page)
202 - // 如果正在加载或没有更多数据,不执行
203 - if (loadingMore.value || !hasMore.value) {
204 - return
205 - }
206 -
207 - // 防抖:300ms 内只触发一次
208 - if (loadMoreTimer) {
209 - clearTimeout(loadMoreTimer)
210 - }
211 215
212 - loadMoreTimer = setTimeout(async () => { 216 + // 更新页码
213 - console.log('[Week Hot] 触底加载更多') 217 + currentPage.value = page
214 -
215 - // 页码 +1
216 - currentPage.value += 1
217 218
218 // 加载下一页数据 219 // 加载下一页数据
219 await fetchWeekHotList( 220 await fetchWeekHotList(
220 - { page: currentPage.value, limit: pageSize }, 221 + { page: page, limit: pageSize },
221 true // 标记为加载更多 222 true // 标记为加载更多
222 ) 223 )
223 - }, 300) 224 +}
224 -})
225 </script> 225 </script>
226 226
227 <style lang="less"> 227 <style lang="less">
228 -/* 列表项进入动画 */ 228 +/* LoadMoreList 组件已内置样式,此处无需额外样式 */
229 -@keyframes slideIn {
230 - from {
231 - opacity: 0;
232 - transform: translateY(20rpx);
233 - }
234 -
235 - to {
236 - opacity: 1;
237 - transform: translateY(0);
238 - }
239 -}
240 -
241 -.material-item {
242 - animation: slideIn 0.5s cubic-bezier(0.2, 0.8, 0.2, 1) backwards;
243 -}
244 -
245 -/* 加载动画 */
246 -@keyframes spin {
247 - 0% {
248 - transform: rotate(0deg);
249 - }
250 - 100% {
251 - transform: rotate(360deg);
252 - }
253 -}
254 -
255 -.loading-spinner {
256 - width: 40rpx;
257 - height: 40rpx;
258 - border: 4rpx solid #E5E7EB;
259 - border-top-color: #4CAF50;
260 - border-radius: 50%;
261 - animation: spin 0.8s linear infinite;
262 -}
263 -
264 -.loading-spinner-small {
265 - width: 32rpx;
266 - height: 32rpx;
267 - border: 3rpx solid #E5E7EB;
268 - border-top-color: #4CAF50;
269 - border-radius: 50%;
270 - animation: spin 0.8s linear infinite;
271 -}
272 -
273 -/* 多行文本省略 */
274 -.line-clamp-2 {
275 - display: -webkit-box;
276 - -webkit-box-orient: vertical;
277 - -webkit-line-clamp: 2;
278 - line-clamp: 2;
279 - overflow: hidden;
280 - word-break: break-all;
281 -}
282 </style> 229 </style>
......
...@@ -53,7 +53,7 @@ function generateRandomFavorite() { ...@@ -53,7 +53,7 @@ function generateRandomFavorite() {
53 * @param {number} max 最大延迟(ms) 53 * @param {number} max 最大延迟(ms)
54 * @returns {Promise} 54 * @returns {Promise}
55 */ 55 */
56 -function mockDelay(min = 300, max = 800) { 56 +function mockDelay(min = 100, max = 300) {
57 const delay = Math.random() * (max - min) + min 57 const delay = Math.random() * (max - min) + min
58 return new Promise(resolve => setTimeout(resolve, delay)) 58 return new Promise(resolve => setTimeout(resolve, delay))
59 } 59 }
......