183 lines
6.4 KiB
Vue
183 lines
6.4 KiB
Vue
<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>
|