615 行
15 KiB
Vue
615 行
15 KiB
Vue
<template>
|
||
<div class="page-gallery">
|
||
<!-- 全屏 Hero:用第一张照片做背景 -->
|
||
<section class="gallery-hero">
|
||
<div class="gallery-hero__bg">
|
||
<img
|
||
:src="gallery.works[7].image"
|
||
alt="呼籁草原旅拍"
|
||
class="gallery-hero__img"
|
||
/>
|
||
<div class="gallery-hero__overlay" />
|
||
</div>
|
||
<div class="gallery-hero__content">
|
||
<nav class="gallery-hero__breadcrumb" aria-label="面包屑导航">
|
||
<NuxtLink to="/">首页</NuxtLink>
|
||
<span>/</span>
|
||
<span>旅拍作品</span>
|
||
</nav>
|
||
<h1 class="gallery-hero__title">呼籁旅拍</h1>
|
||
<p class="gallery-hero__subtitle">在呼籁营地,用蒙古袍定格草原最美的你</p>
|
||
<div class="gallery-hero__stats">
|
||
<div v-for="stat in gallery.intro.stats" :key="stat.label" class="gallery-hero__stat">
|
||
<span class="gallery-hero__stat-value">{{ stat.value }}</span>
|
||
<span class="gallery-hero__stat-label">{{ stat.label }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="gallery-hero__scroll-hint" aria-hidden="true">
|
||
<span>向下滑动浏览作品</span>
|
||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||
<path d="M10 4v12M4 10l6 6 6-6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||
</svg>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- TL;DR -->
|
||
<section class="page-tldr" aria-label="旅拍作品摘要">
|
||
<div class="container">
|
||
<div class="page-tldr-inner">
|
||
<p>呼籁旅行在自营营地配备专属旅拍摄影团队,拥有200+件蒙古族服饰(传统袍服、节日盛装、亲子套装)。旅拍在呼籁营地进行,无需单独预约,每位出行客人均可体验,获得专业摄影师拍摄的草原珍贵瞬间。</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 作品瀑布流 -->
|
||
<section class="gallery-section">
|
||
<div class="container">
|
||
<div class="gallery-section__header">
|
||
<h2 class="gallery-section__title">旅拍作品集</h2>
|
||
<p class="gallery-section__desc">每一张都是真实客户在呼籁营地的草原记忆</p>
|
||
</div>
|
||
|
||
<!-- 瀑布流 -->
|
||
<div class="gallery-masonry">
|
||
<div
|
||
v-for="(work, index) in gallery.works"
|
||
:key="work.id"
|
||
class="gallery-item reveal"
|
||
:class="`gallery-item--${work.orientation}`"
|
||
@click="openLightbox(index)"
|
||
>
|
||
<div class="gallery-card">
|
||
<img
|
||
:src="work.image"
|
||
:alt="`${work.title} - 呼籁营地旅拍`"
|
||
class="gallery-card__img"
|
||
loading="lazy"
|
||
/>
|
||
<div class="gallery-card__hover">
|
||
<div class="gallery-card__hover-icon">
|
||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none">
|
||
<path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v6m3-3H7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Lightbox 灯箱 -->
|
||
<Teleport to="body">
|
||
<Transition name="lightbox">
|
||
<div
|
||
v-if="lightboxOpen"
|
||
class="lightbox"
|
||
@click.self="closeLightbox"
|
||
@keydown.esc="closeLightbox"
|
||
@keydown.left="prevPhoto"
|
||
@keydown.right="nextPhoto"
|
||
tabindex="0"
|
||
ref="lightboxRef"
|
||
>
|
||
<button class="lightbox__close" @click="closeLightbox" aria-label="关闭">
|
||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none">
|
||
<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||
</svg>
|
||
</button>
|
||
|
||
<button class="lightbox__nav lightbox__nav--prev" @click="prevPhoto" aria-label="上一张">
|
||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none">
|
||
<path d="M15 18l-6-6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||
</svg>
|
||
</button>
|
||
|
||
<div class="lightbox__img-wrap">
|
||
<img
|
||
:src="gallery.works[lightboxIndex].image"
|
||
:alt="gallery.works[lightboxIndex].title"
|
||
class="lightbox__img"
|
||
/>
|
||
</div>
|
||
|
||
<button class="lightbox__nav lightbox__nav--next" @click="nextPhoto" aria-label="下一张">
|
||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none">
|
||
<path d="M9 18l6-6-6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||
</svg>
|
||
</button>
|
||
|
||
<div class="lightbox__counter">
|
||
{{ lightboxIndex + 1 }} / {{ gallery.works.length }}
|
||
</div>
|
||
</div>
|
||
</Transition>
|
||
</Teleport>
|
||
|
||
<!-- 旅拍说明 -->
|
||
<section class="gallery-about section">
|
||
<div class="container">
|
||
<CommonSectionTitle
|
||
title="关于呼籁旅拍"
|
||
subtitle="记录真实的草原瞬间,而不是摆拍"
|
||
align="center"
|
||
/>
|
||
<div class="gallery-about__content">
|
||
<p class="gallery-about__lead">我们追求的是在游玩和互动过程中,捕捉最自然、最真实的瞬间——不做浓妆、不做刻意摆拍的重民族风。草原本身就是最好的背景,真实的笑容比任何姿势都好看。</p>
|
||
<div class="gallery-about__details">
|
||
<div class="gallery-about__item">
|
||
<h3>提前沟通,拍出你想要的</h3>
|
||
<p>如果你对旅拍有自己的想法和偏好,可以提前和我们的摄影师沟通。我们会根据你的需求,在拍摄时更有针对性地抓拍。</p>
|
||
</div>
|
||
<div class="gallery-about__item">
|
||
<h3>妆容自由,丰俭由人</h3>
|
||
<p>旅拍默认不含化妆——因为每个人的肤质不同,适合的化妆品也不一样。建议出发前自己画个淡妆,配上草原的蓝天绿草就非常好看。营地有专业化妆师,需要的话可以现场预约,明码标价、费用透明。</p>
|
||
</div>
|
||
<div class="gallery-about__item">
|
||
<h3>底片全送,没有隐形消费</h3>
|
||
<p>所有底片全部赠送,不存在选片环节的加价。收到照片后如果有任何想法,第一时间联系我们——每个人对「好看」的理解不同,我们愿意倾听你的反馈,一起磨合出更满意的作品。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 预约旅拍 CTA -->
|
||
<CommonSectionCTA
|
||
title="预约您的草原旅拍"
|
||
description="旅拍在呼籁营地进行,随行程一并安排,无需单独预约。有任何拍摄想法,可以提前和摄影师沟通。"
|
||
:links="[
|
||
{ to: '/customize', text: '立即预约,开始定制行程' },
|
||
{ to: '/contact', text: '咨询旅拍详情' },
|
||
]"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import galleryData from '~/data/gallery.json'
|
||
import { useBreadcrumbSchema, useGallerySchema } from '~/composables/useJsonLd'
|
||
import useScrollReveal from '~/composables/useScrollReveal'
|
||
|
||
const seo = useSEO('gallery')
|
||
useScrollReveal()
|
||
|
||
const gallery = galleryData
|
||
|
||
// Lightbox
|
||
const lightboxOpen = ref(false)
|
||
const lightboxIndex = ref(0)
|
||
const lightboxRef = ref(null)
|
||
|
||
function openLightbox(index) {
|
||
lightboxIndex.value = index
|
||
lightboxOpen.value = true
|
||
document.body.style.overflow = 'hidden'
|
||
nextTick(() => {
|
||
lightboxRef.value?.focus()
|
||
})
|
||
}
|
||
|
||
function closeLightbox() {
|
||
lightboxOpen.value = false
|
||
document.body.style.overflow = ''
|
||
}
|
||
|
||
function nextPhoto() {
|
||
lightboxIndex.value = (lightboxIndex.value + 1) % gallery.works.length
|
||
}
|
||
|
||
function prevPhoto() {
|
||
lightboxIndex.value = (lightboxIndex.value - 1 + gallery.works.length) % gallery.works.length
|
||
}
|
||
|
||
// 结构化数据
|
||
useBreadcrumbSchema([
|
||
{ text: '首页', to: '/' },
|
||
{ text: '旅拍作品' },
|
||
])
|
||
useGallerySchema(gallery)
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
// ========== Hero ==========
|
||
.gallery-hero {
|
||
position: relative;
|
||
height: 100vh;
|
||
min-height: 600px;
|
||
max-height: 1000px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
overflow: hidden;
|
||
|
||
&__bg {
|
||
position: absolute;
|
||
inset: 0;
|
||
z-index: 0;
|
||
}
|
||
|
||
&__img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
object-position: center 60%;
|
||
}
|
||
|
||
&__overlay {
|
||
position: absolute;
|
||
inset: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
}
|
||
|
||
&__content {
|
||
position: relative;
|
||
z-index: 1;
|
||
text-align: center;
|
||
color: #fff !important;
|
||
padding: 0 @space-lg;
|
||
|
||
* {
|
||
color: #fff !important;
|
||
}
|
||
}
|
||
|
||
&__breadcrumb {
|
||
font-size: @font-size-sm;
|
||
margin-bottom: @space-lg;
|
||
opacity: 0.85;
|
||
|
||
a {
|
||
color: #fff !important;
|
||
text-decoration: none;
|
||
&:hover { text-decoration: underline; }
|
||
}
|
||
|
||
span { margin: 0 @space-xs; }
|
||
}
|
||
|
||
&__title {
|
||
font-size: 48px;
|
||
font-weight: @font-weight-bold;
|
||
letter-spacing: 8px;
|
||
margin: 0 0 @space-md;
|
||
color: #fff !important;
|
||
text-shadow: 0 2px 40px rgba(0, 0, 0, 0.6), 0 1px 8px rgba(0, 0, 0, 0.4);
|
||
|
||
.respond-md({
|
||
font-size: 64px;
|
||
letter-spacing: 12px;
|
||
});
|
||
}
|
||
|
||
&__subtitle {
|
||
font-size: @font-size-lg;
|
||
color: rgba(255, 255, 255, 0.95) !important;
|
||
margin: 0 0 @space-2xl;
|
||
font-weight: 300;
|
||
letter-spacing: 2px;
|
||
text-shadow: 0 1px 10px rgba(0, 0, 0, 0.5);
|
||
|
||
.respond-md({
|
||
font-size: @font-size-xl;
|
||
});
|
||
}
|
||
|
||
&__stats {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: @space-xl;
|
||
flex-wrap: wrap;
|
||
|
||
.respond-md({
|
||
gap: @space-3xl;
|
||
});
|
||
}
|
||
|
||
&__stat {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
|
||
&__stat-value {
|
||
font-size: @font-size-2xl;
|
||
font-weight: @font-weight-bold;
|
||
line-height: 1.2;
|
||
|
||
.respond-md({
|
||
font-size: 32px;
|
||
});
|
||
}
|
||
|
||
&__stat-label {
|
||
font-size: @font-size-xs;
|
||
opacity: 0.8;
|
||
letter-spacing: 1px;
|
||
}
|
||
|
||
&__scroll-hint {
|
||
position: absolute;
|
||
bottom: @space-xl;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
z-index: 1;
|
||
color: #fff;
|
||
opacity: 0.6;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: @space-xs;
|
||
font-size: @font-size-xs;
|
||
letter-spacing: 1px;
|
||
animation: bounce 2s ease-in-out infinite;
|
||
|
||
svg {
|
||
animation: bounce-arrow 2s ease-in-out infinite;
|
||
}
|
||
}
|
||
}
|
||
|
||
@keyframes bounce {
|
||
0%, 100% { transform: translateX(-50%) translateY(0); }
|
||
50% { transform: translateX(-50%) translateY(-4px); }
|
||
}
|
||
|
||
@keyframes bounce-arrow {
|
||
0%, 100% { transform: translateY(0); }
|
||
50% { transform: translateY(4px); }
|
||
}
|
||
|
||
// ========== Gallery Section ==========
|
||
.gallery-section {
|
||
padding: @space-3xl 0;
|
||
|
||
.respond-md({
|
||
padding: 80px 0;
|
||
});
|
||
|
||
&__header {
|
||
text-align: center;
|
||
margin-bottom: @space-2xl;
|
||
|
||
.respond-md({
|
||
margin-bottom: @space-3xl;
|
||
});
|
||
}
|
||
|
||
&__title {
|
||
font-size: @font-size-2xl;
|
||
font-weight: @font-weight-bold;
|
||
color: @color-text-primary;
|
||
margin: 0 0 @space-sm;
|
||
letter-spacing: 2px;
|
||
|
||
.respond-md({
|
||
font-size: 32px;
|
||
});
|
||
}
|
||
|
||
&__desc {
|
||
font-size: @font-size-base;
|
||
color: @color-text-secondary;
|
||
margin: 0;
|
||
}
|
||
}
|
||
|
||
// ========== Masonry ==========
|
||
.gallery-masonry {
|
||
columns: 2;
|
||
column-gap: @space-sm;
|
||
|
||
.respond-md({
|
||
columns: 2;
|
||
column-gap: @space-md;
|
||
});
|
||
|
||
.respond-lg({
|
||
columns: 3;
|
||
column-gap: @space-lg;
|
||
});
|
||
}
|
||
|
||
.gallery-item {
|
||
break-inside: avoid;
|
||
margin-bottom: @space-sm;
|
||
cursor: pointer;
|
||
|
||
.respond-md({
|
||
margin-bottom: @space-md;
|
||
});
|
||
|
||
.respond-lg({
|
||
margin-bottom: @space-lg;
|
||
});
|
||
}
|
||
|
||
// ========== Card ==========
|
||
.gallery-card {
|
||
position: relative;
|
||
border-radius: @border-radius-md;
|
||
overflow: hidden;
|
||
background: @color-bg-gray;
|
||
|
||
.respond-md({
|
||
border-radius: @border-radius-lg;
|
||
});
|
||
|
||
&__img {
|
||
display: block;
|
||
width: 100%;
|
||
height: auto;
|
||
transition: transform 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||
}
|
||
|
||
&__hover {
|
||
position: absolute;
|
||
inset: 0;
|
||
background: rgba(0, 0, 0, 0);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: background 0.3s ease;
|
||
}
|
||
|
||
&__hover-icon {
|
||
color: #fff;
|
||
opacity: 0;
|
||
transform: scale(0.8);
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
&:hover {
|
||
.gallery-card__img {
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
.gallery-card__hover {
|
||
background: rgba(0, 0, 0, 0.25);
|
||
}
|
||
|
||
.gallery-card__hover-icon {
|
||
opacity: 1;
|
||
transform: scale(1);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ========== About Section ==========
|
||
.gallery-about {
|
||
&__content {
|
||
max-width: 800px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
&__lead {
|
||
text-align: center;
|
||
font-size: @font-size-md;
|
||
color: @color-text-secondary;
|
||
line-height: @line-height-relaxed;
|
||
margin: @space-lg auto @space-2xl;
|
||
max-width: 680px;
|
||
}
|
||
|
||
&__details {
|
||
display: grid;
|
||
grid-template-columns: 1fr;
|
||
gap: @space-lg;
|
||
|
||
.respond-md({
|
||
grid-template-columns: repeat(3, 1fr);
|
||
});
|
||
}
|
||
|
||
&__item {
|
||
padding: @space-lg;
|
||
background: @color-bg-white;
|
||
border: 1px solid @color-border;
|
||
border-radius: @border-radius-lg;
|
||
|
||
h3 {
|
||
font-size: @font-size-base;
|
||
font-weight: @font-weight-bold;
|
||
color: @color-text-primary;
|
||
margin: 0 0 @space-sm;
|
||
}
|
||
|
||
p {
|
||
font-size: @font-size-sm;
|
||
color: @color-text-secondary;
|
||
line-height: @line-height-relaxed;
|
||
margin: 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
</style>
|
||
|
||
<!-- Lightbox 样式必须 unscoped,因为 Teleport 把 DOM 移到 body -->
|
||
<style lang="less">
|
||
.lightbox {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 10000;
|
||
background: rgba(0, 0, 0, 0.96);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
outline: none;
|
||
|
||
&__close {
|
||
position: absolute;
|
||
top: 20px;
|
||
right: 20px;
|
||
z-index: 10;
|
||
background: none;
|
||
border: none;
|
||
color: #fff;
|
||
cursor: pointer;
|
||
padding: 8px;
|
||
opacity: 0.7;
|
||
transition: opacity 0.2s;
|
||
&:hover { opacity: 1; }
|
||
}
|
||
|
||
&__nav {
|
||
position: absolute;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
z-index: 10;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border: none;
|
||
color: #fff;
|
||
cursor: pointer;
|
||
padding: 16px;
|
||
border-radius: 50%;
|
||
opacity: 0.6;
|
||
transition: all 0.2s;
|
||
&:hover {
|
||
opacity: 1;
|
||
background: rgba(255, 255, 255, 0.2);
|
||
}
|
||
|
||
&--prev { left: 20px; }
|
||
&--next { right: 20px; }
|
||
}
|
||
|
||
&__img-wrap {
|
||
max-width: 90vw;
|
||
max-height: 85vh;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
&__img {
|
||
max-width: 100%;
|
||
max-height: 85vh;
|
||
object-fit: contain;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
&__counter {
|
||
position: absolute;
|
||
bottom: 24px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
color: rgba(255, 255, 255, 0.6);
|
||
font-size: 14px;
|
||
letter-spacing: 2px;
|
||
}
|
||
}
|
||
|
||
.lightbox-enter-active,
|
||
.lightbox-leave-active {
|
||
transition: opacity 0.3s ease;
|
||
}
|
||
|
||
.lightbox-enter-from,
|
||
.lightbox-leave-to {
|
||
opacity: 0;
|
||
}
|
||
</style>
|