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 @@
整页模式会带来更有仪式感的逐屏切换效果。
-
+
整页/分屏模式下自动翻页的动画时长,默认 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)
})