From 1ce1272dbb9a0f07de93a0eec3e18d41801f82c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E7=90=A6?= Date: Fri, 31 Jul 2026 13:20:01 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E9=A1=B5=E9=9D=A2=E6=95=88?= =?UTF-8?q?=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vite-tailwindcss/src/composables/useAudio.js | 17 +++--- .../src/views/admin/AdminEditor.vue | 12 +++- vite-tailwindcss/src/views/index/index.vue | 57 ++++++++++++++++--- 3 files changed, 65 insertions(+), 21 deletions(-) diff --git a/vite-tailwindcss/src/composables/useAudio.js b/vite-tailwindcss/src/composables/useAudio.js index d0589b8..186988b 100644 --- a/vite-tailwindcss/src/composables/useAudio.js +++ b/vite-tailwindcss/src/composables/useAudio.js @@ -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 }) diff --git a/vite-tailwindcss/src/views/admin/AdminEditor.vue b/vite-tailwindcss/src/views/admin/AdminEditor.vue index 037766c..33a6eea 100644 --- a/vite-tailwindcss/src/views/admin/AdminEditor.vue +++ b/vite-tailwindcss/src/views/admin/AdminEditor.vue @@ -96,7 +96,7 @@
整页模式会带来更有仪式感的逐屏切换效果。
-
+
@@ -111,6 +111,13 @@
整页/分屏模式下自动翻页的动画时长,默认 800ms。
+ +
+ + +
+
首屏效果结束后等待多久开始自动滚动,默认 4500ms。
+
@@ -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) { diff --git a/vite-tailwindcss/src/views/index/index.vue b/vite-tailwindcss/src/views/index/index.vue index da1500c..d30a408 100644 --- a/vite-tailwindcss/src/views/index/index.vue +++ b/vite-tailwindcss/src/views/index/index.vue @@ -96,7 +96,7 @@
- +
@@ -226,6 +226,17 @@
+ +
+
+
+
开启背景音乐
+
浏览器需要一次确认才能播放
+ + +
+
+ @@ -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) })