更新页面效果

This commit is contained in:
李琦
2026-07-31 13:20:01 +08:00
parent e112b94ac9
commit 1ce1272dbb
3 changed files with 65 additions and 21 deletions

View File

@@ -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 })

View File

@@ -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) {

View File

@@ -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>