Files
admin/www/app/pages/news/index.vue
T
2026-08-03 19:09:03 +08:00

173 lines
6.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import type { NewsArticle, NewsListConfig } from '~/types/site';
const { t, locale } = useI18n();
const localePath = useLocalePath();
const { getNews, getPageConfig } = useSiteApi();
// SEO:新闻列表页
await usePageSeo('news', undefined, `${t('app.name')} - ${t('nav.news')}`);
// ===== 页面配置 =====
const config = ref<NewsListConfig | null>(null);
// ===== 文章列表 + 分页 =====
const articles = ref<NewsArticle[]>([]);
const total = ref(0);
const page = ref(1);
const loading = ref(true);
const loadingMore = ref(false);
const noMore = ref(false);
const pageSize = computed(() => config.value?.page_size || 9);
const totalPages = computed(() => Math.max(Math.ceil(total.value / pageSize.value), 1));
async function loadArticles() {
const skip = (page.value - 1) * pageSize.value;
try {
const res = await getNews(skip, pageSize.value);
articles.value = res?.items ?? [];
total.value = res?.total ?? 0;
noMore.value = articles.value.length >= (res?.total ?? 0);
} catch {
articles.value = [];
}
}
async function loadMore() {
if (loadingMore.value || noMore.value) return;
loadingMore.value = true;
try {
const skip = articles.value.length;
const res = await getNews(skip, pageSize.value);
const items = res?.items ?? [];
articles.value = [...articles.value, ...items];
total.value = res?.total ?? 0;
noMore.value = articles.value.length >= total.value;
} catch {
// ignore
} finally {
loadingMore.value = false;
}
}
onMounted(async () => {
try {
const [cfg] = await Promise.all([getPageConfig<NewsListConfig>('news-list')]);
config.value = cfg;
} catch {
config.value = null;
}
await loadArticles();
loading.value = false;
});
function goPage(p: number) {
if (p < 1 || p > totalPages.value || p === page.value) return;
page.value = p;
loadArticles();
window.scrollTo({ top: 0, behavior: 'smooth' });
}
const heroTitle = computed(() => (locale.value === 'en' ? config.value?.hero_title_en : config.value?.hero_title_zh) || (locale.value === 'en' ? 'News' : '新闻中心'));
const heroSubtitle = computed(() => (locale.value === 'en' ? config.value?.hero_subtitle_en : config.value?.hero_subtitle_zh) || '');
function formatDate(iso: string | null): string {
if (!iso) return '';
return new Date(iso).toLocaleDateString(locale.value === 'en' ? 'en-US' : 'zh-CN');
}
</script>
<template>
<div>
<PageHero
:title="heroTitle"
:subtitle="heroSubtitle || undefined"
:bg-url="config?.hero_bg_url || undefined"
/>
<div v-if="loading" class="py-80px text-center text-sm text-gray-400">
{{ t('common.loading') }}
</div>
<div v-else class="section-container py-64px md:py-80px">
<!-- 文章卡片网格 -->
<div v-if="articles.length" class="grid gap-24px sm:grid-cols-2 lg:grid-cols-3">
<NuxtLink
v-for="article in articles"
:key="article.id"
:to="localePath(`/news/${article.slug}`)"
class="group flex flex-col rounded-16px border border-gray-100 dark:border-dark-700 overflow-hidden bg-white dark:bg-dark-800 hover:shadow-xl hover:-translate-y-4px transition-all duration-300"
>
<div class="aspect-[16/9] bg-gray-50 dark:bg-dark-700 overflow-hidden">
<img
v-if="article.cover_url"
:src="article.cover_url"
:alt="locale === 'en' ? article.title_en : article.title_zh"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
/>
<div v-else class="w-full h-full flex-center text-gray-300 dark:text-gray-600 text-4xl">📰</div>
</div>
<div class="p-20px flex flex-col gap-8px flex-1">
<time class="text-xs text-gray-400">{{ formatDate(article.published_at) }}</time>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white group-hover:text-primary transition-colors line-clamp-2">
{{ locale === 'en' ? article.title_en : article.title_zh }}
</h2>
<p class="text-sm text-gray-500 dark:text-gray-400 line-clamp-2">
{{ locale === 'en' ? article.summary_en : article.summary_zh }}
</p>
</div>
</NuxtLink>
</div>
<!-- 空状态 -->
<div v-else class="py-80px text-center">
<p class="text-gray-400">{{ t('common.empty') }}</p>
</div>
<!-- 分页页码模式 -->
<div
v-if="articles.length && config?.pagination_mode !== 'load_more' && totalPages > 1"
class="mt-48px flex items-center justify-center gap-8px"
>
<button
:disabled="page <= 1"
class="px-12px py-6px rounded-6px border border-gray-200 dark:border-dark-600 text-sm text-gray-600 dark:text-gray-300 disabled:opacity-40 disabled:cursor-not-allowed hover:border-primary transition-colors"
@click="goPage(page - 1)"
>
</button>
<button
v-for="p in totalPages"
:key="p"
class="min-w-32px px-8px py-6px rounded-6px border text-sm transition-colors"
:class="p === page
? 'bg-primary border-primary text-white'
: 'border-gray-200 dark:border-dark-600 text-gray-600 dark:text-gray-300 hover:border-primary'"
@click="goPage(p)"
>
{{ p }}
</button>
<button
:disabled="page >= totalPages"
class="px-12px py-6px rounded-6px border border-gray-200 dark:border-dark-600 text-sm text-gray-600 dark:text-gray-300 disabled:opacity-40 disabled:cursor-not-allowed hover:border-primary transition-colors"
@click="goPage(page + 1)"
>
</button>
</div>
<!-- 分页加载更多模式 -->
<div v-else-if="articles.length && !noMore" class="mt-48px flex justify-center">
<button
class="px-32px py-10px rounded-8px border border-gray-200 dark:border-dark-600 text-sm text-gray-700 dark:text-gray-200 hover:border-primary hover:text-primary transition-colors"
:disabled="loadingMore"
@click="loadMore"
>
{{ loadingMore ? t('common.loading') : t('common.loadMore') }}
</button>
</div>
</div>
</div>
</template>