index.vue 8.67 KB
<!--
 * @Date: 2026-02-03
 * @Description: 意见反馈列表页面
-->
<template>
  <view class="feedback-list">
    <NavHeader title="意见反馈" />

    <!-- Loading State -->
    <view v-if="loading" class="flex justify-center items-center py-20">
      <view class="loading-spinner"></view>
    </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="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 ? '已处理' : '待处理' }}
              </text>
            </view>
          </view>

          <!-- Content -->
          <view class="feedback-note">
            {{ item.note }}
          </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>

          <!-- 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>

      <!-- Empty State -->
      <view v-else class="empty-state">
        <nut-empty description="暂无反馈记录" image="empty">
          <view class="text-[#9ca3af] text-[24rpx] mt-[10rpx]">您还没有提交过任何意见反馈</view>
        </nut-empty>
      </view>

      <!-- Load More -->
      <view v-if="hasMore && feedbackList.length > 0" class="load-more" @click="loadMore">
        {{ loadingMore ? '加载中...' : '加载更多' }}
      </view>

      <!-- 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 } 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<boolean>} 加载状态 */
const loading = ref(false)

const loadingMore = 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)

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

    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

      if (hasMore.value) {
        currentPage.value++
      }
    } else {
      Taro.showToast({ title: res.msg || '加载失败', icon: 'none' })
    }
  } catch (err) {
    console.error('加载反馈列表失败:', err)
    Taro.showToast({ title: '网络异常,请重试', icon: 'none' })
  } finally {
    loading.value = false
    loadingMore.value = false
  }
}

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

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

/**
 * @description 页面显示时刷新列表(从提交页返回时也会触发)
 */
useDidShow(() => {
  loadFeedbackList()
})
</script>

<style lang="less">
.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;
  }
}

// 固定按钮样式
.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 {
  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>