- formatDate兼容date和createdAt两种字段名 - 增加空值和无效日期防御 - 同步news.json分类数据(之前category全为null) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
245 行
6.2 KiB
Vue
245 行
6.2 KiB
Vue
<template>
|
||
<div class="page-news">
|
||
<LayoutPageHero
|
||
:title="seo.h1"
|
||
subtitle="品牌动态、产品发布、活动公告,第一时间了解呼籁旅行最新资讯"
|
||
:breadcrumbs="[{ text: '最新动态' }]"
|
||
background-image="/images/seasons/autumn-hero.jpg"
|
||
/>
|
||
|
||
<!-- TL;DR -->
|
||
<section class="page-tldr" aria-label="最新动态摘要">
|
||
<div class="container">
|
||
<div class="page-tldr-inner">
|
||
<p>呼籁旅行最新动态:品牌公告、产品发布、活动资讯、行业合作等信息,第一时间了解呼伦贝尔亲子游最新资讯。</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section v-if="newsData" class="section">
|
||
<div class="container">
|
||
|
||
<!-- Category filter -->
|
||
<div class="news-filter">
|
||
<button
|
||
v-for="cat in categories"
|
||
:key="cat"
|
||
:class="['filter-btn', { 'filter-btn--active': activeCategory === cat }]"
|
||
@click="activeCategory = cat"
|
||
>
|
||
{{ cat }}
|
||
</button>
|
||
</div>
|
||
|
||
<!-- News list -->
|
||
<div class="news-list">
|
||
<article
|
||
v-for="article in filteredArticles"
|
||
:key="article.id"
|
||
class="news-card"
|
||
>
|
||
<div class="news-meta">
|
||
<span :class="['news-cat', `news-cat--${article.category}`]">{{ article.category }}</span>
|
||
<time class="news-date" :datetime="article.date || article.createdAt">{{ formatDate(article.date || article.createdAt) }}</time>
|
||
</div>
|
||
<h2 class="news-title">{{ article.title }}</h2>
|
||
<p class="news-summary">{{ article.summary }}</p>
|
||
</article>
|
||
</div>
|
||
|
||
<!-- CTA -->
|
||
<div class="news-cta">
|
||
<h3>想了解更多?</h3>
|
||
<p>草原的风,已经在等您了。</p>
|
||
<div class="news-cta-links">
|
||
<CommonInternalLink to="/products" text="查看2026旅行产品" />
|
||
<CommonInternalLink to="/summer-camp" text="小蒙马夏令营报名" />
|
||
<CommonInternalLink to="/contact" text="联系定制师" />
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { useBreadcrumbSchema } from '~/composables/useJsonLd'
|
||
|
||
const { data: newsData } = await usePublicApi('news')
|
||
|
||
const seo = useSEO('news')
|
||
|
||
useBreadcrumbSchema([
|
||
{ text: '首页', to: '/' },
|
||
{ text: '最新动态' },
|
||
])
|
||
|
||
// NewsArticle JSON-LD for each article
|
||
;(newsData.value?.articles || []).forEach((article) => {
|
||
useHead({
|
||
script: [{
|
||
key: `news-article-${article.id}`,
|
||
type: 'application/ld+json',
|
||
innerHTML: JSON.stringify({
|
||
'@context': 'https://schema.org',
|
||
'@type': 'NewsArticle',
|
||
headline: article.title,
|
||
description: article.summary,
|
||
articleBody: article.content,
|
||
datePublished: (article.date || article.createdAt) + 'T00:00:00+08:00',
|
||
dateModified: (article.date || article.createdAt) + 'T00:00:00+08:00',
|
||
articleSection: article.category,
|
||
inLanguage: 'zh-CN',
|
||
url: 'https://1814.love/news',
|
||
author: { '@type': 'Organization', name: '呼籁旅行', url: 'https://1814.love' },
|
||
publisher: { '@id': 'https://1814.love/#organization' },
|
||
about: { '@type': 'TravelAgency', name: '呼籁旅行', url: 'https://1814.love' },
|
||
keywords: '呼籁旅行,呼伦贝尔旅游,额吉的故乡,小蒙马夏令营',
|
||
}),
|
||
}],
|
||
})
|
||
})
|
||
|
||
const categories = ['全部', '产品', '活动', '公告']
|
||
const activeCategory = ref('全部')
|
||
|
||
const filteredArticles = computed(() => {
|
||
if (activeCategory.value === '全部') return newsData.value?.articles || []
|
||
return (newsData.value?.articles || []).filter(a => a.category === activeCategory.value)
|
||
})
|
||
|
||
function formatDate(dateStr) {
|
||
if (!dateStr) return ''
|
||
const d = new Date(dateStr.includes('T') ? dateStr : dateStr + 'T00:00:00')
|
||
if (isNaN(d.getTime())) return ''
|
||
return `${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日`
|
||
}
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
.news-filter {
|
||
display: flex;
|
||
gap: @space-sm;
|
||
margin-bottom: @space-2xl;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.filter-btn {
|
||
padding: @space-sm @space-lg;
|
||
border: 1px solid @color-border;
|
||
border-radius: @border-radius-full;
|
||
background: @color-bg-white;
|
||
color: @color-text-secondary;
|
||
font-size: @font-size-sm;
|
||
cursor: pointer;
|
||
transition: all @transition-base;
|
||
|
||
&:hover {
|
||
border-color: @color-primary;
|
||
color: @color-primary;
|
||
}
|
||
|
||
&--active {
|
||
background: @color-primary;
|
||
border-color: @color-primary;
|
||
color: @color-bg-white;
|
||
font-weight: @font-weight-medium;
|
||
}
|
||
}
|
||
|
||
.news-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: @space-lg;
|
||
margin-bottom: @space-3xl;
|
||
}
|
||
|
||
.news-card {
|
||
background: @color-bg-white;
|
||
border: 1px solid @color-border;
|
||
border-radius: @border-radius-md;
|
||
padding: @space-xl;
|
||
transition: box-shadow @transition-base;
|
||
|
||
&:hover {
|
||
box-shadow: @shadow-md;
|
||
}
|
||
}
|
||
|
||
.news-meta {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: @space-md;
|
||
margin-bottom: @space-md;
|
||
}
|
||
|
||
.news-cat {
|
||
display: inline-block;
|
||
padding: 2px @space-sm;
|
||
border-radius: @border-radius-sm;
|
||
font-size: @font-size-xs;
|
||
font-weight: @font-weight-medium;
|
||
|
||
&--产品 {
|
||
background: @color-primary-bg;
|
||
color: @color-primary;
|
||
}
|
||
|
||
&--活动 {
|
||
background: #FFF3E6;
|
||
color: @color-accent;
|
||
}
|
||
|
||
&--公告 {
|
||
background: @color-bg-gray;
|
||
color: @color-text-secondary;
|
||
}
|
||
}
|
||
|
||
.news-date {
|
||
font-size: @font-size-sm;
|
||
color: @color-text-muted;
|
||
}
|
||
|
||
.news-title {
|
||
font-size: @font-size-lg;
|
||
font-weight: @font-weight-bold;
|
||
color: @color-text-primary;
|
||
margin: 0 0 @space-sm;
|
||
line-height: @line-height-tight;
|
||
}
|
||
|
||
.news-summary {
|
||
font-size: @font-size-base;
|
||
color: @color-text-secondary;
|
||
line-height: @line-height-base;
|
||
margin: 0;
|
||
}
|
||
|
||
.news-cta {
|
||
text-align: center;
|
||
padding: @space-2xl;
|
||
background: @color-primary-bg;
|
||
border-radius: @border-radius-lg;
|
||
|
||
h3 {
|
||
font-size: @font-size-xl;
|
||
margin-bottom: @space-sm;
|
||
color: @color-primary;
|
||
}
|
||
|
||
p {
|
||
color: @color-text-secondary;
|
||
margin-bottom: @space-xl;
|
||
}
|
||
}
|
||
|
||
.news-cta-links {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: @space-xl;
|
||
flex-wrap: wrap;
|
||
}
|
||
</style>
|