11<template >
2- <article class =" group flex flex-col h-full bg-transparent" >
3- <div class =" relative w-full aspect-[16/9] overflow-hidden rounded-lg mb-5 bg-gray-100 dark:bg-gray-800" >
4- <router-link :to =" `/posts/${article.id}`" class =" block w-full h-full" >
5- <img
6- v-if =" validImage"
7- :src =" article.coverImage"
2+ <article
3+ class =" group relative overflow-hidden rounded-2xl border border-zinc-200/70 bg-white/60 shadow-sm backdrop-blur
4+ transition hover:-translate-y-[1px] hover:border-zinc-300 hover:bg-white
5+ dark:border-zinc-800/70 dark:bg-zinc-900/30 dark:hover:border-zinc-700 dark:hover:bg-zinc-900/45"
6+ >
7+ <!-- Cover -->
8+ <router-link :to =" `/posts/${article.id}`" class =" block" >
9+ <div class =" relative aspect-[16/10] overflow-hidden border-b border-zinc-200/60 dark:border-zinc-800/60" >
10+ <img
11+ v-if =" validImage"
12+ :src =" article.coverImage"
813 :alt =" article.title"
9- class =" w -full h -full object-cover transition-all duration-700 grayscale group-hover:grayscale-0 group-hover: scale-105 "
14+ class =" h -full w -full object-cover transition-transform duration-700 group-hover:scale-[1.03] "
1015 @error =" handleImageError"
1116 />
12- <div
13- v-else
14- class =" w-full h-full flex items-center justify-center bg-[#f0f0f0] dark:bg-[#1a1a1a] relative overflow-hidden"
15- >
16- <div class =" absolute inset-0 opacity-[0.05] bg-[repeating-linear-gradient(45deg,#000_0px,#000_1px,transparent_1px,transparent_10px)] dark:bg-[repeating-linear-gradient(45deg,#fff_0px,#fff_1px,transparent_1px,transparent_10px)]" ></div >
17- <span class =" font-mono text-xs font-bold text-gray-400 border border-gray-400 px-2 py-1" >NO SIGNAL</span >
17+ <div v-else class =" h-full w-full bg-zinc-100 dark:bg-zinc-900 flex items-center justify-center" >
18+ <div class =" text-xs uppercase tracking-[0.25em] text-zinc-500 dark:text-zinc-400" >
19+ Myshkin451
20+ </div >
1821 </div >
19- </router-link >
2022
21- <router-link
22- v-if =" article.category"
23- :to =" `/categories/${article.category.slug || article.category.id}`"
24- class =" absolute top-3 left-3 bg-white/90 dark:bg-black/90 backdrop-blur text-xs font-mono px-2 py-1 border border-gray-200 dark:border-gray-800 hover:bg-black hover:text-white dark:hover:bg-white dark:hover:text-black transition-colors"
25- >
26- {{ article.category.name }}
27- </router-link >
28- </div >
23+ <div
24+ class =" pointer-events-none absolute inset-0 opacity-0 transition-opacity duration-300 group-hover:opacity-100
25+ [background-image:linear-gradient(to_top,rgba(0,0,0,0.20),transparent_60%)]"
26+ ></div >
27+ </div >
28+ </router-link >
2929
30- <div class =" flex flex-col flex-grow" >
31- <div class =" flex items-center justify-between text-xs font-mono text-gray-500 mb-3 border-b border-gray-100 dark:border-gray-800 pb-2" >
32- <time :datetime =" article.createdAt" >{{ formatDate(article.createdAt) }}</time >
33- <span v-if =" article.views !== undefined" >READ: {{ article.views }}</span >
30+ <!-- Body -->
31+ <div class =" p-5" >
32+ <div class =" flex items-center gap-2 text-xs text-zinc-500 dark:text-zinc-400" >
33+ <span >{{ formatDate(article.createdAt) }}</span >
34+
35+ <template v-if =" article .category " >
36+ <span class =" opacity-50" >·</span >
37+ <router-link
38+ :to =" `/categories/${article.category.slug || article.category.id}`"
39+ class =" truncate hover:text-zinc-900 dark:hover:text-white"
40+ >
41+ {{ article.category.name }}
42+ </router-link >
43+ </template >
44+
45+ <span v-if =" article.views != null" class =" ml-auto opacity-70" >
46+ {{ article.views }} views
47+ </span >
3448 </div >
3549
36- <router-link :to =" `/posts/${article.id}`" class =" block group-hover:opacity-70 transition-opacity" >
37- <h3 class =" text-xl font-serif font-medium leading-snug text-gray-900 dark:text-gray-100 mb-3" >
50+ <router-link :to =" `/posts/${article.id}`" class =" block" >
51+ <h3
52+ class =" mt-3 text-lg font-semibold tracking-tight text-zinc-900 dark:text-zinc-50
53+ decoration-zinc-300 underline-offset-4 group-hover:underline"
54+ >
3855 {{ article.title }}
3956 </h3 >
4057 </router-link >
4158
42- <p class =" text-sm text-gray -600 dark:text-gray-400 leading-relaxed line-clamp-2 mb-4 " >
59+ <p class =" mt-2 text-sm leading-relaxed text-zinc -600 dark:text-zinc-300 line-clamp-3 " >
4360 {{ article.excerpt || stripHtml(article.content) }}
4461 </p >
4562
46- <div class =" mt-auto pt-2 flex flex-wrap gap-2" >
47- <span v-for =" tag in (article.tags || []).slice(0, 3)" :key =" tag.id" class =" text-xs font-mono text-gray-400 before:content-['#']" >
48- {{ tag.name }}
49- </span >
63+ <div v-if =" article.tags && article.tags.length" class =" mt-4 flex flex-wrap gap-2" >
64+ <router-link
65+ v-for =" tag in article.tags.slice(0, 4)"
66+ :key =" tag.id"
67+ :to =" `/tags/${tag.slug || tag.id}`"
68+ class =" rounded-full border border-zinc-200/70 bg-white/70 px-2.5 py-1 text-xs text-zinc-700
69+ transition hover:bg-white hover:border-zinc-300
70+ dark:border-zinc-800/70 dark:bg-zinc-950/30 dark:text-zinc-200 dark:hover:bg-zinc-900/60"
71+ >
72+ # {{ tag.name }}
73+ </router-link >
5074 </div >
5175 </div >
5276 </article >
5680import { defineProps , ref , computed } from ' vue' ;
5781
5882const props = defineProps ({
59- article: { type: Object , required: true , default : () => ({ tags: [] }) }
83+ article: {
84+ type: Object ,
85+ required: true ,
86+ default : () => ({ tags: [] })
87+ }
6088});
6189
6290const imageLoadError = ref (false );
6391
6492const validImage = computed (() => {
65- return props .article .coverImage &&
66- props .article .coverImage .trim () !== ' ' &&
67- ! imageLoadError .value ;
93+ return props .article .coverImage &&
94+ props .article .coverImage .trim () !== ' ' &&
95+ ! imageLoadError .value ;
6896});
6997
70- const handleImageError = () => { imageLoadError .value = true ; };
98+ const handleImageError = () => {
99+ imageLoadError .value = true ;
100+ };
71101
72102const formatDate = (dateString ) => {
73103 if (! dateString) return ' ' ;
74- const d = new Date (dateString);
75- return ` ${ d .getFullYear ()} .${ String (d .getMonth () + 1 ).padStart (2 , ' 0' )} .${ String (d .getDate ()).padStart (2 , ' 0' )} ` ;
104+ return new Date (dateString).toLocaleDateString (' en-US' , {
105+ year: ' numeric' ,
106+ month: ' short' ,
107+ day: ' numeric'
108+ });
76109};
77110
78111const stripHtml = (html ) => {
79112 if (! html) return ' ' ;
80- const tmp = document .createElement (" DIV" );
113+ const tmp = document .createElement (' DIV' );
81114 tmp .innerHTML = html;
82- return tmp .textContent || tmp .innerText || " " ;
115+ return tmp .textContent || tmp .innerText || ' ' ;
83116};
84- </script >
117+ </script >
0 commit comments