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

187 lines
5.8 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, NewsDetail } from '~/types/site';
const { t, locale } = useI18n();
const localePath = useLocalePath();
const route = useRoute();
const { getNewsDetail } = useSiteApi();
const slug = String(route.params.slug || '');
const article = ref<NewsDetail | null>(null);
const loading = ref(true);
const notFound = ref(false);
onMounted(async () => {
try {
const res = await getNewsDetail(slug);
if (!res) {
notFound.value = true;
return;
}
article.value = res;
// SEO:新闻详情页
await usePageSeo('news_detail', res.id, locale.value === 'en' ? res.title_en : res.title_zh);
} catch {
notFound.value = true;
} finally {
loading.value = false;
}
});
const title = computed(() => (locale.value === 'en' ? article.value?.title_en : article.value?.title_zh) || '');
const content = computed(() => (locale.value === 'en' ? article.value?.content_en : article.value?.content_zh) || '');
function formatDate(iso: string | null): string {
if (!iso) return '';
const d = new Date(iso);
return d.toLocaleDateString(locale.value === 'en' ? 'en-US' : 'zh-CN') + ' ' + d.toLocaleTimeString(locale.value === 'en' ? 'en-US' : 'zh-CN', { hour: '2-digit', minute: '2-digit' });
}
function recTitle(a: NewsArticle): string {
return locale.value === 'en' ? a.title_en : a.title_zh;
}
</script>
<template>
<div v-if="loading" class="min-h-60vh flex-col-center text-sm text-gray-400">
{{ t('common.loading') }}
</div>
<!-- 文章不存在 -->
<div v-else-if="notFound || !article" class="min-h-60vh flex-col-center px-16px text-center">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">404</h1>
<p class="mt-12px text-gray-500 dark:text-gray-400">{{ t('common.notFoundDesc') }}</p>
<NuxtLink
:to="localePath('/news')"
class="mt-24px px-24px py-10px rounded-6px bg-primary text-white text-sm hover:opacity-90 transition-opacity"
>
{{ t('nav.news') }}
</NuxtLink>
</div>
<div v-else>
<!-- 文章头部 -->
<section class="section-container py-48px md:py-64px flex flex-col items-center text-center gap-16px">
<nav class="flex items-center gap-8px text-sm text-gray-400">
<NuxtLink :to="localePath('/')" class="hover:text-primary transition-colors">{{ t('nav.home') }}</NuxtLink>
<span>/</span>
<NuxtLink :to="localePath('/news')" class="hover:text-primary transition-colors">{{ t('nav.news') }}</NuxtLink>
</nav>
<h1 class="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white max-w-860px leading-tight">{{ title }}</h1>
<time class="text-sm text-gray-400">{{ formatDate(article.published_at) }}</time>
</section>
<!-- 封面图 -->
<div v-if="article.cover_url" class="section-container">
<img
:src="article.cover_url"
:alt="title"
class="w-full max-h-480px object-cover rounded-16px shadow-md"
/>
</div>
<!-- 正文CMS 输入支持 HTML -->
<article class="section-container py-48px md:py-64px">
<div
class="prose max-w-860px mx-auto text-base md:text-lg leading-relaxed text-gray-700 dark:text-gray-200 break-words"
v-html="content"
/>
</article>
<!-- 推荐文章 -->
<section v-if="article.recommendations?.length" class="border-t border-gray-100 dark:border-dark-700 bg-gray-50 dark:bg-dark-900">
<div class="section-container py-48px md:py-64px">
<h2 class="text-xl md:text-2xl font-bold text-gray-900 dark:text-white mb-24px">
{{ t('common.relatedNews') }}
</h2>
<div class="grid gap-16px sm:grid-cols-2 lg:grid-cols-3">
<NuxtLink
v-for="rec in article.recommendations"
:key="rec.id"
:to="localePath(`/news/${rec.slug}`)"
class="group flex flex-col rounded-12px border border-gray-100 dark:border-dark-700 overflow-hidden bg-white dark:bg-dark-800 hover:shadow-md hover:-translate-y-2px transition-all duration-300"
>
<div class="aspect-[16/8] bg-gray-50 dark:bg-dark-700 overflow-hidden">
<img v-if="rec.cover_url" :src="rec.cover_url" :alt="recTitle(rec)" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500" />
</div>
<div class="p-16px flex flex-col gap-6px">
<h3 class="font-medium text-gray-900 dark:text-white group-hover:text-primary transition-colors line-clamp-1">
{{ recTitle(rec) }}
</h3>
<time class="text-xs text-gray-400">{{ formatDate(rec.published_at) }}</time>
</div>
</NuxtLink>
</div>
</div>
</section>
</div>
</template>
<style scoped>
/* 正文 HTML 排版(CMS 输入支持 HTML */
.prose :deep(h2),
.prose :deep(h3),
.prose :deep(h4) {
margin-top: 1.5em;
margin-bottom: 0.6em;
font-weight: 700;
color: inherit;
}
.prose :deep(h2) {
font-size: 1.5em;
}
.prose :deep(h3) {
font-size: 1.25em;
}
.prose :deep(p) {
margin: 1em 0;
}
.prose :deep(ul),
.prose :deep(ol) {
margin: 1em 0;
padding-left: 1.5em;
list-style: revert;
}
.prose :deep(img) {
max-width: 100%;
height: auto;
border-radius: 12px;
margin: 1.5em auto;
display: block;
}
.prose :deep(a) {
color: #2563eb;
text-decoration: underline;
}
.prose :deep(blockquote) {
border-left: 4px solid #d1d5db;
padding-left: 1em;
color: #6b7280;
margin: 1.5em 0;
}
.prose :deep(table) {
width: 100%;
border-collapse: collapse;
margin: 1.5em 0;
}
.prose :deep(th),
.prose :deep(td) {
border: 1px solid #e5e7eb;
padding: 8px 12px;
}
.prose :deep(pre) {
background: #f3f4f6;
padding: 1em;
border-radius: 8px;
overflow-x: auto;
margin: 1.5em 0;
}
.prose :deep(code) {
background: #f3f4f6;
padding: 2px 6px;
border-radius: 4px;
font-size: 0.9em;
}
</style>