You need to sign in or sign up before continuing.
index.vue 9.34 KB
<!--
 * @Date:2026-02-08
 * @Description: 我的消息页 - 使用 LoadMoreList 组件重构版本
 * @Update:2026-02-13 API 新增 title 字段,直接使用 API 返回的标题
-->
<template>
  <LoadMoreList
    :list="currentList"
    :page="currentPage"
    :page-size="pageSize"
    :has-more="hasMore"
    :loading="loading"
    :loading-more="loadingMore"
    :enable-pull-down-refresh="true"
    key-field="id"
    :has-footer="false"
    @load-more="handleLoadMore"
    @refresh="handleRefresh"
  >
    <!-- 头部 -->
    <template #header>
      <NavHeader title="我的消息" />
    </template>

    <!-- 列表项 -->
    <template #item="{ item }">
      <view
        class="message-item bg-white rounded-xl p-5 mb-3 shadow-sm active:scale-[0.98] transition-all duration-200 border border-gray-100"
        @tap="handleItemClick(item)"
      >
        <!-- 顶部:标题与红点 -->
        <view class="flex justify-between items-start mb-2">
          <view class="flex-1 mr-2 relative">
            <!-- 未读红点 -->
            <view v-if="item.status === 'send'" class="absolute -left-2 top-1.5 w-1.5 h-1.5 bg-red-500 rounded-full"></view>
            <!-- 标题:优先使用 API 返回的 title,降级使用 note 第一行 -->
            <text class="text-lg font-bold text-gray-900 line-clamp-1 leading-snug">
              {{ item.title || getItemTitle(item.note) }}
            </text>
          </view>

          <!-- 状态标签 -->
          <view v-if="item.status === 'send'" class="shrink-0 px-2 py-1 bg-red-50 text-red-600 rounded text-xs font-medium border border-red-100">
            未读
          </view>
          <view v-else-if="item.status === 'read'" class="shrink-0 px-2 py-1 bg-gray-50 text-gray-400 rounded text-xs border border-gray-100">
            已读
          </view>
        </view>

        <!-- 中间:内容预览 -->
        <view class="mb-4">
          <text class="text-sm text-gray-500 line-clamp-2 leading-relaxed">
            {{ getItemPreview(item.note) }}
          </text>
        </view>

        <!-- 底部:时间 -->
        <view class="flex items-center pt-3 border-t border-gray-50">
          <view class="flex items-center text-xs text-gray-400">
            <IconFont name="clock" size="12" color="#9CA3AF" class="mr-1" />
            <text>{{ item.created_time }}</text>
          </view>
        </view>
      </view>
    </template>

    <!-- 空状态 -->
    <template #empty>
      <nut-empty description="暂无消息" image="empty" />
    </template>
  </LoadMoreList>
</template>

<script setup>
import Taro from '@tarojs/taro'
import { ref } from 'vue'
import { useLoad, useDidShow } from '@tarojs/taro'
import { useGo } from '@/hooks/useGo'
import LoadMoreList from '@/components/list/LoadMoreList'
import NavHeader from '@/components/navigation/NavHeader.vue'
import IconFont from '@/components/icons/IconFont.vue'
import { myListAPI } from '@/api/news'
import { mockMessageListAPI } from '@/utils/mockData'
import { USE_MOCK_DATA } from '@/config/app'

// ⚠️ MOCK 数据开关 - 统一从 @/config/app 导入
// const USE_MOCK_DATA = process.env.NODE_ENV === 'development'

const go = useGo()

// 响应式状态
const currentList = ref([])
const currentPage = ref(0)
const pageSize = 10
const hasMore = ref(true)
const loading = ref(false)
const loadingMore = ref(false)

// 标记:是否首次加载(用于区分 useLoad 和 useDidShow)
const isFirstLoad = ref(true)

/**
 * 提取消息标题(降级方案:从 note 第一行提取)
 *
 * @description 当 API 未返回 title 时,从 note 内容的第一行提取标题
 * @param {string} note - 消息内容
 * @returns {string} 标题
 *
 * @example
 * // API 已返回 title
 * getItemTitle(note)  // 不使用,直接显示 item.title
 * // API 未返回 title(降级)
 * getItemTitle('这是第一行标题\n这是内容')  // 返回: '这是第一行标题'
 */
const getItemTitle = (note) => {
  if (!note) return '暂无消息内容'

  // 提取第一行作为标题
  const firstLine = note.split('\n')[0]

  // 移除富文本标签(简单处理)
  const textOnly = firstLine.replace(/<[^>]+>/g, '').trim()

  // 如果第一行太长,截取前 50 个字符
  return textOnly.length > 50 ? textOnly.substring(0, 50) + '...' : textOnly
}

/**
 * 提取消息预览
 *
 * @description 移除第一行标题后的内容作为预览
 * @param {string} note - 消息内容
 * @returns {string} 预览内容
 *
 * @example
 * getItemPreview('标题\n内容第二行\n内容第三行')  // 返回: '内容第二行\n内容第三行'
 * getItemPreview('只有单行内容')  // 返回: '点击查看详情'
 */
const getItemPreview = (note) => {
  if (!note) return '点击查看详情'

  // 移除第一行(已作为标题显示)
  const lines = note.split('\n')
  if (lines.length > 1) {
    // 移除富文本标签(简单处理)
    const preview = lines.slice(1).join('\n').replace(/<[^>]+>/g, '').trim()
    return preview || '点击查看详情'
  }

  return '点击查看详情'  // 只有一行时
}

/**
 * 获取消息列表
 *
 * @param {Object} params - 请求参数
 * @param {number} params.page - 页码(从0开始)
 * @param {number} params.limit - 每页数量
 * @param {boolean} isLoadMore - 是否为加载更多
 * @returns {Promise<void>}
 */
const fetchMessageList = async (params = {}, isLoadMore = false) => {
  try {
    // 如果是加载更多,使用 loadingMore 状态,否则使用 loading 状态
    if (isLoadMore) {
      loadingMore.value = true
    } else {
      loading.value = true
    }

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

    // 根据开关选择使用真实 API 或 Mock 数据
    const res = USE_MOCK_DATA
      ? await mockMessageListAPI(params)
      : await myListAPI(params)

    if (res.code === 1 && res.data) {
      console.log('[Message] 数据:', res.data)

      // 处理列表数据
      if (res.data.list?.length) {
        // 直接使用 API 返回的数据(title 字段由后端提供)
        const listData = res.data.list

        if (isLoadMore) {
          // 加载更多:追加数据
          currentList.value = [...currentList.value, ...listData]
        } else {
          // 首次加载或刷新:替换数据
          currentList.value = listData
        }

        // 判断是否还有更多数据
        // 如果返回的数据量少于请求的量,说明没有更多了
        hasMore.value = listData.length >= params.limit
      } else {
        // 没有数据了
        if (isLoadMore) {
          hasMore.value = false
        } else {
          currentList.value = []
        }
      }
    } else {
      console.error('[Message] API 返回错误:', res.msg)
      Taro.showToast({
        title: res.msg || '获取消息列表失败',
        icon: 'none',
        duration: 3000
      })
    }
  } catch (error) {
    console.error('[Message] 获取消息列表失败:', error)
  } finally {
    if (isLoadMore) {
      loadingMore.value = false
    } else {
      loading.value = false
    }
  }
}

/**
 * 页面加载时获取数据
 */
useLoad(async (options) => {
  console.log('[Message] 页面参数:', options)

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

  // 获取消息列表
  await fetchMessageList({ page: 0, limit: pageSize })

  // 首次加载完成
  isFirstLoad.value = false
})

/**
 * 页面显示时刷新列表(用于从详情页返回时更新已读状态)
 */
useDidShow(async () => {
  // 跳过首次加载(useLoad 已经处理过)
  if (isFirstLoad.value) {
    return
  }

  console.log('[Message] 页面显示,更新已读状态')

  try {
    // 重新获取当前页数据(用于更新已读状态)
    const res = USE_MOCK_DATA
      ? await mockMessageListAPI({ page: currentPage.value, limit: pageSize })
      : await myListAPI({ page: currentPage.value, limit: pageSize })

    if (res.code === 1 && res.data?.list) {
      const newList = res.data.list

      // 只更新现有列表项的状态,不改变列表顺序
      currentList.value = currentList.value.map(oldItem => {
        const newItem = newList.find(item => item.id === oldItem.id)
        // 如果找到了对应的消息,更新其状态;否则保持原样
        return newItem ? { ...oldItem, status: newItem.status } : oldItem
      })

      console.log('[Message] 已读状态已更新')
    }
  } catch (error) {
    console.error('[Message] 更新已读状态失败:', error)
  }
})

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

  // 更新页码
  currentPage.value = page

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

/**
 * 处理下拉刷新事件
 */
const handleRefresh = async () => {
  console.log('[Message] 下拉刷新')

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

  // 刷新数据
  await fetchMessageList({ page: 0, limit: pageSize })
}

/**
 * 跳转到详情页
 *
 * @param {Object} item - 消息对象
 */
const handleItemClick = (item) => {
  go('/pages/message-detail/index', { id: item.id })
}
</script>

<style lang="less">
/* LoadMoreList 组件已内置样式,此处无需额外样式 */
</style>