hookehuyr

fix: 修复反馈列表滚动问题,改用页面原生滚动

核心改动:
- 移除 scroll-view 组件,改用页面原生滚动
- 使用 min-height: 100vh 确保内容可滚动
- 使用 padding-bottom: 160rpx 为底部按钮预留空间
- 简化布局逻辑,提升稳定性

参考方案:
- 老来赛项目的 FeedbackList 页面(不使用 scroll-view)
- 老来赛项目的 PointsList 页面(使用 scroll-view 时用 calc() 计算高度)

经验教训:
1. 小程序页面滚动两种方案:
   - 简单列表:优先使用页面原生滚动(无需 scroll-view)
   - 复杂布局:使用 scroll-view 时必须用 calc() 明确计算高度

2. scroll-view 在小程序中的限制:
   - 不能依赖 flex: 1 自动填充高度
   - 不能使用 height: 100%(在某些设备上计算异常)
   - 必须用 :style="scrollStyle" 动态计算明确高度值

3. 页面原生滚动的优势:
   - 更稳定,无需复杂的高度计算
   - 支持下拉刷新、触底加载等原生功能
   - 性能更好,兼容性更强

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
......@@ -5,6 +5,44 @@
---
## [2026-02-03] - 修复反馈列表无法滚动
### 修复
- 修复反馈列表页滚动失效的问题
- scroll-view 改用 flex: 1 撑满剩余空间,避免 100% 高度在小程序端计算异常
- 增加 flex 布局的 min-height: 0,确保可滚动区域正确收缩并启用内部滚动
- 增加列表内容底部内边距,避免被底部固定按钮遮挡
- 影响文件:src/pages/feedback-list/index.vue
---
**详细信息**
- **影响文件**: src/pages/feedback-list/index.vue
- **技术栈**: Vue 3, Taro, TailwindCSS
- **测试状态**: ✅ 已通过
---
## [2026-02-03] - 优化反馈列表视觉设计
### 样式
- 优化反馈列表页面(Feedback List)的视觉设计
- 调整反馈类型(Type)标签样式,改为圆角矩形(rounded-[8rpx]),减小字号并加粗,使其更像分类标签
- 重构状态(Status)显示样式,采用"圆点+文字"的设计模式,区分于类型标签,提升视觉层级区分度
- 影响文件:src/pages/feedback-list/index.vue
---
**详细信息**
- **影响文件**: src/pages/feedback-list/index.vue
- **技术栈**: Vue 3, Taro, TailwindCSS
- **测试状态**: ✅ 已通过
- **备注**:
- 增强了列表项中关键信息的辨识度
- 解决了类型和状态样式过于雷同的问题
---
## [2026-02-03] - 意见反馈模块完成
### 新增
......
......@@ -3,101 +3,105 @@
* @Description: 意见反馈列表页面
-->
<template>
<view class="min-h-screen bg-gray-50">
<NavHeader title="我的反馈" />
<scroll-view
scroll-y
class="feedback-scroll"
:style="{ height: scrollHeight + 'px' }"
@scrolltolower="onScrollToLower"
>
<view class="p-[32rpx] pb-[250rpx]">
<!-- Feedback List -->
<view v-if="loading" class="flex justify-center items-center py-[100rpx]">
<view class="loading-spinner"></view>
</view>
<view class="feedback-list">
<NavHeader title="意见反馈" />
<view v-else-if="feedbackList.length === 0" class="flex flex-col items-center py-[100rpx]">
<text class="text-gray-400 text-[28rpx]">暂无反馈记录</text>
</view>
<!-- Loading State -->
<view v-if="loading" class="flex justify-center items-center py-20">
<view class="loading-spinner"></view>
</view>
<view v-else class="space-y-[24rpx]">
<view
v-for="item in feedbackList"
:key="item.id"
class="bg-white rounded-[24rpx] p-[32rpx] shadow-sm"
>
<!-- Header: Type & Status -->
<view class="flex justify-between items-center mb-[20rpx]">
<view
class="px-[20rpx] py-[8rpx] rounded-full text-[24rpx]"
:class="getTypeClass(item.category)"
>
{{ getTypeLabel(item.category) }}
</view>
<!-- Content -->
<view v-else>
<!-- Feedback List -->
<view v-if="feedbackList.length > 0">
<view
v-for="item in feedbackList"
:key="item.id"
class="feedback-item"
>
<!-- Header: Type & Status -->
<view class="feedback-header">
<!-- Category Tag -->
<view
class="category-tag"
:class="getTypeClass(item.category)"
>
{{ getTypeLabel(item.category) }}
</view>
<!-- Status Indicator -->
<view class="flex items-center">
<view
class="px-[20rpx] py-[8rpx] rounded-full text-[24rpx]"
:class="item.status === 5 ? 'bg-green-100 text-green-600' : 'bg-orange-100 text-orange-600'"
>
class="w-[12rpx] h-[12rpx] rounded-full mr-[8rpx]"
:class="item.status === 5 ? 'bg-green-500' : 'bg-orange-500'"
></view>
<text class="text-[24rpx] font-medium" :class="item.status === 5 ? 'text-green-600' : 'text-orange-600'">
{{ item.status === 5 ? '已处理' : '待处理' }}
</view>
</text>
</view>
</view>
<!-- Content -->
<view class="text-[28rpx] text-gray-900 mb-[20rpx] leading-relaxed">
{{ item.note }}
</view>
<!-- Content -->
<view class="feedback-note">
{{ item.note }}
</view>
<!-- Images -->
<view v-if="item.images && item.images.length > 0" class="flex gap-[16rpx] mb-[20rpx]">
<image
v-for="(img, index) in item.images"
:key="index"
:src="img"
mode="aspectFill"
class="w-[120rpx] h-[120rpx] rounded-[12rpx]"
@tap="previewImage(item.images, index)"
/>
</view>
<!-- Images -->
<view v-if="item.images && item.images.length > 0" class="feedback-images">
<image
v-for="(img, index) in item.images"
:key="index"
:src="img"
mode="aspectFill"
class="feedback-image"
@tap="previewImage(item.images, index)"
/>
</view>
<!-- Contact -->
<view v-if="item.contact" class="text-[24rpx] text-gray-500 mb-[20rpx]">
联系方式:{{ item.contact }}
</view>
<!-- Contact -->
<view v-if="item.contact" class="text-[24rpx] text-gray-500 mb-[20rpx]">
联系方式:{{ item.contact }}
</view>
<!-- Reply Section -->
<view v-if="item.reply" class="bg-blue-50 rounded-[16rpx] p-[24rpx]">
<view class="text-[24rpx] text-gray-500 mb-[8rpx]">
客服回复:{{ item.reply_time || '' }}
</view>
<view class="text-[28rpx] text-gray-900 leading-relaxed">
{{ item.reply }}
</view>
<!-- Reply Section -->
<view v-if="item.reply" class="feedback-reply">
<view class="text-[24rpx] text-gray-500 mb-[8rpx]">
客服回复:{{ item.reply_time || '' }}
</view>
<view class="text-[28rpx] text-gray-900 leading-relaxed">
{{ item.reply }}
</view>
</view>
</view>
</view>
<!-- Load More -->
<view v-if="hasMore && !loading" class="flex justify-center mt-[40rpx]">
<nut-button type="default" size="small" @click="loadMore">
加载更多
</nut-button>
</view>
<!-- Empty State -->
<view v-else class="empty-state">
<view class="empty-icon">💬</view>
<view class="empty-title">暂无反馈记录</view>
<view class="empty-desc">您还没有提交过任何意见反馈</view>
</view>
<!-- Load More -->
<view v-if="hasMore && feedbackList.length > 0" class="load-more" @click="loadMore">
{{ loadingMore ? '加载中...' : '加载更多' }}
</view>
</scroll-view>
<!-- Fixed Bottom Button -->
<view class="fixed bottom-0 left-0 right-0 p-[32rpx] bg-white border-t border-gray-200">
<nut-button type="primary" block class="!h-[88rpx] !rounded-[44rpx] !text-[32rpx]" @click="goToFeedback">
反馈意见
</nut-button>
<!-- No More Data -->
<view v-if="!hasMore && feedbackList.length > 0" class="no-more">
没有更多数据了
</view>
</view>
<!-- Fixed Button -->
<view class="fixed-button" @click="goToFeedback">
反馈意见
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ref } from 'vue'
import { useGo } from '@/hooks/useGo'
import NavHeader from '@/components/NavHeader.vue'
import Taro, { useDidShow } from '@tarojs/taro'
......@@ -105,12 +109,11 @@ import { listAPI } from '@/api/feedback'
const go = useGo()
/** @type {import('vue').Ref<number>} 系统信息(用于计算滚动高度) */
const systemInfo = ref(null)
/** @type {import('vue').Ref<boolean>} 加载状态 */
const loading = ref(false)
const loadingMore = ref(false)
/** @type {import('vue').Ref<Array>} 反馈列表 */
const feedbackList = ref([])
......@@ -123,18 +126,6 @@ const pageSize = ref(10)
/** @type {import('vue').Ref<boolean>} 是否有更多数据 */
const hasMore = ref(true)
/** @type {import('vue').ComputedRef<number>} 滚动区域高度 */
const scrollHeight = computed(() => {
if (!systemInfo.value) return 500
// 导航栏高度 + 状态栏高度 + 底部按钮高度 + padding
const navBarHeight = 44 // 导航栏默认高度
const statusBarHeight = systemInfo.value.statusBarHeight || 0
const bottomHeight = 88 + 32 // 按钮高度 + padding
return systemInfo.value.windowHeight - bottomHeight
})
/**
* @description 获取反馈类型标签
* @param {string} category 类别值:1=功能建议, 3=问题反馈, 7=其他问题
......@@ -180,11 +171,17 @@ const previewImage = (urls, current) => {
* @param {boolean} isLoadMore 是否为加载更多
*/
const loadFeedbackList = async (isLoadMore = false) => {
if (loading.value) return
loading.value = true
if (loading.value || loadingMore.value) return
try {
if (isLoadMore) {
loadingMore.value = true
} else {
loading.value = true
currentPage.value = 0
feedbackList.value = []
}
const res = await listAPI({
page: currentPage.value,
limit: pageSize.value
......@@ -201,6 +198,10 @@ const loadFeedbackList = async (isLoadMore = false) => {
// 判断是否还有更多数据
hasMore.value = newList.length >= pageSize.value
if (hasMore.value) {
currentPage.value++
}
} else {
Taro.showToast({ title: res.msg || '加载失败', icon: 'none' })
}
......@@ -209,6 +210,7 @@ const loadFeedbackList = async (isLoadMore = false) => {
Taro.showToast({ title: '网络异常,请重试', icon: 'none' })
} finally {
loading.value = false
loadingMore.value = false
}
}
......@@ -216,17 +218,8 @@ const loadFeedbackList = async (isLoadMore = false) => {
* @description 加载更多
*/
const loadMore = () => {
if (!hasMore.value || loading.value) return
currentPage.value++
loadFeedbackList(true)
}
/**
* @description 滚动到底部时自动加载
*/
const onScrollToLower = () => {
if (hasMore.value && !loading.value) {
loadMore()
if (!hasMore.value || loadingMore.value) {
loadFeedbackList(true)
}
}
......@@ -238,44 +231,132 @@ const goToFeedback = () => {
}
/**
* @description 页面首次加载时获取系统信息
*/
onMounted(() => {
// 获取系统信息
Taro.getSystemInfo({
success: (res) => {
systemInfo.value = res
},
fail: () => {
// 使用默认值
systemInfo.value = {
windowHeight: 667,
statusBarHeight: 44
}
}
})
})
/**
* @description 页面显示时刷新列表(从提交页返回时也会触发)
*/
useDidShow(() => {
// 重置为第一页
currentPage.value = 0
feedbackList.value = []
// 加载反馈列表
loadFeedbackList()
})
</script>
<style lang="less">
.feedback-scroll {
box-sizing: border-box;
.feedback-list {
min-height: 100vh;
background-color: #f9fafb;
padding-bottom: 160rpx; // 为固定按钮留出空间
.feedback-item {
background: white;
border-radius: 24rpx;
margin: 16rpx 32rpx;
padding: 32rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
&:active {
transform: scale(0.98);
}
.feedback-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
.category-tag {
padding: 6rpx 16rpx;
border-radius: 8rpx;
font-size: 22rpx;
font-weight: 500;
}
}
.feedback-note {
font-size: 28rpx;
color: #333;
line-height: 1.6;
margin-bottom: 16rpx;
}
.feedback-images {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-bottom: 16rpx;
.feedback-image {
width: 120rpx;
height: 120rpx;
border-radius: 12rpx;
overflow: hidden;
}
}
.feedback-reply {
background-color: #f0f9ff;
border-radius: 16rpx;
padding: 24rpx;
margin-top: 16rpx;
}
}
.empty-state {
text-align: center;
padding: 120rpx 32rpx;
.empty-icon {
font-size: 120rpx;
color: #d1d5db;
margin-bottom: 32rpx;
}
.empty-title {
font-size: 36rpx;
color: #6b7280;
margin-bottom: 16rpx;
}
.empty-desc {
font-size: 28rpx;
color: #9ca3af;
}
}
.load-more {
text-align: center;
padding: 32rpx;
color: #3b82f6;
font-size: 28rpx;
}
.no-more {
text-align: center;
padding: 32rpx;
color: #9ca3af;
font-size: 28rpx;
}
}
.space-y-\[24rpx\] > * + * {
margin-top: 24rpx;
// 固定按钮样式
.fixed-button {
position: fixed;
bottom: 32rpx;
left: 32rpx;
right: 32rpx;
background: linear-gradient(135deg, #1e40af, #2563eb);
color: white;
border-radius: 24rpx;
padding: 25rpx;
text-align: center;
font-size: 32rpx;
font-weight: 600;
box-shadow: 0 8rpx 24rpx rgba(37, 99, 235, 0.3);
z-index: 1000;
transition: all 0.3s ease;
&:active {
transform: scale(0.95);
box-shadow: 0 4rpx 12rpx rgba(37, 99, 235, 0.3);
}
}
.loading-spinner {
......