refactor(ActivityCard): 优化卡片布局以支持紧凑模式
调整ActivityCard组件布局,新增isSmall属性以支持紧凑模式显示。移除冗余代码并优化样式,提升代码可维护性和用户体验。
Showing
2 changed files
with
61 additions
and
65 deletions
| 1 | <!-- | 1 | <!-- |
| 2 | * @Date: 2025-04-17 13:16:20 | 2 | * @Date: 2025-04-17 13:16:20 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-04-21 10:26:22 | 4 | + * @LastEditTime: 2025-04-21 10:52:50 |
| 5 | * @FilePath: /mlaj-reading-club/src/components/shared/ActivityCard.vue | 5 | * @FilePath: /mlaj-reading-club/src/components/shared/ActivityCard.vue |
| 6 | * @Description: 文件描述 | 6 | * @Description: 文件描述 |
| 7 | --> | 7 | --> |
| 8 | <template> | 8 | <template> |
| 9 | - <router-link | 9 | + <router-link :to="`/activity/${activity.id}`" |
| 10 | - :to="`/activity/${activity.id}`" | ||
| 11 | class="block bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition duration-200" | 10 | class="block bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition duration-200" |
| 12 | - > | 11 | + :class="isSmall ? 'flex-row h-32' : 'flex-col'"> |
| 13 | - <div class="relative pb-48 overflow-hidden"> | 12 | + <div :class="['relative', isSmall ? 'w-1/3' : 'w-full h-48']"> |
| 14 | - <img | 13 | + <img :src="activity.cover_image" :alt="activity.title" class="w-full h-full object-cover" /> |
| 15 | - :src="activity.cover_image" | ||
| 16 | - :alt="activity.title" | ||
| 17 | - class="absolute inset-0 h-full w-full object-cover" | ||
| 18 | - /> | ||
| 19 | - <!-- <div | ||
| 20 | - v-if="activity.tags && activity.tags.length" | ||
| 21 | - class="absolute top-0 left-0 p-4 flex flex-wrap gap-2" | ||
| 22 | - > | ||
| 23 | - <span | ||
| 24 | - v-for="tag in activity.tags" | ||
| 25 | - :key="tag" | ||
| 26 | - class="px-3 py-1 text-sm bg-green-500 text-white rounded-full" | ||
| 27 | - > | ||
| 28 | - {{ tag }} | ||
| 29 | - </span> | ||
| 30 | - </div> --> | ||
| 31 | <div class="absolute top-2 left-2"> | 14 | <div class="absolute top-2 left-2"> |
| 32 | - <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium" | 15 | + <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium" :class="statusBadgeClass"> |
| 33 | - :class="statusBadgeClass"> | ||
| 34 | {{ statusText }} | 16 | {{ statusText }} |
| 35 | </span> | 17 | </span> |
| 36 | </div> | 18 | </div> |
| 37 | </div> | 19 | </div> |
| 38 | 20 | ||
| 39 | - <div class="p-6"> | 21 | + <div :class="['flex', 'flex-col', 'justify-between', isSmall ? 'w-2/3 p-3' : 'p-4']"> |
| 40 | - <h3 class="text-xl font-semibold text-gray-800 mb-2">{{ activity.title }}</h3> | 22 | + <div> |
| 41 | - <p class="text-gray-600 text-sm mb-4 line-clamp-2">{{ activity.description }}</p> | 23 | + <h3 :class="['font-semibold', 'text-gray-800', isSmall ? 'text-sm line-clamp-2' : 'text-lg mb-2']">{{ |
| 24 | + activity.title }}</h3> | ||
| 25 | + <p v-if="!isSmall" class="text-gray-600 text-sm mb-3 line-clamp-2">{{ activity.description?.split('\n')[0] }} | ||
| 26 | + </p> | ||
| 42 | 27 | ||
| 43 | - <div class="flex items-center text-sm text-gray-500 mb-4"> | 28 | + <div class="flex items-center text-sm text-gray-500 mb-4"> |
| 44 | - <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | 29 | + <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| 45 | - <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" /> | 30 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" |
| 46 | - </svg> | 31 | + 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" /> |
| 47 | - <span>{{ formatDateTime(activity.start_time) }}</span> | 32 | + </svg> |
| 48 | - </div> | 33 | + <span>{{ formatDateTime(activity.start_time) }}</span> |
| 34 | + </div> | ||
| 49 | 35 | ||
| 50 | - <div class="flex items-center text-sm text-gray-500 mb-4"> | 36 | + <div class="flex items-center text-sm text-gray-500 mb-4"> |
| 51 | - <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | 37 | + <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| 52 | - <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" /> | 38 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" |
| 53 | - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" /> | 39 | + 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" /> |
| 54 | - </svg> | 40 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" |
| 55 | - <span>{{activity.activity_type === 'online' ? '线上活动' : (activity.location ? (typeof activity.location === 'object' ? activity.location.name : activity.location) : '地点未设置')}}</span> | 41 | + d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" /> |
| 42 | + </svg> | ||
| 43 | + <span>{{ activity.activity_type === 'online' ? '线上活动' : (activity.location ? (typeof activity.location === | ||
| 44 | + 'object' ? activity.location.name : activity.location) : '地点未设置') }}</span> | ||
| 45 | + </div> | ||
| 56 | </div> | 46 | </div> |
| 57 | 47 | ||
| 58 | - <div class="flex items-center justify-between"> | 48 | + <div v-if="!isSmall" class="mt-3"> |
| 59 | - <div class="flex items-center"> | 49 | + <div class="flex items-center justify-between text-sm"> |
| 60 | - <img | 50 | + <span class="text-gray-600">已报名: {{ activity.participant_count }}/{{ activity.max_participants }}</span> |
| 61 | - :src="activity.organizer_avatar" | 51 | + <span class="text-blue-600">{{ Math.round(capacityPercentage) }}%</span> |
| 62 | - :alt="activity.organizer_name" | 52 | + </div> |
| 63 | - class="w-8 h-8 rounded-full mr-2" | 53 | + <div class="w-full bg-gray-200 rounded-full h-1.5 mt-1"> |
| 64 | - /> | 54 | + <div class="bg-gradient-to-r from-green-400 to-blue-500 h-1.5 rounded-full" |
| 65 | - <span class="text-sm text-gray-600">{{ activity.organizer_name }}</span> | 55 | + :style="{ width: capacityPercentage + '%' }"></div> |
| 66 | </div> | 56 | </div> |
| 67 | - <span class="text-sm font-medium text-green-500"> | ||
| 68 | - {{ activity.participant_count }}人参与 | ||
| 69 | - </span> | ||
| 70 | </div> | 57 | </div> |
| 58 | + | ||
| 59 | + <router-link to="`/activity/${activity.id}`" | ||
| 60 | + 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"> | ||
| 61 | + {{ isPast ? '查看详情' : isRegistrationOpen ? '立即报名' : '了解更多' }} | ||
| 62 | + </router-link> | ||
| 71 | </div> | 63 | </div> |
| 72 | </router-link> | 64 | </router-link> |
| 73 | </template> | 65 | </template> |
| ... | @@ -79,6 +71,10 @@ const props = defineProps({ | ... | @@ -79,6 +71,10 @@ const props = defineProps({ |
| 79 | activity: { | 71 | activity: { |
| 80 | type: Object, | 72 | type: Object, |
| 81 | required: true | 73 | required: true |
| 74 | + }, | ||
| 75 | + isSmall: { | ||
| 76 | + type: Boolean, | ||
| 77 | + default: false | ||
| 82 | } | 78 | } |
| 83 | }) | 79 | }) |
| 84 | 80 | ||
| ... | @@ -94,22 +90,22 @@ const formatDateTime = (dateTimeStr) => { | ... | @@ -94,22 +90,22 @@ const formatDateTime = (dateTimeStr) => { |
| 94 | }).format(date) | 90 | }).format(date) |
| 95 | } | 91 | } |
| 96 | 92 | ||
| 97 | - // Calculate if registration is still open | 93 | +// Calculate if registration is still open |
| 98 | - const now = new Date(); | 94 | +const now = new Date(); |
| 99 | - const registrationStart = new Date(props.activity.registration_start); | 95 | +const registrationStart = new Date(props.activity.registration_start); |
| 100 | - const registrationEnd = new Date(props.activity.registration_end); | 96 | +const registrationEnd = new Date(props.activity.registration_end); |
| 101 | - const activityStart = new Date(props.activity.start_time); | 97 | +const activityStart = new Date(props.activity.start_time); |
| 102 | 98 | ||
| 103 | - const isRegistrationOpen = ref(now >= registrationStart && now <= registrationEnd); | 99 | +const isRegistrationOpen = ref(now >= registrationStart && now <= registrationEnd); |
| 104 | - const isUpcoming = ref(now < activityStart); | 100 | +const isUpcoming = ref(now < activityStart); |
| 105 | - const isPast = ref(now > new Date(props.activity.end_time)); | 101 | +const isPast = ref(now > new Date(props.activity.end_time)); |
| 106 | - const isOngoing = ref(!isUpcoming && !isPast); | 102 | +const isOngoing = ref(!isUpcoming && !isPast); |
| 107 | 103 | ||
| 108 | - // Calculate capacity percentage | 104 | +// Calculate capacity percentage |
| 109 | - const capacityPercentage = Math.min((props.activity.participant_count / props.activity.max_participants) * 100, 100); | 105 | +const capacityPercentage = Math.min((props.activity.participant_count / props.activity.max_participants) * 100, 100); |
| 110 | 106 | ||
| 111 | - // Dynamically set status badge colors | 107 | +// Dynamically set status badge colors |
| 112 | - const statusBadgeClass = computed(() => { | 108 | +const statusBadgeClass = computed(() => { |
| 113 | if (isPast.value) { | 109 | if (isPast.value) { |
| 114 | return 'bg-gray-100 text-gray-800'; | 110 | return 'bg-gray-100 text-gray-800'; |
| 115 | } else if (isOngoing.value) { | 111 | } else if (isOngoing.value) { | ... | ... |
| ... | @@ -175,7 +175,7 @@ | ... | @@ -175,7 +175,7 @@ |
| 175 | <div class="mt-2 flex items-center"> | 175 | <div class="mt-2 flex items-center"> |
| 176 | <span class="text-sm text-green-700 mr-2">报名状态:</span> | 176 | <span class="text-sm text-green-700 mr-2">报名状态:</span> |
| 177 | <span :class="registrationStatusBadge.class">{{ registrationStatusBadge.text | 177 | <span :class="registrationStatusBadge.class">{{ registrationStatusBadge.text |
| 178 | - }}</span> | 178 | + }}</span> |
| 179 | </div> | 179 | </div> |
| 180 | </div> | 180 | </div> |
| 181 | 181 | ||
| ... | @@ -205,7 +205,7 @@ | ... | @@ -205,7 +205,7 @@ |
| 205 | <h3 class="text-lg font-medium text-gray-900 mb-4">相似活动推荐</h3> | 205 | <h3 class="text-lg font-medium text-gray-900 mb-4">相似活动推荐</h3> |
| 206 | <div class="space-y-4"> | 206 | <div class="space-y-4"> |
| 207 | <ActivityCard v-for="activity in similarActivities" :key="activity.id" | 207 | <ActivityCard v-for="activity in similarActivities" :key="activity.id" |
| 208 | - :activity="activity" variant="compact" /> | 208 | + :activity="activity" :isSmall="true" /> |
| 209 | </div> | 209 | </div> |
| 210 | </div> | 210 | </div> |
| 211 | </div> | 211 | </div> | ... | ... |
-
Please register or login to post a comment