Files
nl-blogs/client/src/pages/ColumnDetail.vue
2026-06-25 17:57:51 +08:00

183 lines
6.4 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.
<template>
<section class="page-section block animate-slide-down">
<div class="max-w-4xl mx-auto pt-32 px-6 pb-20">
<!-- Loading state -->
<div v-if="loading" class="flex justify-center items-center h-64">
<div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-art-accent"></div>
</div>
<div v-else-if="column">
<!-- Header -->
<div class="mb-16 text-center">
<div class="inline-block mb-4 px-3 py-1 rounded-full border border-art-accent/30 text-art-accent text-xs font-mono">专栏</div>
<h2 class="font-serif text-4xl italic text-white mb-6">{{ column.name }}</h2>
<p class="text-art-muted max-w-lg mx-auto leading-relaxed mb-6">{{ column.description }}</p>
<!-- 统计信息 -->
<div class="flex items-center justify-center gap-6 text-sm text-art-muted">
<div class="flex items-center gap-2">
<i data-lucide="file-text" class="w-4 h-4"></i>
<span>{{ posts.length }} 篇文章</span>
</div>
<div v-if="lastUpdated" class="flex items-center gap-2">
<i data-lucide="clock" class="w-4 h-4"></i>
<span>最近更新:{{ formatDate(lastUpdated) }}</span>
</div>
</div>
</div>
<!-- Posts List -->
<div v-if="posts.length > 0" class="space-y-8">
<article
v-for="post in posts"
:key="post.id"
class="group cursor-pointer border-b border-white/5 pb-8"
@click="router.push('/blog/' + post.id)"
>
<div class="flex flex-col md:flex-row gap-6 items-start">
<div class="md:w-1/4 pt-2">
<span class="font-mono text-xs text-art-accent block mb-1">{{ post.categoryName || '未分类' }}</span>
<span class="text-sm text-art-muted">{{ post.date }}</span>
<AuthorInfo
class="mt-3"
:name="post.userName"
:email="post.userEmail"
:avatar="post.userAvatar"
:user-id="post.userId"
variant="compact"
size="sm"
/>
<div v-if="post.tags && post.tags.length > 0" class="flex flex-wrap gap-2 mt-2">
<span v-for="tag in post.tags" :key="tag.id" class="text-xs text-white/40 bg-white/5 px-1 rounded">#{{ tag.name }}</span>
</div>
</div>
<div class="md:w-3/4 space-y-3">
<h3 class="font-serif text-2xl text-white group-hover:text-art-accent transition-colors leading-tight">
{{ post.title }}
</h3>
<p class="text-art-muted font-light leading-relaxed line-clamp-2">
{{ post.excerpt }}
</p>
<div class="text-xs text-art-accent font-medium mt-4 group-hover:text-white transition-colors">阅读全文 -></div>
</div>
</div>
</article>
</div>
<div v-else class="text-center py-10 border border-dashed border-white/10 rounded-lg">
<p class="text-art-muted">该专栏暂无文章</p>
</div>
</div>
<div v-else class="text-center py-20 text-art-muted">
未找到专栏
</div>
</div>
</section>
</template>
<script setup lang="ts">
import { ref, onMounted, nextTick, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { fetchColumn, fetchColumnPosts, Column, Post } from '../services/api'
import { useScrollAnimation } from '../composables/useScrollAnimation'
import AuthorInfo from '../components/AuthorInfo.vue'
const route = useRoute()
const router = useRouter()
const column = ref<Column | null>(null)
const posts = ref<Post[]>([])
const loading = ref(true)
const { initObserver } = useScrollAnimation()
// 计算最近更新时间
const lastUpdated = computed(() => {
if (posts.value.length === 0) {
// 如果没有文章,使用专栏的更新时间
if (column.value?.updatedAt) {
const timestamp = typeof column.value.updatedAt === 'string'
? parseInt(column.value.updatedAt)
: column.value.updatedAt
return new Date(timestamp * 1000)
}
return null
}
// 找到所有文章中最新的日期
const dates = posts.value
.map(post => {
// 使用 post.date,它通常是格式化的日期字符串
if (!post.date) return null
// 尝试解析日期字符串
const date = new Date(post.date)
if (isNaN(date.getTime())) return null
return date
})
.filter((date): date is Date => date !== null)
if (dates.length === 0) {
// 如果无法解析文章日期,使用专栏更新时间
if (column.value?.updatedAt) {
const timestamp = typeof column.value.updatedAt === 'string'
? parseInt(column.value.updatedAt)
: column.value.updatedAt
return new Date(timestamp * 1000)
}
return null
}
// 返回最新的日期
return new Date(Math.max(...dates.map(d => d.getTime())))
})
// 格式化日期
const formatDate = (date: Date | null): string => {
if (!date) return ''
const now = new Date()
const diff = now.getTime() - date.getTime()
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
if (days === 0) return '今天'
if (days === 1) return '昨天'
if (days < 7) return `${days} 天前`
if (days < 30) return `${Math.floor(days / 7)} 周前`
if (days < 365) return `${Math.floor(days / 30)} 个月前`
// 超过一年,显示具体日期
return date.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' })
}
onMounted(async () => {
try {
const id = parseInt(route.params.id as string)
const [colData, postsData] = await Promise.all([
fetchColumn(id),
fetchColumnPosts(id)
])
column.value = colData
posts.value = postsData
// 初始化动画观察者
await nextTick()
initObserver()
// 刷新图标
if ((window as any).lucide) {
(window as any).lucide.createIcons()
}
} catch (error) {
console.error('Failed to fetch column:', error)
} finally {
loading.value = false
// 确保在加载完成后也初始化动画
await nextTick()
initObserver()
if ((window as any).lucide) {
(window as any).lucide.createIcons()
}
}
})
</script>