Files
SuBlog/pages/posts/[slug].vue
T
sutong 9756bbaa5a fix: 表格单元格缺少内边距,内容紧贴左边框
Tailwind 的 @tailwindcss/typography 插件默认不给 th/td 设置 padding,
加上 prose-th:px-3 prose-th:py-2 / prose-td:px-3 prose-td:py-2 解决。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-17 23:54:43 +08:00

203 lines
7.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { siteConfig } from '~/config/site'
import { formatPostDate, isVisiblePost, sortPostsForSeries } from '~/utils/posts'
const router = useRouter()
const route = useRoute()
const slug = route.params.slug as string
const { data: post } = await useAsyncData(`post-${slug}`, () =>
queryCollection('posts')
.path(`/posts/${slug}`)
.first()
)
if (!post.value) {
throw createError({ statusCode: 404, message: '文章不存在' })
}
useSeoMeta({
title: post.value.title || slug,
description: post.value.description || '',
ogType: 'article',
ogTitle: post.value.title || slug,
ogDescription: post.value.description || '',
ogImage: post.value.image || siteConfig.ogImage,
twitterCard: 'summary_large_image',
twitterTitle: post.value.title || slug,
twitterDescription: post.value.description || '',
twitterImage: post.value.image || siteConfig.ogImage,
})
const { data: seriesSourcePosts } = await useAsyncData(`series-source-${slug}`, () =>
queryCollection('posts')
.order('published', 'ASC')
.all()
)
const seriesPosts = computed(() => {
if (!post.value?.series) return []
return sortPostsForSeries(
(seriesSourcePosts.value || [])
.filter(isVisiblePost)
.filter(item => item.series === post.value?.series)
)
})
const currentSeriesIndex = computed(() =>
seriesPosts.value.findIndex(item => item.path === post.value?.path)
)
const previousSeriesPost = computed(() => {
if (currentSeriesIndex.value <= 0) return null
return seriesPosts.value[currentSeriesIndex.value - 1]
})
const nextSeriesPost = computed(() => {
if (currentSeriesIndex.value < 0) return null
return seriesPosts.value[currentSeriesIndex.value + 1] || null
})
function formatDate(date: string) {
return formatPostDate(date)
}
</script>
<template>
<article v-if="post" class="container mx-auto max-w-3xl px-4 py-12">
<header class="mb-8 anim-fade-in-up">
<div class="mb-4 flex flex-wrap items-center gap-x-3 gap-y-2">
<time v-if="post.published" class="text-sm text-muted-foreground">
{{ formatDate(post.published) }}
</time>
<span v-if="post.updated" class="text-sm text-muted-foreground">
更新 {{ formatDate(post.updated) }}
</span>
<span v-if="post.difficulty" class="text-sm text-muted-foreground">
{{ post.difficulty }}
</span>
<NuxtLink
v-if="post.series"
:to="`/series/${encodeURIComponent(post.series)}`"
class="inline-flex items-center rounded-full px-2.5 py-1 text-xs font-medium hover:opacity-80"
style="background: var(--color-bg-hover); color: var(--color-primary);"
>
{{ post.series }}
</NuxtLink>
</div>
<h1 class="mb-4 text-4xl font-bold">{{ post.title }}</h1>
<p v-if="post.description" class="text-lg text-muted-foreground">
{{ post.description }}
</p>
<div v-if="post.tags?.length" class="mt-4 flex flex-wrap gap-2">
<span
v-for="tag in post.tags"
:key="tag"
class="tag-pill cursor-pointer"
@click="router.push(`/posts?tag=${encodeURIComponent(tag)}`)"
>
{{ tag }}
</span>
</div>
<div v-if="post.image" class="mt-6">
<img
:src="post.image"
:alt="post.title"
class="w-full rounded-lg object-cover"
/>
</div>
</header>
<div class="prose prose-neutral dark:prose-invert max-w-none break-words
prose-headings:text-foreground prose-headings:scroll-mt-14
prose-p:text-foreground
prose-strong:text-foreground
prose-a:text-primary prose-a:underline prose-a:underline-offset-4 prose-a:break-all prose-a:transition-opacity prose-a:hover:opacity-80
prose-blockquote:border-l-primary prose-blockquote:text-muted-foreground
prose-code:bg-muted prose-code:text-foreground prose-code:rounded prose-code:px-1.5 prose-code:py-0.5 prose-code:before:content-none prose-code:after:content-none
prose-pre:bg-muted prose-pre:px-4 prose-pre:py-2 prose-pre:text-foreground prose-pre:overflow-x-auto
prose-hr:border-border
prose-th:border prose-th:border-border prose-th:bg-muted prose-th:px-3 prose-th:py-2
prose-td:border prose-td:border-border prose-td:px-3 prose-td:py-2
prose-img:rounded-lg
anim-fade-in-up anim-delay-1">
<ContentRenderer :value="post" />
</div>
<section
v-if="post.series && seriesPosts.length > 1"
class="mt-12 pt-8 anim-fade-in-up anim-delay-2"
style="border-top: 1px solid var(--color-border)"
>
<div class="mb-4 flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between">
<div>
<p class="text-sm text-muted-foreground">当前系列</p>
<h2 class="text-xl font-semibold">{{ post.series }}</h2>
</div>
<NuxtLink
:to="`/series/${encodeURIComponent(post.series)}`"
class="text-sm hover:underline"
style="color: var(--color-primary)"
>
查看全部 {{ seriesPosts.length }}
</NuxtLink>
</div>
<div class="grid gap-3 md:grid-cols-2">
<NuxtLink
v-if="previousSeriesPost"
:to="previousSeriesPost.path || '/posts'"
class="group rounded-lg border p-4 transition-colors hover:bg-muted"
style="border-color: var(--color-border);"
>
<p class="mb-1 text-xs text-muted-foreground">上一篇</p>
<p class="font-medium transition-opacity group-hover:opacity-70">{{ previousSeriesPost.title }}</p>
</NuxtLink>
<div
v-else
class="rounded-lg border p-4 text-sm text-muted-foreground"
style="border-color: var(--color-border);"
>
已经是本系列第一篇
</div>
<NuxtLink
v-if="nextSeriesPost"
:to="nextSeriesPost.path || '/posts'"
class="group rounded-lg border p-4 text-right transition-colors hover:bg-muted"
style="border-color: var(--color-border);"
>
<p class="mb-1 text-xs text-muted-foreground">下一篇</p>
<p class="font-medium transition-opacity group-hover:opacity-70">{{ nextSeriesPost.title }}</p>
</NuxtLink>
<div
v-else
class="rounded-lg border p-4 text-right text-sm text-muted-foreground"
style="border-color: var(--color-border);"
>
已经是本系列最后一篇
</div>
</div>
</section>
<div class="anim-fade-in-up anim-delay-2">
<BlogGiscus class="mt-16" />
</div>
<div class="mt-8 pt-6 anim-fade-in-up anim-delay-3" style="border-top: 1px solid var(--color-border)">
<NuxtLink to="/posts" class="text-sm hover:underline" style="color: var(--color-primary)">
&larr; 返回文章列表
</NuxtLink>
</div>
<!-- 右侧目录导航 body AST 提取所有标题SSR 阶段全量渲染 -->
<BlogPostToc :body="post.body" />
</article>
</template>