|
1 | 1 | <template> |
2 | | - <a |
3 | | - :href="linkUrl" |
4 | | - class="block h-full min-h-40 text-inherit no-underline! visited:no-underline! hover:no-underline! active:no-underline!" |
5 | | - > |
6 | | - <div |
7 | | - class="bg-soft-bg border-border flex h-full cursor-pointer flex-col rounded-lg border p-3 transition-all duration-200 ease-in-out hover:-translate-y-0.5 hover:shadow-md" |
8 | | - > |
9 | | - <div class="flex items-center gap-2"> |
10 | | - <Icon :icon="icon" class="text-xl" :class="iconColor" /> |
11 | | - <h3 class="text-text1 !mt-0 !mb-0 text-base font-semibold"> |
12 | | - {{ title }} |
13 | | - </h3> |
14 | | - </div> |
15 | | - <p class="text-text2 !mt-2 flex-grow text-sm leading-relaxed"> |
16 | | - {{ description }} |
17 | | - </p> |
| 2 | + <div class="flex h-full flex-col rounded-lg border border-border bg-soft-bg p-6 transition-all duration-200 ease-in-out hover:-translate-y-0.5 hover:shadow-md"> |
| 3 | + <div class="flex items-center gap-2 mb-4"> |
| 4 | + <Icon :icon="icon" class="text-xl" :class="iconColor" /> |
| 5 | + <h3 class="text-text1 !mt-0 !mb-0 text-base font-semibold"> |
| 6 | + {{ title }} |
| 7 | + </h3> |
18 | 8 | </div> |
19 | | - </a> |
| 9 | + |
| 10 | + <!-- Description mode --> |
| 11 | + <p v-if="description && !features" class="text-text2 !mt-2 flex-grow text-sm leading-relaxed"> |
| 12 | + {{ description }} |
| 13 | + </p> |
| 14 | + |
| 15 | + <!-- Features mode --> |
| 16 | + <ul v-if="features" class="space-y-2 text-sm text-gray-700 dark:text-gray-300 flex-grow"> |
| 17 | + <li |
| 18 | + v-for="feature in processedFeatures" |
| 19 | + :key="feature.key" |
| 20 | + class="flex items-start gap-2" |
| 21 | + > |
| 22 | + <span class="text-gray-600">•</span> |
| 23 | + <span v-if="feature.link"> |
| 24 | + <a |
| 25 | + :href="feature.link" |
| 26 | + target="_blank" |
| 27 | + rel="noopener noreferrer" |
| 28 | + class="text-blue-600 underline transition-colors hover:text-blue-800 hover:no-underline dark:text-blue-400 dark:hover:text-blue-300" |
| 29 | + > |
| 30 | + {{ feature.text }} |
| 31 | + </a> |
| 32 | + </span> |
| 33 | + <span v-else>{{ feature.text }}</span> |
| 34 | + </li> |
| 35 | + </ul> |
| 36 | + </div> |
20 | 37 | </template> |
21 | 38 |
|
22 | 39 | <script setup lang="ts"> |
| 40 | +import { computed } from 'vue'; |
23 | 41 | import { Icon } from '@iconify/vue'; |
24 | 42 |
|
| 43 | +interface FeatureItem { |
| 44 | + text: string; |
| 45 | + link?: string; |
| 46 | +} |
| 47 | +
|
25 | 48 | interface Props { |
26 | 49 | icon: string; |
27 | 50 | title: string; |
28 | | - description: string; |
29 | | - linkUrl: string; |
| 51 | + description?: string; |
| 52 | + features?: (string | FeatureItem)[]; |
| 53 | + linkUrl?: string; |
30 | 54 | iconColor?: string; |
31 | 55 | } |
32 | 56 |
|
33 | | -withDefaults(defineProps<Props>(), { |
| 57 | +const props = withDefaults(defineProps<Props>(), { |
34 | 58 | iconColor: 'text-blue-500', |
35 | 59 | linkUrl: '', |
36 | 60 | }); |
| 61 | +
|
| 62 | +const processedFeatures = computed(() => { |
| 63 | + if (!props.features) return []; |
| 64 | + return props.features.map((feature, index) => { |
| 65 | + if (typeof feature === 'string') { |
| 66 | + return { key: index, text: feature }; |
| 67 | + } else { |
| 68 | + return { key: index, text: feature.text, link: feature.link }; |
| 69 | + } |
| 70 | + }); |
| 71 | +}); |
37 | 72 | </script> |
0 commit comments