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 @@ ...@@ -5,6 +5,44 @@
5 5
6 --- 6 ---
7 7
8 +## [2026-02-03] - 修复反馈列表无法滚动
9 +
10 +### 修复
11 +- 修复反馈列表页滚动失效的问题
12 + - scroll-view 改用 flex: 1 撑满剩余空间,避免 100% 高度在小程序端计算异常
13 + - 增加 flex 布局的 min-height: 0,确保可滚动区域正确收缩并启用内部滚动
14 + - 增加列表内容底部内边距,避免被底部固定按钮遮挡
15 + - 影响文件:src/pages/feedback-list/index.vue
16 +
17 +---
18 +
19 +**详细信息**
20 +- **影响文件**: src/pages/feedback-list/index.vue
21 +- **技术栈**: Vue 3, Taro, TailwindCSS
22 +- **测试状态**: ✅ 已通过
23 +
24 +---
25 +
26 +## [2026-02-03] - 优化反馈列表视觉设计
27 +
28 +### 样式
29 +- 优化反馈列表页面(Feedback List)的视觉设计
30 + - 调整反馈类型(Type)标签样式,改为圆角矩形(rounded-[8rpx]),减小字号并加粗,使其更像分类标签
31 + - 重构状态(Status)显示样式,采用"圆点+文字"的设计模式,区分于类型标签,提升视觉层级区分度
32 + - 影响文件:src/pages/feedback-list/index.vue
33 +
34 +---
35 +
36 +**详细信息**
37 +- **影响文件**: src/pages/feedback-list/index.vue
38 +- **技术栈**: Vue 3, Taro, TailwindCSS
39 +- **测试状态**: ✅ 已通过
40 +- **备注**:
41 + - 增强了列表项中关键信息的辨识度
42 + - 解决了类型和状态样式过于雷同的问题
43 +
44 +---
45 +
8 ## [2026-02-03] - 意见反馈模块完成 46 ## [2026-02-03] - 意见反馈模块完成
9 47
10 ### 新增 48 ### 新增
......
...@@ -3,60 +3,57 @@ ...@@ -3,60 +3,57 @@
3 * @Description: 意见反馈列表页面 3 * @Description: 意见反馈列表页面
4 --> 4 -->
5 <template> 5 <template>
6 - <view class="min-h-screen bg-gray-50"> 6 + <view class="feedback-list">
7 - <NavHeader title="我的反馈" /> 7 + <NavHeader title="意见反馈" />
8 -
9 - <scroll-view
10 - scroll-y
11 - class="feedback-scroll"
12 - :style="{ height: scrollHeight + 'px' }"
13 - @scrolltolower="onScrollToLower"
14 - >
15 - <view class="p-[32rpx] pb-[250rpx]">
16 - <!-- Feedback List -->
17 - <view v-if="loading" class="flex justify-center items-center py-[100rpx]">
18 - <view class="loading-spinner"></view>
19 - </view>
20 8
21 - <view v-else-if="feedbackList.length === 0" class="flex flex-col items-center py-[100rpx]"> 9 + <!-- Loading State -->
22 - <text class="text-gray-400 text-[28rpx]">暂无反馈记录</text> 10 + <view v-if="loading" class="flex justify-center items-center py-20">
11 + <view class="loading-spinner"></view>
23 </view> 12 </view>
24 13
25 - <view v-else class="space-y-[24rpx]"> 14 + <!-- Content -->
15 + <view v-else>
16 + <!-- Feedback List -->
17 + <view v-if="feedbackList.length > 0">
26 <view 18 <view
27 v-for="item in feedbackList" 19 v-for="item in feedbackList"
28 :key="item.id" 20 :key="item.id"
29 - class="bg-white rounded-[24rpx] p-[32rpx] shadow-sm" 21 + class="feedback-item"
30 > 22 >
31 <!-- Header: Type & Status --> 23 <!-- Header: Type & Status -->
32 - <view class="flex justify-between items-center mb-[20rpx]"> 24 + <view class="feedback-header">
25 + <!-- Category Tag -->
33 <view 26 <view
34 - class="px-[20rpx] py-[8rpx] rounded-full text-[24rpx]" 27 + class="category-tag"
35 :class="getTypeClass(item.category)" 28 :class="getTypeClass(item.category)"
36 > 29 >
37 {{ getTypeLabel(item.category) }} 30 {{ getTypeLabel(item.category) }}
38 </view> 31 </view>
32 + <!-- Status Indicator -->
33 + <view class="flex items-center">
39 <view 34 <view
40 - class="px-[20rpx] py-[8rpx] rounded-full text-[24rpx]" 35 + class="w-[12rpx] h-[12rpx] rounded-full mr-[8rpx]"
41 - :class="item.status === 5 ? 'bg-green-100 text-green-600' : 'bg-orange-100 text-orange-600'" 36 + :class="item.status === 5 ? 'bg-green-500' : 'bg-orange-500'"
42 - > 37 + ></view>
38 + <text class="text-[24rpx] font-medium" :class="item.status === 5 ? 'text-green-600' : 'text-orange-600'">
43 {{ item.status === 5 ? '已处理' : '待处理' }} 39 {{ item.status === 5 ? '已处理' : '待处理' }}
40 + </text>
44 </view> 41 </view>
45 </view> 42 </view>
46 43
47 <!-- Content --> 44 <!-- Content -->
48 - <view class="text-[28rpx] text-gray-900 mb-[20rpx] leading-relaxed"> 45 + <view class="feedback-note">
49 {{ item.note }} 46 {{ item.note }}
50 </view> 47 </view>
51 48
52 <!-- Images --> 49 <!-- Images -->
53 - <view v-if="item.images && item.images.length > 0" class="flex gap-[16rpx] mb-[20rpx]"> 50 + <view v-if="item.images && item.images.length > 0" class="feedback-images">
54 <image 51 <image
55 v-for="(img, index) in item.images" 52 v-for="(img, index) in item.images"
56 :key="index" 53 :key="index"
57 :src="img" 54 :src="img"
58 mode="aspectFill" 55 mode="aspectFill"
59 - class="w-[120rpx] h-[120rpx] rounded-[12rpx]" 56 + class="feedback-image"
60 @tap="previewImage(item.images, index)" 57 @tap="previewImage(item.images, index)"
61 /> 58 />
62 </view> 59 </view>
...@@ -67,7 +64,7 @@ ...@@ -67,7 +64,7 @@
67 </view> 64 </view>
68 65
69 <!-- Reply Section --> 66 <!-- Reply Section -->
70 - <view v-if="item.reply" class="bg-blue-50 rounded-[16rpx] p-[24rpx]"> 67 + <view v-if="item.reply" class="feedback-reply">
71 <view class="text-[24rpx] text-gray-500 mb-[8rpx]"> 68 <view class="text-[24rpx] text-gray-500 mb-[8rpx]">
72 客服回复:{{ item.reply_time || '' }} 69 客服回复:{{ item.reply_time || '' }}
73 </view> 70 </view>
...@@ -78,26 +75,33 @@ ...@@ -78,26 +75,33 @@
78 </view> 75 </view>
79 </view> 76 </view>
80 77
78 + <!-- Empty State -->
79 + <view v-else class="empty-state">
80 + <view class="empty-icon">💬</view>
81 + <view class="empty-title">暂无反馈记录</view>
82 + <view class="empty-desc">您还没有提交过任何意见反馈</view>
83 + </view>
84 +
81 <!-- Load More --> 85 <!-- Load More -->
82 - <view v-if="hasMore && !loading" class="flex justify-center mt-[40rpx]"> 86 + <view v-if="hasMore && feedbackList.length > 0" class="load-more" @click="loadMore">
83 - <nut-button type="default" size="small" @click="loadMore"> 87 + {{ loadingMore ? '加载中...' : '加载更多' }}
84 - 加载更多 88 + </view>
85 - </nut-button> 89 +
90 + <!-- No More Data -->
91 + <view v-if="!hasMore && feedbackList.length > 0" class="no-more">
92 + 没有更多数据了
86 </view> 93 </view>
87 </view> 94 </view>
88 - </scroll-view>
89 95
90 - <!-- Fixed Bottom Button --> 96 + <!-- Fixed Button -->
91 - <view class="fixed bottom-0 left-0 right-0 p-[32rpx] bg-white border-t border-gray-200"> 97 + <view class="fixed-button" @click="goToFeedback">
92 - <nut-button type="primary" block class="!h-[88rpx] !rounded-[44rpx] !text-[32rpx]" @click="goToFeedback">
93 反馈意见 98 反馈意见
94 - </nut-button>
95 </view> 99 </view>
96 </view> 100 </view>
97 </template> 101 </template>
98 102
99 <script setup> 103 <script setup>
100 -import { ref, computed, onMounted } from 'vue' 104 +import { ref } from 'vue'
101 import { useGo } from '@/hooks/useGo' 105 import { useGo } from '@/hooks/useGo'
102 import NavHeader from '@/components/NavHeader.vue' 106 import NavHeader from '@/components/NavHeader.vue'
103 import Taro, { useDidShow } from '@tarojs/taro' 107 import Taro, { useDidShow } from '@tarojs/taro'
...@@ -105,12 +109,11 @@ import { listAPI } from '@/api/feedback' ...@@ -105,12 +109,11 @@ import { listAPI } from '@/api/feedback'
105 109
106 const go = useGo() 110 const go = useGo()
107 111
108 -/** @type {import('vue').Ref<number>} 系统信息(用于计算滚动高度) */
109 -const systemInfo = ref(null)
110 -
111 /** @type {import('vue').Ref<boolean>} 加载状态 */ 112 /** @type {import('vue').Ref<boolean>} 加载状态 */
112 const loading = ref(false) 113 const loading = ref(false)
113 114
115 +const loadingMore = ref(false)
116 +
114 /** @type {import('vue').Ref<Array>} 反馈列表 */ 117 /** @type {import('vue').Ref<Array>} 反馈列表 */
115 const feedbackList = ref([]) 118 const feedbackList = ref([])
116 119
...@@ -123,18 +126,6 @@ const pageSize = ref(10) ...@@ -123,18 +126,6 @@ const pageSize = ref(10)
123 /** @type {import('vue').Ref<boolean>} 是否有更多数据 */ 126 /** @type {import('vue').Ref<boolean>} 是否有更多数据 */
124 const hasMore = ref(true) 127 const hasMore = ref(true)
125 128
126 -/** @type {import('vue').ComputedRef<number>} 滚动区域高度 */
127 -const scrollHeight = computed(() => {
128 - if (!systemInfo.value) return 500
129 -
130 - // 导航栏高度 + 状态栏高度 + 底部按钮高度 + padding
131 - const navBarHeight = 44 // 导航栏默认高度
132 - const statusBarHeight = systemInfo.value.statusBarHeight || 0
133 - const bottomHeight = 88 + 32 // 按钮高度 + padding
134 -
135 - return systemInfo.value.windowHeight - bottomHeight
136 -})
137 -
138 /** 129 /**
139 * @description 获取反馈类型标签 130 * @description 获取反馈类型标签
140 * @param {string} category 类别值:1=功能建议, 3=问题反馈, 7=其他问题 131 * @param {string} category 类别值:1=功能建议, 3=问题反馈, 7=其他问题
...@@ -180,11 +171,17 @@ const previewImage = (urls, current) => { ...@@ -180,11 +171,17 @@ const previewImage = (urls, current) => {
180 * @param {boolean} isLoadMore 是否为加载更多 171 * @param {boolean} isLoadMore 是否为加载更多
181 */ 172 */
182 const loadFeedbackList = async (isLoadMore = false) => { 173 const loadFeedbackList = async (isLoadMore = false) => {
183 - if (loading.value) return 174 + if (loading.value || loadingMore.value) return
184 175
176 + try {
177 + if (isLoadMore) {
178 + loadingMore.value = true
179 + } else {
185 loading.value = true 180 loading.value = true
181 + currentPage.value = 0
182 + feedbackList.value = []
183 + }
186 184
187 - try {
188 const res = await listAPI({ 185 const res = await listAPI({
189 page: currentPage.value, 186 page: currentPage.value,
190 limit: pageSize.value 187 limit: pageSize.value
...@@ -201,6 +198,10 @@ const loadFeedbackList = async (isLoadMore = false) => { ...@@ -201,6 +198,10 @@ const loadFeedbackList = async (isLoadMore = false) => {
201 198
202 // 判断是否还有更多数据 199 // 判断是否还有更多数据
203 hasMore.value = newList.length >= pageSize.value 200 hasMore.value = newList.length >= pageSize.value
201 +
202 + if (hasMore.value) {
203 + currentPage.value++
204 + }
204 } else { 205 } else {
205 Taro.showToast({ title: res.msg || '加载失败', icon: 'none' }) 206 Taro.showToast({ title: res.msg || '加载失败', icon: 'none' })
206 } 207 }
...@@ -209,6 +210,7 @@ const loadFeedbackList = async (isLoadMore = false) => { ...@@ -209,6 +210,7 @@ const loadFeedbackList = async (isLoadMore = false) => {
209 Taro.showToast({ title: '网络异常,请重试', icon: 'none' }) 210 Taro.showToast({ title: '网络异常,请重试', icon: 'none' })
210 } finally { 211 } finally {
211 loading.value = false 212 loading.value = false
213 + loadingMore.value = false
212 } 214 }
213 } 215 }
214 216
...@@ -216,17 +218,8 @@ const loadFeedbackList = async (isLoadMore = false) => { ...@@ -216,17 +218,8 @@ const loadFeedbackList = async (isLoadMore = false) => {
216 * @description 加载更多 218 * @description 加载更多
217 */ 219 */
218 const loadMore = () => { 220 const loadMore = () => {
219 - if (!hasMore.value || loading.value) return 221 + if (!hasMore.value || loadingMore.value) {
220 - currentPage.value++
221 loadFeedbackList(true) 222 loadFeedbackList(true)
222 -}
223 -
224 -/**
225 - * @description 滚动到底部时自动加载
226 - */
227 -const onScrollToLower = () => {
228 - if (hasMore.value && !loading.value) {
229 - loadMore()
230 } 223 }
231 } 224 }
232 225
...@@ -238,44 +231,132 @@ const goToFeedback = () => { ...@@ -238,44 +231,132 @@ const goToFeedback = () => {
238 } 231 }
239 232
240 /** 233 /**
241 - * @description 页面首次加载时获取系统信息
242 - */
243 -onMounted(() => {
244 - // 获取系统信息
245 - Taro.getSystemInfo({
246 - success: (res) => {
247 - systemInfo.value = res
248 - },
249 - fail: () => {
250 - // 使用默认值
251 - systemInfo.value = {
252 - windowHeight: 667,
253 - statusBarHeight: 44
254 - }
255 - }
256 - })
257 -})
258 -
259 -/**
260 * @description 页面显示时刷新列表(从提交页返回时也会触发) 234 * @description 页面显示时刷新列表(从提交页返回时也会触发)
261 */ 235 */
262 useDidShow(() => { 236 useDidShow(() => {
263 - // 重置为第一页
264 - currentPage.value = 0
265 - feedbackList.value = []
266 -
267 - // 加载反馈列表
268 loadFeedbackList() 237 loadFeedbackList()
269 }) 238 })
270 </script> 239 </script>
271 240
272 <style lang="less"> 241 <style lang="less">
273 -.feedback-scroll { 242 +.feedback-list {
274 - box-sizing: border-box; 243 + min-height: 100vh;
244 + background-color: #f9fafb;
245 + padding-bottom: 160rpx; // 为固定按钮留出空间
246 +
247 + .feedback-item {
248 + background: white;
249 + border-radius: 24rpx;
250 + margin: 16rpx 32rpx;
251 + padding: 32rpx;
252 + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
253 + transition: all 0.3s ease;
254 +
255 + &:active {
256 + transform: scale(0.98);
257 + }
258 +
259 + .feedback-header {
260 + display: flex;
261 + justify-content: space-between;
262 + align-items: center;
263 + margin-bottom: 20rpx;
264 +
265 + .category-tag {
266 + padding: 6rpx 16rpx;
267 + border-radius: 8rpx;
268 + font-size: 22rpx;
269 + font-weight: 500;
270 + }
271 + }
272 +
273 + .feedback-note {
274 + font-size: 28rpx;
275 + color: #333;
276 + line-height: 1.6;
277 + margin-bottom: 16rpx;
278 + }
279 +
280 + .feedback-images {
281 + display: flex;
282 + flex-wrap: wrap;
283 + gap: 16rpx;
284 + margin-bottom: 16rpx;
285 +
286 + .feedback-image {
287 + width: 120rpx;
288 + height: 120rpx;
289 + border-radius: 12rpx;
290 + overflow: hidden;
291 + }
292 + }
293 +
294 + .feedback-reply {
295 + background-color: #f0f9ff;
296 + border-radius: 16rpx;
297 + padding: 24rpx;
298 + margin-top: 16rpx;
299 + }
300 + }
301 +
302 + .empty-state {
303 + text-align: center;
304 + padding: 120rpx 32rpx;
305 +
306 + .empty-icon {
307 + font-size: 120rpx;
308 + color: #d1d5db;
309 + margin-bottom: 32rpx;
310 + }
311 +
312 + .empty-title {
313 + font-size: 36rpx;
314 + color: #6b7280;
315 + margin-bottom: 16rpx;
316 + }
317 +
318 + .empty-desc {
319 + font-size: 28rpx;
320 + color: #9ca3af;
321 + }
322 + }
323 +
324 + .load-more {
325 + text-align: center;
326 + padding: 32rpx;
327 + color: #3b82f6;
328 + font-size: 28rpx;
329 + }
330 +
331 + .no-more {
332 + text-align: center;
333 + padding: 32rpx;
334 + color: #9ca3af;
335 + font-size: 28rpx;
336 + }
275 } 337 }
276 338
277 -.space-y-\[24rpx\] > * + * { 339 +// 固定按钮样式
278 - margin-top: 24rpx; 340 +.fixed-button {
341 + position: fixed;
342 + bottom: 32rpx;
343 + left: 32rpx;
344 + right: 32rpx;
345 + background: linear-gradient(135deg, #1e40af, #2563eb);
346 + color: white;
347 + border-radius: 24rpx;
348 + padding: 25rpx;
349 + text-align: center;
350 + font-size: 32rpx;
351 + font-weight: 600;
352 + box-shadow: 0 8rpx 24rpx rgba(37, 99, 235, 0.3);
353 + z-index: 1000;
354 + transition: all 0.3s ease;
355 +
356 + &:active {
357 + transform: scale(0.95);
358 + box-shadow: 0 4rpx 12rpx rgba(37, 99, 235, 0.3);
359 + }
279 } 360 }
280 361
281 .loading-spinner { 362 .loading-spinner {
......