/ src / renderer / components / pages / McpNewsPage.vue
McpNewsPage.vue
 1  <script setup lang="ts">
 2  import { computed, ref, onMounted } from 'vue'
 3  import type { StartupNewsType } from '@/types/startup'
 4  
 5  const mcpNews = ref<StartupNewsType[]>([])
 6  
 7  onMounted((): void => {
 8    mcpNews.value = window.startupApis?.get().news || []
 9  })
10  
11  const placeholderColor = computed(() => {
12    return (index: number) => {
13      const hue = (index * 137.508) % 360 // Evenly distribute hues using golden angle approximation
14      const saturation = 30 + Math.sin(index * 0.5) * 20 // 30%~50% saturation (soft pastel tones)
15      const lightness = 50 + Math.cos(index * 0.3) * 10 // 40%~60% lightness (avoids extreme brightness)
16      return `data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Crect width='10' height='6' fill='hsl(${hue}, ${saturation}%, ${lightness}%)'/%3E%3C/svg%3E`
17    }
18  })
19  </script>
20  
21  <template>
22    <v-data-iterator :items="mcpNews" :items-per-page="-1">
23      <template #default="{ items }">
24        <v-container class="pa-0" fluid>
25          <v-row density="compact">
26            <v-col v-for="(item, index) in items" :key="item.raw.title" cols="auto" md="4">
27              <v-card class="pb-3" border flat>
28                <v-img
29                  class="ma-2"
30                  rounded="lg"
31                  :cover="item.raw.cover ?? true"
32                  :src="item.raw.img"
33                  :height="140"
34                  :lazy-src="placeholderColor(index)"
35                >
36                  <template #placeholder>
37                    <div class="d-flex align-center justify-center fill-height">
38                      <v-progress-circular color="grey-lighten-4" indeterminate></v-progress-circular>
39                    </div>
40                  </template>
41                  <template #error>
42                    <v-img></v-img>
43                  </template>
44                </v-img>
45                <v-divider></v-divider>
46  
47                <v-list-item lines="two">
48                  <template #title>
49                    <strong class="text-h6 mb-1">{{ item.raw.title }}</strong>
50                  </template>
51                  <template #subtitle>
52                    <div class="subtitle-text">{{ item.raw.subtitle }}</div>
53                  </template>
54                </v-list-item>
55  
56                <div class="d-flex justify-space-between px-4">
57                  <div class="d-flex align-center text-caption text-medium-emphasis me-1">
58                    <v-icon icon="mdi-clock" start></v-icon>
59  
60                    <div class="text-truncate">{{ `${item.raw.duration} ` + $t('mcp.minutes') }}</div>
61                  </div>
62  
63                  <v-btn
64                    class="text-none"
65                    size="small"
66                    :text="$t('mcp.read')"
67                    variant="flat"
68                    border
69                    :href="item.raw.link"
70                    target="_blank"
71                  >
72                  </v-btn>
73                </div>
74              </v-card>
75            </v-col>
76          </v-row>
77        </v-container>
78      </template>
79    </v-data-iterator>
80  </template>
81  
82  <style scoped>
83  .subtitle-text {
84    padding-bottom: 2px;
85  }
86  </style>