更新页面效果
This commit is contained in:
@@ -32,16 +32,18 @@ export function useAudio() {
|
||||
}
|
||||
|
||||
const playUrl = async (url) => {
|
||||
if (!url) return
|
||||
if (!url) return false
|
||||
const a = ensureAudio()
|
||||
if (a.src !== url) a.src = url
|
||||
activeUrl.value = url
|
||||
try {
|
||||
await a.play()
|
||||
isPlaying.value = true
|
||||
return true
|
||||
} catch (e) {
|
||||
// 被浏览器拦截,等待用户手势
|
||||
isPlaying.value = false
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,12 +68,7 @@ export function useAudio() {
|
||||
// 尝试自动播放,返回是否成功(被浏览器拦截则返回 false,由调用方决定是否弹授权框)
|
||||
const attemptAutoplay = async () => {
|
||||
if (!activeUrl.value) return false
|
||||
try {
|
||||
await playUrl(activeUrl.value)
|
||||
return isPlaying.value
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
return playUrl(activeUrl.value)
|
||||
}
|
||||
|
||||
const pause = () => {
|
||||
@@ -85,9 +82,9 @@ export function useAudio() {
|
||||
|
||||
// 首次交互解锁自动播放
|
||||
const armAutoplay = () => {
|
||||
const handler = () => {
|
||||
play()
|
||||
removeUnlock()
|
||||
const handler = async () => {
|
||||
const ok = await play()
|
||||
if (ok) removeUnlock()
|
||||
}
|
||||
unlockHandler = handler
|
||||
window.addEventListener('pointerdown', handler, { once: true })
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
</a-radio-group>
|
||||
<div class="text-[11px] text-[#8A8680] mt-1">整页模式会带来更有仪式感的逐屏切换效果。</div>
|
||||
</a-form-item>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-x-6">
|
||||
<a-form-item label="自由滚动速度(滚动间隔)">
|
||||
<div class="flex gap-3 items-center">
|
||||
<a-slider v-model:value="cfg.freeScrollInterval" :min="10" :max="500" class="flex-1" />
|
||||
@@ -111,6 +111,13 @@
|
||||
</div>
|
||||
<div class="text-[11px] text-[#8A8680] mt-1">整页/分屏模式下自动翻页的动画时长,默认 800ms。</div>
|
||||
</a-form-item>
|
||||
<a-form-item label="首屏停留时间">
|
||||
<div class="flex gap-3 items-center">
|
||||
<a-slider v-model:value="cfg.firstScreenDuration" :min="0" :max="12000" :step="100" class="flex-1" />
|
||||
<a-input-number v-model:value="cfg.firstScreenDuration" :min="0" :max="12000" :step="100" addon-after="ms" class="w-[120px]" />
|
||||
</div>
|
||||
<div class="text-[11px] text-[#8A8680] mt-1">首屏效果结束后等待多久开始自动滚动,默认 4500ms。</div>
|
||||
</a-form-item>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<a-form-item label="飘落花瓣">
|
||||
@@ -397,7 +404,7 @@ function defaultConfig() {
|
||||
musicList: [{ url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3', name: '背景音乐 1' }],
|
||||
activeMusicUrl: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3',
|
||||
scrollMode: 'snap', petalEnabled: true, bubbleEnabled: true, introEnabled: true,
|
||||
freeScrollInterval: 50, pageSwitchDuration: 800,
|
||||
freeScrollInterval: 50, pageSwitchDuration: 800, firstScreenDuration: 4500,
|
||||
photoPages: [
|
||||
{ type: 'single', title: '初遇', subtitle: 'FIRST MET', desc: '世界那么大,还是遇见了你。\n于千万人之中,没有早一步也没有晚一步。', img1: 'https://images.unsplash.com/photo-1606800052052-a08af7148866?auto=format&fit=crop&w=600&q=80', height: '100vh', borderStyle: 'mat' },
|
||||
{ type: 'overlap', title: '相知', subtitle: 'FALL IN LOVE', desc: '纵然万劫不复,纵然相思入骨。\n我也待你眉眼如初,岁月如故。', img1: 'https://images.unsplash.com/photo-1532712938310-34cb3982ef74?auto=format&fit=crop&w=600&q=80', img2: 'https://images.unsplash.com/photo-1511285560929-80b456fea0bc?auto=format&fit=crop&w=600&q=80', height: '100vh', borderStyle: 'mat' },
|
||||
@@ -662,6 +669,7 @@ onMounted(async () => {
|
||||
// 数值型参数兜底(兼容旧配置)
|
||||
cfg.freeScrollInterval = Number(cfg.freeScrollInterval) || 50
|
||||
cfg.pageSwitchDuration = Number(cfg.pageSwitchDuration) || 800
|
||||
cfg.firstScreenDuration = Number.isFinite(Number(cfg.firstScreenDuration)) ? Number(cfg.firstScreenDuration) : 4500
|
||||
}
|
||||
} catch (e) { /* 使用默认配置 */ }
|
||||
if (adminStore.isAdmin) {
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
<div v-reveal="{ variant: 'scale', delay: 120 }" class="w-full max-w-[260px] aspect-[3/4] relative z-10">
|
||||
<div class="absolute inset-0 border border-[#a88c6b] translate-x-3 translate-y-3 opacity-30"></div>
|
||||
<div class="relative w-full h-full z-10 bg-[#F7F6F2] shadow-md overflow-hidden">
|
||||
<img :src="data.heroImg" class="w-full h-full object-cover will-change-transform" :style="{ transform: `translateY(${parallax}px) scale(1.12)` }" />
|
||||
<img :src="data.heroImg" class="w-full h-full object-cover" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -226,6 +226,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 音乐自动播放被浏览器拦截时才显示确认 -->
|
||||
<div v-if="showMusicAuth" class="absolute inset-0 z-[80] bg-black/35 backdrop-blur-sm flex items-center justify-center px-6">
|
||||
<div class="music-auth-card">
|
||||
<div class="music-auth-note">♪</div>
|
||||
<div class="music-auth-title">开启背景音乐</div>
|
||||
<div class="music-auth-sub">浏览器需要一次确认才能播放</div>
|
||||
<button class="music-auth-btn" type="button" @click="authorizeMusic">点击播放</button>
|
||||
<button class="music-auth-skip" type="button" @click="onMusicAuthSkip">稍后再说</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -240,6 +251,7 @@ const DEFAULTS = {
|
||||
musicList: [], activeMusicUrl: '',
|
||||
freeScrollInterval: 50, // 自由滚动速度:滚动间隔 ms(越小越快)
|
||||
pageSwitchDuration: 800, // 页面切换速度:翻页动画时长 ms
|
||||
firstScreenDuration: 4500, // 首屏停留时间:首屏效果结束后再开始自动滚动
|
||||
}
|
||||
|
||||
const data = ref({
|
||||
@@ -260,12 +272,12 @@ const data = ref({
|
||||
|
||||
const audio = useAudio()
|
||||
const isAutoScroll = ref(true), isDrawerOpen = ref(false), isSubmitted = ref(false)
|
||||
const isInteracting = ref(false), scrollContainerRef = ref(null), progress = ref(0), parallax = ref(0)
|
||||
const isInteracting = ref(false), scrollContainerRef = ref(null), progress = ref(0)
|
||||
const showIntro = ref(false), introOpening = ref(false), showTrackList = ref(false), showMusicAuth = ref(false)
|
||||
const rsvpForm = reactive({ name: '', guest_count: '1', wishes: '' })
|
||||
const rsvpOptions = [ { value: '1', label: '1 人出席' }, { value: '2', label: '2 人出席' }, { value: '3', label: '3 人及以上' }, { value: '0', label: '遗憾缺席' } ]
|
||||
|
||||
let rafId = null, snapTimer = null, pauseTimer = null, introTimer = null, freeTimer = null, animId = null
|
||||
let rafId = null, snapTimer = null, pauseTimer = null, introTimer = null, freeTimer = null, animId = null, scrollStartTimer = null
|
||||
|
||||
// 后台可配置的滚动参数(带默认值兜底,防止脏数据)
|
||||
const freeInterval = computed(() => {
|
||||
@@ -276,6 +288,10 @@ const switchDuration = computed(() => {
|
||||
const v = Number(data.value.pageSwitchDuration)
|
||||
return v >= 0 && v <= 5000 ? v : 800
|
||||
})
|
||||
const firstScreenDuration = computed(() => {
|
||||
const v = Number(data.value.firstScreenDuration)
|
||||
return v >= 0 && v <= 30000 ? v : 4500
|
||||
})
|
||||
|
||||
// 装饰元素配色(柔化后的低饱和金粉系,与主题协调不抢眼)
|
||||
const loveColors = ['#c9b08a', '#e2b3c0', '#d4af37', '#e8c9d2']
|
||||
@@ -329,14 +345,15 @@ const pageHeightStyle = (page) => {
|
||||
}
|
||||
|
||||
const closeDrawer = () => { isDrawerOpen.value = false }
|
||||
const authorizeMusic = () => { audio.play(); showMusicAuth.value = false }
|
||||
audio.play()
|
||||
const authorizeMusic = async () => {
|
||||
const ok = await audio.play()
|
||||
if (ok) showMusicAuth.value = false
|
||||
}
|
||||
const handleScroll = () => {
|
||||
const el = scrollContainerRef.value
|
||||
if (!el) return
|
||||
const max = el.scrollHeight - el.clientHeight
|
||||
progress.value = max > 0 ? el.scrollTop / max : 0
|
||||
parallax.value = el.scrollTop * 0.25
|
||||
}
|
||||
|
||||
const handleUserInteraction = () => {
|
||||
@@ -375,7 +392,11 @@ const goNextSection = () => {
|
||||
}
|
||||
|
||||
const startAutoScroll = () => {
|
||||
clearTimeout(scrollStartTimer)
|
||||
clearInterval(snapTimer)
|
||||
clearInterval(freeTimer)
|
||||
if (data.value.scrollMode === 'snap') {
|
||||
if (isAutoScroll.value && !isInteracting.value && !isDrawerOpen.value) goNextSection()
|
||||
snapTimer = setInterval(() => {
|
||||
if (isAutoScroll.value && !isInteracting.value && !isDrawerOpen.value) goNextSection()
|
||||
}, 4500)
|
||||
@@ -391,6 +412,11 @@ const startAutoScroll = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const scheduleAutoScrollStart = () => {
|
||||
clearTimeout(scrollStartTimer)
|
||||
scrollStartTimer = setTimeout(() => startAutoScroll(), firstScreenDuration.value)
|
||||
}
|
||||
|
||||
const toggleAutoScroll = () => {
|
||||
isAutoScroll.value = !isAutoScroll.value
|
||||
const el = scrollContainerRef.value
|
||||
@@ -400,10 +426,18 @@ const toggleAutoScroll = () => {
|
||||
// 入场揭幕:开门动画
|
||||
const openInvitation = () => {
|
||||
if (introOpening.value) return
|
||||
clearTimeout(introTimer)
|
||||
introOpening.value = true
|
||||
introTimer = setTimeout(() => { showIntro.value = false }, 1000)
|
||||
introTimer = setTimeout(() => {
|
||||
showIntro.value = false
|
||||
scheduleAutoScrollStart()
|
||||
}, 1000)
|
||||
// 点击揭幕是明确的用户手势:直接尝试播放背景音乐(最可靠的自动播放路径)
|
||||
if (data.value.musicList.length) audio.play()
|
||||
if (data.value.musicList.length) {
|
||||
audio.play().then((ok) => {
|
||||
if (ok) showMusicAuth.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
const onMusicAuthSkip = () => {
|
||||
showMusicAuth.value = false
|
||||
@@ -432,6 +466,9 @@ onMounted(async () => {
|
||||
loaded.activeMusicUrl = loaded.musicUrl
|
||||
}
|
||||
loaded.endImg = loaded.endImg || loaded.heroImg || data.value.endImg
|
||||
loaded.firstScreenDuration = Number.isFinite(Number(loaded.firstScreenDuration))
|
||||
? Number(loaded.firstScreenDuration)
|
||||
: DEFAULTS.firstScreenDuration
|
||||
data.value = { ...data.value, ...DEFAULTS, ...loaded }
|
||||
}
|
||||
} catch (e) { /* 使用默认配置 */ }
|
||||
@@ -445,9 +482,10 @@ onMounted(async () => {
|
||||
if (data.value.introEnabled) {
|
||||
showIntro.value = true
|
||||
introTimer = setTimeout(() => openInvitation(), 3600)
|
||||
} else {
|
||||
scheduleAutoScrollStart()
|
||||
}
|
||||
|
||||
startAutoScroll()
|
||||
handleScroll()
|
||||
})
|
||||
|
||||
@@ -458,6 +496,7 @@ onUnmounted(() => {
|
||||
clearInterval(freeTimer)
|
||||
clearTimeout(pauseTimer)
|
||||
clearTimeout(introTimer)
|
||||
clearTimeout(scrollStartTimer)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user