- 后台全面引入 Naive UI 组件库,统一 UI 规范 - 前台 usePublicApi 切换到 API 调用(GET /api/content/[key]) - 前后台数据源统一(site_content 表) - 产品线统一管理(tour/camp/course 三套编辑器) - 产品数据归一化(itinerary/faq/pricing 格式统一) - 表单提交 API 改写入 submissions 表 - 新增 submissions 标记已读 API - site-content PUT 支持 upsert - 修复前台 bug(stories 路由冲突、占位符警告、selector breadcrumb) - 消除 PageHero 类型警告(useSEO reactive 修复) - 25 个前台页面全部 HTTP 200,展示效果不变 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
563 行
14 KiB
Vue
563 行
14 KiB
Vue
<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: '咨询旅拍详情' },
|
||
]"
|
||
/>
|
||
</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>
|