Files
jczxw-pc/app/components/ArticleListPage.vue
T
2026-09-11 12:25:38 +08:00

398 lines
8.8 KiB
Vue

<template>
<div class="list-page">
<PortalHeader v-if="config.useSiteHeader" />
<!-- <div v-else class="page-banner" :style="{ background: config.bannerGradient }" />-->
<div class="mx-auto max-w-screen-xl px-4 py-8">
<a-row :gutter="[32, 0]">
<!-- 左侧分类导航 -->
<a-col :xs="24" :lg="5" 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="category-item"
:class="{ active: activeType === cat.type }"
@click="selectType(cat.type)"
>
{{ cat.label }}
</div>
</div>
</a-col>
<!-- 右侧内容区 -->
<a-col :xs="24" :lg="19">
<!-- 当前分类提示 -->
<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" active :paragraph="{ rows: 2 }" 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 class="article-thumb" v-if="article.image">
<img :src="article.image" :alt="article.title" />
</div>
<div class="article-main">
<h3 class="article-title">{{ article.title }}</h3>
<p class="article-overview">{{ article.overview }}</p>
<div class="article-meta">
<span class="meta-tag" v-if="article.type">{{ getCategoryLabel(article.type) }}</span>
<span class="meta-item">{{ article.source }}</span>
<span class="meta-item">{{ article.publishTime }}</span>
<span class="meta-item" v-if="article.views">👁 {{ article.views }}</span>
</div>
</div>
</div>
</div>
<div v-if="articles.length === 0" class="empty-state">
<a-empty description="暂无内容" />
</div>
<div class="pagination-wrap" v-if="total > pageSize">
<a-pagination
v-model:current="currentPage"
:total="total"
:page-size="pageSize"
show-quick-jumper
@change="handlePageChange"
/>
</div>
</div>
</a-col>
</a-row>
</div>
</div>
</template>
<script setup lang="ts">
import { message } from 'ant-design-vue'
import { pageSiteArticles, resolveSiteAssetUrl, type SiteArticle } from '@/api/site'
interface PageConfig {
title: string
desc: string
bannerGradient: string
categories: Array<{ type: string; label: string }>
baseRoute?: string
useSiteHeader?: boolean
}
interface ArticleItem {
id: number
title: string
overview: string
image: string
source: string
publishTime: string
views: number
type?: 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<ArticleItem[]>([])
const keyword = computed(() => (route.query.keyword as string) || '')
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: {
...(keyword.value ? { keyword: keyword.value } : {}),
...(type ? { type } : {}),
}
})
loadArticles()
}
function normalizeArticle(item: SiteArticle): ArticleItem {
return {
id: item.articleId || item.id,
title: item.title,
overview: item.overview || '暂无摘要',
image: resolveSiteAssetUrl(item.image),
source: item.source || '广西决策咨询网',
publishTime: item.publishTime || item.createTime || '',
views: Number(item.views || item.actualViews || 0),
type: item.type,
}
}
async function loadArticles() {
loading.value = true
try {
const data = await pageSiteArticles({
page: currentPage.value,
limit: pageSize.value,
keywords: keyword.value || undefined,
category: props.config.baseRoute || undefined,
type: activeType.value || undefined,
})
total.value = data.count || 0
articles.value = (data.list || []).map(normalizeArticle)
} catch (e) {
message.error(e instanceof Error ? e.message : '加载失败')
total.value = 0
articles.value = []
} finally {
loading.value = false
}
}
function handlePageChange(page: number) {
currentPage.value = page
loadArticles()
window.scrollTo({ top: 0, behavior: 'smooth' })
}
function handleView(article: ArticleItem) {
router.push(`/article/${article.id}`)
}
watch(() => route.query.type, (newType) => {
activeType.value = (newType as string) || ''
currentPage.value = 1
loadArticles()
})
watch(() => route.query.keyword, () => {
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>