169e22b4cb
- 文章详情页和文章卡片展示标签 - 搜索框右侧添加 × 清除按钮 - 标签点击跳转文章列表页自动搜索 - 修复 hasAnyFilter 未包含 tags 的 bug Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
122 lines
4.0 KiB
Vue
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>
|