qs_xinchun2026_h5/components/SinglePageContainer.vue

1160 lines
31 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup>
import { ref, onMounted, onUnmounted, computed, watch, nextTick } from 'vue'
import { useSceneStore } from '../store/scene'
import { useCollectionStore } from '../store/collection'
import { recordPageVisit, saveUserInfo, generateCoupletPoster } from '../api/api.js'
import LongImageViewer from './LongImageViewer.vue'
import MediaPlayer from './MediaPlayer.vue'
import QianmenScene from './QianmenScene.vue'
import DongzhimenScene from './DongzhimenScene.vue'
import LongfusiScene from './LongfusiScene.vue'
import WangfujingScene from './WangfujingScene.vue'
import ChongwenScene from './ChongwenScene.vue'
import EndPage from './EndPage.vue'
import LotteryFormModal from './LotteryFormModal.vue'
import AICoupletForm from './AICoupletForm.vue'
import CoupletDisplay from './CoupletDisplay.vue'
import LoadingComponent from './LoadingComponent.vue'
const sceneStore = useSceneStore()
const collectionStore = useCollectionStore()
// 当前活动场景索引
const activeSceneIndex = ref(0)
// 标题图片是否已显示过(用于保持显示状态)
const titleImageShown = ref(false)
// 滑动容器引用
const scrollContainer = ref(null)
// 是否在滚动中
const isScrolling = ref(false)
// 首页 section ID用于 scroll-into-view
const homeSectionId = ref('')
// 页面是否准备就绪(滚动完成)
const isPageReady = ref(false)
// 前门板块高度
const qianmenSceneHeight = ref(0)
// 前门板块引用
const qianmenSceneRef = ref(null)
// 是否显示抽奖留资弹窗
const showLotteryForm = ref(false)
// 是否显示AI春联生成弹窗
const showAICoupletForm = ref(false)
// 是否显示春联展示页面
const showCoupletDisplay = ref(false)
// AI春联关键词
const coupletKeyword = ref('')
// 生成的春联
const generatedCouplet = ref(null)
// 页面访问UUID
const pageVisitUuid = ref('')
// 是否已提交用户信息
const hasSubmittedUserInfo = ref(false)
// 是否已手动滑动(用于隐藏滑动提示)
const hasScrolled = ref(false)
// 标记是否正在自动滚动
const isAutoScrolling = ref(false)
// 是否显示加载组件
const showLoading = ref(true)
// 是否已完成图片加载
const isImagesLoaded = ref(false)
// 音乐播放状态
const isMusicPlaying = ref(false)
// 音乐播放器实例
const audioPlayer = ref(null)
// 场景数据数组
// 顺序:结束页 → 东直门 → 隆福寺 → 王府井 → 崇文门 → 前门 → 首页
const scenes = ref([
{
id: 'end',
name: '结束页',
description: '恭喜完成全部场景游览'
},
{
id: 'dongzhimen',
name: '东直门商圈',
description: '京城东部的交通枢纽和商业中心',
collectedItem: '团圆福筷',
videoUrl: 'http://192.168.2.149:8090/sample-3.mp4'
},
{
id: 'longfusi',
name: '隆福寺商圈',
description: '传统文化与现代艺术融合的潮流地标',
collectedItem: '文化福灯',
videoUrl: 'http://192.168.2.149:8090/sample-3.mp4'
},
{
id: 'wangfujing',
name: '王府井商圈',
description: '北京最繁华的商业中心之一',
collectedItem: '金袋福卡',
videoUrl: 'http://192.168.2.149:8090/sample-3.mp4'
},
{
id: 'chongwen',
name: '崇文门商圈',
description: '融合传统文化与现代商业的活力区域',
collectedItem: '国潮福字',
videoUrl: 'http://192.168.2.149:8090/sample-3.mp4'
},
{
id: 'qianmen',
name: '前门商圈',
description: '北京最具历史文化底蕴的商圈之一',
collectedItem: '非遗福印',
videoUrl: 'http://192.168.2.149:8090/sample-3.mp4'
},
{
id: 'home',
name: '首页',
description: '马年新春2026'
}
])
// 交互状态
const sceneInteractiveStates = ref(scenes.value.map(() => false))
// 福印收集状态
const sealCollectedStates = ref(scenes.value.map(() => false))
// 视频播放状态
const isVideoPlaying = ref(false)
// 鼓声播放状态
const isDrumPlaying = ref(false)
// 计算当前收集的物品
const collectedItems = computed(() => {
const items = []
scenes.value.forEach((scene, index) => {
if (index > 0 && index < scenes.value.length - 1) {
if (sealCollectedStates.value[index]) {
items.push(scene.collectedItem)
}
}
})
return items
})
// 计算收集进度
const collectionProgress = computed(() => {
const total = scenes.value.length - 1 // 排除结束页
const collected = sealCollectedStates.value.filter((state, index) =>
index > 0 && index < scenes.value.length && state
).length
return Math.round((collected / total) * 100)
})
// 计算是否禁用BGM控制按钮
const isBgmButtonDisabled = computed(() => {
return isVideoPlaying.value || isDrumPlaying.value
})
// 计算福印收集对象(传递给 EndPage
const collectedSeals = computed(() => {
return {
qianmen: sealCollectedStates.value[5] || false, // 前门 (索引 5)
chongwen: sealCollectedStates.value[4] || false, // 崇文门 (索引 4)
wangfujing: sealCollectedStates.value[3] || false, // 王府井 (索引 3)
longfusi: sealCollectedStates.value[2] || false, // 隆福寺 (索引 2)
dongzhimen: sealCollectedStates.value[1] || false // 东直门 (索引 1)
}
})
// 处理加载完成事件
const handleLoadingComplete = () => {
isImagesLoaded.value = true
console.log('图片加载完成,准备初始化页面')
// 确保 scrollContainer 已初始化并滚动到最下方
ensureScrollContainerReady()
}
// 确保 scrollContainer 已初始化并滚动到最下方
const ensureScrollContainerReady = () => {
// 等待 DOM 渲染完成
nextTick(() => {
const checkScrollContainer = () => {
if (!scrollContainer.value) {
console.log('scrollContainer 尚未初始化,延迟重试...')
setTimeout(checkScrollContainer, 100)
return
}
const container = scrollContainer.value
console.log('scrollContainer 已初始化:', {
scrollHeight: container.scrollHeight,
clientHeight: container.clientHeight
})
// 如果 scrollHeight 为 0说明内容还未渲染
if (container.scrollHeight === 0) {
console.log('内容尚未渲染,延迟重试...')
setTimeout(checkScrollContainer, 100)
return
}
// 标记正在自动滚动
isAutoScrolling.value = true
// 设置首页 section ID使用 scroll-into-view 滚动到首页
homeSectionId.value = 'home-section'
console.log('使用 scroll-into-view 滚动到首页')
// 设置活动场景为首页
activeSceneIndex.value = scenes.value.length - 1
// 等待 scroll-into-view 生效
setTimeout(() => {
// 再次检查并滚动到最下方,确保滚动位置正确
try {
const targetScrollTop = container.scrollHeight - container.clientHeight
if (typeof container.scrollTo === 'function') {
container.scrollTo({
top: targetScrollTop,
duration: 0
})
} else {
container.scrollTop = targetScrollTop
}
console.log('已滚动到最下方scrollTop:', container.scrollTop)
} catch (error) {
console.error('滚动失败:', error)
}
// 自动滚动结束
isAutoScrolling.value = false
// 强制将 hasScrolled 设置为 false确保滑动提示能够显示
hasScrolled.value = false
console.log('scrollContainer 准备就绪hasScrolled:', hasScrolled.value)
}, 300)
}
// 开始检查 scrollContainer
checkScrollContainer()
})
}
// 处理开始按钮点击事件
const handleStart = () => {
// 隐藏加载组件
showLoading.value = false
// 确保滑动提示能够显示
hasScrolled.value = false
console.log('点击开始按钮hasScrolled:', hasScrolled.value)
// 检查并播放背景音乐
if (!isMusicPlaying.value) {
console.log('背景音乐未播放,开始播放')
toggleMusic()
}
// 初始化页面
initPage()
}
// 初始化页面函数
const initPage = () => {
// 记录页面访问
recordPageVisit({
user_agent: navigator.userAgent,
page_name: 'home'
}).then(res => {
if (res && res.uuid) {
pageVisitUuid.value = res.uuid
console.log('页面访问UUID:', res.uuid)
}
// 处理后端返回的场景数据只更新5个商圈的videoUrl
if (res && res.scenes && Array.isArray(res.scenes)) {
console.log('后端返回的场景数据:', res.scenes)
// 商业区ID列表
const commercialDistricts = ['dongzhimen', 'longfusi', 'wangfujing', 'chongwen', 'qianmen']
// 创建场景ID到数据的映射
const apiScenesMap = {}
res.scenes.forEach(scene => {
apiScenesMap[scene.id] = scene
})
// 只更新商业区的videoUrl
scenes.value.forEach((scene) => {
if (commercialDistricts.includes(scene.id) && apiScenesMap[scene.id]) {
const apiScene = apiScenesMap[scene.id]
// 后端返回的字段名是 video_url
const videoUrl = apiScene.video_url || apiScene.videoUrl
if (videoUrl) {
console.log(`更新场景 ${scene.id} 的videoUrl:`, videoUrl)
scene.videoUrl = videoUrl
}
}
})
console.log('场景数据更新完成:', scenes.value)
}
}).catch(err => {
console.log('页面访问记录失败:', err)
})
// 检查并初始化场景交互状态
scenes.value.forEach((scene, index) => {
if (index > 0 && index < scenes.value.length) {
const isCollected = collectionStore.isSceneSealCollected(scene.id)
sealCollectedStates.value[index] = isCollected
}
})
// 标记页面准备就绪
titleImageShown.value = true
isAutoScrolling.value = false
console.log('页面准备就绪')
}
// 初始化音乐播放器
const initMusicPlayer = () => {
try {
// 创建音频播放器实例
audioPlayer.value = uni.createInnerAudioContext()
const bgmUrl = new URL('/static/music/bgm.mp3', import.meta.url)
audioPlayer.value.src = bgmUrl.href
audioPlayer.value.loop = true
// 播放完成事件
audioPlayer.value.onEnded(() => {
audioPlayer.value.seek(0)
audioPlayer.value.play()
})
// 播放错误事件
audioPlayer.value.onError((err) => {
console.error('音乐播放错误:', err)
})
console.log('音乐播放器初始化完成')
} catch (error) {
console.error('初始化音乐播放器失败:', error)
}
}
// 播放/暂停音乐
const toggleMusic = () => {
// 如果按钮被禁用,不处理点击
if (isBgmButtonDisabled.value) return
if (!audioPlayer.value) {
initMusicPlayer()
}
if (isMusicPlaying.value) {
// 暂停音乐
audioPlayer.value.pause()
isMusicPlaying.value = false
console.log('音乐已暂停')
} else {
// 播放音乐
try {
const result = audioPlayer.value.play()
// 检查play()方法是否返回PromiseWeb平台
if (result && typeof result.then === 'function') {
// Web平台使用Promise处理
result.then(() => {
isMusicPlaying.value = true
console.log('音乐已开始播放')
}).catch(error => {
console.error('音乐播放失败(需要用户交互):', error)
// 不更新isMusicPlaying状态保持为false
})
} else {
// 其他平台:同步处理
isMusicPlaying.value = true
console.log('音乐已开始播放')
}
} catch (error) {
console.error('音乐播放失败(需要用户交互):', error)
// 不更新isMusicPlaying状态保持为false
}
}
}
// 组件挂载后初始化
onMounted(() => {
console.log('SinglePageContainer 组件挂载')
// 初始化音乐播放器
initMusicPlayer()
// 移除自动播放音乐的代码,避免触发浏览器自动播放限制
// 音乐将在用户点击开始按钮或音乐控制按钮时播放
})
// 处理滚动事件(从下往上滑动)
const handleScroll = (event) => {
if (isScrolling.value) return
// 只有在非自动滚动状态下,才标记为已手动滑动
if (!isAutoScrolling.value && !hasScrolled.value && event.detail.scrollTop > 50) {
hasScrolled.value = true
}
// 在uniapp中scroll-view的scroll事件传递的是event.detail对象
const scrollTop = event.detail.scrollTop
const viewportHeight = window.innerHeight
const totalHeight = scrollContainer.value.scrollHeight
const totalScenes = scenes.value.length
// 计算当前活动场景(从下往上滚动)
// 首页在底部索引6向上滑动依次看到前门(5)、崇文门(4)、王府井(3)、隆福寺(2)、东直门(1)、结束页(0)
const distanceFromBottom = totalHeight - scrollTop - viewportHeight
const sceneIndexFromBottom = Math.round(distanceFromBottom / viewportHeight)
// 将索引转换为正确的场景顺序
// sceneIndexFromBottom: 0=首页, 1=前门, 2=崇文门, 3=王府井, 4=隆福寺, 5=东直门, 6=结束页
const sceneIndex = totalScenes - 1 - sceneIndexFromBottom
// 确保索引在有效范围内
const clampedIndex = Math.max(0, Math.min(sceneIndex, totalScenes - 1))
if (clampedIndex !== activeSceneIndex.value) {
activeSceneIndex.value = clampedIndex
// 激活当前场景
if (activeSceneIndex.value > 0 && activeSceneIndex.value < scenes.value.length - 1) {
sceneStore.activateScene(scenes.value[activeSceneIndex.value].id)
}
// 如果滚动到首页,标记标题图片已显示
if (clampedIndex === 6) {
titleImageShown.value = true
}
}
}
// 滚动到指定场景(从下往上滑动逻辑)
const scrollToScene = (index) => {
if (!scrollContainer.value || isScrolling.value) return
isScrolling.value = true
const viewportHeight = window.innerHeight
const totalHeight = scrollContainer.value.scrollHeight
const totalScenes = scenes.value.length
// 计算目标滚动位置(从下往上滚动)
// index=0: 结束页(最顶部)
// index=6: 首页(最底部)
const targetScrollTop = totalHeight - (totalScenes - index) * viewportHeight
scrollContainer.value.scrollTo({
top: targetScrollTop,
duration: 1000
})
setTimeout(() => {
isScrolling.value = false
}, 1000)
}
// 收集福印
const collectSeal = (index) => {
if (index < 1 || index >= scenes.value.length - 1) return
const scene = scenes.value[index]
const success = collectionStore.collectSealBySceneId(scene.id)
if (success) {
sealCollectedStates.value[index] = true
uni.showToast({
title: `恭喜获得${scene.name}${scene.collectedItem}`,
duration: 2000
})
}
}
// 打开抽奖留资弹窗
const openLotteryForm = () => {
showLotteryForm.value = true
}
// 关闭抽奖留资弹窗
const closeLotteryForm = () => {
showLotteryForm.value = false
}
// 提交抽奖表单
const submitLotteryForm = async (formData) => {
// 检查是否已经提交过
if (hasSubmittedUserInfo.value) {
uni.showToast({
title: '您已经提交过信息了,请勿重复提交',
icon: 'none',
duration: 2000
})
return
}
// 检查是否有UUID
if (!pageVisitUuid.value) {
uni.showToast({
title: '系统错误,请刷新页面重试',
icon: 'none',
duration: 2000
})
return
}
uni.showLoading({
title: '提交中...',
mask: true
})
try {
// 调用用户信息保存接口
await saveUserInfo({
name: formData.name,
phone: formData.phone,
address: formData.address,
msg: formData.msg,
page_visit_uuid: pageVisitUuid.value
})
// 标记为已提交
hasSubmittedUserInfo.value = true
uni.hideLoading()
uni.showToast({
title: '提交成功!我们将在活动结束后通知您抽奖结果',
duration: 2000
})
showLotteryForm.value = false
} catch (error) {
uni.hideLoading()
console.error('提交用户信息失败:', error)
// 直接展示后端返回的错误信息
let errorMessage = '提交失败,请稍后重试'
// 检查是否有后端返回的错误信息
if (error.data && error.data.error) {
errorMessage = error.data.error
} else if (error.response && error.response.data && error.response.data.error) {
errorMessage = error.response.data.error
} else if (error.message) {
// 如果是网络错误或其他错误,显示通用提示
errorMessage = '提交失败,请稍后重试'
}
uni.showToast({
title: errorMessage,
icon: 'none',
duration: 2000
})
}
}
// 打开AI春联生成弹窗
const openAICoupletForm = () => {
showAICoupletForm.value = true
}
// 处理生成春联
const handleGenerateCouplet = async (keyword) => {
uni.showLoading({
title: '生成中...',
mask: true
})
try {
// 调用后端 API 生成春联海报
const response = await generateCoupletPoster({
title: keyword,
page_visit_uuid: pageVisitUuid.value
})
uni.hideLoading()
// 构建春联数据 - 使用 API 返回的参数
generatedCouplet.value = {
share_url: response.share_url,
image_url: response.image_url
}
showCoupletDisplay.value = true
showAICoupletForm.value = false
} catch (error) {
uni.hideLoading()
// 显示错误信息
uni.showToast({
title: error.message || '生成春联失败,请稍后重试',
icon: 'none',
duration: 2000
})
}
}
// 处理分享春联
const handleShareCouplet = () => {
// 分享逻辑可以在这里扩展
console.log('分享春联:', generatedCouplet.value)
}
// 返回顶部
const scrollToTop = () => {
scrollToScene(0)
}
// 处理图片加载错误
const handleImageError = (event) => {
// event.target.src = 'https://placeholder.pics/svg/640x1136/FDE9DF/FF6B35/2026新春东城商圈'
}
// 处理前门板块高度变化
const handleQianmenHeightChanged = (height) => {
qianmenSceneHeight.value = height
// 这里可以添加逻辑,将所有其他场景的高度设置为与前门板块相同的高度
// 可以通过CSS变量或直接修改DOM元素的高度
document.documentElement.style.setProperty('--scene-height', `${height}px`)
console.log('前门板块高度变化:', height, 'px')
console.log('当前CSS变量:', getComputedStyle(document.documentElement).getPropertyValue('--scene-height'))
}
// 跟踪视频打开前的BGM状态
const wasMusicPlayingBeforeVideo = ref(false)
// 跟踪鼓声开始前的BGM状态
const wasMusicPlayingBeforeDrum = ref(false)
// 处理视频打开事件
const handleVideoOpen = () => {
// 标记视频正在播放
isVideoPlaying.value = true
// 保存原始BGM状态
wasMusicPlayingBeforeVideo.value = isMusicPlaying.value
// 停止全局BGM播放
if (audioPlayer.value && isMusicPlaying.value) {
audioPlayer.value.pause()
isMusicPlaying.value = false
console.log('全局BGM已停止视频播放中')
}
}
// 处理视频关闭事件
const handleVideoClose = () => {
// 标记视频已停止播放
isVideoPlaying.value = false
// 恢复全局BGM到原始状态
if (audioPlayer.value && wasMusicPlayingBeforeVideo.value && !isMusicPlaying.value) {
try {
const result = audioPlayer.value.play()
// 检查play()方法是否返回PromiseWeb平台
if (result && typeof result.then === 'function') {
// Web平台使用Promise处理
result.then(() => {
isMusicPlaying.value = true
console.log('全局BGM已恢复播放视频已关闭')
}).catch(error => {
console.error('BGM恢复播放失败需要用户交互:', error)
// 不更新isMusicPlaying状态保持为false
})
} else {
// 其他平台:同步处理
isMusicPlaying.value = true
console.log('全局BGM已恢复播放视频已关闭')
}
} catch (error) {
console.error('BGM恢复播放失败需要用户交互:', error)
// 不更新isMusicPlaying状态保持为false
}
}
}
// 暂停BGM用于鼓声播放
const pauseBgm = () => {
// 标记鼓声正在播放
isDrumPlaying.value = true
// 保存原始BGM状态
wasMusicPlayingBeforeDrum.value = isMusicPlaying.value
if (audioPlayer.value && isMusicPlaying.value) {
audioPlayer.value.pause()
isMusicPlaying.value = false
console.log('全局BGM已暂停鼓声播放中')
}
}
// 恢复BGM用于鼓声结束
const resumeBgm = () => {
// 标记鼓声已停止播放
isDrumPlaying.value = false
// 恢复BGM到原始状态
if (audioPlayer.value && wasMusicPlayingBeforeDrum.value && !isMusicPlaying.value) {
try {
const result = audioPlayer.value.play()
// 检查play()方法是否返回PromiseWeb平台
if (result && typeof result.then === 'function') {
// Web平台使用Promise处理
result.then(() => {
isMusicPlaying.value = true
console.log('全局BGM已恢复播放鼓声已结束')
}).catch(error => {
console.error('BGM恢复播放失败需要用户交互:', error)
// 不更新isMusicPlaying状态保持为false
})
} else {
// 其他平台:同步处理
isMusicPlaying.value = true
console.log('全局BGM已恢复播放鼓声已结束')
}
} catch (error) {
console.error('BGM恢复播放失败需要用户交互:', error)
// 不更新isMusicPlaying状态保持为false
}
}
}
// 使用scroll-view组件后不再需要手动处理触摸事件
// scroll-view会自动处理触摸滚动
// 组件卸载前清理资源
onUnmounted(() => {
// 无需手动移除事件监听器uniapp会自动处理
})
</script>
<template>
<div class="single-page-wrapper">
<!-- 音乐控制按钮 -->
<div
class="music-control"
:class="{ 'music-playing': isMusicPlaying, 'disabled': isBgmButtonDisabled }"
@click="toggleMusic"
>
<img src="/static/images/music_on.png" alt="音乐" class="music-icon" />
</div>
<!-- 加载组件 -->
<LoadingComponent
v-if="showLoading"
@loaded="handleLoadingComplete"
@start="handleStart"
/>
<scroll-view
class="single-page-container"
ref="scrollContainer"
scroll-y="true"
scroll-with-animation="true"
enhanced="true"
:refresher-enabled="false"
:refresher-triggered="false"
show-scrollbar="false"
:bounce="false"
@scroll="handleScroll"
:scroll-into-view="homeSectionId"
>
<!-- 结束页 -->
<EndPage
:is-active="activeSceneIndex === 0"
:title="scenes[0].description"
:collection-progress="collectionProgress"
:collected-count="collectedItems.length"
:total-count="scenes.length - 1"
:collected-seals="collectedSeals"
:has-submitted="hasSubmittedUserInfo"
:generated-couplet="generatedCouplet"
@lottery="openLotteryForm"
@couplet="openAICoupletForm"
@show-couplet="showCoupletDisplay = true"
@restart="scrollToTop"
/>
<!-- 东直门商圈 -->
<DongzhimenScene
:active="activeSceneIndex === 1"
:scroll-position="scrollContainer && scrollContainer.value ? scrollContainer.value.scrollTop : 0"
:video-url="scenes[1].videoUrl"
@collect-seal="collectSeal(1)"
@video-open="handleVideoOpen"
@video-close="handleVideoClose"
/>
<!-- 隆福寺商圈 -->
<LongfusiScene
:active="activeSceneIndex === 2"
:scroll-position="scrollContainer && scrollContainer.value ? scrollContainer.value.scrollTop : 0"
:video-url="scenes[2].videoUrl"
@collect-seal="collectSeal(2)"
@video-open="handleVideoOpen"
@video-close="handleVideoClose"
/>
<!-- 王府井商圈 -->
<WangfujingScene
:active="activeSceneIndex === 3"
:scroll-position="scrollContainer?.value?.scrollTop || 0"
:video-url="scenes[3].videoUrl"
@collect-seal="collectSeal(3)"
@video-open="handleVideoOpen"
@video-close="handleVideoClose"
/>
<!-- 崇文门商圈 -->
<ChongwenScene
:active="activeSceneIndex === 4"
:scroll-position="scrollContainer?.value?.scrollTop || 0"
:video-url="scenes[4].videoUrl"
@collect-seal="collectSeal(4)"
@video-open="handleVideoOpen"
@video-close="handleVideoClose"
/>
<!-- 前门商圈 -->
<QianmenScene
:active="activeSceneIndex === 5"
:scroll-position="scrollContainer?.value?.scrollTop || 0"
:video-url="scenes[5].videoUrl"
:is-music-playing="isMusicPlaying"
@collect-seal="collectSeal(5)"
@height-changed="handleQianmenHeightChanged"
@video-open="handleVideoOpen"
@video-close="handleVideoClose"
@pause-bgm="pauseBgm"
@resume-bgm="resumeBgm"
/>
<!-- 新年文案模块 -->
<section class="new-year-section">
<div class="new-year-content">
<img
src="/static/images/main_text.png"
alt="新年文案"
class="new-year-text"
/>
</div>
</section>
<!-- 首页 -->
<section id="home-section" class="scene-section home-section" :class="{ 'active': activeSceneIndex === 6 }">
<!-- 背景图片层 -->
<div class="home-bg">
<img
src="/static/bg/bg_main.jpg"
alt="2026新春东城商圈"
class="bg-image"
/>
</div>
<!-- 标题图片 -->
<img
src="/static/images/img_title.png"
alt="马年新春2026"
class="title-image"
:class="{ 'title-image-active': titleImageShown }"
/>
</section>
<!-- 向上滑动提示 -->
<div class="scroll-tip-bottom" v-if="!hasScrolled">
<img src="/static/images/icon_hand1.png" class="tip-icon" alt="滑动提示" />
<p>向下滑动探索商圈</p>
</div>
<!-- 抽奖留资弹窗 -->
<LotteryFormModal
:visible="showLotteryForm"
:has-submitted="hasSubmittedUserInfo"
@close="closeLotteryForm"
@submit="submitLotteryForm"
/>
<!-- AI春联生成弹窗 -->
<AICoupletForm
:visible="showAICoupletForm"
@close="showAICoupletForm = false"
@generate="handleGenerateCouplet"
/>
<!-- 春联展示页面 -->
<CoupletDisplay
:visible="showCoupletDisplay"
:couplet="generatedCouplet"
@close="showCoupletDisplay = false"
@share="handleShareCouplet"
/>
</scroll-view>
</div>
</template>
<style scoped>
.single-page-wrapper {
position: relative;
width: 100%;
height: 100vh;
overflow: hidden;
}
.single-page-container {
position: relative;
width: 100%;
height: 100%;
overflow-y: scroll; /* 改为 scroll 确保可以滚动 */
overflow-x: hidden;
-webkit-overflow-scrolling: touch; /* 启用iOS平滑滚动 */
touch-action: pan-y; /* 允许垂直方向的触摸滚动 */
user-select: none; /* 防止触摸时文本被选中 */
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* ========== 隐藏滚动条样式(所有浏览器)========== */
/* WebKit 浏览器Chrome、Safari、新版 Edge*/
.single-page-container::-webkit-scrollbar {
width: 0 !important;
height: 0 !important;
display: none !important;
}
.single-page-container::-webkit-scrollbar-track {
background: transparent !important;
}
.single-page-container::-webkit-scrollbar-thumb {
background: transparent !important;
}
.single-page-container::-webkit-scrollbar-corner {
background: transparent !important;
}
/* Firefox */
.single-page-container {
scrollbar-width: none !important;
}
/* IE 和旧版 Edge */
.single-page-container {
-ms-overflow-style: none !important;
}
/* 通用样式 */
.single-page-container {
scrollbar-color: transparent transparent !important;
}
/* 场景部分样式 */
.scene-section {
position: relative;
width: 100%;
height: var(--scene-height, 100vh);
display: flex;
align-items: center;
justify-content: center;
background-color: #f5f5f5;
}
/* 首页样式 */
.home-section {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
height: 100vh;
overflow: hidden;
}
.home-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
.bg-image {
width: 100%;
height: 100%;
object-fit: cover; /* 裁切方式适配不同高度 */
object-position: center center;
}
/* 向上滑动提示样式 */
.scroll-tip-bottom {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
animation: bounce 1.5s infinite;
z-index: 100;
color: white;
}
.tip-icon {
width: 64rpx;
height: 78rpx;
}
@keyframes bounce {
0%, 20%, 50%, 80%, 100% {
transform: translateX(-50%) translateY(0);
}
40% {
transform: translateX(-50%) translateY(-10px);
}
60% {
transform: translateX(-50%) translateY(-5px);
}
}
/* 新年文案模块样式 */
.new-year-section {
width: 100%;
background-color: #f94332;
display: flex;
align-items: center;
justify-content: center;
position: relative;
padding: 15px 0;
}
.new-year-content {
text-align: center;
}
.new-year-text {
display: inline-block;
width: 516rpx;
height: auto;
object-fit: contain;
}
/* 标题图片样式 */
.title-image {
width: 80%;
max-width: 400px;
height: auto;
margin: 40rpx auto 20px;
display: block;
transform: scale(0);
transition: transform 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275);
position: relative;
z-index: 2;
}
.title-image-active {
transform: scale(1);
}
.scroll-tip {
position: absolute;
bottom: 50px;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
animation: bounce 1.5s infinite;
}
@keyframes bounce {
0%, 20%, 50%, 80%, 100% {
transform: translateX(-50%) translateY(0);
}
40% {
transform: translateX(-50%) translateY(-20px);
}
60% {
transform: translateX(-50%) translateY(-10px);
}
}
/* 音乐控制按钮样式 */
.music-control {
position: fixed;
top: 20px;
right: 20px;
width: 50px;
height: 50px;
border-radius: 50%;
/* background-color: rgba(0, 0, 0, 0.3); */
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
cursor: pointer;
transition: all 0.3s ease;
}
.music-control:hover:not(.disabled) {
transform: scale(1.1);
}
.music-control:active:not(.disabled) {
transform: scale(0.95);
}
.music-control.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.music-icon {
width: 49rpx;
height: 49rpx;
transition: transform 0.3s ease;
}
/* 音乐播放时的转动动画 */
.music-playing .music-icon {
animation: rotate 2s linear infinite;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* 响应式设计 */
@media (max-width: 480px) {
.scene-title {
font-size: 24px;
}
.scene-description {
font-size: 14px;
}
.couplet-item {
font-size: 20px;
}
.top-couplet, .bottom-couplet {
margin-right: 20px;
margin-left: 20px;
}
.music-control {
width: 40px;
height: 40px;
top: 10px;
right: 10px;
}
.music-icon {
width: 24px;
height: 24px;
}
}
</style>