hookehuyr

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

调整ActivityCard组件布局,新增isSmall属性以支持紧凑模式显示。移除冗余代码并优化样式,提升代码可维护性和用户体验。
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>
......