Files
gongxue-new/docs/.vitepress/theme/pages/KnowledgeList.vue

195 lines
7.4 KiB
Vue

<script setup lang="ts">
import { ref, computed } from 'vue'
import { Library, BookOpen, TrendingUp, ChevronRight, Search, Loader2 } from 'lucide-vue-next'
import { categories, categoryLabel, formatDate, formatViewCount } from '../../utils/knowledge'
import articleIndex from '../../../knowledge/index.json'
const loading = ref(false)
const activeCategory = ref('')
const search = ref('')
const page = ref(1)
const perPage = 10
const allArticles = computed(() => {
return (articleIndex as any[])
.map((item) => ({
path: `/knowledge/${item.id}`,
title: item.title,
frontmatter: {
id: item.id,
title: item.title,
summary: item.summary || '',
category: item.category || '',
tags: item.tags || [],
coverImage: item.coverImage || '',
author: item.author || '',
isTop: item.isTop || false,
publishedAt: item.publishedAt || '',
viewCount: item.viewCount || 0,
}
}))
.sort((a, b) => {
if (a.frontmatter.isTop && !b.frontmatter.isTop) return -1
if (!a.frontmatter.isTop && b.frontmatter.isTop) return 1
return new Date(b.frontmatter.publishedAt || 0).getTime() - new Date(a.frontmatter.publishedAt || 0).getTime()
})
})
const filteredArticles = computed(() => {
let result = allArticles.value
if (activeCategory.value) {
result = result.filter(a => a.frontmatter.category === activeCategory.value)
}
if (search.value.trim()) {
const q = search.value.trim().toLowerCase()
result = result.filter(a =>
(a.frontmatter.title || '').toLowerCase().includes(q) ||
(a.frontmatter.summary || '').toLowerCase().includes(q)
)
}
return result
})
const totalPages = computed(() => Math.ceil(filteredArticles.value.length / perPage))
const paginatedArticles = computed(() => {
const start = (page.value - 1) * perPage
return filteredArticles.value.slice(start, start + perPage)
})
const handleCategoryChange = (cat: string) => {
activeCategory.value = cat
page.value = 1
}
const onSearchInput = () => {
page.value = 1
}
</script>
<template>
<div class="pt-24 min-h-screen bg-apple-grey animate-fade-in">
<div class="max-w-5xl mx-auto px-4 md:px-6 pb-20">
<div class="flex items-center gap-4 mb-10">
<div class="p-3 bg-apple-blue rounded-xl text-white">
<Library :size="32" />
</div>
<div class="flex-1">
<h1 class="text-3xl font-bold text-apple-dark">知识库</h1>
<p class="text-apple-text-gray">天津专升本百科全书</p>
</div>
<div class="hidden md:flex relative w-64">
<Search :size="16" class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
<input
type="text"
placeholder="搜索文章..."
v-model="search"
@input="onSearchInput"
class="w-full pl-9 pr-4 py-2 bg-white border border-gray-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-apple-blue/20 focus:border-apple-blue"
/>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="col-span-1 space-y-2">
<button
v-for="cat in categories"
:key="cat.key"
@click="handleCategoryChange(cat.key)"
:class="[
'w-full text-left p-4 rounded-xl cursor-pointer transition-colors',
activeCategory === cat.key
? 'bg-white shadow-sm font-semibold text-apple-blue'
: 'hover:bg-white hover:text-apple-dark text-gray-500'
]"
>
{{ cat.label }}
</button>
</div>
<div class="col-span-1 md:col-span-2 space-y-3 md:space-y-4">
<div v-if="loading" class="flex flex-col items-center justify-center py-20 text-gray-400">
<Loader2 :size="32" class="animate-spin mb-3" />
<p class="text-sm">加载文章中...</p>
</div>
<div v-else-if="paginatedArticles.length === 0" class="flex flex-col items-center justify-center py-20 text-gray-400">
<BookOpen :size="40" class="mb-3 opacity-30" />
<p class="text-sm">暂无文章</p>
</div>
<template v-else>
<a
v-for="article in paginatedArticles"
:key="article.path"
:href="article.path"
class="block bg-white p-4 md:p-6 rounded-xl md:rounded-2xl shadow-sm hover:shadow-md active:bg-gray-50 transition-all cursor-pointer group touch-manipulation"
>
<div class="flex gap-4">
<div
v-if="article.frontmatter.coverImage"
class="hidden sm:block w-28 h-20 rounded-lg overflow-hidden flex-shrink-0"
>
<img
:src="article.frontmatter.coverImage"
:alt="article.frontmatter.title"
class="w-full h-full object-cover"
loading="lazy"
/>
</div>
<div class="flex-1 min-w-0">
<div class="flex justify-between items-start gap-2 mb-2">
<h3 class="font-bold text-base md:text-lg text-apple-dark group-hover:text-apple-blue transition-colors leading-snug line-clamp-2">
<span
v-if="article.frontmatter.isTop"
class="inline-block bg-orange-100 text-orange-600 text-[10px] px-1.5 py-0.5 rounded mr-2 align-middle"
>置顶</span>
{{ article.frontmatter.title }}
</h3>
</div>
<p
v-if="article.frontmatter.summary"
class="text-sm text-gray-500 line-clamp-2 mb-2 hidden md:block"
>{{ article.frontmatter.summary }}</p>
<div class="flex items-center justify-between mt-2 md:mt-3">
<div class="flex items-center text-xs text-gray-400 gap-3 md:gap-4">
<span class="flex items-center gap-1">
<BookOpen :size="12" /> {{ categoryLabel[article.frontmatter.category || ''] || article.frontmatter.category }}
</span>
<span class="flex items-center gap-1">
<TrendingUp :size="12" /> {{ formatViewCount(article.frontmatter.viewCount || 0) }} 阅读
</span>
<span class="hidden sm:inline">{{ formatDate(article.frontmatter.publishedAt) }}</span>
</div>
<ChevronRight :size="18" class="text-gray-300 group-hover:text-apple-blue transition-colors flex-shrink-0" />
</div>
</div>
</div>
</a>
<div v-if="totalPages > 1" class="flex items-center justify-center gap-2 pt-6">
<button
v-for="p in totalPages"
:key="p"
@click="page = p"
:class="[
'w-10 h-10 rounded-xl text-sm font-medium transition-colors',
p === page
? 'bg-apple-blue text-white'
: 'bg-white text-gray-600 hover:bg-gray-100'
]"
>
{{ p }}
</button>
</div>
</template>
</div>
</div>
</div>
</div>
</template>