614 行
16 KiB
Vue
614 行
16 KiB
Vue
<template>
|
||
<div v-if="note" class="page-xhs-detail">
|
||
<LayoutPageHero
|
||
:title="note.title"
|
||
:subtitle="`来自小红书用户 @${note.author} 的真实旅行分享`"
|
||
:breadcrumbs="[{ text: '小红书游记', to: '/xiaohongshu' }, { text: note.title }]"
|
||
compact
|
||
/>
|
||
|
||
<section class="section">
|
||
<div class="container">
|
||
<div class="detail-layout">
|
||
<!-- 主内容 -->
|
||
<main class="detail-main">
|
||
<!-- 封面图 -->
|
||
<div class="detail-cover" :class="{ 'detail-cover--wide': note.ratio === 'horizontal' }">
|
||
<img :src="proxyImage(note.cover)" :alt="note.title" referrerpolicy="no-referrer" />
|
||
</div>
|
||
|
||
<!-- 笔记正文预览 -->
|
||
<article class="detail-content">
|
||
<div class="detail-author-bar">
|
||
<div class="detail-author-bar__left">
|
||
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor" opacity="0.5"><circle cx="12" cy="8" r="4"/><path d="M12 14c-6 0-8 3-8 3v1h16v-1s-2-3-8-3z"/></svg>
|
||
<span class="detail-author-bar__name">{{ note.author }}</span>
|
||
</div>
|
||
<span class="detail-author-bar__likes">
|
||
<svg viewBox="0 0 24 24" width="14" height="14"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" fill="#ff2442"/></svg>
|
||
{{ formatLikes(note.likes) }} 赞
|
||
</span>
|
||
</div>
|
||
|
||
<h2 class="detail-content__title">{{ note.title }}</h2>
|
||
|
||
<div class="detail-content__body">
|
||
<p>{{ note.summary }}</p>
|
||
</div>
|
||
|
||
<!-- 标签 -->
|
||
<div class="detail-tags">
|
||
<span v-for="tag in note.tags" :key="tag" class="detail-tag">#{{ tag }}</span>
|
||
</div>
|
||
</article>
|
||
|
||
<!-- 查看原文 CTA -->
|
||
<div class="detail-xhs-cta">
|
||
<div class="detail-xhs-cta__icon">
|
||
<svg viewBox="0 0 24 24" width="32" height="32"><circle cx="12" cy="12" r="11" fill="#ff2442" /><text x="12" y="16" text-anchor="middle" fill="white" font-size="10" font-weight="bold">书</text></svg>
|
||
</div>
|
||
<div class="detail-xhs-cta__text">
|
||
<p class="detail-xhs-cta__hint">以上为笔记摘要,查看完整图文内容请前往小红书</p>
|
||
<a
|
||
:href="note.url"
|
||
target="_blank"
|
||
rel="noopener nofollow"
|
||
class="detail-xhs-cta__btn"
|
||
>
|
||
在小红书查看原文
|
||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 前后导航 -->
|
||
<div class="detail-nav">
|
||
<NuxtLink
|
||
v-if="prevNote"
|
||
:to="`/xiaohongshu/${prevNote.id}`"
|
||
class="detail-nav__item detail-nav__item--prev"
|
||
>
|
||
<span class="detail-nav__label">上一篇</span>
|
||
<span class="detail-nav__title">{{ prevNote.title }}</span>
|
||
</NuxtLink>
|
||
<div v-else />
|
||
<NuxtLink
|
||
v-if="nextNote"
|
||
:to="`/xiaohongshu/${nextNote.id}`"
|
||
class="detail-nav__item detail-nav__item--next"
|
||
>
|
||
<span class="detail-nav__label">下一篇</span>
|
||
<span class="detail-nav__title">{{ nextNote.title }}</span>
|
||
</NuxtLink>
|
||
</div>
|
||
</main>
|
||
|
||
<!-- 侧边栏 -->
|
||
<aside class="detail-sidebar">
|
||
<!-- 小红书账号卡 -->
|
||
<div class="sidebar-card sidebar-card--xhs">
|
||
<div class="sidebar-card__account">
|
||
<img :src="'/images/xiaohongshu/profile.jpg'" alt="呼籁旅行" class="sidebar-card__avatar" />
|
||
<div>
|
||
<h3 class="sidebar-card__name">{{ wallData.account.name }}</h3>
|
||
<span class="sidebar-card__badge">蓝V认证 · 旅行社</span>
|
||
</div>
|
||
</div>
|
||
<div class="sidebar-card__stats">
|
||
<span>粉丝 <strong>{{ wallData.account.followers }}</strong></span>
|
||
<span>获赞 <strong>{{ wallData.account.likes }}</strong></span>
|
||
</div>
|
||
<a
|
||
:href="wallData.account.url"
|
||
target="_blank"
|
||
rel="noopener nofollow"
|
||
class="sidebar-card__follow"
|
||
>
|
||
关注小红书
|
||
</a>
|
||
</div>
|
||
|
||
<!-- 咨询 CTA -->
|
||
<div class="sidebar-card sidebar-card--cta">
|
||
<h3 class="sidebar-card__title">想要同款旅行体验?</h3>
|
||
<p class="sidebar-card__desc">联系呼籁定制师,一对一规划专属行程</p>
|
||
<CommonInternalLink to="/contact" text="免费咨询定制师" />
|
||
</div>
|
||
|
||
<!-- 更多笔记 -->
|
||
<div class="sidebar-card">
|
||
<h3 class="sidebar-card__title">更多客户笔记</h3>
|
||
<ul class="sidebar-notes">
|
||
<li
|
||
v-for="n in relatedNotes"
|
||
:key="n.id"
|
||
class="sidebar-note"
|
||
:class="{ 'sidebar-note--active': n.id === note.id }"
|
||
>
|
||
<NuxtLink :to="`/xiaohongshu/${n.id}`" class="sidebar-note__link">
|
||
<img :src="proxyImage(n.cover)" :alt="n.title" class="sidebar-note__thumb" referrerpolicy="no-referrer" />
|
||
<div class="sidebar-note__info">
|
||
<span class="sidebar-note__title">{{ n.title }}</span>
|
||
<span class="sidebar-note__author">@{{ n.author }}</span>
|
||
</div>
|
||
</NuxtLink>
|
||
</li>
|
||
</ul>
|
||
<NuxtLink to="/xiaohongshu" class="sidebar-card__more">查看全部笔记 →</NuxtLink>
|
||
</div>
|
||
</aside>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- CTA -->
|
||
<CommonSectionCTA
|
||
title="想拥有同款草原体验?"
|
||
description="联系定制师,开始规划您的呼伦贝尔之旅"
|
||
:links="[
|
||
{ to: '/products', text: '查看旅行产品' },
|
||
{ to: '/contact', text: '联系定制师' },
|
||
]"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 404 -->
|
||
<div v-else class="page-not-found">
|
||
<div class="container">
|
||
<h1>笔记不存在</h1>
|
||
<NuxtLink to="/xiaohongshu">返回小红书游记</NuxtLink>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { useBreadcrumbSchema } from '~/composables/useJsonLd'
|
||
import { proxyImage } from '~/composables/useImageProxy'
|
||
|
||
const { data: wallData } = await usePublicApi('xiaohongshu-wall')
|
||
|
||
const route = useRoute()
|
||
const allNotes = computed(() => wallData.value?.notes || [])
|
||
const note = computed(() => allNotes.value.find(n => n.id === route.params.id))
|
||
|
||
if (!note.value) {
|
||
throw createError({ statusCode: 404, statusMessage: '笔记不存在' })
|
||
}
|
||
|
||
// SEO
|
||
useHead({
|
||
title: `${note.value.title} - 客户旅行笔记 | 呼籁旅行`,
|
||
meta: [
|
||
{ name: 'description', content: note.value.summary || `来自小红书用户 @${note.value.author} 的呼伦贝尔旅行笔记:${note.value.title}` },
|
||
{ name: 'keywords', content: `呼伦贝尔,${note.value.tags.join(',')},呼籁旅行,小红书,旅行笔记,${note.value.author}` },
|
||
{ name: 'author', content: note.value.author },
|
||
{ property: 'og:title', content: note.value.title },
|
||
{ property: 'og:description', content: note.value.summary },
|
||
{ property: 'og:type', content: 'article' },
|
||
{ property: 'og:image', content: note.value.cover },
|
||
{ property: 'og:url', content: `https://1814.love/xiaohongshu/${note.value.id}` },
|
||
{ property: 'og:site_name', content: '呼籁旅行' },
|
||
],
|
||
link: [
|
||
{ rel: 'canonical', href: `https://1814.love/xiaohongshu/${note.value.id}` },
|
||
],
|
||
})
|
||
|
||
useBreadcrumbSchema([
|
||
{ text: '首页', to: '/' },
|
||
{ text: '小红书游记', to: '/xiaohongshu' },
|
||
{ text: note.value.title },
|
||
])
|
||
|
||
// 上下篇
|
||
const currentIndex = computed(() => allNotes.value.findIndex(n => n.id === note.value?.id))
|
||
const prevNote = computed(() => currentIndex.value > 0 ? allNotes.value[currentIndex.value - 1] : null)
|
||
const nextNote = computed(() => currentIndex.value < allNotes.value.length - 1 ? allNotes.value[currentIndex.value + 1] : null)
|
||
|
||
// 相关笔记:同标签优先,取6条
|
||
const relatedNotes = computed(() => {
|
||
if (!note.value) return []
|
||
const sameTag = allNotes.value.filter(n => n.id !== note.value.id && n.tags.some(t => note.value.tags.includes(t)))
|
||
const others = allNotes.value.filter(n => n.id !== note.value.id && !sameTag.includes(n))
|
||
return [...sameTag, ...others].slice(0, 6)
|
||
})
|
||
|
||
function formatLikes(num) {
|
||
if (num >= 10000) return (num / 10000).toFixed(1) + 'w'
|
||
if (num >= 1000) return (num / 1000).toFixed(1) + 'k'
|
||
return num
|
||
}
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
.detail-layout {
|
||
display: grid;
|
||
grid-template-columns: 1fr;
|
||
gap: @space-2xl;
|
||
|
||
.respond-lg({
|
||
grid-template-columns: 1fr 320px;
|
||
align-items: start;
|
||
});
|
||
}
|
||
|
||
.detail-main {
|
||
min-width: 0;
|
||
}
|
||
|
||
// ===== 封面 =====
|
||
.detail-cover {
|
||
border-radius: @border-radius-lg;
|
||
overflow: hidden;
|
||
margin-bottom: @space-xl;
|
||
aspect-ratio: 3 / 4;
|
||
max-height: 560px;
|
||
background: @gradient-cool;
|
||
|
||
&--wide {
|
||
aspect-ratio: 4 / 3;
|
||
max-height: 480px;
|
||
}
|
||
|
||
img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
display: block;
|
||
}
|
||
}
|
||
|
||
// ===== 正文 =====
|
||
.detail-content {
|
||
margin-bottom: @space-xl;
|
||
}
|
||
|
||
.detail-author-bar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: @space-lg;
|
||
padding-bottom: @space-md;
|
||
border-bottom: 1px solid @color-divider;
|
||
|
||
&__left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: @space-sm;
|
||
}
|
||
|
||
&__name {
|
||
font-size: @font-size-sm;
|
||
font-weight: @font-weight-medium;
|
||
color: @color-text-secondary;
|
||
}
|
||
|
||
&__likes {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-size: @font-size-sm;
|
||
color: @color-text-muted;
|
||
|
||
svg { flex-shrink: 0; }
|
||
}
|
||
}
|
||
|
||
.detail-content__title {
|
||
font-size: @font-size-2xl;
|
||
font-weight: @font-weight-bold;
|
||
color: @color-text-primary;
|
||
margin: 0 0 @space-lg;
|
||
line-height: @line-height-tight;
|
||
|
||
.respond-md({
|
||
font-size: @font-size-3xl;
|
||
});
|
||
}
|
||
|
||
.detail-content__body {
|
||
p {
|
||
font-size: @font-size-base;
|
||
color: @color-text-secondary;
|
||
line-height: @line-height-relaxed;
|
||
margin: 0 0 @space-md;
|
||
|
||
.respond-md({
|
||
font-size: @font-size-md;
|
||
});
|
||
}
|
||
}
|
||
|
||
.detail-tags {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: @space-sm;
|
||
margin-top: @space-lg;
|
||
}
|
||
|
||
.detail-tag {
|
||
padding: 4px 14px;
|
||
background: @color-primary-bg;
|
||
color: @color-primary;
|
||
border-radius: @border-radius-full;
|
||
font-size: @font-size-sm;
|
||
font-weight: @font-weight-medium;
|
||
}
|
||
|
||
// ===== 小红书原文 CTA =====
|
||
.detail-xhs-cta {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: @space-lg;
|
||
padding: @space-xl;
|
||
background: linear-gradient(135deg, #fff5f7 0%, #ffe8ec 100%);
|
||
border-radius: @border-radius-lg;
|
||
border: 1px solid rgba(255, 36, 66, 0.1);
|
||
margin-bottom: @space-xl;
|
||
|
||
&__icon {
|
||
flex-shrink: 0;
|
||
|
||
svg {
|
||
display: block;
|
||
}
|
||
}
|
||
|
||
&__hint {
|
||
font-size: @font-size-sm;
|
||
color: @color-text-muted;
|
||
margin: 0 0 @space-sm;
|
||
}
|
||
|
||
&__btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: @space-sm;
|
||
padding: 10px @space-xl;
|
||
background: #ff2442;
|
||
color: #fff;
|
||
border-radius: @border-radius-full;
|
||
text-decoration: none;
|
||
font-size: @font-size-sm;
|
||
font-weight: @font-weight-bold;
|
||
transition: all @transition-base;
|
||
white-space: nowrap;
|
||
|
||
svg {
|
||
transition: transform @transition-fast;
|
||
}
|
||
|
||
&:hover {
|
||
background: #e6203b;
|
||
box-shadow: 0 4px 16px rgba(255, 36, 66, 0.3);
|
||
|
||
svg {
|
||
transform: translateX(3px);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ===== 前后导航 =====
|
||
.detail-nav {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: @space-md;
|
||
padding-top: @space-lg;
|
||
border-top: 1px solid @color-border;
|
||
|
||
&__item {
|
||
padding: @space-md;
|
||
background: @color-bg-white;
|
||
border: 1px solid @color-border;
|
||
border-radius: @border-radius-md;
|
||
text-decoration: none;
|
||
transition: all @transition-base;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: @space-xs;
|
||
|
||
&:hover {
|
||
border-color: @color-primary;
|
||
background: @color-primary-bg;
|
||
}
|
||
|
||
&--next {
|
||
text-align: right;
|
||
}
|
||
}
|
||
|
||
&__label {
|
||
font-size: @font-size-xs;
|
||
color: @color-text-muted;
|
||
}
|
||
|
||
&__title {
|
||
font-size: @font-size-sm;
|
||
color: @color-text-primary;
|
||
line-height: @line-height-tight;
|
||
.text-clamp(2);
|
||
}
|
||
}
|
||
|
||
// ===== 侧边栏 =====
|
||
.detail-sidebar {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: @space-lg;
|
||
position: sticky;
|
||
top: 90px;
|
||
}
|
||
|
||
.sidebar-card {
|
||
background: @color-bg-white;
|
||
border: 1px solid @color-border;
|
||
border-radius: @border-radius-lg;
|
||
padding: @space-lg;
|
||
|
||
&--xhs {
|
||
border-color: rgba(255, 36, 66, 0.15);
|
||
background: linear-gradient(180deg, #fff5f7 0%, #fff 40%);
|
||
}
|
||
|
||
&--cta {
|
||
background: @color-primary-bg;
|
||
border-color: @color-primary-lighter;
|
||
}
|
||
|
||
&__title {
|
||
font-size: @font-size-base;
|
||
font-weight: @font-weight-bold;
|
||
color: @color-text-primary;
|
||
margin: 0 0 @space-sm;
|
||
}
|
||
|
||
&__desc {
|
||
font-size: @font-size-sm;
|
||
color: @color-text-secondary;
|
||
line-height: @line-height-base;
|
||
margin: 0 0 @space-md;
|
||
}
|
||
|
||
&__more {
|
||
display: block;
|
||
text-align: center;
|
||
margin-top: @space-md;
|
||
padding-top: @space-md;
|
||
border-top: 1px solid @color-divider;
|
||
font-size: @font-size-sm;
|
||
color: @color-primary;
|
||
text-decoration: none;
|
||
font-weight: @font-weight-medium;
|
||
|
||
&:hover {
|
||
color: @color-primary-dark;
|
||
}
|
||
}
|
||
}
|
||
|
||
.sidebar-card__account {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: @space-md;
|
||
margin-bottom: @space-md;
|
||
}
|
||
|
||
.sidebar-card__avatar {
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: 50%;
|
||
object-fit: cover;
|
||
border: 2px solid #fff;
|
||
box-shadow: 0 2px 8px rgba(255, 36, 66, 0.15);
|
||
background: #fff;
|
||
}
|
||
|
||
.sidebar-card__name {
|
||
font-size: @font-size-sm;
|
||
font-weight: @font-weight-bold;
|
||
color: @color-text-primary;
|
||
margin: 0 0 2px;
|
||
}
|
||
|
||
.sidebar-card__badge {
|
||
font-size: 11px;
|
||
color: #ff2442;
|
||
font-weight: @font-weight-medium;
|
||
}
|
||
|
||
.sidebar-card__stats {
|
||
display: flex;
|
||
gap: @space-lg;
|
||
margin-bottom: @space-md;
|
||
font-size: @font-size-sm;
|
||
color: @color-text-muted;
|
||
|
||
strong {
|
||
color: @color-text-primary;
|
||
font-weight: @font-weight-bold;
|
||
}
|
||
}
|
||
|
||
.sidebar-card__follow {
|
||
display: block;
|
||
text-align: center;
|
||
padding: @space-sm;
|
||
background: #ff2442;
|
||
color: #fff;
|
||
border-radius: @border-radius-full;
|
||
text-decoration: none;
|
||
font-size: @font-size-sm;
|
||
font-weight: @font-weight-medium;
|
||
transition: background @transition-fast;
|
||
|
||
&:hover {
|
||
background: #e6203b;
|
||
}
|
||
}
|
||
|
||
.sidebar-notes {
|
||
list-style: none;
|
||
margin: 0;
|
||
padding: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: @space-sm;
|
||
}
|
||
|
||
.sidebar-note {
|
||
&--active .sidebar-note__link {
|
||
background: @color-primary-bg;
|
||
}
|
||
|
||
&__link {
|
||
display: flex;
|
||
gap: @space-sm;
|
||
padding: @space-sm;
|
||
border-radius: @border-radius-md;
|
||
text-decoration: none;
|
||
transition: background @transition-fast;
|
||
|
||
&:hover {
|
||
background: @color-bg-gray;
|
||
}
|
||
}
|
||
|
||
&__thumb {
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: @border-radius-sm;
|
||
object-fit: cover;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
&__info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
min-width: 0;
|
||
}
|
||
|
||
&__title {
|
||
font-size: @font-size-sm;
|
||
color: @color-text-primary;
|
||
line-height: @line-height-tight;
|
||
.text-clamp(2);
|
||
}
|
||
|
||
&__author {
|
||
font-size: 11px;
|
||
color: @color-text-muted;
|
||
}
|
||
}
|
||
|
||
// 404
|
||
.page-not-found {
|
||
text-align: center;
|
||
padding: @space-3xl 0;
|
||
|
||
h1 { margin-bottom: @space-lg; }
|
||
a { color: @color-primary; }
|
||
}
|
||
</style>
|