hulai-website/pages/gallery.vue
wx ac90205395 feat: 官网代码全量更新
覆盖最新版本代码,包含:
- 新增品牌百科、公司百科页面及CMS管理
- 新增微信引导组件(WechatCTA)
- 新增品牌/公司图片资源
- 全站组件/页面/数据/样式更新

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:21:11 +08:00

569 行
14 KiB
Vue

此文件含有不可见的 Unicode 字符

此文件含有人类无法区分的不可见的 Unicode 字符,但可以由计算机进行不同的处理。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<template>
<div class="page-gallery">
<LayoutPageHero
:title="seo.h1"
subtitle="自营摄影团队 · 200+件蒙古服饰 · 专属秘境取景地"
:breadcrumbs="[{ text: '旅拍作品' }]"
background-image="/images/seasons/autumn-forest.jpg"
/>
<!-- TL;DR -->
<section class="page-tldr" aria-label="旅拍作品摘要">
<div class="container">
<div class="page-tldr-inner">
<p>呼籁旅行旅拍作品展示自营摄影团队随行拍摄200+件蒙古服饰可选专属秘境取景地无需单独预约旅行中自然记录家庭珍贵瞬间</p>
</div>
</div>
</section>
<!-- 团队数据亮点 -->
<section v-if="gallery" class="gallery-stats-bar">
<div class="container">
<div class="gallery-stats">
<div v-for="stat in gallery.intro.stats" :key="stat.label" class="gallery-stat">
<span class="gallery-stat__value">{{ stat.value }}</span>
<span class="gallery-stat__label">{{ stat.label }}</span>
<span class="gallery-stat__note">{{ stat.note }}</span>
</div>
</div>
</div>
</section>
<!-- 旅拍团队介绍 -->
<section v-if="gallery" class="gallery-intro section">
<div class="container">
<CommonSectionTitle
title="呼籁旅拍 · 草原最美的记忆"
subtitle="随行拍摄,无需单独预约,每组旅拍都是独一无二的故事"
align="center"
/>
<p class="gallery-intro__desc">{{ gallery.intro.description }}</p>
<div class="gallery-intro__highlights">
<div class="gallery-highlight">
<span class="gallery-highlight__icon">👘</span>
<div>
<strong>200+件蒙古服饰</strong>
<p>传统袍服节日盛装亲子套装按体型量身挑选</p>
</div>
</div>
<div class="gallery-highlight">
<span class="gallery-highlight__icon">📷</span>
<div>
<strong>专业旅拍摄影师</strong>
<p>全程随行把握每一个自然光线最好的瞬间</p>
</div>
</div>
<div class="gallery-highlight">
<span class="gallery-highlight__icon">🏔</span>
<div>
<strong>独家秘境取景地</strong>
<p>莫日格勒河源头营地花海白桦林深处不对外开放</p>
</div>
</div>
</div>
</div>
</section>
<!-- 分类筛选 + 作品展示 -->
<section v-if="gallery" class="section section--gray">
<div class="container">
<CommonSectionTitle
title="旅拍作品"
subtitle="每一帧都是真实客户的草原记忆"
align="center"
/>
<!-- 分类筛选 -->
<div class="gallery-filter">
<button
v-for="cat in gallery.categories"
:key="cat.key"
class="filter-btn"
:class="{ 'filter-btn--active': activeCategory === cat.key }"
@click="activeCategory = cat.key"
>
{{ cat.label }}
<span class="filter-count">{{ getCategoryCount(cat.key) }}</span>
</button>
</div>
<!-- 作品瀑布流 -->
<div class="gallery-masonry">
<div
v-for="work in filteredWorks"
:key="work.id"
class="gallery-item reveal"
>
<div class="gallery-card" :class="`gallery-card--${work.orientation || 'landscape'}`">
<!-- 图片区域 -->
<div class="gallery-card__img-wrap">
<img
:src="work.image"
:alt="`${work.title} - 呼籁草原旅拍`"
class="gallery-card__img"
loading="lazy"
@error="handleImgError($event, work.id)"
/>
<!-- 占位背景图片加载失败时可见 -->
<div class="gallery-card__placeholder" aria-hidden="true">
<span class="gallery-card__placeholder-icon">📸</span>
<span class="gallery-card__placeholder-text">{{ work.title }}</span>
</div>
</div>
<!-- 分类标签 -->
<span class="gallery-card__badge">{{ getCategoryLabel(work.category) }}</span>
<!-- 底部信息条常态可见 -->
<div class="gallery-card__info-bar">
<span class="gallery-card__location">📍 {{ work.location }}</span>
<span class="gallery-card__season">{{ work.season }}</span>
</div>
<!-- 悬浮遮罩hover 可见 -->
<div class="gallery-card__overlay">
<div class="gallery-card__overlay-inner">
<h3 class="gallery-card__title">{{ work.title }}</h3>
<p class="gallery-card__description">{{ work.description }}</p>
<ul class="gallery-card__meta">
<li>📍 {{ work.location }}</li>
<li>🌿 {{ work.season }}</li>
<li>👘 {{ work.costumeType }}</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- 空状态 -->
<div v-if="filteredWorks.length === 0" class="gallery-empty">
<p>暂无该分类的作品</p>
</div>
<!-- 说明文字 -->
<p class="gallery-notice">
以上均为呼籁旅拍真实客户作品已获客户授权展示
</p>
</div>
</section>
<!-- 预约旅拍 CTA -->
<CommonSectionCTA
title="预约您的草原旅拍"
description="旅拍随行程一并安排,无需单独预约。告诉定制师您的旅拍偏好,我们提前准备好服饰和取景计划。"
:links="[
{ to: '/customize', text: '立即预约,开始定制行程' },
{ to: '/contact', text: '咨询旅拍详情' },
]"
/>
<CommonWechatCTA
title="想拥有同款旅拍?扫码聊聊"
description="告诉定制师你喜欢的风格,旅拍随行程一并安排,提前备好服饰和取景方案。"
bg="white"
/>
</div>
</template>
<script setup>
import { useBreadcrumbSchema, useGallerySchema } from '~/composables/useJsonLd'
import useScrollReveal from '~/composables/useScrollReveal'
const { data: gallery } = await usePublicApi('gallery')
const seo = useSEO('gallery')
useScrollReveal()
// 分类筛选
const activeCategory = ref('all')
const filteredWorks = computed(() => {
if (activeCategory.value === 'all') return gallery.value?.works || []
return (gallery.value?.works || []).filter(w => w.category === activeCategory.value)
})
function getCategoryCount(key) {
if (key === 'all') return gallery.value?.works?.length || 0
return (gallery.value?.works || []).filter(w => w.category === key).length
}
function getCategoryLabel(key) {
const cat = (gallery.value?.categories || []).find(c => c.key === key)
return cat ? cat.label : key
}
// 图片加载失败时显示占位背景
function handleImgError(event, id) {
event.target.style.display = 'none'
}
// 结构化数据
useBreadcrumbSchema([
{ text: '首页', to: '/' },
{ text: '旅拍作品' },
])
useGallerySchema(gallery.value)
</script>
<style lang="less" scoped>
// 数据亮点条
.gallery-stats-bar {
background: @color-primary-bg;
padding: @space-xl 0;
border-bottom: 1px solid @color-border;
}
.gallery-stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: @space-lg;
text-align: center;
.respond-md({
grid-template-columns: repeat(4, 1fr);
});
}
.gallery-stat {
display: flex;
flex-direction: column;
gap: 2px;
&__value {
font-size: @font-size-2xl;
font-weight: @font-weight-bold;
color: @color-primary;
line-height: 1.2;
}
&__label {
font-size: @font-size-sm;
font-weight: @font-weight-medium;
color: @color-text-primary;
}
&__note {
font-size: @font-size-xs;
color: @color-text-muted;
}
}
// 介绍区
.gallery-intro {
&__desc {
max-width: 680px;
margin: @space-lg auto @space-2xl;
text-align: center;
color: @color-text-secondary;
line-height: @line-height-relaxed;
font-size: @font-size-base;
}
}
.gallery-intro__highlights {
display: grid;
grid-template-columns: 1fr;
gap: @space-lg;
max-width: 860px;
margin: 0 auto;
.respond-md({
grid-template-columns: repeat(3, 1fr);
});
}
.gallery-highlight {
display: flex;
align-items: flex-start;
gap: @space-md;
padding: @space-lg;
background: @color-bg-white;
border: 1px solid @color-border;
border-radius: @border-radius-lg;
&__icon {
font-size: 28px;
line-height: 1;
flex-shrink: 0;
}
strong {
display: block;
font-size: @font-size-base;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin-bottom: @space-xs;
}
p {
font-size: @font-size-sm;
color: @color-text-secondary;
line-height: @line-height-base;
margin: 0;
}
}
// 分类筛选
.gallery-filter {
display: flex;
flex-wrap: wrap;
gap: @space-sm;
margin-bottom: @space-2xl;
justify-content: center;
}
.filter-btn {
display: inline-flex;
align-items: center;
gap: @space-xs;
padding: @space-sm @space-md;
background: @color-bg-white;
border: 1px solid @color-border;
border-radius: @border-radius-full;
font-size: @font-size-sm;
color: @color-text-secondary;
cursor: pointer;
transition: all @transition-base;
&:hover {
border-color: @color-primary;
color: @color-primary;
}
&--active {
background: @color-primary;
border-color: @color-primary;
color: #fff;
.filter-count {
background: rgba(255, 255, 255, 0.3);
color: #fff;
}
}
}
.filter-count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 20px;
height: 20px;
padding: 0 5px;
background: @color-primary-bg;
color: @color-primary;
border-radius: @border-radius-full;
font-size: 11px;
font-weight: @font-weight-medium;
}
// 瀑布流布局
.gallery-masonry {
columns: 1;
column-gap: @space-lg;
.respond-md({
columns: 2;
});
.respond-lg({
columns: 3;
});
}
.gallery-item {
break-inside: avoid;
margin-bottom: @space-lg;
}
// 作品卡片
.gallery-card {
position: relative;
border-radius: @border-radius-lg;
overflow: hidden;
background: @color-bg-gray;
cursor: pointer;
&:hover {
.gallery-card__overlay {
opacity: 1;
}
.gallery-card__img {
transform: scale(1.04);
}
}
// 不同比例
&--portrait .gallery-card__img-wrap {
aspect-ratio: 3 / 4;
}
&--landscape .gallery-card__img-wrap {
aspect-ratio: 4 / 3;
}
&__img-wrap {
position: relative;
width: 100%;
overflow: hidden;
background: @color-bg-gray;
// 默认比例兜底
aspect-ratio: 4 / 3;
}
&__img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
transition: transform @transition-slow;
z-index: 1;
}
// 占位样式(图片未加载时可见)
&__placeholder {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: @space-sm;
background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);
z-index: 0;
}
&__placeholder-icon {
font-size: 32px;
}
&__placeholder-text {
font-size: @font-size-sm;
color: @color-text-secondary;
text-align: center;
padding: 0 @space-md;
font-weight: @font-weight-medium;
}
&__placeholder-id {
font-size: @font-size-xs;
color: @color-text-muted;
background: rgba(255, 255, 255, 0.7);
padding: 2px @space-sm;
border-radius: @border-radius-sm;
}
// 分类标签
&__badge {
position: absolute;
top: @space-sm;
left: @space-sm;
z-index: 3;
padding: 3px @space-sm;
background: @color-primary;
color: #fff;
border-radius: @border-radius-sm;
font-size: @font-size-xs;
font-weight: @font-weight-medium;
}
// 底部信息条
&__info-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: @space-sm @space-md;
background: @color-bg-white;
border-top: 1px solid @color-border;
}
&__location {
font-size: @font-size-xs;
color: @color-text-secondary;
.text-clamp(1);
}
&__season {
font-size: @font-size-xs;
color: @color-text-muted;
white-space: nowrap;
margin-left: @space-sm;
flex-shrink: 0;
}
// hover 遮罩
&__overlay {
position: absolute;
inset: 0;
z-index: 2;
background: linear-gradient(
to top,
rgba(0, 0, 0, 0.85) 0%,
rgba(0, 0, 0, 0.4) 50%,
rgba(0, 0, 0, 0.1) 100%
);
opacity: 0;
transition: opacity @transition-base;
display: flex;
align-items: flex-end;
}
&__overlay-inner {
padding: @space-lg;
color: #fff;
width: 100%;
}
&__title {
font-size: @font-size-base;
font-weight: @font-weight-bold;
margin: 0 0 @space-sm;
line-height: @line-height-tight;
}
&__description {
font-size: @font-size-sm;
line-height: @line-height-base;
color: rgba(255, 255, 255, 0.85);
margin: 0 0 @space-md;
.text-clamp(3);
}
&__meta {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: @space-xs @space-md;
li {
font-size: @font-size-xs;
color: rgba(255, 255, 255, 0.75);
}
}
}
// 空状态
.gallery-empty {
text-align: center;
padding: @space-3xl;
color: @color-text-muted;
font-size: @font-size-md;
}
// 说明文字
.gallery-notice {
margin-top: @space-2xl;
text-align: center;
font-size: @font-size-xs;
color: @color-text-muted;
padding: @space-md;
background: @color-bg-white;
border: 1px dashed @color-border;
border-radius: @border-radius-md;
}
</style>