143 lines
4.9 KiB
Vue
143 lines
4.9 KiB
Vue
<template>
|
|
<section id="blog" class="page-section block animate-slide-down">
|
|
<div class="max-w-4xl mx-auto pt-32 px-6 pb-20">
|
|
<div class="mb-16 text-center">
|
|
<h2 class="font-serif text-5xl italic text-white mb-6">深度思考</h2>
|
|
<p class="text-art-muted max-w-lg mx-auto mb-8">关于前端技术、交互设计以及数字艺术的深度思考。</p>
|
|
|
|
<!-- Search UI -->
|
|
<div class="max-w-md mx-auto relative">
|
|
<input
|
|
type="text"
|
|
v-model="searchQuery"
|
|
@keyup.enter="handleSearch"
|
|
placeholder="搜索文章标题或内容..."
|
|
class="w-full bg-white/5 border border-white/10 rounded-full px-6 py-3 text-white placeholder-white/30 focus:outline-none focus:border-art-accent transition-colors"
|
|
/>
|
|
<button
|
|
v-if="searchQuery"
|
|
@click="clearSearch"
|
|
class="absolute right-12 top-1/2 -translate-y-1/2 text-white/30 hover:text-white transition-colors"
|
|
>
|
|
<i data-lucide="x" class="w-4 h-4"></i>
|
|
</button>
|
|
<button
|
|
@click="handleSearch"
|
|
class="absolute right-4 top-1/2 -translate-y-1/2 text-white/50 hover:text-art-accent transition-colors"
|
|
>
|
|
<i data-lucide="search" class="w-5 h-5"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 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>
|
|
|
|
<!-- Error state -->
|
|
<div v-else-if="error" class="text-center text-red-500 py-20">
|
|
<p class="mb-4">{{ error }}</p>
|
|
<button @click="fetchBlogPosts" class="px-4 py-2 bg-art-accent text-white rounded hover:bg-opacity-80 transition-colors">
|
|
重试
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Empty state -->
|
|
<div v-else-if="blogPosts.length === 0" class="text-center py-20 text-art-muted">
|
|
<p>没有找到相关文章</p>
|
|
<button v-if="isSearching" @click="clearSearch" class="mt-4 text-art-accent hover:underline">
|
|
清除搜索条件
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Blog posts list -->
|
|
<div v-else class="space-y-12" id="blog-list-container">
|
|
<!-- Blog posts will be rendered here -->
|
|
<article
|
|
v-for="post in blogPosts"
|
|
:key="post.id"
|
|
class="group cursor-pointer border-b border-white/5 pb-12"
|
|
@click="$router.push('/blog/' + post.id)"
|
|
>
|
|
<div class="flex flex-col md:flex-row gap-8 items-start">
|
|
<div class="md:w-1/4 pt-2">
|
|
<span class="font-mono text-xs text-art-accent block mb-1">{{ post.category }}</span>
|
|
<span class="text-sm text-art-muted">{{ post.date }}</span>
|
|
</div>
|
|
<div class="md:w-3/4 space-y-4">
|
|
<h3 class="font-serif text-3xl text-white group-hover:text-art-accent transition-colors leading-tight" v-html="highlightText(post.title)">
|
|
</h3>
|
|
<p class="text-art-muted font-light leading-relaxed" v-html="highlightText(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>
|
|
</section>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted, nextTick } from 'vue'
|
|
import { fetchPosts, Post } from '../services/api'
|
|
import { useScrollAnimation } from '../composables/useScrollAnimation'
|
|
|
|
const blogPosts = ref<Post[]>([])
|
|
const loading = ref(false)
|
|
const error = ref('')
|
|
const searchQuery = ref('')
|
|
const isSearching = ref(false)
|
|
|
|
const { initObserver } = useScrollAnimation()
|
|
|
|
const refreshIcons = () => {
|
|
if ((window as any).lucide) {
|
|
(window as any).lucide.createIcons()
|
|
}
|
|
}
|
|
|
|
const fetchBlogPosts = async (query?: string) => {
|
|
loading.value = true
|
|
error.value = ''
|
|
try {
|
|
const posts = await fetchPosts(query)
|
|
blogPosts.value = posts
|
|
nextTick(() => {
|
|
initObserver()
|
|
refreshIcons()
|
|
})
|
|
} catch (err) {
|
|
console.error('Error fetching blog posts:', err)
|
|
error.value = '获取博客文章失败,请稍后重试'
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const handleSearch = () => {
|
|
if (!searchQuery.value.trim() && !isSearching.value) return
|
|
isSearching.value = !!searchQuery.value.trim()
|
|
fetchBlogPosts(searchQuery.value)
|
|
}
|
|
|
|
const clearSearch = () => {
|
|
searchQuery.value = ''
|
|
isSearching.value = false
|
|
fetchBlogPosts()
|
|
}
|
|
|
|
const highlightText = (text: string | undefined) => {
|
|
if (!text) return ''
|
|
if (!isSearching.value || !searchQuery.value) return text
|
|
|
|
const regex = new RegExp(`(${searchQuery.value})`, 'gi')
|
|
return text.replace(regex, '<span class="text-art-accent bg-art-accent/10 font-bold">$1</span>')
|
|
}
|
|
|
|
onMounted(() => {
|
|
fetchBlogPosts()
|
|
refreshIcons()
|
|
})
|
|
</script> |