1414 <div class =" card-body" style =" padding : 22px " >加载中...</div >
1515 </div >
1616
17- <div v-else class =" card" >
18- <div class =" article-content" >
19- <div v-if =" !article?.id" style =" padding : 20px 0 " >暂无文章</div >
20-
21- <div v-else >
22- <div style =" margin-bottom : 14px " >
23- <img
24- v-if =" article.coverMediaKey"
25- :src =" coverUrl(article.coverMediaKey)"
26- alt =" cover"
27- style =" width : 100% ; max-height : 340px ; object-fit : cover ; border-radius : 12px "
28- />
17+ <div v-else >
18+ <div class =" card" >
19+ <div class =" article-content" >
20+ <div v-if =" !article?.id" style =" padding : 20px 0 " >暂无文章</div >
21+
22+ <div v-else >
23+ <div style =" margin-bottom : 14px " >
24+ <img
25+ v-if =" article.coverMediaKey"
26+ :src =" coverUrl(article.coverMediaKey)"
27+ alt =" cover"
28+ style =" width : 100% ; max-height : 340px ; object-fit : cover ; border-radius : 12px "
29+ />
30+ </div >
31+
32+ <h1 class =" article-hero-title" >{{ article.title }}</h1 >
33+ <div class =" article-hero-meta" >
34+ 发布于 {{ formatDate(article.publishedAt) }} · 作者 {{ article.authorNickname || '' }} · 阅读
35+ {{ article.viewCount ?? 0 }}
36+ </div >
37+
38+ <div class =" markdown-body article-markdown" v-html =" html" />
2939 </div >
40+ </div >
41+ </div >
3042
31- <h1 class =" article-hero-title" >{{ article.title }}</h1 >
32- <div class =" article-hero-meta" >
33- 发布于 {{ formatDate(article.publishedAt) }} · 作者 {{ article.authorNickname || '' }} · 阅读
34- {{ article.viewCount ?? 0 }}
43+ <div v-if =" article?.id" class =" card" style =" margin-top : 18px " >
44+ <div class =" card-body" >
45+ <div style =" display : flex ; align-items : center ; justify-content : space-between ; gap : 12px " >
46+ <div style =" font-weight : 900 " >最新评论</div >
47+ <button type =" button" class =" btn" @click =" go(article.id)" >进入文章</button >
3548 </div >
3649
37- <div class =" markdown-body article-markdown" v-html =" html" />
50+ <div v-if =" commentsLoading" style =" color : var (--muted ); margin-top : 10px " >加载中…</div >
51+ <div v-else-if =" commentPreviews.length === 0" style =" color : var (--muted ); margin-top : 10px " >
52+ 暂无评论
53+ </div >
54+ <div v-else class =" mini-list" style =" margin-top : 12px " >
55+ <div v-for =" c in commentPreviews" :key =" c.id" class =" mini-item" style =" grid-template-columns : 44px 1fr " >
56+ <div class =" mini-cover" style =" width : 44px ; height : 44px ; border-radius : 999px " >
57+ <img :src =" c.user?.avatarUrl || 'https://www.gravatar.com/avatar/?d=mp&s=96'" alt =" avatar" />
58+ </div >
59+ <div >
60+ <div style =" display : flex ; gap : 10px ; align-items : baseline ; flex-wrap : wrap " >
61+ <div style =" font-weight : 850 ; font-size : 13px " >{{ c.user?.nickname || '匿名' }}</div >
62+ <div style =" color : var (--muted ); font-size : 12px " >{{ formatRelative(c.createdAt) }}</div >
63+ </div >
64+ <div style =" margin-top : 4px ; color : var (--text ); font-size : 13px ; line-height : 1.6 " >
65+ {{ c.content }}
66+ </div >
67+ </div >
68+ </div >
69+ </div >
3870 </div >
3971 </div >
4072 </div >
@@ -48,18 +80,21 @@ import { computed, onMounted, ref } from 'vue'
4880import DOMPurify from ' dompurify'
4981import { marked } from ' marked'
5082
51- import { fetchArticles , fetchArticleDetail } from ' ../api/article'
83+ import { fetchArticles , fetchArticleDetail , coverUrl } from ' ../api/article'
5284import ProfileCard from ' ../components/ProfileCard.vue'
5385import BlogInfoCard from ' ../components/BlogInfoCard.vue'
5486import AdminEntryCard from ' ../components/AdminEntryCard.vue'
5587import { useRouter } from ' vue-router'
5688import { fetchPublicProfile } from ' ../api/profile'
89+ import { listComments } from ' ../api/comment'
5790
5891const router = useRouter ()
5992
6093const loading = ref (true )
6194const article = ref ({})
6295const profile = ref (null )
96+ const commentsLoading = ref (false )
97+ const commentPreviews = ref ([])
6398
6499onMounted (async () => {
65100 loading .value = true
@@ -69,6 +104,18 @@ onMounted(async () => {
69104 article .value = first? .id ? await fetchArticleDetail (first .id ) : {}
70105
71106 profile .value = await fetchPublicProfile ()
107+
108+ if (article .value ? .id ) {
109+ commentsLoading .value = true
110+ try {
111+ const resp = await listComments (article .value .id , 0 , 3 )
112+ const top = resp? .items || []
113+ // 首页预览只展示顶层评论
114+ commentPreviews .value = top .slice (0 , 3 )
115+ } finally {
116+ commentsLoading .value = false
117+ }
118+ }
72119 } finally {
73120 loading .value = false
74121 }
@@ -92,5 +139,21 @@ function formatDate(v) {
92139 return ' '
93140 }
94141}
142+
143+ function formatRelative (v ) {
144+ if (! v) return ' '
145+ const t = new Date (v).getTime ()
146+ if (Number .isNaN (t)) return ' '
147+ let diff = Date .now () - t
148+ if (diff < 0 ) diff = 0
149+ if (diff < 60_000 ) return ' 刚刚'
150+ const m = Math .floor (diff / 60_000 )
151+ if (m < 60 ) return ` ${ m} 分钟前`
152+ const h = Math .floor (m / 60 )
153+ if (h < 24 ) return ` ${ h} 小时前`
154+ const d = Math .floor (h / 24 )
155+ if (d < 7 ) return ` ${ d} 天前`
156+ return new Date (t).toISOString ().slice (0 , 10 )
157+ }
95158< / script>
96159
0 commit comments