index.vue 9.46 KB
<!--
 * @Date: 2026-02-08
 * @Description: 意见反馈列表页面 - 使用 LoadMoreList 组件重构版本
-->
<template>
  <view class="feedback-list">
    <NavHeader title="意见反馈" />

    <!-- LoadMoreList 组件 -->
    <LoadMoreList
      :list="currentList"
      :page="currentPage"
      :page-size="pageSize"
      :has-more="hasMore"
      :loading="loading"
      :loading-more="loadingMore"
      key-field="id"
      @load-more="handleLoadMore"
    >
      <!-- 列表项 -->
      <template #item="{ item }">
        <view 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>
      </template>
    </LoadMoreList>

    <!-- Fixed Button -->
    <view class="fixed-button" @click="goToFeedback">
      反馈意见
    </view>
  </view>
</template>

<script setup>
import { ref, Ref, onMounted, onUnmounted } from 'vue'
import { useGo } from '@/hooks/useGo'
import LoadMoreList from '@/components/LoadMoreList'
import NavHeader from '@/components/NavHeader.vue'
import Taro, { useLoad } from '@tarojs/taro'
import { listAPI } from '@/api/feedback'
import { mockFeedbackListAPI } from '@/utils/mockData'
import eventBus, { Events } from '@/utils/eventBus'

// ⚠️ MOCK 数据开关 - 开发环境使用 mock 数据,生产环境使用真实 API
const USE_MOCK_DATA = process.env.NODE_ENV === 'development'

const go = useGo()

/**
 * 当前列表数据
 * @type {Ref<Array<any>>}
 */
const currentList = ref([])

/**
 * 当前页码(从0开始)
 * @type {Ref<number>}
 */
const currentPage = ref(0)

/**
 * 每页数量
 * @type {number}
 */
const pageSize = 10

/**
 * 是否还有更多数据
 * @type {Ref<boolean>}
 */
const hasMore = ref(true)

/**
 * 首次加载状态
 * @type {Ref<boolean>}
 */
const loading = ref(false)

/**
 * 加载更多状态
 * @type {Ref<boolean>}
 */
const loadingMore = ref(false)

/**
 * @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 {Object} params - 请求参数
 * @param {number} params.page - 页码(从0开始)
 * @param {number} params.limit - 每页数量
 * @param {boolean} isLoadMore - 是否为加载更多
 * @returns {Promise<void>}
 */
const loadFeedbackList = async (params = {}, isLoadMore = false) => {
  if (loading.value || loadingMore.value) return

  try {
    if (isLoadMore) {
      loadingMore.value = true
    } else {
      loading.value = true
      currentPage.value = 0
      currentList.value = []
    }

    console.log('[Feedback] 请求参数:', params)
    console.log('[Feedback] 使用 Mock 数据:', USE_MOCK_DATA)

    // 根据开关选择使用真实 API 或 Mock 数据
    const res = USE_MOCK_DATA
      ? await mockFeedbackListAPI(params)
      : await listAPI({
          page: String(params.page),
          limit: String(params.limit)
        })

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

      console.log('[Feedback] 数据:', newList)

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

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

/**
 * @description 处理加载更多事件
 * @param {number} page - 下一页页码
 * @returns {Promise<void>}
 */
const handleLoadMore = async (page) => {
  console.log('[Feedback] 加载更多,页码:', page)

  // 更新页码
  currentPage.value = page

  // 加载下一页数据
  await loadFeedbackList(
    { page: page, limit: pageSize },
    true  // 标记为加载更多
  )
}

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

/**
 * @description 刷新反馈列表
 * @returns {Promise<void>}
 */
const refreshList = async () => {
  console.log('[Feedback] 刷新列表')

  // 重置分页状态
  currentPage.value = 0
  hasMore.value = true

  // 重新获取列表
  await loadFeedbackList({ page: 0, limit: pageSize })
}

/**
 * @description 页面加载时获取列表(只执行一次)
 */
useLoad(() => {
  console.log('[Feedback] 页面加载,获取列表')

  // 重置分页状态
  currentPage.value = 0
  hasMore.value = true

  // 获取反馈列表
  loadFeedbackList({ page: 0, limit: pageSize })
})

/**
 * @description 监听反馈提交成功事件
 */
onMounted(() => {
  console.log('[Feedback] 注册事件监听')

  // 监听反馈提交成功事件
  const unsubscribe = eventBus.on(Events.FEEDBACK_SUBMIT, async (data) => {
    console.log('[Feedback] 收到反馈提交事件:', data)

    // 刷新列表
    await refreshList()
  })

  // 组件卸载时取消监听
  onUnmounted(() => {
    unsubscribe()
    console.log('[Feedback] 取消事件监听')
  })
})
</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;
    }
  }
}

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

/* LoadMoreList 组件已内置样式,包括 Loading 和 Empty State */
</style>