feat: 实现 LoadMoreList 动态高度计算并修复双重滚动问题
### 新增功能 - LoadMoreList 组件支持动态高度测量 - 使用 Taro.createSelectorQuery() 运行时测量 - 新增 hasFooter prop(TabBar 页面) - 新增 extraBottomSpace prop(固定按钮等) - 支持响应式监听 props 变化 - 支持页面生命周期重新测量(useDidShow) - 自动处理底部安全区域 ### 修复 - 修复 4 个页面的双重滚动问题 - feedback-list, favorites, material-list, product-center - 添加页面容器 height: 100vh 和 overflow: hidden - 移除所有调试 console.log,保持代码整洁 ### 配置更新 - feedback-list: extraBottomSpace=280(固定按钮) - favorites: hasFooter=false - material-list: hasFooter=false - product-center: hasFooter=false - search: hasFooter=false - message: hasFooter=false - week-hot-material: hasFooter=false ### 文档 - CHANGELOG.md: 记录双重滚动问题修复 - lessons-learned.md: 添加坑 5(双重滚动问题) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Showing
10 changed files
with
354 additions
and
32 deletions
| ... | @@ -5,6 +5,57 @@ | ... | @@ -5,6 +5,57 @@ |
| 5 | 5 | ||
| 6 | --- | 6 | --- |
| 7 | 7 | ||
| 8 | +## [2026-02-08] - 修复 LoadMoreList 页面双重滚动问题 | ||
| 9 | + | ||
| 10 | +### 修复 | ||
| 11 | +- 修复 4 个使用 LoadMoreList 组件的页面出现双重滚动问题 | ||
| 12 | + - 页面级和 scroll-view 都可以滚动,导致用户体验混乱 | ||
| 13 | + - `src/pages/feedback-list/index.vue` | ||
| 14 | + - `src/pages/favorites/index.vue` | ||
| 15 | + - `src/pages/material-list/index.vue` | ||
| 16 | + - `src/pages/product-center/index.vue` | ||
| 17 | +- 在页面容器添加 `height: 100vh` 和 `overflow: hidden` | ||
| 18 | + - TailwindCSS: `h-screen overflow-hidden` | ||
| 19 | + - Less: `height: 100vh; overflow: hidden;` | ||
| 20 | + | ||
| 21 | +### 问题原因 | ||
| 22 | +- 页面容器未设置固定高度(使用 `min-height` 或未设置) | ||
| 23 | +- 页面容器未禁用溢出(缺少 `overflow: hidden`) | ||
| 24 | +- 导致页面级和组件级滚动同时生效 | ||
| 25 | + | ||
| 26 | +### 解决方案 | ||
| 27 | +- 页面容器设置固定高度:`height: 100vh`(或 `h-screen`) | ||
| 28 | +- 页面容器禁用溢出:`overflow: hidden`(或 `overflow-hidden`) | ||
| 29 | +- 让 LoadMoreList 内部的 scroll-view 处理所有滚动 | ||
| 30 | +- 固定元素(导航栏、搜索栏)放在 `#header` 插槽中 | ||
| 31 | + | ||
| 32 | +### 文档 | ||
| 33 | +- 在 `docs/lessons-learned.md` 中添加"LoadMoreList 页面的双重滚动问题"记录 | ||
| 34 | + - 说明问题表现、原因分析、解决方案 | ||
| 35 | + - 提供最佳实践和检查清单 | ||
| 36 | + - 列出修复的 4 个页面和已正确的 3 个页面 | ||
| 37 | + | ||
| 38 | +### 收益 | ||
| 39 | +- ✅ 消除双重滚动,提升用户体验 | ||
| 40 | +- ✅ 固定元素(导航栏、搜索栏)始终可见 | ||
| 41 | +- ✅ 统一 LoadMoreList 页面的滚动行为 | ||
| 42 | +- ✅ 为所有使用 LoadMoreList 的页面提供标准模式 | ||
| 43 | + | ||
| 44 | +--- | ||
| 45 | + | ||
| 46 | +**详细信息**: | ||
| 47 | +- **影响文件**: | ||
| 48 | + - `src/pages/feedback-list/index.vue`(修复双重滚动) | ||
| 49 | + - `src/pages/favorites/index.vue`(修复双重滚动) | ||
| 50 | + - `src/pages/material-list/index.vue`(修复双重滚动) | ||
| 51 | + - `src/pages/product-center/index.vue`(修复双重滚动) | ||
| 52 | + - `docs/lessons-learned.md`(添加经验教训) | ||
| 53 | +- **技术栈**: Vue 3, Taro 4, TailwindCSS, Less | ||
| 54 | +- **测试状态**: ✅ 已通过 | ||
| 55 | +- **备注**: 共检查 7 个使用 LoadMoreList 的页面,修复 4 个,3 个已正确 | ||
| 56 | + | ||
| 57 | +--- | ||
| 58 | + | ||
| 8 | ## [2026-02-08] - 修复 LoadMoreList 组件底部 padding 堆叠问题 | 59 | ## [2026-02-08] - 修复 LoadMoreList 组件底部 padding 堆叠问题 |
| 9 | 60 | ||
| 10 | ### 修复 | 61 | ### 修复 | ... | ... |
| ... | @@ -583,6 +583,137 @@ padding: [top] [right] [bottom] [left]; | ... | @@ -583,6 +583,137 @@ padding: [top] [right] [bottom] [left]; |
| 583 | - **第 1 次**: 在 material-list 页面发现底部 padding 过高 | 583 | - **第 1 次**: 在 material-list 页面发现底部 padding 过高 |
| 584 | - **教训**: ⚠️ **LESS 嵌套选择器中,修饰符类的属性会与基础类堆叠,需要覆盖整个属性而不是只写子属性** | 584 | - **教训**: ⚠️ **LESS 嵌套选择器中,修饰符类的属性会与基础类堆叠,需要覆盖整个属性而不是只写子属性** |
| 585 | 585 | ||
| 586 | +### ❌ 坑 5: LoadMoreList 页面的双重滚动问题 ⭐ 2026-02-08 新增 | ||
| 587 | + | ||
| 588 | +**问题描述**: | ||
| 589 | + | ||
| 590 | +使用 `LoadMoreList` 组件的页面出现双重滚动问题: | ||
| 591 | +- 整个页面可以滚动 | ||
| 592 | +- LoadMoreList 内部的 scroll-view 也可以滚动 | ||
| 593 | +- 用户滚动时体验混乱 | ||
| 594 | + | ||
| 595 | +**错误代码**: | ||
| 596 | +```vue | ||
| 597 | +<!-- ❌ 页面容器没有限制高度和溢出 --> | ||
| 598 | +<template> | ||
| 599 | + <view class="bg-[#F9FAFB]"> | ||
| 600 | + <LoadMoreList | ||
| 601 | + :list="currentList" | ||
| 602 | + :page="currentPage" | ||
| 603 | + @load-more="handleLoadMore" | ||
| 604 | + > | ||
| 605 | + <!-- 列表项 --> | ||
| 606 | + </LoadMoreList> | ||
| 607 | + </view> | ||
| 608 | +</template> | ||
| 609 | +``` | ||
| 610 | + | ||
| 611 | +```less | ||
| 612 | +// ❌ 缺少高度和溢出控制 | ||
| 613 | +.feedback-list { | ||
| 614 | + min-height: 100vh; // ❌ 使用 min-height 而非 height | ||
| 615 | + // ❌ 缺少 overflow: hidden | ||
| 616 | +} | ||
| 617 | +``` | ||
| 618 | + | ||
| 619 | +**错误表现**: | ||
| 620 | +- 页面级和组件级都可以滚动 | ||
| 621 | +- 用户滚动时不确定是哪个在滚动 | ||
| 622 | +- 固定元素(如导航栏、筛选栏)可能随页面滚动消失 | ||
| 623 | + | ||
| 624 | +**原因分析**: | ||
| 625 | +1. **页面容器未限制高度**: 使用 `min-height: 100vh` 或没有设置高度 | ||
| 626 | +2. **未禁用页面级溢出**: 缺少 `overflow: hidden` | ||
| 627 | +3. **滚动上下文混乱**: 浏览器无法确定应该滚动哪个容器 | ||
| 628 | + | ||
| 629 | +**解决方案**: 在页面容器添加 `height: 100vh` 和 `overflow: hidden` | ||
| 630 | + | ||
| 631 | +```vue | ||
| 632 | +<!-- ✅ 正确:页面容器固定高度并禁用溢出 --> | ||
| 633 | +<template> | ||
| 634 | + <!-- ✅ TailwindCSS 方式 --> | ||
| 635 | + <view class="h-screen overflow-hidden bg-[#F9FAFB]"> | ||
| 636 | + <LoadMoreList | ||
| 637 | + :list="currentList" | ||
| 638 | + :page="currentPage" | ||
| 639 | + @load-more="handleLoadMore" | ||
| 640 | + > | ||
| 641 | + <!-- 列表项 --> | ||
| 642 | + </LoadMoreList> | ||
| 643 | + </view> | ||
| 644 | +</template> | ||
| 645 | +``` | ||
| 646 | + | ||
| 647 | +```less | ||
| 648 | +// ✅ Less 方式 | ||
| 649 | +.feedback-list { | ||
| 650 | + height: 100vh; // ✅ 固定高度 | ||
| 651 | + overflow: hidden; // ✅ 禁用页面级滚动 | ||
| 652 | +} | ||
| 653 | +``` | ||
| 654 | + | ||
| 655 | +**关键点**: | ||
| 656 | +- ✅ 页面容器必须设置 `height: 100vh`(固定高度) | ||
| 657 | +- ✅ 页面容器必须设置 `overflow: hidden`(禁用页面级滚动) | ||
| 658 | +- ✅ 让 LoadMoreList 内部的 scroll-view 处理所有滚动 | ||
| 659 | +- ✅ 如果页面有固定顶部(如导航栏、搜索栏),放在 LoadMoreList 的 `#header` 插槽中 | ||
| 660 | + | ||
| 661 | +**修复的页面**(共 4 个): | ||
| 662 | +1. `src/pages/feedback-list/index.vue` | ||
| 663 | +2. `src/pages/favorites/index.vue` | ||
| 664 | +3. `src/pages/material-list/index.vue` | ||
| 665 | +4. `src/pages/product-center/index.vue` | ||
| 666 | + | ||
| 667 | +**已正确的页面**(共 3 个): | ||
| 668 | +1. `src/pages/search/index.vue`(已正确配置) | ||
| 669 | +2. `src/pages/message/index.vue`(LoadMoreList 是根元素) | ||
| 670 | +3. `src/pages/week-hot-material/index.vue`(LoadMoreList 是根元素) | ||
| 671 | + | ||
| 672 | +**最佳实践**: | ||
| 673 | +```vue | ||
| 674 | +<!-- ✅ 推荐的 LoadMoreList 页面结构 --> | ||
| 675 | +<template> | ||
| 676 | + <!-- 外层容器:固定高度,禁用溢出 --> | ||
| 677 | + <view class="h-screen overflow-hidden bg-[#F9FAFB]"> | ||
| 678 | + <LoadMoreList | ||
| 679 | + :list="currentList" | ||
| 680 | + :page="currentPage" | ||
| 681 | + :page-size="pageSize" | ||
| 682 | + :has-more="hasMore" | ||
| 683 | + :loading="loading" | ||
| 684 | + :loading-more="loadingMore" | ||
| 685 | + key-field="id" | ||
| 686 | + @load-more="handleLoadMore" | ||
| 687 | + > | ||
| 688 | + <!-- 固定头部(可选) --> | ||
| 689 | + <template #header> | ||
| 690 | + <view class="sticky top-0 bg-white z-10"> | ||
| 691 | + <NavHeader title="页面标题" /> | ||
| 692 | + <SearchBar v-model="searchValue" /> | ||
| 693 | + </view> | ||
| 694 | + </template> | ||
| 695 | + | ||
| 696 | + <!-- 列表项 --> | ||
| 697 | + <template #item="{ item }"> | ||
| 698 | + <ProductCard :product="item" /> | ||
| 699 | + </template> | ||
| 700 | + </LoadMoreList> | ||
| 701 | + </view> | ||
| 702 | +</template> | ||
| 703 | +``` | ||
| 704 | + | ||
| 705 | +**检查清单**: | ||
| 706 | +使用 LoadMoreList 组件时,确认: | ||
| 707 | +- [ ] 页面容器设置了 `height: 100vh`(或 `h-screen`) | ||
| 708 | +- [ ] 页面容器设置了 `overflow: hidden`(或 `overflow-hidden`) | ||
| 709 | +- [ ] 固定元素(导航栏、搜索栏)放在 `#header` 插槽中 | ||
| 710 | +- [ ] 只有 LoadMoreList 内部的 scroll-view 可以滚动 | ||
| 711 | + | ||
| 712 | +**适用场景**: | ||
| 713 | +- ✅ 所有使用 LoadMoreList 组件的分页列表页面 | ||
| 714 | +- ✅ 需要固定顶部元素的滚动页面 | ||
| 715 | +- ✅ 需要防止双重滚动的任何场景 | ||
| 716 | + | ||
| 586 | --- | 717 | --- |
| 587 | 718 | ||
| 588 | ### ⚠️ 双设计宽度系统 | 719 | ### ⚠️ 双设计宽度系统 | ... | ... |
| ... | @@ -31,7 +31,7 @@ | ... | @@ -31,7 +31,7 @@ |
| 31 | <template> | 31 | <template> |
| 32 | <view class="load-more-list" :class="{ 'has-header': showHeader }"> | 32 | <view class="load-more-list" :class="{ 'has-header': showHeader }"> |
| 33 | <!-- 可选固定头部 --> | 33 | <!-- 可选固定头部 --> |
| 34 | - <view v-if="showHeader" class="load-more-header"> | 34 | + <view v-if="showHeader" class="load-more-header" ref="headerRef"> |
| 35 | <slot name="header"></slot> | 35 | <slot name="header"></slot> |
| 36 | </view> | 36 | </view> |
| 37 | 37 | ||
| ... | @@ -127,8 +127,9 @@ | ... | @@ -127,8 +127,9 @@ |
| 127 | </template> | 127 | </template> |
| 128 | 128 | ||
| 129 | <script setup> | 129 | <script setup> |
| 130 | -import { computed } from 'vue' | 130 | +import { computed, ref, onMounted, watch, nextTick as vueNextTick } from 'vue' |
| 131 | -import { usePullDownRefresh, stopPullDownRefresh } from '@tarojs/taro' | 131 | +import Taro from '@tarojs/taro' |
| 132 | +import { usePullDownRefresh, stopPullDownRefresh, useDidShow } from '@tarojs/taro' | ||
| 132 | 133 | ||
| 133 | /** | 134 | /** |
| 134 | * 通用加载更多列表组件 | 135 | * 通用加载更多列表组件 |
| ... | @@ -298,6 +299,28 @@ const props = defineProps({ | ... | @@ -298,6 +299,28 @@ const props = defineProps({ |
| 298 | enableScrollLoad: { | 299 | enableScrollLoad: { |
| 299 | type: Boolean, | 300 | type: Boolean, |
| 300 | default: true | 301 | default: true |
| 302 | + }, | ||
| 303 | + | ||
| 304 | + /** | ||
| 305 | + * 页面是否有底部导航(TabBar 等) | ||
| 306 | + * @type {boolean} | ||
| 307 | + * @default false | ||
| 308 | + * @description 如果页面有底部导航(如 TabBar),设为 true 会自动预留底部空间 | ||
| 309 | + */ | ||
| 310 | + hasFooter: { | ||
| 311 | + type: Boolean, | ||
| 312 | + default: false | ||
| 313 | + }, | ||
| 314 | + | ||
| 315 | + /** | ||
| 316 | + * 额外底部空间(rpx) | ||
| 317 | + * @type {number} | ||
| 318 | + * @default 0 | ||
| 319 | + * @description 额外预留的底部空间(单位:rpx),用于固定按钮等非标准底部元素 | ||
| 320 | + */ | ||
| 321 | + extraBottomSpace: { | ||
| 322 | + type: Number, | ||
| 323 | + default: 0 | ||
| 301 | } | 324 | } |
| 302 | }) | 325 | }) |
| 303 | 326 | ||
| ... | @@ -323,17 +346,118 @@ const emit = defineEmits({ | ... | @@ -323,17 +346,118 @@ const emit = defineEmits({ |
| 323 | const displayList = computed(() => props.list || []) | 346 | const displayList = computed(() => props.list || []) |
| 324 | 347 | ||
| 325 | /** | 348 | /** |
| 349 | + * 动态测量的头部高度(px) | ||
| 350 | + * @description 使用 Taro.createSelectorQuery() 在运行时测量实际头部高度 | ||
| 351 | + */ | ||
| 352 | +const headerHeight = ref(0) | ||
| 353 | + | ||
| 354 | +/** | ||
| 355 | + * 动态测量的底部导航高度(px) | ||
| 356 | + * @description 如果页面有 TabBar 等底部导航,预留底部空间 | ||
| 357 | + */ | ||
| 358 | +const footerHeight = ref(0) | ||
| 359 | + | ||
| 360 | +/** | ||
| 361 | + * 是否已完成高度测量 | ||
| 362 | + * @description 避免重复测量 | ||
| 363 | + */ | ||
| 364 | +const heightMeasured = ref(false) | ||
| 365 | + | ||
| 366 | +/** | ||
| 367 | + * 测量头部和底部高度 | ||
| 368 | + * @description 使用 Taro.createSelectorQuery() 动态测量页面元素高度 | ||
| 369 | + */ | ||
| 370 | +const measureHeaderHeight = async () => { | ||
| 371 | + if (heightMeasured.value) return | ||
| 372 | + | ||
| 373 | + try { | ||
| 374 | + // 获取系统信息 | ||
| 375 | + const systemInfo = await Taro.getSystemInfo() | ||
| 376 | + const { windowHeight, screenHeight, safeArea } = systemInfo | ||
| 377 | + | ||
| 378 | + // 计算底部安全区域高度 | ||
| 379 | + const bottomSafeArea = screenHeight - safeArea.bottom | ||
| 380 | + | ||
| 381 | + /** | ||
| 382 | + * 计算底部高度 | ||
| 383 | + * @description 处理底部导航栏、安全区域和额外底部空间 | ||
| 384 | + */ | ||
| 385 | + const calculateFooterHeight = () => { | ||
| 386 | + let totalFooterHeight = 0 | ||
| 387 | + | ||
| 388 | + if (props.hasFooter) { | ||
| 389 | + // 假设 TabBar 高度约为 50px(小程序标准 TabBar 高度) | ||
| 390 | + totalFooterHeight = 50 + bottomSafeArea | ||
| 391 | + } else { | ||
| 392 | + // 无底部导航,只考虑安全区域 | ||
| 393 | + totalFooterHeight = bottomSafeArea | ||
| 394 | + } | ||
| 395 | + | ||
| 396 | + // 添加额外底部空间(将 rpx 转换为 px:750rpx 设计宽度标准,rpx / 2 = px) | ||
| 397 | + if (props.extraBottomSpace > 0) { | ||
| 398 | + const extraBottomSpacePx = props.extraBottomSpace / 2 | ||
| 399 | + totalFooterHeight += extraBottomSpacePx | ||
| 400 | + } | ||
| 401 | + | ||
| 402 | + footerHeight.value = totalFooterHeight | ||
| 403 | + heightMeasured.value = true | ||
| 404 | + } | ||
| 405 | + | ||
| 406 | + let totalHeaderHeight = 0 | ||
| 407 | + | ||
| 408 | + // 如果有头部,测量头部实际高度 | ||
| 409 | + if (props.showHeader) { | ||
| 410 | + await vueNextTick() // 等待 DOM 渲染完成 | ||
| 411 | + | ||
| 412 | + const query = Taro.createSelectorQuery() | ||
| 413 | + query.select('.load-more-header').boundingClientRect() | ||
| 414 | + query.exec((res) => { | ||
| 415 | + if (res && res[0]) { | ||
| 416 | + totalHeaderHeight = res[0].height | ||
| 417 | + } else { | ||
| 418 | + // 如果测量失败,使用默认估算值 | ||
| 419 | + totalHeaderHeight = 178 // 356rpx ≈ 178px | ||
| 420 | + } | ||
| 421 | + | ||
| 422 | + headerHeight.value = totalHeaderHeight | ||
| 423 | + calculateFooterHeight() // 现在可以正确调用了 | ||
| 424 | + }) | ||
| 425 | + } else { | ||
| 426 | + // 无头部 | ||
| 427 | + calculateFooterHeight() | ||
| 428 | + } | ||
| 429 | + } catch (err) { | ||
| 430 | + console.error('[LoadMoreList] 测量高度失败:', err) | ||
| 431 | + // 测量失败时使用默认值 | ||
| 432 | + headerHeight.value = props.showHeader ? 178 : 0 | ||
| 433 | + footerHeight.value = props.hasFooter ? 80 : 34 // 约50px TabBar + 30px安全区域 | ||
| 434 | + heightMeasured.value = true | ||
| 435 | + } | ||
| 436 | +} | ||
| 437 | + | ||
| 438 | +// 组件挂载后测量高度 | ||
| 439 | +onMounted(() => { | ||
| 440 | + measureHeaderHeight() | ||
| 441 | +}) | ||
| 442 | + | ||
| 443 | +/** | ||
| 326 | * scroll-view 高度 | 444 | * scroll-view 高度 |
| 327 | - * @description 动态计算 scroll-view 的高度 | 445 | + * @description 动态计算 scroll-view 的高度,使用运行时测量的实际高度 |
| 328 | */ | 446 | */ |
| 329 | const scrollHeight = computed(() => { | 447 | const scrollHeight = computed(() => { |
| 330 | - // 头部高度估算(rpx 单位) | 448 | + if (!heightMeasured.value) { |
| 331 | - // NavHeader(约88rpx) + SearchBar(约120rpx) + Tabs(约88rpx) + ResultCount(约60rpx) ≈ 356rpx | 449 | + // 未完成测量时,使用默认估算值 |
| 332 | - const headerHeight = props.showHeader ? '356rpx' : '0rpx' | 450 | + const defaultHeaderHeight = props.showHeader ? '178px' : '0px' // 356rpx ≈ 178px |
| 451 | + const defaultFooterHeight = props.hasFooter ? '80px' : '34px' | ||
| 452 | + return `calc(100vh - ${defaultHeaderHeight} - ${defaultFooterHeight})` | ||
| 453 | + } | ||
| 333 | 454 | ||
| 334 | - // 使用 calc() 计算剩余高度 | 455 | + // 使用测量到的实际高度(转换为 px) |
| 335 | - // 100vh 是视口高度,减去头部高度 | 456 | + const headerPx = headerHeight.value |
| 336 | - return `calc(100vh - ${headerHeight})` | 457 | + const footerPx = footerHeight.value |
| 458 | + | ||
| 459 | + // 计算滚动高度:视口高度 - 头部高度 - 底部高度 | ||
| 460 | + return `calc(100vh - ${headerPx}px - ${footerPx}px)` | ||
| 337 | }) | 461 | }) |
| 338 | 462 | ||
| 339 | /** | 463 | /** |
| ... | @@ -356,20 +480,11 @@ function getAnimationDelay(index) { | ... | @@ -356,20 +480,11 @@ function getAnimationDelay(index) { |
| 356 | * @description 当 scroll-view 滚动到底部时触发 | 480 | * @description 当 scroll-view 滚动到底部时触发 |
| 357 | */ | 481 | */ |
| 358 | const handleScrollToLower = () => { | 482 | const handleScrollToLower = () => { |
| 359 | - console.log('[LoadMoreList] scroll-view 触底事件触发', { | ||
| 360 | - loadingMore: props.loadingMore, | ||
| 361 | - loading: props.loading, | ||
| 362 | - hasMore: props.hasMore, | ||
| 363 | - page: props.page | ||
| 364 | - }) | ||
| 365 | - | ||
| 366 | // 如果正在加载或没有更多数据,不执行 | 483 | // 如果正在加载或没有更多数据,不执行 |
| 367 | if (props.loadingMore || props.loading || !props.hasMore) { | 484 | if (props.loadingMore || props.loading || !props.hasMore) { |
| 368 | - console.log('[LoadMoreList] 跳过加载:正在加载或没有更多数据') | ||
| 369 | return | 485 | return |
| 370 | } | 486 | } |
| 371 | 487 | ||
| 372 | - console.log('[LoadMoreList] 触底加载更多,当前页:', props.page, '下一页:', props.page + 1) | ||
| 373 | const nextPage = props.page + 1 | 488 | const nextPage = props.page + 1 |
| 374 | emit('load-more', nextPage) | 489 | emit('load-more', nextPage) |
| 375 | } | 490 | } |
| ... | @@ -380,11 +495,28 @@ const handleScrollToLower = () => { | ... | @@ -380,11 +495,28 @@ const handleScrollToLower = () => { |
| 380 | */ | 495 | */ |
| 381 | if (props.enablePullDownRefresh) { | 496 | if (props.enablePullDownRefresh) { |
| 382 | usePullDownRefresh(() => { | 497 | usePullDownRefresh(() => { |
| 383 | - console.log('[LoadMoreList] 下拉刷新') | ||
| 384 | emit('refresh') | 498 | emit('refresh') |
| 385 | stopPullDownRefresh() | 499 | stopPullDownRefresh() |
| 386 | }) | 500 | }) |
| 387 | } | 501 | } |
| 502 | + | ||
| 503 | +/** | ||
| 504 | + * 监听 showHeader、hasFooter 和 extraBottomSpace 变化 | ||
| 505 | + * @description 当头部或底部配置变化时,重新测量高度 | ||
| 506 | + */ | ||
| 507 | +watch([() => props.showHeader, () => props.hasFooter, () => props.extraBottomSpace], () => { | ||
| 508 | + heightMeasured.value = false | ||
| 509 | + measureHeaderHeight() | ||
| 510 | +}) | ||
| 511 | + | ||
| 512 | +/** | ||
| 513 | + * 页面显示时重新测量高度 | ||
| 514 | + * @description 确保在页面显示时高度计算正确(处理从其他页面返回的情况) | ||
| 515 | + */ | ||
| 516 | +useDidShow(() => { | ||
| 517 | + heightMeasured.value = false | ||
| 518 | + measureHeaderHeight() | ||
| 519 | +}) | ||
| 388 | </script> | 520 | </script> |
| 389 | 521 | ||
| 390 | <style lang="less"> | 522 | <style lang="less"> |
| ... | @@ -392,10 +524,6 @@ if (props.enablePullDownRefresh) { | ... | @@ -392,10 +524,6 @@ if (props.enablePullDownRefresh) { |
| 392 | background-color: #F9FAFB; | 524 | background-color: #F9FAFB; |
| 393 | height: 100vh; | 525 | height: 100vh; |
| 394 | overflow: hidden; | 526 | overflow: hidden; |
| 395 | - | ||
| 396 | - &.has-header { | ||
| 397 | - padding-bottom: calc(160rpx + env(safe-area-inset-bottom)); | ||
| 398 | - } | ||
| 399 | } | 527 | } |
| 400 | 528 | ||
| 401 | .load-more-header { | 529 | .load-more-header { |
| ... | @@ -419,8 +547,9 @@ if (props.enablePullDownRefresh) { | ... | @@ -419,8 +547,9 @@ if (props.enablePullDownRefresh) { |
| 419 | &.scrollable { | 547 | &.scrollable { |
| 420 | // 高度通过 :style 动态绑定 | 548 | // 高度通过 :style 动态绑定 |
| 421 | box-sizing: border-box; | 549 | box-sizing: border-box; |
| 422 | - // 重置基础 padding,只保留顶部和左右,底部使用特殊计算值 | 550 | + // 重置基础 padding,只保留顶部和左右,底部预留空间 + 安全区域 |
| 423 | - padding: 32rpx 32rpx calc(160rpx + env(safe-area-inset-bottom)); | 551 | + // 确保在所有设备(包括刘海屏)上"没有更多了"文字都能完整显示 |
| 552 | + padding: 32rpx 32rpx calc(env(safe-area-inset-bottom)); | ||
| 424 | } | 553 | } |
| 425 | } | 554 | } |
| 426 | 555 | ... | ... |
| ... | @@ -3,7 +3,7 @@ | ... | @@ -3,7 +3,7 @@ |
| 3 | * @Description: 我的收藏 - 使用 LoadMoreList 组件重构版本 | 3 | * @Description: 我的收藏 - 使用 LoadMoreList 组件重构版本 |
| 4 | --> | 4 | --> |
| 5 | <template> | 5 | <template> |
| 6 | - <view class="h-screen bg-gray-50 flex flex-col"> | 6 | + <view class="h-screen bg-gray-50 flex flex-col overflow-hidden"> |
| 7 | <view class="bg-gray-50 z-10"> | 7 | <view class="bg-gray-50 z-10"> |
| 8 | <NavHeader title="我的收藏" /> | 8 | <NavHeader title="我的收藏" /> |
| 9 | </view> | 9 | </view> |
| ... | @@ -17,6 +17,8 @@ | ... | @@ -17,6 +17,8 @@ |
| 17 | :loading="loading" | 17 | :loading="loading" |
| 18 | :loading-more="loadingMore" | 18 | :loading-more="loadingMore" |
| 19 | key-field="meta_id" | 19 | key-field="meta_id" |
| 20 | + :show-header="false" | ||
| 21 | + :has-footer="false" | ||
| 20 | @load-more="handleLoadMore" | 22 | @load-more="handleLoadMore" |
| 21 | > | 23 | > |
| 22 | <!-- 列表项 --> | 24 | <!-- 列表项 --> | ... | ... |
| ... | @@ -15,6 +15,9 @@ | ... | @@ -15,6 +15,9 @@ |
| 15 | :loading="loading" | 15 | :loading="loading" |
| 16 | :loading-more="loadingMore" | 16 | :loading-more="loadingMore" |
| 17 | key-field="id" | 17 | key-field="id" |
| 18 | + :show-header="false" | ||
| 19 | + :has-footer="false" | ||
| 20 | + :extra-bottom-space="280" | ||
| 18 | @load-more="handleLoadMore" | 21 | @load-more="handleLoadMore" |
| 19 | > | 22 | > |
| 20 | <!-- 列表项 --> | 23 | <!-- 列表项 --> |
| ... | @@ -314,9 +317,10 @@ onMounted(() => { | ... | @@ -314,9 +317,10 @@ onMounted(() => { |
| 314 | 317 | ||
| 315 | <style lang="less"> | 318 | <style lang="less"> |
| 316 | .feedback-list { | 319 | .feedback-list { |
| 317 | - min-height: 100vh; | 320 | + height: 100vh; |
| 321 | + overflow: hidden; | ||
| 318 | background-color: #f9fafb; | 322 | background-color: #f9fafb; |
| 319 | - padding-bottom: 160rpx; // 为固定按钮留出空间 | 323 | + // padding-bottom: 160rpx; // ❌ 已移除:LoadMoreList 已经通过 extraBottomSpace=114 预留了 57px 空间 |
| 320 | 324 | ||
| 321 | .feedback-item { | 325 | .feedback-item { |
| 322 | background: white; | 326 | background: white; | ... | ... |
| ... | @@ -3,7 +3,7 @@ | ... | @@ -3,7 +3,7 @@ |
| 3 | * @Description: 资料/文档列表页 - 使用 LoadMoreList 组件重构版本 | 3 | * @Description: 资料/文档列表页 - 使用 LoadMoreList 组件重构版本 |
| 4 | --> | 4 | --> |
| 5 | <template> | 5 | <template> |
| 6 | - <view class="bg-[#F9FAFB]"> | 6 | + <view class="h-screen overflow-hidden bg-[#F9FAFB]"> |
| 7 | <LoadMoreList | 7 | <LoadMoreList |
| 8 | :list="currentList" | 8 | :list="currentList" |
| 9 | :page="currentPage" | 9 | :page="currentPage" |
| ... | @@ -12,6 +12,7 @@ | ... | @@ -12,6 +12,7 @@ |
| 12 | :loading="loading" | 12 | :loading="loading" |
| 13 | :loading-more="loadingMore" | 13 | :loading-more="loadingMore" |
| 14 | key-field="meta_id" | 14 | key-field="meta_id" |
| 15 | + :has-footer="false" | ||
| 15 | @load-more="handleLoadMore" | 16 | @load-more="handleLoadMore" |
| 16 | > | 17 | > |
| 17 | <!-- 头部:导航 + 搜索 + Tabs --> | 18 | <!-- 头部:导航 + 搜索 + Tabs --> | ... | ... |
| ... | @@ -12,6 +12,7 @@ | ... | @@ -12,6 +12,7 @@ |
| 12 | :loading-more="loadingMore" | 12 | :loading-more="loadingMore" |
| 13 | :enable-pull-down-refresh="true" | 13 | :enable-pull-down-refresh="true" |
| 14 | key-field="id" | 14 | key-field="id" |
| 15 | + :has-footer="false" | ||
| 15 | @load-more="handleLoadMore" | 16 | @load-more="handleLoadMore" |
| 16 | @refresh="handleRefresh" | 17 | @refresh="handleRefresh" |
| 17 | > | 18 | > | ... | ... |
| ... | @@ -3,7 +3,7 @@ | ... | @@ -3,7 +3,7 @@ |
| 3 | * @Description: 产品中心 - 使用 LoadMoreList 组件重构版本 | 3 | * @Description: 产品中心 - 使用 LoadMoreList 组件重构版本 |
| 4 | --> | 4 | --> |
| 5 | <template> | 5 | <template> |
| 6 | - <view class="bg-[#F9FAFB]"> | 6 | + <view class="h-screen overflow-hidden bg-[#F9FAFB]"> |
| 7 | <!-- 计划书弹窗 --> | 7 | <!-- 计划书弹窗 --> |
| 8 | <view v-if="showPlanPopup && selectedProduct"> | 8 | <view v-if="showPlanPopup && selectedProduct"> |
| 9 | <PlanFormContainer | 9 | <PlanFormContainer |
| ... | @@ -22,6 +22,7 @@ | ... | @@ -22,6 +22,7 @@ |
| 22 | :loading="loading" | 22 | :loading="loading" |
| 23 | :loading-more="loadingMore" | 23 | :loading-more="loadingMore" |
| 24 | key-field="id" | 24 | key-field="id" |
| 25 | + :has-footer="false" | ||
| 25 | @load-more="handleLoadMore" | 26 | @load-more="handleLoadMore" |
| 26 | > | 27 | > |
| 27 | <!-- 头部:导航 + 搜索 + Tabs --> | 28 | <!-- 头部:导航 + 搜索 + Tabs --> |
| ... | @@ -30,7 +31,7 @@ | ... | @@ -30,7 +31,7 @@ |
| 30 | <NavHeader title="产品中心" /> | 31 | <NavHeader title="产品中心" /> |
| 31 | 32 | ||
| 32 | <!-- Search Bar --> | 33 | <!-- Search Bar --> |
| 33 | - <view class="px-[24rpx] py-[16rpx] bg-white"> | 34 | + <view class="px-[24rpx] py-[16rpx] bg-[#F9FAFB]"> |
| 34 | <SearchBar | 35 | <SearchBar |
| 35 | v-model="searchValue" | 36 | v-model="searchValue" |
| 36 | placeholder="搜索产品名称..." | 37 | placeholder="搜索产品名称..." | ... | ... |
| ... | @@ -14,6 +14,7 @@ | ... | @@ -14,6 +14,7 @@ |
| 14 | :loading-more="loadingMore" | 14 | :loading-more="loadingMore" |
| 15 | :show-header="true" | 15 | :show-header="true" |
| 16 | :enable-scroll-load="shouldEnableScrollLoad" | 16 | :enable-scroll-load="shouldEnableScrollLoad" |
| 17 | + :has-footer="false" | ||
| 17 | key-field="id" | 18 | key-field="id" |
| 18 | @load-more="handleLoadMore" | 19 | @load-more="handleLoadMore" |
| 19 | > | 20 | > | ... | ... |
| ... | @@ -11,6 +11,7 @@ | ... | @@ -11,6 +11,7 @@ |
| 11 | :loading="loading" | 11 | :loading="loading" |
| 12 | :loading-more="loadingMore" | 12 | :loading-more="loadingMore" |
| 13 | key-field="meta_id" | 13 | key-field="meta_id" |
| 14 | + :has-footer="false" | ||
| 14 | @load-more="handleLoadMore" | 15 | @load-more="handleLoadMore" |
| 15 | > | 16 | > |
| 16 | <!-- 头部 --> | 17 | <!-- 头部 --> | ... | ... |
-
Please register or login to post a comment