390 行
9.1 KiB
Vue
390 行
9.1 KiB
Vue
<template>
|
|
<main class="destination-detail" v-if="dest">
|
|
<section class="dest-hero">
|
|
<div class="container">
|
|
<nav class="breadcrumb" aria-label="面包屑导航">
|
|
<NuxtLink to="/">首页</NuxtLink> / <NuxtLink to="/destinations">目的地</NuxtLink> / <span>{{ dest.name }}</span>
|
|
</nav>
|
|
<div class="hero-content">
|
|
<span class="dest-tag">{{ dest.tag }}</span>
|
|
<h1>{{ dest.name }}</h1>
|
|
<p class="hero-subtitle">{{ dest.subtitle }}</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="dest-description">
|
|
<div class="container">
|
|
<p>{{ dest.description }}</p>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="dest-highlights" v-if="dest.highlights?.length">
|
|
<div class="container">
|
|
<h2>景点亮点</h2>
|
|
<ul>
|
|
<li v-for="(h, i) in dest.highlights" :key="i">{{ h }}</li>
|
|
</ul>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="dest-season">
|
|
<div class="container">
|
|
<h2>推荐游览季节</h2>
|
|
<div class="season-card">
|
|
<p class="season-desc">{{ dest.bestSeason.description }}</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="dest-tips" v-if="dest.tips?.length">
|
|
<div class="container">
|
|
<h2>实用旅行贴士</h2>
|
|
<ul>
|
|
<li v-for="(tip, i) in dest.tips" :key="i">{{ tip }}</li>
|
|
</ul>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="dest-products" v-if="relatedProducts.length">
|
|
<div class="container">
|
|
<h2>包含此景点的行程</h2>
|
|
<p class="products-intro">以下呼籁定制游产品涵盖{{ dest.name }},由专属领队带队深度游览。</p>
|
|
<div class="product-links">
|
|
<NuxtLink
|
|
v-for="p in relatedProducts"
|
|
:key="p.id"
|
|
:to="productUrl(p.id)"
|
|
class="product-link"
|
|
>
|
|
<span class="product-name">{{ p.name }}</span>
|
|
<span class="product-meta">{{ p.days }}天{{ p.nights }}晚 · {{ p.tag }}</span>
|
|
</NuxtLink>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="dest-cta">
|
|
<div class="container">
|
|
<h2>想去{{ dest.name }}?让定制师为你规划</h2>
|
|
<p>微信添加 hulai1814,定制师一对一为你安排深度游览方案</p>
|
|
<NuxtLink to="/contact" class="cta-button">联系定制师</NuxtLink>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
</template>
|
|
|
|
<script setup>
|
|
import useSEO from '~/composables/useSEO'
|
|
import { useBreadcrumbSchema } from '~/composables/useJsonLd'
|
|
|
|
const { data: destinationsData } = await usePublicApi('destinations-detail')
|
|
const { data: productsData } = await usePublicApi('products')
|
|
const { data: winterProductsData } = await usePublicApi('winter-products')
|
|
const { data: autumnProductsData } = await usePublicApi('autumn-products')
|
|
|
|
const route = useRoute()
|
|
const destId = route.params.id
|
|
|
|
const dest = computed(() => (destinationsData.value?.destinations || []).find(d => d.id === destId))
|
|
|
|
if (!dest.value) {
|
|
throw createError({ statusCode: 404, statusMessage: '目的地不存在' })
|
|
}
|
|
|
|
// SEO
|
|
const seoOverrides = {
|
|
title: dest.value?.seo?.title,
|
|
description: dest.value?.seo?.description,
|
|
}
|
|
useSEO('destinations', seoOverrides)
|
|
|
|
useHead({
|
|
meta: [
|
|
{ name: 'keywords', content: dest.value?.seo?.keywords },
|
|
],
|
|
})
|
|
|
|
// JSON-LD: TouristAttraction
|
|
useHead({
|
|
script: [
|
|
{
|
|
type: 'application/ld+json',
|
|
innerHTML: JSON.stringify({
|
|
'@context': 'https://schema.org',
|
|
'@type': 'TouristAttraction',
|
|
name: dest.value?.name,
|
|
description: dest.value?.description,
|
|
url: `https://1814.love/destinations/${dest.value?.id}`,
|
|
touristType: '家庭游客,亲子游,摄影爱好者',
|
|
isAccessibleForFree: false,
|
|
photo: dest.value?.heroImage ? { '@type': 'ImageObject', url: `https://1814.love${dest.value.heroImage}` } : undefined,
|
|
containedInPlace: {
|
|
'@type': 'AdministrativeArea',
|
|
name: '呼伦贝尔',
|
|
address: {
|
|
'@type': 'PostalAddress',
|
|
addressLocality: '呼伦贝尔市',
|
|
addressRegion: '内蒙古自治区',
|
|
addressCountry: 'CN',
|
|
},
|
|
},
|
|
publicAccess: true,
|
|
availableLanguage: 'Chinese',
|
|
}),
|
|
},
|
|
{
|
|
type: 'application/ld+json',
|
|
innerHTML: JSON.stringify({
|
|
'@context': 'https://schema.org',
|
|
'@type': 'Article',
|
|
headline: dest.value?.seo?.title || dest.value?.name,
|
|
description: dest.value?.seo?.description || dest.value?.description,
|
|
url: `https://1814.love/destinations/${dest.value?.id}`,
|
|
author: { '@id': 'https://1814.love/#organization' },
|
|
publisher: { '@id': 'https://1814.love/#organization' },
|
|
dateModified: new Date().toISOString().split('T')[0],
|
|
about: {
|
|
'@type': 'TouristAttraction',
|
|
name: dest.value?.name,
|
|
},
|
|
keywords: dest.value?.seo?.keywords,
|
|
}),
|
|
},
|
|
],
|
|
})
|
|
|
|
// 面包屑
|
|
useBreadcrumbSchema([
|
|
{ text: '首页', to: '/' },
|
|
{ text: '景区攻略', to: '/destinations' },
|
|
{ text: dest.value?.name },
|
|
])
|
|
|
|
// 关联产品详情——从三个产品线数据中查找
|
|
const allVersions = computed(() => [
|
|
...(productsData.value?.versions || []),
|
|
...(winterProductsData.value?.versions || []),
|
|
...(autumnProductsData.value?.versions || []),
|
|
])
|
|
const relatedProducts = computed(() => (dest.value?.relatedProducts || [])
|
|
.map(pid => allVersions.value.find(v => v.id === pid))
|
|
.filter(Boolean)
|
|
)
|
|
|
|
function productUrl(id) {
|
|
if (id.startsWith('autumn-')) return '/products/autumn'
|
|
if (id.startsWith('winter-')) return '/products/winter'
|
|
if (id === 'summer-camp') return '/summer-camp'
|
|
return `/products/${id}`
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.container {
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
padding: 0 20px;
|
|
}
|
|
.dest-hero {
|
|
background: linear-gradient(135deg, #f0f5f1, #e8ede9);
|
|
padding: 40px 0 48px;
|
|
}
|
|
.breadcrumb {
|
|
font-size: 14px;
|
|
color: #7a857c;
|
|
margin-bottom: 24px;
|
|
}
|
|
.breadcrumb a {
|
|
color: #3a7d44;
|
|
text-decoration: none;
|
|
}
|
|
.dest-tag {
|
|
display: inline-block;
|
|
background: #3a7d44;
|
|
color: #fff;
|
|
padding: 4px 12px;
|
|
border-radius: 4px;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
margin-bottom: 12px;
|
|
}
|
|
h1 {
|
|
font-size: 32px;
|
|
font-weight: 800;
|
|
color: #1a1f1c;
|
|
line-height: 1.3;
|
|
margin-bottom: 10px;
|
|
}
|
|
.hero-subtitle {
|
|
font-size: 17px;
|
|
color: #4a5e4d;
|
|
}
|
|
.dest-description {
|
|
padding: 40px 0;
|
|
}
|
|
.dest-description p {
|
|
font-size: 17px;
|
|
line-height: 1.9;
|
|
color: #3d4a40;
|
|
}
|
|
.dest-highlights {
|
|
padding: 40px 0;
|
|
background: #fafbfa;
|
|
}
|
|
.dest-highlights h2,
|
|
.dest-season h2,
|
|
.dest-tips h2,
|
|
.dest-products h2 {
|
|
font-size: 24px;
|
|
font-weight: 700;
|
|
margin-bottom: 20px;
|
|
color: #1a1f1c;
|
|
}
|
|
.dest-highlights ul {
|
|
list-style: none;
|
|
padding: 0;
|
|
}
|
|
.dest-highlights li {
|
|
padding: 14px 0;
|
|
border-bottom: 1px solid #eef0ef;
|
|
font-size: 16px;
|
|
line-height: 1.6;
|
|
color: #3d4a40;
|
|
padding-left: 24px;
|
|
position: relative;
|
|
}
|
|
.dest-highlights li::before {
|
|
content: '✓';
|
|
position: absolute;
|
|
left: 0;
|
|
color: #3a7d44;
|
|
font-weight: 700;
|
|
}
|
|
.dest-season {
|
|
padding: 40px 0;
|
|
}
|
|
.season-card {
|
|
background: #f0f5f1;
|
|
border-radius: 10px;
|
|
padding: 24px;
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 20px;
|
|
align-items: flex-start;
|
|
}
|
|
.season-primary,
|
|
.season-secondary {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
min-width: 120px;
|
|
}
|
|
.season-label {
|
|
font-size: 12px;
|
|
color: #7a857c;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
.season-value {
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
color: #3a7d44;
|
|
}
|
|
.season-desc {
|
|
flex: 1 1 100%;
|
|
font-size: 15px;
|
|
line-height: 1.7;
|
|
color: #4a5e4d;
|
|
margin: 0;
|
|
}
|
|
.dest-tips {
|
|
padding: 40px 0;
|
|
background: #fafbfa;
|
|
}
|
|
.dest-tips ul {
|
|
list-style: none;
|
|
padding: 0;
|
|
}
|
|
.dest-tips li {
|
|
padding: 14px 0;
|
|
border-bottom: 1px solid #eef0ef;
|
|
font-size: 16px;
|
|
line-height: 1.6;
|
|
color: #3d4a40;
|
|
padding-left: 28px;
|
|
position: relative;
|
|
}
|
|
.dest-tips li::before {
|
|
content: '💡';
|
|
position: absolute;
|
|
left: 0;
|
|
font-size: 14px;
|
|
}
|
|
.dest-products {
|
|
padding: 40px 0;
|
|
}
|
|
.products-intro {
|
|
font-size: 15px;
|
|
color: #4a5e4d;
|
|
margin-bottom: 20px;
|
|
}
|
|
.product-links {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
.product-link {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 16px 20px;
|
|
border: 1px solid #d8e4da;
|
|
border-radius: 8px;
|
|
text-decoration: none;
|
|
transition: border-color 0.2s, background 0.2s;
|
|
}
|
|
.product-link:hover {
|
|
border-color: #3a7d44;
|
|
background: #f0f5f1;
|
|
}
|
|
.product-name {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: #1a1f1c;
|
|
}
|
|
.product-meta {
|
|
font-size: 14px;
|
|
color: #7a857c;
|
|
}
|
|
.dest-cta {
|
|
padding: 48px 0;
|
|
text-align: center;
|
|
background: linear-gradient(135deg, #f0f5f1, #e8ede9);
|
|
}
|
|
.dest-cta h2 {
|
|
font-size: 24px;
|
|
font-weight: 700;
|
|
margin-bottom: 8px;
|
|
color: #1a1f1c;
|
|
}
|
|
.dest-cta p {
|
|
color: #4a5e4d;
|
|
margin-bottom: 20px;
|
|
}
|
|
.cta-button {
|
|
display: inline-block;
|
|
background: #3a7d44;
|
|
color: #fff;
|
|
padding: 12px 32px;
|
|
border-radius: 8px;
|
|
text-decoration: none;
|
|
font-weight: 600;
|
|
font-size: 16px;
|
|
}
|
|
.cta-button:hover {
|
|
background: #2e6435;
|
|
}
|
|
</style>
|