ArticleCard.vue 5.53 KB
<template>
  <view class="flex flex-row bg-white rounded-[24rpx] p-[24rpx] shadow-sm border border-gray-100 article-card" @tap="handleCardClick">
    <!-- 左侧封面图 -->
    <view v-if="showCover" class="w-[160rpx] h-[120rpx] mr-[24rpx] flex-shrink-0 rounded-[16rpx] overflow-hidden bg-gray-100 self-start">
      <image
        v-if="coverUrl"
        :src="coverUrl"
        class="w-full h-full"
        mode="aspectFill"
      />
      <view v-else class="w-full h-full flex items-center justify-center bg-gradient-to-br from-blue-50 to-blue-100">
        <text class="text-gray-400 text-[40rpx]">📄</text>
      </view>
    </view>

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

      <!-- 简介 -->
      <view v-if="excerpt" class="text-[#6B7280] text-[24rpx] leading-[1.4] mb-[8rpx] line-clamp-1">
        {{ excerpt }}
      </view>

      <!-- 元信息:日期 + 学习人数 -->
      <view class="flex items-center gap-[12rpx] mb-[16rpx]">
        <view class="text-[#9CA3AF] text-[22rpx]">
          📅 {{ formattedDate }}
        </view>
        <!-- 学习人数 -->
        <view v-if="learners" class="inline-flex items-center justify-center px-[8rpx] py-[2rpx] bg-orange-50 text-orange-600 text-[20rpx] font-medium rounded-[6rpx]">
          <text>{{ learners }}人学习</text>
        </view>
        <!-- 热度百分比 -->
        <view v-if="readPeoplePercent !== undefined && readPeoplePercent !== null" class="inline-flex items-center justify-center px-[8rpx] py-[2rpx] bg-green-50 text-green-600 text-[20rpx] font-medium rounded-[6rpx]">
          <text>{{ readPeoplePercent }}%热度</text>
        </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 ArticleCard
 */

import { defineProps, defineEmits, computed } from 'vue';
import { useGo } from '@/hooks/useGo';
import ListItemActions from '@/components/list/ListItemActions/index.vue';
import { useCollectOperation } from '@/composables/useCollectOperation';
import dayjs from 'dayjs';

/**
 * 组件属性
 */
const props = defineProps({
  /** 文章 ID */
  id: {
    type: [Number, String],
    required: true
  },
  /** 文章标题 */
  title: {
    type: String,
    required: true
  },
  /** 文章简介 */
  excerpt: {
    type: String,
    default: ''
  },
  /** 封面图 URL */
  coverUrl: {
    type: String,
    default: ''
  },
  /** 发布日期(格式:YYYY-MM-DD HH:mm:ss) */
  date: {
    type: String,
    default: ''
  },
  /** 学习人数文本 */
  learners: {
    type: [String, Number],
    default: ''
  },
  /** 学习人数百分比(热度) */
  readPeoplePercent: {
    type: Number,
    default: null
  },
  /** 是否已收藏 */
  collected: {
    type: Boolean,
    default: false
  },
  /** 是否显示封面图 */
  showCover: {
    type: Boolean,
    default: false
  }
});

/**
 * 组件事件
 */
const emit = defineEmits(['viewed', 'collectChanged']);

const go = useGo();

/**
 * 格式化日期显示
 *
 * @description 将 2025-08-19 06:25:46 格式化为 08-19
 * @returns {string} 格式化后的日期
 */
const formattedDate = computed(() => {
  if (!props.date) return '';
  try {
    return dayjs(props.date).format('MM-DD');
  } catch {
    return props.date;
  }
});

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

/**
 * 处理卡片点击
 *
 * @description 跳转到文章详情页
 */
const handleCardClick = () => {
  go('/pages/article-detail/index', { id: props.id });
};

/**
 * 处理查看点击
 *
 * @description 跳转到文章详情页
 */
const handleView = () => {
  handleCardClick();
  // 通知父组件查看完成
  emit('viewed', { id: props.id });
};

/**
 * 处理收藏点击
 *
 * @description 调用收藏操作,成功后通知父组件更新状态
 */
const handleCollect = async () => {
  // 调用收藏操作 API
  const result = await toggleCollect({
    id: props.id,
    title: props.title,
    excerpt: props.excerpt,
    coverUrl: props.coverUrl,
    date: props.date,
    collected: props.collected
  });

  // API 调用成功后,通知父组件更新本地状态
  if (result.success) {
    emit('collectChanged', {
      id: props.id,
      title: props.title,
      excerpt: props.excerpt,
      coverUrl: props.coverUrl,
      date: props.date,
      collected: result.newStatus // ← 使用 API 返回的新状态
    });
  }
  // 如果 API 失败,不发出事件,UI 保持原状态
};
</script>

<style lang="less" scoped>
.article-card {
  transition: all 0.2s ease;

  &:active {
    transform: scale(0.98);
    background-color: #F9FAFB;
  }

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

  .line-clamp-2 {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    overflow: hidden;
    word-break: break-all;
  }
}
</style>