Files
template-nuxt4/app/components/ArticleListPage.vue
2026-04-29 01:33:33 +08:00

369 lines
8.7 KiB
Vue

<template>
<div class="list-page">
<!-- 页面头部 Banner -->
<div :style="{ background: config.bannerGradient }" class="page-banner">
<div class="mx-auto max-w-screen-xl px-4">
<h1 class="banner-title">{{ config.title }}</h1>
<p class="banner-desc">{{ config.desc }}</p>
</div>
</div>
<div class="mx-auto max-w-screen-xl px-4 py-8">
<a-row :gutter="[32, 0]">
<!-- 左侧分类导航 -->
<a-col :lg="5" :xs="24" class="mb-6 lg:mb-0">
<div class="category-sidebar">
<div class="category-sidebar-title">{{ config.title }}</div>
<div
v-for="cat in config.categories"
:key="cat.type"
:class="{ active: activeType === cat.type }"
class="category-item"
@click="selectType(cat.type)"
>
{{ cat.label }}
</div>
</div>
</a-col>
<!-- 右侧内容区 -->
<a-col :lg="19" :xs="24">
<!-- 当前分类提示 -->
<div class="category-breadcrumb">
<span class="category-name">{{ currentCategoryLabel }}</span>
<span class="article-count"> {{ total }} 篇文章</span>
</div>
<div v-if="loading" class="loading-state">
<a-skeleton v-for="i in 5" :key="i" :paragraph="{ rows: 2 }" active style="margin-bottom:16px" />
</div>
<div v-else>
<div class="article-list">
<div
v-for="article in articles"
:key="article.id"
class="article-item"
@click="handleView(article)"
>
<div v-if="article.image" class="article-thumb">
<img :alt="article.title" :src="article.image" />
</div>
<div class="article-main">
<h3 class="article-title">{{ article.title }}</h3>
<p class="article-overview">{{ article.overview }}</p>
<div class="article-meta">
<span v-if="article.type" class="meta-tag">{{ getCategoryLabel(article.type) }}</span>
<span class="meta-item">{{ article.source }}</span>
<span class="meta-item">{{ article.publishTime }}</span>
<span v-if="article.views" class="meta-item">👁 {{ article.views }}</span>
</div>
</div>
</div>
</div>
<div v-if="articles.length === 0" class="empty-state">
<a-empty description="暂无内容" />
</div>
<div v-if="total > pageSize" class="pagination-wrap">
<a-pagination
v-model:current="currentPage"
:page-size="pageSize"
:total="total"
show-quick-jumper
@change="handlePageChange"
/>
</div>
</div>
</a-col>
</a-row>
</div>
</div>
</template>
<script lang="ts" setup>
import { message } from 'ant-design-vue'
interface PageConfig {
title: string
desc: string
bannerGradient: string
categories: Array<{ type: string; label: string }>
baseRoute: string
}
const props = defineProps<{
config: PageConfig
}>()
const route = useRoute()
const router = useRouter()
const activeType = ref((route.query.type as string) || '')
const currentPage = ref(1)
const pageSize = ref(12)
const total = ref(0)
const loading = ref(false)
const articles = ref<any[]>([])
const currentCategoryLabel = computed(() => {
if (!activeType.value) return '全部文章'
const found = props.config.categories.find(c => c.type === activeType.value)
return found?.label || '全部文章'
})
function getCategoryLabel(type: string) {
const found = props.config.categories.find(c => c.type === type)
return found?.label || type
}
function selectType(type: string) {
activeType.value = type
currentPage.value = 1
router.replace({ query: type ? { type } : {} })
loadArticles()
}
async function loadArticles() {
loading.value = true
try {
// TODO: 接入实际API
// const res = await listArticles({ category: props.config.baseRoute, type: activeType.value, page: currentPage.value })
// Fallback mock data
total.value = 35
articles.value = Array.from({ length: Math.min(pageSize.value, 35 - (currentPage.value - 1) * pageSize.value) }, (_, i) => ({
id: (currentPage.value - 1) * pageSize.value + i + 1,
title: `${currentCategoryLabel.value}文章标题 ${(currentPage.value - 1) * pageSize.value + i + 1}:广西政策研究成果发布`,
overview: '摘要内容:本文就广西经济社会发展中的若干重大问题进行深入研究,提出了切实可行的政策建议和对策措施,为相关决策提供参考依据...',
image: `https://picsum.photos/200/130?random=${(currentPage.value - 1) * pageSize.value + i + 1}`,
source: '广西决策咨询中心',
publishTime: `2024-12-${String(20 - i).padStart(2, '0')}`,
views: Math.floor(Math.random() * 2000) + 100,
type: activeType.value || props.config.categories[i % props.config.categories.length]?.type,
}))
} catch (e: any) {
message.error('加载失败')
} finally {
loading.value = false
}
}
function handlePageChange(page: number) {
currentPage.value = page
loadArticles()
window.scrollTo({ top: 0, behavior: 'smooth' })
}
function handleView(article: any) {
router.push(`/article/${article.id}`)
}
watch(() => route.query.type, (newType) => {
activeType.value = (newType as string) || ''
currentPage.value = 1
loadArticles()
})
onMounted(() => {
loadArticles()
})
</script>
<style scoped>
.list-page {
background: #f5f7fa;
min-height: 60vh;
}
.page-banner {
padding: 48px 0 32px;
position: relative;
overflow: hidden;
}
.banner-title {
color: #fff;
font-size: 30px;
font-weight: 700;
margin: 0 0 8px;
}
.banner-desc {
color: rgba(255,255,255,0.75);
font-size: 15px;
margin: 0;
}
/* 左侧分类 */
.category-sidebar {
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
overflow: hidden;
position: sticky;
top: 80px;
}
.category-sidebar-title {
padding: 14px 18px;
background: #1e3a5f;
color: #fff;
font-size: 14px;
font-weight: 600;
}
.category-item {
padding: 12px 18px;
font-size: 14px;
color: #374151;
cursor: pointer;
border-bottom: 1px solid #f5f5f5;
transition: all 0.2s;
}
.category-item:hover {
background: #f0f7ff;
color: #1e3a5f;
}
.category-item.active {
background: #eff6ff;
color: #1e3a5f;
font-weight: 600;
border-left: 3px solid #1e3a5f;
}
/* 内容区 */
.category-breadcrumb {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: #fff;
border-radius: 8px;
margin-bottom: 16px;
box-shadow: 0 1px 4px rgba(0,0,0,0.04);
}
.category-name {
font-size: 16px;
font-weight: 700;
color: #1e3a5f;
}
.article-count {
font-size: 13px;
color: #9ca3af;
}
.article-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.article-item {
display: flex;
gap: 20px;
padding: 20px;
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 6px rgba(0,0,0,0.05);
cursor: pointer;
transition: all 0.2s;
}
.article-item:hover {
box-shadow: 0 4px 16px rgba(0,0,0,0.1);
transform: translateY(-2px);
}
.article-thumb {
width: 160px;
height: 108px;
border-radius: 8px;
overflow: hidden;
flex-shrink: 0;
}
.article-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
}
.article-main {
flex: 1;
display: flex;
flex-direction: column;
}
.article-title {
font-size: 17px;
font-weight: 600;
color: #1f2937;
margin: 0 0 8px;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.article-overview {
font-size: 13px;
color: #6b7280;
margin: 0 0 auto;
line-height: 1.6;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.article-meta {
display: flex;
align-items: center;
gap: 12px;
margin-top: 12px;
}
.meta-tag {
padding: 2px 8px;
background: #eff6ff;
color: #1e40af;
font-size: 11px;
border-radius: 4px;
font-weight: 500;
}
.meta-item {
font-size: 12px;
color: #9ca3af;
}
.loading-state {
background: #fff;
border-radius: 12px;
padding: 20px;
}
.empty-state {
background: #fff;
border-radius: 12px;
padding: 60px;
text-align: center;
}
.pagination-wrap {
margin-top: 32px;
text-align: center;
padding-bottom: 20px;
}
@media (max-width: 768px) {
.article-item { flex-direction: column; }
.article-thumb { width: 100%; height: 180px; }
}
</style>