|
|
@ -3,16 +3,20 @@ import { ref, computed, watch } from 'vue' |
|
|
import { useRoute } from 'vue-router' |
|
|
import { useRoute } from 'vue-router' |
|
|
import { articleApi } from '@/api/client' |
|
|
import { articleApi } from '@/api/client' |
|
|
import { renderMarkdown } from '@/utils/markdown' |
|
|
import { renderMarkdown } from '@/utils/markdown' |
|
|
|
|
|
import { setPageMeta } from '@/utils/seo' |
|
|
|
|
|
import { useApi } from '@/composables/useApi' |
|
|
import CommentSection from '@/components/CommentSection.vue' |
|
|
import CommentSection from '@/components/CommentSection.vue' |
|
|
import type { ArticleDetail } from '@/generated/api' |
|
|
|
|
|
|
|
|
import type { ArticleDetail, ResponseArticleDetail } from '@/generated/api' |
|
|
|
|
|
|
|
|
const route = useRoute() |
|
|
const route = useRoute() |
|
|
const article = ref<ArticleDetail | null>(null) |
|
|
|
|
|
const loading = ref(false) |
|
|
|
|
|
const error = ref<string | null>(null) |
|
|
|
|
|
|
|
|
|
|
|
const slug = computed(() => String(route.params.slug ?? '')) |
|
|
const slug = computed(() => String(route.params.slug ?? '')) |
|
|
|
|
|
|
|
|
|
|
|
const { data: resp, loading, error, execute } = useApi<ResponseArticleDetail>( |
|
|
|
|
|
() => articleApi.apiArticlesSlugGet(slug.value).then((r) => r.data) |
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
const article = ref<ArticleDetail | null>(null) |
|
|
|
|
|
|
|
|
const renderedContent = computed(() => { |
|
|
const renderedContent = computed(() => { |
|
|
if (!article.value?.content) return '' |
|
|
if (!article.value?.content) return '' |
|
|
return renderMarkdown(article.value.content) |
|
|
return renderMarkdown(article.value.content) |
|
|
@ -25,17 +29,21 @@ function formatDate(iso?: string): string { |
|
|
return d.toLocaleString('zh-CN', { hour12: false }) |
|
|
return d.toLocaleString('zh-CN', { hour12: false }) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function applyMeta(data: ArticleDetail) { |
|
|
|
|
|
setPageMeta({ |
|
|
|
|
|
title: data.title, |
|
|
|
|
|
description: data.summary || data.content.slice(0, 200), |
|
|
|
|
|
image: data.coverImage || undefined, |
|
|
|
|
|
url: `http://localhost/post/${data.slug}`, |
|
|
|
|
|
type: 'article' |
|
|
|
|
|
}) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
async function load() { |
|
|
async function load() { |
|
|
loading.value = true |
|
|
|
|
|
error.value = null |
|
|
|
|
|
try { |
|
|
|
|
|
const resp = await articleApi.apiArticlesSlugGet(slug.value) |
|
|
|
|
|
article.value = resp.data.data ?? null |
|
|
|
|
|
} catch (e) { |
|
|
|
|
|
console.error(e) |
|
|
|
|
|
error.value = '文章不存在或加载失败' |
|
|
|
|
|
} finally { |
|
|
|
|
|
loading.value = false |
|
|
|
|
|
|
|
|
await execute() |
|
|
|
|
|
article.value = resp.value?.data ?? null |
|
|
|
|
|
if (article.value) { |
|
|
|
|
|
applyMeta(article.value) |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@ -43,7 +51,10 @@ watch(slug, load, { immediate: true }) |
|
|
</script> |
|
|
</script> |
|
|
|
|
|
|
|
|
<template> |
|
|
<template> |
|
|
<article v-if="article" class="detail"> |
|
|
|
|
|
|
|
|
<div v-if="loading" class="hint-text">加载中...</div> |
|
|
|
|
|
<div v-else-if="error" class="error-text">{{ error }}</div> |
|
|
|
|
|
<div v-else-if="!article" class="hint-text">文章不存在</div> |
|
|
|
|
|
<article v-else class="detail"> |
|
|
<h1 class="detail-title">{{ article.title }}</h1> |
|
|
<h1 class="detail-title">{{ article.title }}</h1> |
|
|
<p v-if="article.summary" class="detail-summary">{{ article.summary }}</p> |
|
|
<p v-if="article.summary" class="detail-summary">{{ article.summary }}</p> |
|
|
<div class="detail-meta"> |
|
|
<div class="detail-meta"> |
|
|
@ -57,9 +68,6 @@ watch(slug, load, { immediate: true }) |
|
|
<div class="article-content" v-html="renderedContent"></div> |
|
|
<div class="article-content" v-html="renderedContent"></div> |
|
|
<CommentSection :slug="slug" /> |
|
|
<CommentSection :slug="slug" /> |
|
|
</article> |
|
|
</article> |
|
|
|
|
|
|
|
|
<p v-else-if="loading" class="hint-text">加载中...</p> |
|
|
|
|
|
<p v-else class="error-text">{{ error || '文章不存在' }}</p> |
|
|
|
|
|
</template> |
|
|
</template> |
|
|
|
|
|
|
|
|
<style scoped> |
|
|
<style scoped> |
|
|
|