hookehuyr

refactor(ActivityCard): 优化卡片布局以支持紧凑模式

调整ActivityCard组件布局,新增isSmall属性以支持紧凑模式显示。移除冗余代码并优化样式,提升代码可维护性和用户体验。
<!--
* @Date: 2025-04-17 13:16:20
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-04-21 10:26:22
* @LastEditTime: 2025-04-21 10:52:50
* @FilePath: /mlaj-reading-club/src/components/shared/ActivityCard.vue
* @Description: 文件描述
-->
<template>
<router-link
:to="`/activity/${activity.id}`"
<router-link :to="`/activity/${activity.id}`"
class="block bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition duration-200"
>
<div class="relative pb-48 overflow-hidden">
<img
:src="activity.cover_image"
:alt="activity.title"
class="absolute inset-0 h-full w-full object-cover"
/>
<!-- <div
v-if="activity.tags && activity.tags.length"
class="absolute top-0 left-0 p-4 flex flex-wrap gap-2"
>
<span
v-for="tag in activity.tags"
:key="tag"
class="px-3 py-1 text-sm bg-green-500 text-white rounded-full"
>
{{ tag }}
</span>
</div> -->
:class="isSmall ? 'flex-row h-32' : 'flex-col'">
<div :class="['relative', isSmall ? 'w-1/3' : 'w-full h-48']">
<img :src="activity.cover_image" :alt="activity.title" class="w-full h-full object-cover" />
<div class="absolute top-2 left-2">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
:class="statusBadgeClass">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium" :class="statusBadgeClass">
{{ statusText }}
</span>
</div>
</div>
<div class="p-6">
<h3 class="text-xl font-semibold text-gray-800 mb-2">{{ activity.title }}</h3>
<p class="text-gray-600 text-sm mb-4 line-clamp-2">{{ activity.description }}</p>
<div :class="['flex', 'flex-col', 'justify-between', isSmall ? 'w-2/3 p-3' : 'p-4']">
<div>
<h3 :class="['font-semibold', 'text-gray-800', isSmall ? 'text-sm line-clamp-2' : 'text-lg mb-2']">{{
activity.title }}</h3>
<p v-if="!isSmall" class="text-gray-600 text-sm mb-3 line-clamp-2">{{ activity.description?.split('\n')[0] }}
</p>
<div class="flex items-center text-sm text-gray-500 mb-4">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<span>{{ formatDateTime(activity.start_time) }}</span>
</div>
<div class="flex items-center text-sm text-gray-500 mb-4">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<span>{{ formatDateTime(activity.start_time) }}</span>
</div>
<div class="flex items-center text-sm text-gray-500 mb-4">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span>{{activity.activity_type === 'online' ? '线上活动' : (activity.location ? (typeof activity.location === 'object' ? activity.location.name : activity.location) : '地点未设置')}}</span>
<div class="flex items-center text-sm text-gray-500 mb-4">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span>{{ activity.activity_type === 'online' ? '线上活动' : (activity.location ? (typeof activity.location ===
'object' ? activity.location.name : activity.location) : '地点未设置') }}</span>
</div>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center">
<img
:src="activity.organizer_avatar"
:alt="activity.organizer_name"
class="w-8 h-8 rounded-full mr-2"
/>
<span class="text-sm text-gray-600">{{ activity.organizer_name }}</span>
<div v-if="!isSmall" class="mt-3">
<div class="flex items-center justify-between text-sm">
<span class="text-gray-600">已报名: {{ activity.participant_count }}/{{ activity.max_participants }}</span>
<span class="text-blue-600">{{ Math.round(capacityPercentage) }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-1.5 mt-1">
<div class="bg-gradient-to-r from-green-400 to-blue-500 h-1.5 rounded-full"
:style="{ width: capacityPercentage + '%' }"></div>
</div>
<span class="text-sm font-medium text-green-500">
{{ activity.participant_count }}人参与
</span>
</div>
<router-link to="`/activity/${activity.id}`"
class="mt-4 inline-flex items-center justify-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-gradient-to-r from-green-500 to-blue-500 hover:from-green-600 hover:to-blue-600">
{{ isPast ? '查看详情' : isRegistrationOpen ? '立即报名' : '了解更多' }}
</router-link>
</div>
</router-link>
</template>
......@@ -79,6 +71,10 @@ const props = defineProps({
activity: {
type: Object,
required: true
},
isSmall: {
type: Boolean,
default: false
}
})
......@@ -94,22 +90,22 @@ const formatDateTime = (dateTimeStr) => {
}).format(date)
}
// Calculate if registration is still open
const now = new Date();
const registrationStart = new Date(props.activity.registration_start);
const registrationEnd = new Date(props.activity.registration_end);
const activityStart = new Date(props.activity.start_time);
// Calculate if registration is still open
const now = new Date();
const registrationStart = new Date(props.activity.registration_start);
const registrationEnd = new Date(props.activity.registration_end);
const activityStart = new Date(props.activity.start_time);
const isRegistrationOpen = ref(now >= registrationStart && now <= registrationEnd);
const isUpcoming = ref(now < activityStart);
const isPast = ref(now > new Date(props.activity.end_time));
const isOngoing = ref(!isUpcoming && !isPast);
const isRegistrationOpen = ref(now >= registrationStart && now <= registrationEnd);
const isUpcoming = ref(now < activityStart);
const isPast = ref(now > new Date(props.activity.end_time));
const isOngoing = ref(!isUpcoming && !isPast);
// Calculate capacity percentage
const capacityPercentage = Math.min((props.activity.participant_count / props.activity.max_participants) * 100, 100);
// Calculate capacity percentage
const capacityPercentage = Math.min((props.activity.participant_count / props.activity.max_participants) * 100, 100);
// Dynamically set status badge colors
const statusBadgeClass = computed(() => {
// Dynamically set status badge colors
const statusBadgeClass = computed(() => {
if (isPast.value) {
return 'bg-gray-100 text-gray-800';
} else if (isOngoing.value) {
......
......@@ -175,7 +175,7 @@
<div class="mt-2 flex items-center">
<span class="text-sm text-green-700 mr-2">报名状态:</span>
<span :class="registrationStatusBadge.class">{{ registrationStatusBadge.text
}}</span>
}}</span>
</div>
</div>
......@@ -205,7 +205,7 @@
<h3 class="text-lg font-medium text-gray-900 mb-4">相似活动推荐</h3>
<div class="space-y-4">
<ActivityCard v-for="activity in similarActivities" :key="activity.id"
:activity="activity" variant="compact" />
:activity="activity" :isSmall="true" />
</div>
</div>
</div>
......