Files
SuBlog/components/blog/PostCard.vue
T
sutong 169e22b4cb feat: 文章标签展示、搜索框清除按钮
- 文章详情页和文章卡片展示标签
- 搜索框右侧添加 × 清除按钮
- 标签点击跳转文章列表页自动搜索
- 修复 hasAnyFilter 未包含 tags 的 bug

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 22:43:59 +08:00

122 lines
4.0 KiB
Vue

<script setup lang="ts">
const props = defineProps<{
post: any
matchedLines?: string[]
highlight?: (text: string) => string
}>()
const expanded = ref(false)
function formatDate(date: string) {
return new Date(date).toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
}
function getWordCount(post: any): number {
const text = JSON.stringify(post.body || '').replace(/<[^>]*>/g, '')
const chinese = text.match(/[一-龥]/g) || []
const english = text.match(/[a-zA-Z]+/g) || []
return chinese.length + english.length
}
function getReadTime(count: number): number {
return Math.max(1, Math.ceil(count / 300))
}
const wordCount = computed(() => getWordCount(props.post))
const readTime = computed(() => getReadTime(wordCount.value))
function renderTitle(): string {
const text = props.post.title || '无标题'
return props.highlight ? props.highlight(text) : text
}
function renderDesc(): string {
if (!props.post.description) return ''
return props.highlight ? props.highlight(props.post.description) : props.post.description
}
</script>
<template>
<NuxtLink :to="post.path" class="block">
<div class="group transition-all hover:shadow-lg rounded-2xl py-6 overflow-hidden" style="border: 1px solid var(--color-border);">
<div class="px-6">
<div class="flex flex-col gap-4 md:flex-row">
<div v-if="post.image" class="md:w-48 md:flex-shrink-0">
<img
:src="post.image"
:alt="post.title"
loading="lazy"
decoding="async"
class="h-48 w-full rounded-md object-cover md:h-32"
/>
</div>
<div class="flex-1">
<div class="mb-2 flex flex-wrap items-center gap-x-2 gap-y-0">
<span
v-if="post.pinned"
class="shrink-0 inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium"
style="background: var(--color-text); color: var(--color-bg);"
>
置顶
</span>
<time v-if="post.published" class="shrink-0 text-sm text-muted-foreground whitespace-nowrap">
{{ formatDate(post.published) }}
</time>
<span class="shrink-0 text-sm text-muted-foreground whitespace-nowrap">
· {{ wordCount }}
</span>
<span class="shrink-0 text-sm text-muted-foreground whitespace-nowrap">
· {{ readTime }} 分钟
</span>
</div>
<h2
class="mb-2 text-2xl font-semibold group-hover:opacity-70 transition-opacity"
v-html="renderTitle()"
/>
<p
v-if="post.description"
class="text-muted-foreground"
v-html="renderDesc()"
/>
<div v-if="post.tags?.length" class="mt-2 flex flex-wrap gap-1.5">
<span
v-for="tag in post.tags"
:key="tag"
class="tag-pill"
>
{{ tag }}
</span>
</div>
<div v-if="matchedLines && matchedLines.length > 0" class="mt-3 space-y-1.5">
<div
v-for="(line, idx) in (expanded ? matchedLines : matchedLines.slice(0, 3))"
:key="idx"
class="text-sm text-muted-foreground border-l-2 pl-3"
style="border-color: var(--color-border);"
v-html="highlight ? highlight(line) : line"
/>
<button
v-if="matchedLines.length > 3"
type="button"
class="text-xs text-muted-foreground hover:opacity-70 transition-opacity mt-1"
@click.prevent.stop="expanded = !expanded"
>
{{ expanded ? '收起' : `展开 (还有 ${matchedLines.length - 3} 条)` }}
</button>
</div>
</div>
</div>
</div>
</div>
</NuxtLink>
</template>