恢复滚动方案
This commit is contained in:
@@ -8,8 +8,8 @@ import { onMounted, onUnmounted, watch, ref } from 'vue'
|
||||
const props = defineProps({
|
||||
petalEnabled: { type: Boolean, default: true },
|
||||
bubbleEnabled: { type: Boolean, default: true },
|
||||
/** 预览/抽屉等遮罩时关闭粒子绘制 */
|
||||
active: { type: Boolean, default: true },
|
||||
performanceMode: { type: Boolean, default: false },
|
||||
})
|
||||
|
||||
const canvasRef = ref(null)
|
||||
@@ -24,7 +24,6 @@ let width = 0
|
||||
let height = 0
|
||||
let dpr = 1
|
||||
let lastTime = 0
|
||||
let lastPaint = 0
|
||||
let resizeObserver = null
|
||||
let reduceMotion = false
|
||||
|
||||
@@ -55,16 +54,14 @@ function resetHeart(h, initial = false) {
|
||||
}
|
||||
|
||||
function seedParticles() {
|
||||
const petalCount = props.performanceMode ? 8 : 18
|
||||
const heartCount = props.performanceMode ? 5 : 12
|
||||
petals.length = 0
|
||||
hearts.length = 0
|
||||
for (let i = 0; i < petalCount; i += 1) {
|
||||
for (let i = 0; i < 18; i += 1) {
|
||||
const p = {}
|
||||
resetPetal(p, true)
|
||||
petals.push(p)
|
||||
}
|
||||
for (let i = 0; i < heartCount; i += 1) {
|
||||
for (let i = 0; i < 12; i += 1) {
|
||||
const h = {}
|
||||
resetHeart(h, true)
|
||||
hearts.push(h)
|
||||
@@ -77,7 +74,7 @@ function resize() {
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
width = Math.max(1, rect.width)
|
||||
height = Math.max(1, rect.height)
|
||||
dpr = props.performanceMode ? 1 : Math.min(window.devicePixelRatio || 1, 1.5)
|
||||
dpr = Math.min(window.devicePixelRatio || 1, 2)
|
||||
canvas.width = Math.round(width * dpr)
|
||||
canvas.height = Math.round(height * dpr)
|
||||
ctx = canvas.getContext('2d')
|
||||
@@ -130,15 +127,8 @@ function tick(now) {
|
||||
return
|
||||
}
|
||||
|
||||
const frameInterval = props.performanceMode ? 50 : 16.7
|
||||
if (lastPaint && now - lastPaint < frameInterval) {
|
||||
rafId = requestAnimationFrame(tick)
|
||||
return
|
||||
}
|
||||
|
||||
const delta = Math.min(48, now - (lastTime || now)) / 1000
|
||||
lastTime = now
|
||||
lastPaint = now
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
|
||||
if (props.petalEnabled) {
|
||||
@@ -164,7 +154,6 @@ function tick(now) {
|
||||
function start() {
|
||||
cancelAnimationFrame(rafId)
|
||||
lastTime = 0
|
||||
lastPaint = 0
|
||||
if (!props.active || reduceMotion || (!props.petalEnabled && !props.bubbleEnabled)) {
|
||||
ctx?.clearRect(0, 0, width, height)
|
||||
rafId = 0
|
||||
@@ -181,13 +170,7 @@ onMounted(() => {
|
||||
start()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => [props.petalEnabled, props.bubbleEnabled, props.active, props.performanceMode],
|
||||
() => {
|
||||
resize()
|
||||
start()
|
||||
}
|
||||
)
|
||||
watch(() => [props.petalEnabled, props.bubbleEnabled, props.active], start)
|
||||
|
||||
onUnmounted(() => {
|
||||
cancelAnimationFrame(rafId)
|
||||
@@ -203,7 +186,5 @@ onUnmounted(() => {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
pointer-events: none;
|
||||
contain: strict;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div ref="galleryRef" class="gallery">
|
||||
<div class="gallery">
|
||||
<!-- 标题区 -->
|
||||
<div v-reveal="'up'" class="text-center mb-5 px-4">
|
||||
<span class="gallery-title calligraphy text-xl text-[#a88c6b] tracking-[0.3em] font-bold block mb-2">{{ page.title }}</span>
|
||||
@@ -10,18 +10,20 @@
|
||||
<!-- 单图(默认居中卡幅 / 可配置宽度铺满) -->
|
||||
<div v-if="page.type === 'single'" v-reveal="{ variant: 'scale', delay: 100 }"
|
||||
class="w-full mx-auto mt-6" :class="singleWrapClass" :style="singleWrapStyle">
|
||||
<div :class="singleFrameClass"><img :src="page.img1" class="preview-img" decoding="async" @click.stop="previewImage(page.img1)" @load="notifyLayoutChange" /></div>
|
||||
<div :class="singleFrameClass">
|
||||
<img :src="page.img1" class="preview-img" @click.stop="previewImage(page.img1)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错落双图(交错拼贴:左上大图 + 右下压角叠放,微旋转;clamp 封顶不溢出) -->
|
||||
<!-- 错落双图 -->
|
||||
<div v-else-if="page.type === 'overlap'" class="overlap-wrap relative w-full mt-2">
|
||||
<div v-reveal="{ variant: 'left', delay: 100 }"
|
||||
class="absolute left-0 top-0 w-[72%] h-[78%] z-10 -rotate-2" :class="frameClass">
|
||||
<img :src="page.img1" class="preview-img" decoding="async" @click.stop="previewImage(page.img1)" @load="notifyLayoutChange" />
|
||||
<img :src="page.img1" class="preview-img" @click.stop="previewImage(page.img1)" />
|
||||
</div>
|
||||
<div v-reveal="{ variant: 'right', delay: 300 }"
|
||||
class="absolute right-0 bottom-0 w-[56%] h-[62%] z-20 rotate-3" :class="frameClass">
|
||||
<img :src="page.img2" class="preview-img" decoding="async" @click.stop="previewImage(page.img2)" @load="notifyLayoutChange" />
|
||||
<img :src="page.img2" class="preview-img" @click.stop="previewImage(page.img2)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -29,27 +31,27 @@
|
||||
<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">
|
||||
<div v-reveal="{ variant: 'scale', delay: 100 }" class="col-span-2 row-span-2 relative" :class="frameClass">
|
||||
<img :src="page.images[0]" class="preview-img hover:scale-105 transition-transform duration-1000" decoding="async" @click.stop="previewImage(page.images[0])" @load="notifyLayoutChange" />
|
||||
<img :src="page.images[0]" class="preview-img hover:scale-105 transition-transform duration-1000" @click.stop="previewImage(page.images[0])" />
|
||||
</div>
|
||||
<div v-for="i in [1,2,3,4,5]" :key="i" v-reveal="{ variant: 'up', delay: i * 80 }" class="relative" :class="frameClass">
|
||||
<img :src="page.images[i]" class="preview-img" decoding="async" @click.stop="previewImage(page.images[i])" @load="notifyLayoutChange" />
|
||||
<img :src="page.images[i]" class="preview-img" @click.stop="previewImage(page.images[i])" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 九宫格 -->
|
||||
<!-- 九宫格:保留方向性 v-reveal 新动画 -->
|
||||
<div v-else-if="page.type === 'nine-grid'" class="nine-grid grid grid-cols-3 gap-1.5 px-4 mt-6">
|
||||
<div v-for="(img, imgIdx) in page.images" :key="imgIdx"
|
||||
v-reveal="getRevealConfig(imgIdx)"
|
||||
class="aspect-square relative" :class="frameClass">
|
||||
<img :src="img" class="nine-grid-img preview-img" decoding="async" @click.stop="previewImage(img)" @load="notifyLayoutChange" />
|
||||
<img :src="img" class="nine-grid-img preview-img" @click.stop="previewImage(img)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 瀑布流(错落多列) -->
|
||||
<!-- 瀑布流 -->
|
||||
<div v-else-if="page.type === 'masonry'" class="g-masonry mt-6 px-1">
|
||||
<div v-for="(img, imgIdx) in page.images" :key="imgIdx" v-reveal="{ variant: 'up', delay: imgIdx * 50 }"
|
||||
class="g-masonry-item" :class="frameClass">
|
||||
<img :src="img" class="preview-img" decoding="async" @click.stop="previewImage(img)" @load="notifyLayoutChange" />
|
||||
<img :src="img" class="preview-img" @click.stop="previewImage(img)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -57,7 +59,7 @@
|
||||
<div v-else-if="page.type === 'carousel'" class="relative mt-6">
|
||||
<div ref="trackRef" class="g-carousel-track no-scrollbar" @scroll="onScroll">
|
||||
<div v-for="(img, imgIdx) in page.images" :key="imgIdx" class="g-carousel-slide" :class="frameClass">
|
||||
<img :src="img" class="preview-img" decoding="async" @click.stop="previewImage(img)" @load="notifyLayoutChange" />
|
||||
<img :src="img" class="preview-img" @click.stop="previewImage(img)" />
|
||||
</div>
|
||||
</div>
|
||||
<button class="g-arrow g-prev" @click="goTo(index - 1)" aria-label="上一张">‹</button>
|
||||
@@ -68,15 +70,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 拍立得拼贴(自带边框,不受 borderStyle 影响) -->
|
||||
<!-- 拍立得拼贴 -->
|
||||
<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) }">
|
||||
<img :src="img" class="preview-img" decoding="async" @click.stop="previewImage(img)" @load="notifyLayoutChange" />
|
||||
<img :src="img" class="preview-img" @click.stop="previewImage(img)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 兜底 -->
|
||||
<div v-else class="text-center text-[#8A8680] text-[12px] mt-6">未识别的版式:{{ page.type }}</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -84,48 +85,28 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { borderClass } from '@/composables/borderStyles'
|
||||
import { useImageWarmup } from '@/composables/useImageWarmup'
|
||||
|
||||
const props = defineProps({
|
||||
page: { type: Object, required: true },
|
||||
})
|
||||
const emit = defineEmits(['preview-image', 'layout-change', 'warmup-change'])
|
||||
const emit = defineEmits(['preview-image'])
|
||||
|
||||
const galleryRef = ref(null)
|
||||
useImageWarmup(galleryRef, {
|
||||
onBusyChange: (busy) => emit('warmup-change', busy),
|
||||
})
|
||||
|
||||
// 边框样式:前台按 page.borderStyle 套用全局 gf-* 类
|
||||
const frameClass = computed(() => borderClass(props.page.borderStyle))
|
||||
|
||||
/* ---------- 单图宽度模式 ---------- */
|
||||
// singleWidth: 'default'(居中卡幅) | 'full'(宽度铺满);singleKeepRatio: 铺满时是否保持原始宽高比
|
||||
const singleWrapClass = computed(() =>
|
||||
props.page.singleWidth === 'full' ? 'max-w-none' : 'max-w-[300px]'
|
||||
)
|
||||
const singleWrapStyle = computed(() => {
|
||||
if (props.page.singleWidth === 'full') {
|
||||
// 沾满宽度:负边距抵消 page-section 的 p-6(24px×2),图片贴到屏幕两缘
|
||||
const bleed = { width: 'calc(100% + 48px)', marginLeft: '-24px', marginRight: '-24px' }
|
||||
// 保持原始比例 → 高度由图片自然撑开;否则固定 4:5 裁切
|
||||
return props.page.singleKeepRatio ? bleed : { ...bleed, aspectRatio: '4 / 5' }
|
||||
}
|
||||
return { aspectRatio: '4 / 5' }
|
||||
})
|
||||
// 保持原始比例时 img 需要自然高度(gf-natural 覆盖 gf-frame 的 height:100%)
|
||||
const singleFrameClass = computed(() =>
|
||||
frameClass.value + (props.page.singleWidth === 'full' && props.page.singleKeepRatio ? ' gf-natural' : '')
|
||||
)
|
||||
|
||||
/** 仅高度可能随图片加载变化的版式才通知父级重算 scroll metrics */
|
||||
const needsLayoutNotify = computed(() => {
|
||||
if (props.page.height === 'auto') return true
|
||||
if (props.page.type === 'single' && props.page.singleWidth === 'full' && props.page.singleKeepRatio) return true
|
||||
return false
|
||||
})
|
||||
|
||||
/* ---------- 轮播状态 ---------- */
|
||||
const index = ref(0)
|
||||
const trackRef = ref(null)
|
||||
|
||||
@@ -147,12 +128,6 @@ function previewImage(src) {
|
||||
if (src) emit('preview-image', src)
|
||||
}
|
||||
|
||||
function notifyLayoutChange() {
|
||||
if (!needsLayoutNotify.value) return
|
||||
emit('layout-change')
|
||||
}
|
||||
|
||||
/* ---------- 拍立得旋转/位移 ---------- */
|
||||
const polaroidAngles = [-5, 4, -3, 6, -4, 3, -2, 5]
|
||||
const polaroidShift = [0, 10, -8, 6, -10, 8, 4, -6]
|
||||
function polaroidTransform(i) {
|
||||
@@ -165,23 +140,15 @@ function getRevealConfig(imgIdx) {
|
||||
const row = Math.floor(imgIdx / 3)
|
||||
const col = imgIdx % 3
|
||||
|
||||
// 正中心:用 scale 代替 blur,避免大图 filter 动画与解码叠峰
|
||||
if (imgIdx === 4) {
|
||||
return {
|
||||
variant: 'scale',
|
||||
delay: 200,
|
||||
}
|
||||
return { variant: 'scale', delay: 200 }
|
||||
}
|
||||
|
||||
// 中间列:上 → down,下 → up
|
||||
if (col === 1) {
|
||||
return {
|
||||
variant: row === 0 ? 'down' : 'up',
|
||||
delay: 80 + imgIdx * 50,
|
||||
}
|
||||
}
|
||||
|
||||
// 左右列
|
||||
return {
|
||||
variant: col === 0 ? 'left' : 'right',
|
||||
delay: 80 + ((imgIdx + 80) * 20),
|
||||
@@ -208,14 +175,11 @@ function getRevealConfig(imgIdx) {
|
||||
}
|
||||
.nine-grid-img:hover { transform: translateZ(0) scale(1.03); }
|
||||
|
||||
/* 错落双图:高度随视口缩放并封顶,保证标题+图片总高始终落在页高预算内 */
|
||||
.overlap-wrap { height: clamp(320px, 54vh, 460px); }
|
||||
|
||||
/* 瀑布流 */
|
||||
.g-masonry { column-count: 2; column-gap: 10px; }
|
||||
.g-masonry-item { break-inside: avoid; margin-bottom: 10px; border-radius: 6px; }
|
||||
|
||||
/* 轮播 */
|
||||
.g-carousel-track {
|
||||
display: flex; overflow-x: auto; scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch;
|
||||
border-radius: 10px; scrollbar-width: none;
|
||||
@@ -235,7 +199,6 @@ function getRevealConfig(imgIdx) {
|
||||
.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;
|
||||
|
||||
Reference in New Issue
Block a user