You need to sign in or sign up before continuing.
MaterialCard.vue 7.34 KB
<template>
  <view class="flex flex-row bg-white rounded-[24rpx] p-[24rpx] shadow-md border border-gray-200 material-card">
    <!-- 左侧图标 -->
    <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="iconUrl" class="w-[48rpx] h-[48rpx]" mode="aspectFit" />
    </view>

    <!-- 内容区域 -->
    <view class="flex-1 min-w-0">
      <!-- 标题 -->
      <view class="text-[#1F2937] text-[32rpx] font-bold leading-[1.4] mb-[8rpx] line-clamp-2">
        {{ title }}
      </view>

      <!-- 学习人数信息 -->
      <view v-if="learners" 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>{{ learners }}</text>
        </view>
        <!-- 学习人数比例 -->
        <view v-if="readPeoplePercent !== undefined && readPeoplePercent !== null" class="inline-flex items-center justify-center px-[12rpx] py-[4rpx] bg-green-50 text-green-600 text-[20rpx] font-medium rounded-[8rpx]">
          <text>{{ readPeoplePercent }}%热度</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]">
          <text>{{ docTypeLabel }}</text>
        </view>
        <view v-if="fileSize" class="text-[#9CA3AF] text-[22rpx]">
          {{ fileSize }}
        </view>
      </view>

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

      <!-- 操作按钮 -->
      <ListItemActions
        :viewable="true"
        :collectable="true"
        :deletable="false"
        :collected="collected"
        :item-id="String(id)"
        @view="handleView"
        @collect="handleCollect"
      />
    </view>
  </view>
</template>

<script setup>
/**
 * 资料卡片组件
 *
 * @description 热门资料列表项卡片,展示资料图标、标题、学习人数和操作按钮
 * @component MaterialCard
 */

import { defineProps, defineEmits } from 'vue';
import Taro from '@tarojs/taro';
import { getDocumentIcon, getDocumentLabel } from '@/utils/documentIcons';
import ListItemActions from '@/components/ListItemActions/index.vue';
import { useCollectOperation } from '@/composables/useCollectOperation';
import { useListItemClick, ListType } from '@/composables/useListItemClick';

/**
 * 组件属性
 */
const props = defineProps({
  /** 资料 ID */
  id: {
    type: [Number, String],
    required: true
  },
  /** 资料标题 */
  title: {
    type: String,
    required: true
  },
  /** 文件名(用于获取图标和标签) */
  fileName: {
    type: String,
    default: ''
  },
  /** 文件大小 */
  fileSize: {
    type: String,
    default: ''
  },
  /** 学习人数文本 */
  learners: {
    type: String,
    default: ''
  },
  /** 学习人数百分比(热度) */
  readPeoplePercent: {
    type: Number,
    default: null
  },
  /** 是否已收藏 */
  collected: {
    type: Boolean,
    default: false
  },
  /** 文件扩展名 */
  extension: {
    type: String,
    default: ''
  },
  /** 下载URL */
  downloadUrl: {
    type: String,
    default: ''
  }
});

/**
 * 组件事件(简化版)
 */
const emit = defineEmits({
  /** 查看完成 */
  viewed: (item) => true,
  /** 收藏状态改变 */
  collectChanged: (item) => true
});

/**
 * 获取文档图标 URL
 *
 * @description 根据文件名获取对应的文档类型图标
 * @returns {string} 图标 URL
 */
const iconUrl = props.fileName ? getDocumentIcon(props.fileName) : '';

/**
 * 获取文档类型标签
 *
 * @description 根据文件名获取文档类型标签文本
 * @returns {string} 文档类型标签
 */
const docTypeLabel = props.fileName ? getDocumentLabel(props.fileName) : '';

/**
 * 使用收藏操作 composable
 */
const { toggleCollect } = useCollectOperation();

/**
 * 使用文件列表点击处理器(内部实现)
 */
const { handleClick } = 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('[MaterialCard] 文件类型:', extension, '文件名:', item.title)

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

      // 构建图片列表(当前图片)
      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('[MaterialCard] 图片预览失败:', err)
        Taro.showToast({
          title: '图片预览失败',
          icon: 'none',
          duration: 2000
        })
        // 预览失败,返回 true 继续默认行为
        return true
      }
    }

    // 非图片文件:继续默认的文件打开流程
    console.log('[MaterialCard] 非图片文件,使用默认打开方式')
    return true
  },
  onAfterClick: (item) => {
    console.log('[MaterialCard] 用户打开了资料:', item.title)
    // 通知父组件查看完成
    emit('viewed', item)
  }
})

/**
 * 处理查看点击
 *
 * @description 内部处理查看逻辑,调用 useListItemClick
 */
const handleView = () => {
  handleClick({
    id: props.id,
    title: props.title,
    fileName: props.fileName,
    fileSize: props.fileSize,
    learners: props.learners,
    readPeoplePercent: props.readPeoplePercent,
    collected: props.collected,
    extension: props.extension,
    downloadUrl: props.downloadUrl
  })
};

/**
 * 处理收藏点击
 *
 * @description 内部处理收藏逻辑,调用 useCollectOperation 并通知父组件
 */
const handleCollect = () => {
  // 调用收藏操作
  toggleCollect({
    id: props.id,
    title: props.title,
    fileName: props.fileName,
    fileSize: props.fileSize,
    learners: props.learners,
    readPeoplePercent: props.readPeoplePercent,
    collected: props.collected,
    extension: props.extension,
    downloadUrl: props.downloadUrl
  })

  // 通知父组件收藏状态改变
  emit('collectChanged', {
    id: props.id,
    title: props.title,
    fileName: props.fileName,
    fileSize: props.fileSize,
    learners: props.learners,
    readPeoplePercent: props.readPeoplePercent,
    collected: !props.collected, // 新状态(取反)
    extension: props.extension,
    downloadUrl: props.downloadUrl
  })
};
</script>

<style lang="less" scoped>
.material-card {
  // 多行文本省略
  .line-clamp-2 {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    overflow: hidden;
    word-break: break-all;
  }
}
</style>