MaterialCard.vue 6.52 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 { getDocumentIcon, getDocumentLabel } from '@/utils/documentIcons';
import ListItemActions from '@/components/list/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 优先使用 extension 字段,其次从 fileName、src、downloadUrl 解析
 * @returns {string} 图标 URL
 */
const iconUrl = getDocumentIcon({
  extension: props.extension,
  fileName: props.fileName,
  downloadUrl: props.downloadUrl
});

/**
 * 获取文档类型标签
 *
 * @description 优先使用 extension 字段,其次从 fileName、src、downloadUrl 解析
 * @returns {string} 文档类型标签
 */
const docTypeLabel = getDocumentLabel({
  extension: props.extension,
  fileName: props.fileName,
  downloadUrl: props.downloadUrl
});

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

/**
 * 使用文件列表点击处理器(内部实现)
 *
 * @description 直接使用 useFileOperation 的自动文件类型判断
 * - 图片文件:自动使用 Taro.previewImage 预览
 * - 视频文件:自动跳转到视频播放页面
 * - 其他文件:自动下载并使用 Taro.openDocument 打开
 */
const { handleClick } = useListItemClick({
  listType: ListType.FILE,
  onAfterClick: (item) => {
    console.log('[MaterialCard] 用户打开了资料:', item.title)
    // 通知父组件查看完成
    emit('viewed', item)
  }
});

/**
 * 处理查看点击
 *
 * @description 内部处理查看逻辑,调用 useListItemClick
 * 注意:权限检查和埋点由 ListItemActions 组件内部处理
 */
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 调用收藏操作,成功后通知父组件更新状态
 */
const handleCollect = async () => {
  // 调用收藏操作 API
  const result = await 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
  })

  // API 调用成功后,通知父组件更新本地状态
  if (result.success) {
    emit('collectChanged', {
      id: props.id,
      title: props.title,
      fileName: props.fileName,
      fileSize: props.fileSize,
      learners: props.learners,
      readPeoplePercent: props.readPeoplePercent,
      collected: result.newStatus, // ← 使用 API 返回的新状态
      extension: props.extension,
      downloadUrl: props.downloadUrl
    })
  }
  // 如果 API 失败,不发出事件,UI 保持原状态
};
</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>