11<template >
2- <div class =" blog-container" >
2+ <div class =" blog-container article-detail-page " >
33 <div class =" article-detail-wrap" >
4- <div >
5- <div v-if =" loading" class =" card" >
6- <div class =" card-body" style =" padding : 22px " >加载中...</div >
7- </div >
8-
9- <div v-else >
10- <div class =" card" >
11- <div class =" article-content" >
12- <div v-if =" !article?.id" style =" padding : 20px 0 " >文章不存在</div >
13-
14- <div v-else >
15- <div style =" margin-bottom : 14px " >
16- <img
17- v-if =" article.coverMediaKey"
18- :src =" coverUrl(article.coverMediaKey)"
19- alt =" cover"
20- style =" width : 100% ; max-height : 340px ; object-fit : cover ; border-radius : 12px "
21- />
22- </div >
23-
24- <h1 class =" article-hero-title" >{{ article.title }}</h1 >
25- <div class =" article-hero-meta" >
26- 发布于 {{ formatDate(article.publishedAt) }} · 作者 {{ article.authorNickname || '' }} · 阅读
27- {{ article.viewCount ?? 0 }}
28- </div >
29-
30- <div class =" markdown-body article-markdown" v-html =" html" />
31- </div >
32- </div >
33- </div >
34-
35- <CommentSection v-if =" article ?.id " :article-id =" article .id " />
36- </div >
37- </div >
4+ <div v-if =" loading" class =" article-detail-loading" >加载中...</div >
5+
6+ <template v-else >
7+ <article v-if =" article?.id" class =" article-reader" >
8+ <router-link class =" article-back" to =" /all" >
9+ <span class =" article-back-chev" aria-hidden =" true" >‹</span >
10+ 返回博客
11+ </router-link >
12+
13+ <p class =" article-kicker-meta" >
14+ <span >{{ formatDate(article.publishedAt) }}</span >
15+ <span class =" article-kicker-dot" >·</span >
16+ <span >{{ readMinutes }} 分钟</span >
17+ <template v-if =" categoryLabel " >
18+ <span class =" article-kicker-dot" >·</span >
19+ <span >{{ categoryLabel }}</span >
20+ </template >
21+ </p >
22+
23+ <h1 class =" article-reader-title" >{{ article.title }}</h1 >
24+ <p v-if =" article.summary" class =" article-reader-lead" >{{ article.summary }}</p >
25+
26+ <figure v-if =" article.coverMediaKey" class =" article-reader-cover" >
27+ <img :src =" coverUrl(article.coverMediaKey)" alt =" " />
28+ </figure >
29+
30+ <div class =" markdown-body article-markdown article-markdown--body" v-html =" html" />
31+ </article >
32+
33+ <div v-else class =" article-detail-empty" >文章不存在</div >
34+
35+ <CommentSection v-if =" article ?.id " :article-id =" article .id " />
36+ </template >
3837 </div >
3938 </div >
4039</template >
@@ -65,10 +64,23 @@ onMounted(async () => {
6564const html = computed (() => {
6665 const md = article .value .contentMarkdown || ' '
6766 const raw = marked .parse (md)
68- // MVP:做基础消毒,避免把恶意 HTML 直接注入
6967 return DOMPurify .sanitize (raw)
7068})
7169
70+ /** 后端若将来返回分类名则展示 */
71+ const categoryLabel = computed (() => {
72+ const a = article .value
73+ return a? .categoryName || a? .categoryLabel || ' '
74+ })
75+
76+ const readMinutes = computed (() => estimateReadMinutes (article .value ? .contentMarkdown ))
77+
78+ function estimateReadMinutes (md ) {
79+ if (! md || typeof md !== ' string' ) return 1
80+ const chars = md .replace (/ \s + / g , ' ' ).length
81+ return Math .max (1 , Math .round (chars / 450 ))
82+ }
83+
7284function formatDate (v ) {
7385 if (! v) return ' '
7486 try {
@@ -78,4 +90,3 @@ function formatDate(v) {
7890 }
7991}
8092< / script>
81-
0 commit comments