index.vue 11.1 KB
<!--
 * @Date: 2026-02-05
 * @Description: 本周热门资料页 - 简化版资料列表(无搜索和Tab)
-->
<template>
  <view class="h-screen bg-[#F9FAFB] flex flex-col py-[32rpx]">
    <NavHeader title="本周热门资料" />

    <!-- 列表容器 -->
    <view
      v-if="listVisible"
      :key="listRenderKey"
      class="flex-1 min-h-0 overflow-y-auto px-[32rpx] pb-[calc(160rpx+env(safe-area-inset-bottom))] box-border"
    >
      <!-- 加载状态 -->
      <view v-if="loading && currentList.length === 0" class="flex items-center justify-center py-[60rpx]">
        <view class="loading-spinner"></view>
        <text class="ml-[16rpx] text-[#9CA3AF] text-[28rpx]">加载中...</text>
      </view>

      <view v-else class="flex flex-col gap-[24rpx]">
        <view v-for="(item, index) in currentList" :key="item.meta_id"
          class="material-item bg-white rounded-[24rpx] p-[24rpx] shadow-md transition-all duration-200 border border-gray-50 flex flex-row"
          :style="{ animationDelay: `${index * 50}ms` }">

          <view
            class="w-[88rpx] h-[88rpx] mr-[24rpx] flex-shrink-0 flex items-center justify-center bg-gradient-to-br from-blue-50 to-blue-100 rounded-[20rpx] shadow-inner self-start">
            <image
              :src="getDocumentIcon(item.extension ? `file.${item.extension}` : item.name)"
              class="w-[48rpx] h-[48rpx]"
              mode="aspectFit"
            />
          </view>

          <view class="flex-1 min-w-0">
            <view class="text-[#1F2937] text-[30rpx] font-bold leading-[1.4] mb-[8rpx] line-clamp-2">
              {{ item.name }}
            </view>

            <!-- 学习人数信息(本周热门特有) -->
            <view v-if="item.read_people_count !== undefined" class="flex items-center gap-[12rpx] mb-[16rpx]">
              <view class="inline-flex items-center justify-center px-[12rpx] py-[4rpx] bg-orange-50 text-orange-600 text-[20rpx] font-medium rounded-[8rpx]">
                <text>{{ item.read_people_count }}人学习</text>
              </view>
              <!-- 热度百分比 -->
              <view v-if="item.read_people_percent !== undefined" class="inline-flex items-center justify-center px-[12rpx] py-[4rpx] bg-green-50 text-green-600 text-[20rpx] font-medium rounded-[8rpx]">
                <text>{{ item.read_people_percent }}%热度</text>
              </view>
            </view>

            <view class="flex items-center gap-[12rpx] mb-[16rpx]">
              <view
                class="inline-flex items-center justify-center px-[12rpx] py-[4rpx] bg-gray-100 text-gray-500 text-[20rpx] font-medium rounded-[8rpx]">
                {{ getDocumentLabel(item.extension ? `file.${item.extension}` : item.name) }}
              </view>
              <view class="text-[#9CA3AF] text-[22rpx]">
                {{ item.size }}
              </view>
            </view>

            <view class="h-[1rpx] bg-gray-100 my-[20rpx]"></view>

            <ListItemActions
              :viewable="true"
              :collectable="true"
              :collected="item.collected"
              :item-id="String(item.meta_id)"
              @view="onView(item)"
              @collect="toggleCollect(item)"
            />
          </view>
        </view>

        <!-- 空状态 -->
        <view v-if="currentList.length === 0 && !loading && !loadingMore">
          <nut-empty description="暂无热门资料" image="empty" />
        </view>

        <!-- 加载更多提示 -->
        <view v-if="currentList.length > 0" class="flex items-center justify-center py-[40rpx]">
          <view v-if="loadingMore" class="flex items-center">
            <view class="loading-spinner-small"></view>
            <text class="ml-[12rpx] text-[#9CA3AF] text-[24rpx]">加载中...</text>
          </view>
          <view v-else-if="!hasMore" class="text-[#9CA3AF] text-[24rpx]">
            没有更多了
          </view>
        </view>
      </view>
    </view>
  </view>
</template>

<script setup>
import { ref } from 'vue'
import { useLoad, useReachBottom } from '@tarojs/taro'
import NavHeader from '@/components/NavHeader.vue'
import ListItemActions from '@/components/ListItemActions/index.vue'
import { useListItemClick, ListType } from '@/composables/useListItemClick'
import { getDocumentIcon, getDocumentLabel } from '@/utils/documentIcons'
import { weekHotAPI } from '@/api/file'
import { useCollectOperation } from '@/composables/useCollectOperation'
import Taro from '@tarojs/taro'

const listVisible = ref(true)
const listRenderKey = ref(0)
const loading = ref(false)
const loadingMore = ref(false)  // 加载更多状态
const hasMore = ref(true)  // 是否还有更多数据
const currentList = ref([])
const currentPage = ref(0)  // 当前页码(从0开始)
const pageSize = 20  // 每页数量

/**
 * 转换文档数据格式
 * @description 将 API 返回的文档数据转换为组件需要的格式
 * @param {Object} doc - API 返回的文档对象
 * @returns {Object} 转换后的文档对象
 */
const transformDocItem = (doc) => {
  // 处理文件名为空的情况
  const fileName = doc.name || '未命名文件'
  // 如果没有扩展名,从文件名中提取(如果有)
  const extension = doc.extension || fileName.split('.').pop()?.toLowerCase() || ''

  return {
    meta_id: doc.meta_id,
    name: fileName,
    size: doc.size || '',
    downloadUrl: doc.src,
    extension: extension,
    collected: doc.is_favorite === '1' || doc.is_favorite === 1 || doc.is_favorite === true,
    read_people_count: doc.read_people_count,
    read_people_percent: doc.read_people_percent
  }
}

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

    console.log('[Week Hot] 请求参数:', params)

    // 调用接口
    const res = await weekHotAPI(params)

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

      // 处理列表数据
      if (res.data.list?.length) {
        const listData = res.data.list.map(transformDocItem)

        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 {
      Taro.showToast({
        title: res.msg || '获取热门资料失败',
        icon: 'none',
        duration: 2000
      })
    }
  } catch (error) {
    console.error('[Week Hot] 获取热门资料失败:', error)
    Taro.showToast({
      title: '加载失败',
      icon: 'error',
      duration: 2000
    })
  } finally {
    if (isLoadMore) {
      loadingMore.value = false
    } else {
      loading.value = false
    }
  }
}

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

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

  // 获取本周热门资料列表
  await fetchWeekHotList({ page: 0, limit: pageSize })
})

/**
 * 触底加载更多
 * @description 使用防抖避免频繁触发
 */
let loadMoreTimer = null
useReachBottom(() => {
  // 如果正在加载或没有更多数据,不执行
  if (loadingMore.value || !hasMore.value) {
    return
  }

  // 防抖:300ms 内只触发一次
  if (loadMoreTimer) {
    clearTimeout(loadMoreTimer)
  }

  loadMoreTimer = setTimeout(async () => {
    console.log('[Week Hot] 触底加载更多')

    // 页码 +1
    currentPage.value += 1

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

/**
 * 使用文件列表点击处理器
 * @description 添加图片预览功能,点击图片文件时使用 Taro.previewImage
 */
const { handleClick: onView } = useListItemClick({
  listType: ListType.FILE,
  onBeforeClick: async (item) => {
    /**
     * 检查文件类型并使用对应的预览方式
     * - 图片文件:使用 Taro.previewImage 预览
     * - 其他文件:继续默认的文件打开流程
     */
    const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg']
    const extension = item.extension?.toLowerCase() || ''

    console.log('[Week Hot] 文件类型:', extension, '文件名:', item.name)

    if (imageExtensions.includes(extension)) {
      // 图片文件:使用 Taro 预览
      console.log('[Week Hot] 检测到图片文件,使用图片预览')

      // 构建图片列表(当前图片)
      const urls = [item.downloadUrl]

      try {
        // 短暂延迟后打开预览(让用户看到提示)
        await new Promise(resolve => setTimeout(resolve, 300))

        await Taro.previewImage({
          current: item.downloadUrl,
          urls: urls
        })

        // 预览成功,阻止默认的文件打开行为
        return false
      } catch (err) {
        console.error('[Week Hot] 图片预览失败:', err)
        Taro.showToast({
          title: '图片预览失败',
          icon: 'none',
          duration: 2000
        })
        // 预览失败,返回 true 继续默认行为
        return true
      }
    }

    // 非图片文件:继续默认的文件打开流程
    console.log('[Week Hot] 非图片文件,使用默认打开方式')
    return true
  },
  onAfterClick: (item) => {
    console.log('用户打开了资料:', item.name)
  }
})

/**
 * 切换收藏状态
 * @description 使用 useCollectOperation composable 处理收藏操作
 */
const { toggleCollect } = useCollectOperation()
</script>

<style lang="less">
/* 列表项进入动画 */
@keyframes slideIn {
  from {
    opacity: 0;
    transform: translateY(20rpx);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.material-item {
  animation: slideIn 0.5s cubic-bezier(0.2, 0.8, 0.2, 1) backwards;
}

/* 加载动画 */
@keyframes spin {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

.loading-spinner {
  width: 40rpx;
  height: 40rpx;
  border: 4rpx solid #E5E7EB;
  border-top-color: #4CAF50;
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}

.loading-spinner-small {
  width: 32rpx;
  height: 32rpx;
  border: 3rpx solid #E5E7EB;
  border-top-color: #4CAF50;
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}

/* 多行文本省略 */
.line-clamp-2 {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  word-break: break-all;
}
</style>