index.vue 7.72 KB
<!--
 * @Date: 2026-02-03
 * @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 v-else-if="feedbackList.length === 0" class="flex flex-col items-center py-[100rpx]">
          <text class="text-gray-400 text-[28rpx]">暂无反馈记录</text>
        </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>
              <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'"
              >
                {{ item.status === 5 ? '已处理' : '待处理' }}
              </view>
            </view>

            <!-- Content -->
            <view class="text-[28rpx] text-gray-900 mb-[20rpx] leading-relaxed">
              {{ 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>

            <!-- 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>
            </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>
      </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>
    </view>
  </view>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue'
import { useGo } from '@/hooks/useGo'
import NavHeader from '@/components/NavHeader.vue'
import Taro, { useDidShow } from '@tarojs/taro'
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)

/** @type {import('vue').Ref<Array>} 反馈列表 */
const feedbackList = ref([])

/** @type {import('vue').Ref<number>} 当前页码 */
const currentPage = ref(0)

/** @type {import('vue').Ref<number>} 每页数量 */
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=其他问题
 * @returns {string} 类别标签
 */
const getTypeLabel = (category) => {
  const map = {
    '1': '功能建议',
    '3': '问题反馈',
    '7': '其他问题'
  }
  return map[category] || '其他'
}

/**
 * @description 获取反馈类型样式类
 * @param {string} category 类别值
 * @returns {string} 样式类名
 */
const getTypeClass = (category) => {
  const map = {
    '1': 'bg-blue-100 text-blue-600',
    '3': 'bg-red-100 text-red-600',
    '7': 'bg-gray-100 text-gray-600'
  }
  return map[category] || 'bg-gray-100 text-gray-600'
}

/**
 * @description 预览图片
 * @param {Array<string>} urls 图片 URL 列表
 * @param {number} current 当前图片索引
 */
const previewImage = (urls, current) => {
  Taro.previewImage({
    current: urls[current],
    urls: urls
  })
}

/**
 * @description 加载反馈列表
 * @param {boolean} isLoadMore 是否为加载更多
 */
const loadFeedbackList = async (isLoadMore = false) => {
  if (loading.value) return

  loading.value = true

  try {
    const res = await listAPI({
      page: currentPage.value,
      limit: pageSize.value
    })

    if (res.code === 1) {
      const newList = res.data.list || []

      if (isLoadMore) {
        feedbackList.value.push(...newList)
      } else {
        feedbackList.value = newList
      }

      // 判断是否还有更多数据
      hasMore.value = newList.length >= pageSize.value
    } else {
      Taro.showToast({ title: res.msg || '加载失败', icon: 'none' })
    }
  } catch (err) {
    console.error('加载反馈列表失败:', err)
    Taro.showToast({ title: '网络异常,请重试', icon: 'none' })
  } finally {
    loading.value = false
  }
}

/**
 * @description 加载更多
 */
const loadMore = () => {
  if (!hasMore.value || loading.value) return
  currentPage.value++
  loadFeedbackList(true)
}

/**
 * @description 滚动到底部时自动加载
 */
const onScrollToLower = () => {
  if (hasMore.value && !loading.value) {
    loadMore()
  }
}

/**
 * @description 跳转到反馈提交页面
 */
const goToFeedback = () => {
  go('/pages/feedback/index')
}

/**
 * @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;
}

.space-y-\[24rpx\] > * + * {
  margin-top: 24rpx;
}

.loading-spinner {
  width: 40rpx;
  height: 40rpx;
  border: 4rpx solid #f3f3f3;
  border-top: 4rpx solid #3498db;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}
</style>