2026-07-31 09:05:15 +08:00
|
|
|
|
<template>
|
2026-08-01 14:55:21 +08:00
|
|
|
|
<div class="gallery">
|
2026-07-31 09:05:15 +08:00
|
|
|
|
<!-- 标题区 -->
|
|
|
|
|
|
<div v-reveal="'up'" class="text-center mb-5 px-4">
|
2026-08-01 09:15:40 +08:00
|
|
|
|
<span class="gallery-title calligraphy text-xl text-[#a88c6b] tracking-[0.3em] font-bold block mb-2">{{ page.title }}</span>
|
|
|
|
|
|
<span class="gallery-subtitle text-[9px] text-[#8A8680] first-letter:uppercase lowercase tracking-[0.4em] block mb-4">{{ page.subtitle }}</span>
|
2026-07-31 14:43:54 +08:00
|
|
|
|
<span class="gallery-desc text-[12px] text-[#6b6863] font-light leading-relaxed mx-auto max-w-[280px] tracking-widest block whitespace-pre-wrap">{{ page.desc }}</span>
|
2026-07-31 09:05:15 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-07-31 11:36:19 +08:00
|
|
|
|
<!-- 单图(默认居中卡幅 / 可配置宽度铺满) -->
|
2026-07-31 09:05:15 +08:00
|
|
|
|
<div v-if="page.type === 'single'" v-reveal="{ variant: 'scale', delay: 100 }"
|
2026-07-31 11:36:19 +08:00
|
|
|
|
class="w-full mx-auto mt-6" :class="singleWrapClass" :style="singleWrapStyle">
|
2026-08-01 14:55:21 +08:00
|
|
|
|
<div :class="singleFrameClass">
|
|
|
|
|
|
<img :src="page.img1" class="preview-img" @click.stop="previewImage(page.img1)" />
|
|
|
|
|
|
</div>
|
2026-07-31 09:05:15 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-08-01 14:55:21 +08:00
|
|
|
|
<!-- 错落双图 -->
|
2026-07-31 11:36:19 +08:00
|
|
|
|
<div v-else-if="page.type === 'overlap'" class="overlap-wrap relative w-full mt-2">
|
2026-07-31 12:13:41 +08:00
|
|
|
|
<div v-reveal="{ variant: 'left', delay: 100 }"
|
|
|
|
|
|
class="absolute left-0 top-0 w-[72%] h-[78%] z-10 -rotate-2" :class="frameClass">
|
2026-08-01 14:55:21 +08:00
|
|
|
|
<img :src="page.img1" class="preview-img" @click.stop="previewImage(page.img1)" />
|
2026-07-31 09:05:15 +08:00
|
|
|
|
</div>
|
2026-07-31 12:13:41 +08:00
|
|
|
|
<div v-reveal="{ variant: 'right', delay: 300 }"
|
|
|
|
|
|
class="absolute right-0 bottom-0 w-[56%] h-[62%] z-20 rotate-3" :class="frameClass">
|
2026-08-01 14:55:21 +08:00
|
|
|
|
<img :src="page.img2" class="preview-img" @click.stop="previewImage(page.img2)" />
|
2026-07-31 09:05:15 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 一大五小 -->
|
|
|
|
|
|
<div v-else-if="page.type === 'one-large-five-small'"
|
|
|
|
|
|
class="relative w-full aspect-square mt-6 grid grid-cols-3 grid-rows-3 gap-1.5 z-20">
|
2026-07-31 10:29:14 +08:00
|
|
|
|
<div v-reveal="{ variant: 'scale', delay: 100 }" class="col-span-2 row-span-2 relative" :class="frameClass">
|
2026-08-01 14:55:21 +08:00
|
|
|
|
<img :src="page.images[0]" class="preview-img hover:scale-105 transition-transform duration-1000" @click.stop="previewImage(page.images[0])" />
|
2026-07-31 09:05:15 +08:00
|
|
|
|
</div>
|
2026-07-31 10:29:14 +08:00
|
|
|
|
<div v-for="i in [1,2,3,4,5]" :key="i" v-reveal="{ variant: 'up', delay: i * 80 }" class="relative" :class="frameClass">
|
2026-08-01 14:55:21 +08:00
|
|
|
|
<img :src="page.images[i]" class="preview-img" @click.stop="previewImage(page.images[i])" />
|
2026-07-31 09:05:15 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-08-01 14:55:21 +08:00
|
|
|
|
<!-- 九宫格:保留方向性 v-reveal 新动画 -->
|
2026-08-01 09:48:16 +08:00
|
|
|
|
<div v-else-if="page.type === 'nine-grid'" class="nine-grid grid grid-cols-3 gap-1.5 px-4 mt-6">
|
2026-08-01 08:18:59 +08:00
|
|
|
|
<div v-for="(img, imgIdx) in page.images" :key="imgIdx"
|
2026-08-01 10:34:21 +08:00
|
|
|
|
v-reveal="getRevealConfig(imgIdx)"
|
2026-08-01 09:15:40 +08:00
|
|
|
|
class="aspect-square relative" :class="frameClass">
|
2026-08-01 14:55:21 +08:00
|
|
|
|
<img :src="img" class="nine-grid-img preview-img" @click.stop="previewImage(img)" />
|
2026-07-31 09:05:15 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-08-01 14:55:21 +08:00
|
|
|
|
<!-- 瀑布流 -->
|
2026-07-31 09:05:15 +08:00
|
|
|
|
<div v-else-if="page.type === 'masonry'" class="g-masonry mt-6 px-1">
|
2026-07-31 10:29:14 +08:00
|
|
|
|
<div v-for="(img, imgIdx) in page.images" :key="imgIdx" v-reveal="{ variant: 'up', delay: imgIdx * 50 }"
|
|
|
|
|
|
class="g-masonry-item" :class="frameClass">
|
2026-08-01 14:55:21 +08:00
|
|
|
|
<img :src="img" class="preview-img" @click.stop="previewImage(img)" />
|
2026-07-31 09:05:15 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 轮播滑动 -->
|
|
|
|
|
|
<div v-else-if="page.type === 'carousel'" class="relative mt-6">
|
|
|
|
|
|
<div ref="trackRef" class="g-carousel-track no-scrollbar" @scroll="onScroll">
|
2026-07-31 10:29:14 +08:00
|
|
|
|
<div v-for="(img, imgIdx) in page.images" :key="imgIdx" class="g-carousel-slide" :class="frameClass">
|
2026-08-01 14:55:21 +08:00
|
|
|
|
<img :src="img" class="preview-img" @click.stop="previewImage(img)" />
|
2026-07-31 09:05:15 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button class="g-arrow g-prev" @click="goTo(index - 1)" aria-label="上一张">‹</button>
|
|
|
|
|
|
<button class="g-arrow g-next" @click="goTo(index + 1)" aria-label="下一张">›</button>
|
|
|
|
|
|
<div class="g-dots">
|
|
|
|
|
|
<button v-for="(img, imgIdx) in page.images" :key="imgIdx"
|
|
|
|
|
|
class="g-dot" :class="{ active: imgIdx === index }" @click="goTo(imgIdx)"></button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-08-01 14:55:21 +08:00
|
|
|
|
<!-- 拍立得拼贴 -->
|
2026-07-31 09:05:15 +08:00
|
|
|
|
<div v-else-if="page.type === 'polaroid'" class="g-polaroid mt-6">
|
|
|
|
|
|
<div v-for="(img, imgIdx) in page.images" :key="imgIdx" v-reveal="{ variant: 'scale', delay: imgIdx * 70 }"
|
|
|
|
|
|
class="g-polaroid-item" :style="{ transform: polaroidTransform(imgIdx) }">
|
2026-08-01 14:55:21 +08:00
|
|
|
|
<img :src="img" class="preview-img" @click.stop="previewImage(img)" />
|
2026-07-31 09:05:15 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-else class="text-center text-[#8A8680] text-[12px] mt-6">未识别的版式:{{ page.type }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2026-07-31 10:29:14 +08:00
|
|
|
|
import { ref, computed } from 'vue'
|
|
|
|
|
|
import { borderClass } from '@/composables/borderStyles'
|
2026-07-31 09:05:15 +08:00
|
|
|
|
|
|
|
|
|
|
const props = defineProps({
|
|
|
|
|
|
page: { type: Object, required: true },
|
|
|
|
|
|
})
|
2026-08-01 14:55:21 +08:00
|
|
|
|
const emit = defineEmits(['preview-image'])
|
2026-07-31 09:05:15 +08:00
|
|
|
|
|
2026-07-31 10:29:14 +08:00
|
|
|
|
const frameClass = computed(() => borderClass(props.page.borderStyle))
|
|
|
|
|
|
|
2026-07-31 11:36:19 +08:00
|
|
|
|
const singleWrapClass = computed(() =>
|
|
|
|
|
|
props.page.singleWidth === 'full' ? 'max-w-none' : 'max-w-[300px]'
|
|
|
|
|
|
)
|
|
|
|
|
|
const singleWrapStyle = computed(() => {
|
|
|
|
|
|
if (props.page.singleWidth === 'full') {
|
2026-07-31 12:13:41 +08:00
|
|
|
|
const bleed = { width: 'calc(100% + 48px)', marginLeft: '-24px', marginRight: '-24px' }
|
|
|
|
|
|
return props.page.singleKeepRatio ? bleed : { ...bleed, aspectRatio: '4 / 5' }
|
2026-07-31 11:36:19 +08:00
|
|
|
|
}
|
|
|
|
|
|
return { aspectRatio: '4 / 5' }
|
|
|
|
|
|
})
|
|
|
|
|
|
const singleFrameClass = computed(() =>
|
|
|
|
|
|
frameClass.value + (props.page.singleWidth === 'full' && props.page.singleKeepRatio ? ' gf-natural' : '')
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-07-31 09:05:15 +08:00
|
|
|
|
const index = ref(0)
|
|
|
|
|
|
const trackRef = ref(null)
|
|
|
|
|
|
|
|
|
|
|
|
function goTo(i) {
|
|
|
|
|
|
const len = props.page.images?.length || 0
|
|
|
|
|
|
if (!len) return
|
|
|
|
|
|
index.value = (i + len) % len
|
|
|
|
|
|
const el = trackRef.value
|
|
|
|
|
|
if (el) el.scrollTo({ left: index.value * el.clientWidth, behavior: 'smooth' })
|
|
|
|
|
|
}
|
|
|
|
|
|
function onScroll() {
|
|
|
|
|
|
const el = trackRef.value
|
|
|
|
|
|
if (!el) return
|
|
|
|
|
|
const i = Math.round(el.scrollLeft / el.clientWidth)
|
|
|
|
|
|
if (i !== index.value) index.value = i
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-01 12:36:58 +08:00
|
|
|
|
function previewImage(src) {
|
|
|
|
|
|
if (src) emit('preview-image', src)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-31 09:05:15 +08:00
|
|
|
|
const polaroidAngles = [-5, 4, -3, 6, -4, 3, -2, 5]
|
|
|
|
|
|
const polaroidShift = [0, 10, -8, 6, -10, 8, 4, -6]
|
|
|
|
|
|
function polaroidTransform(i) {
|
|
|
|
|
|
const a = polaroidAngles[i % polaroidAngles.length]
|
|
|
|
|
|
const s = polaroidShift[i % polaroidShift.length]
|
|
|
|
|
|
return `rotate(${a}deg) translateY(${s}px)`
|
|
|
|
|
|
}
|
2026-08-01 10:34:21 +08:00
|
|
|
|
|
|
|
|
|
|
function getRevealConfig(imgIdx) {
|
|
|
|
|
|
const row = Math.floor(imgIdx / 3)
|
|
|
|
|
|
const col = imgIdx % 3
|
|
|
|
|
|
|
|
|
|
|
|
if (imgIdx === 4) {
|
2026-08-01 14:55:21 +08:00
|
|
|
|
return { variant: 'scale', delay: 200 }
|
2026-08-01 10:34:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (col === 1) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
variant: row === 0 ? 'down' : 'up',
|
|
|
|
|
|
delay: 80 + imgIdx * 50,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
|
|
|
variant: col === 0 ? 'left' : 'right',
|
2026-08-01 13:57:14 +08:00
|
|
|
|
delay: 80 + ((imgIdx + 80) * 20),
|
2026-08-01 10:34:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-31 09:05:15 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2026-07-31 14:43:54 +08:00
|
|
|
|
.gallery { width: 100%; font-family: var(--font-sans); }
|
2026-08-01 12:36:58 +08:00
|
|
|
|
.preview-img { cursor: zoom-in; }
|
2026-08-01 09:15:40 +08:00
|
|
|
|
.gallery-title {
|
|
|
|
|
|
font-family: var(--font-calligraphy);
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
}
|
2026-07-31 14:43:54 +08:00
|
|
|
|
.gallery-desc { font-family: var(--font-kai); }
|
2026-07-31 09:05:15 +08:00
|
|
|
|
|
2026-07-31 17:21:41 +08:00
|
|
|
|
.nine-grid-img {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
|
transform: translateZ(0);
|
|
|
|
|
|
transition: transform 420ms ease-out;
|
|
|
|
|
|
backface-visibility: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
.nine-grid-img:hover { transform: translateZ(0) scale(1.03); }
|
|
|
|
|
|
|
2026-07-31 11:36:19 +08:00
|
|
|
|
.overlap-wrap { height: clamp(320px, 54vh, 460px); }
|
|
|
|
|
|
|
2026-07-31 09:05:15 +08:00
|
|
|
|
.g-masonry { column-count: 2; column-gap: 10px; }
|
2026-07-31 10:29:14 +08:00
|
|
|
|
.g-masonry-item { break-inside: avoid; margin-bottom: 10px; border-radius: 6px; }
|
2026-07-31 09:05:15 +08:00
|
|
|
|
|
|
|
|
|
|
.g-carousel-track {
|
|
|
|
|
|
display: flex; overflow-x: auto; scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch;
|
|
|
|
|
|
border-radius: 10px; scrollbar-width: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
.g-carousel-slide { flex: 0 0 100%; min-width: 100%; height: 52vh; scroll-snap-align: center; }
|
|
|
|
|
|
.g-carousel-slide img { width: 100%; height: 100%; object-fit: cover; }
|
|
|
|
|
|
.g-arrow {
|
|
|
|
|
|
position: absolute; top: 50%; transform: translateY(-50%);
|
|
|
|
|
|
width: 34px; height: 34px; border-radius: 9999px; border: none;
|
|
|
|
|
|
background: rgba(255,255,255,0.78); color: #a88c6b; font-size: 20px; line-height: 1;
|
|
|
|
|
|
display: flex; align-items: center; justify-content: center; cursor: pointer;
|
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.12); backdrop-filter: blur(6px);
|
|
|
|
|
|
}
|
|
|
|
|
|
.g-prev { left: 8px; }
|
|
|
|
|
|
.g-next { right: 8px; }
|
|
|
|
|
|
.g-dots { display: flex; justify-content: center; gap: 6px; margin-top: 10px; }
|
|
|
|
|
|
.g-dot { width: 7px; height: 7px; border-radius: 9999px; border: none; background: rgba(168,140,107,0.3); padding: 0; cursor: pointer; transition: all 0.3s; }
|
|
|
|
|
|
.g-dot.active { background: #a88c6b; width: 18px; border-radius: 9999px; }
|
|
|
|
|
|
|
|
|
|
|
|
.g-polaroid { display: flex; flex-wrap: wrap; justify-content: center; gap: 14px 10px; padding: 14px 4px 4px; }
|
|
|
|
|
|
.g-polaroid-item {
|
|
|
|
|
|
width: 42%; background: #fff; padding: 8px 8px 24px; border-radius: 2px;
|
|
|
|
|
|
box-shadow: 0 6px 16px rgba(0,0,0,0.12); transition: transform 0.4s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
.g-polaroid-item:hover { z-index: 10; }
|
|
|
|
|
|
.g-polaroid-item img { width: 100%; aspect-ratio: 4 / 5; object-fit: cover; display: block; }
|
|
|
|
|
|
|
|
|
|
|
|
.no-scrollbar::-webkit-scrollbar { display: none; }
|
|
|
|
|
|
</style>
|