195 lines
7.4 KiB
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>
|