1338 lines
33 KiB
Vue
1338 lines
33 KiB
Vue
<script setup>
|
||
import { ref, onMounted, onUnmounted, computed, watch, nextTick } from 'vue'
|
||
import { useSceneStore } from '../store/scene'
|
||
import { useCollectionStore } from '../store/collection'
|
||
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'
|
||
|
||
const sceneStore = useSceneStore()
|
||
const collectionStore = useCollectionStore()
|
||
|
||
// 当前活动场景索引
|
||
const activeSceneIndex = ref(0)
|
||
// 滑动容器引用
|
||
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)
|
||
// 抽奖表单数据
|
||
const lotteryForm = ref({
|
||
name: '',
|
||
phone: '',
|
||
address: ''
|
||
})
|
||
|
||
// 推荐关键词
|
||
const recommendedKeywords = ref(['吉祥', '如意', '平安', '健康', '幸福', '快乐', '富贵', '安康'])
|
||
|
||
// 场景数据数组
|
||
// 顺序:结束页 → 东直门 → 隆福寺 → 王府井 → 崇文门 → 前门 → 首页
|
||
const scenes = ref([
|
||
{
|
||
id: 'end',
|
||
name: '结束页',
|
||
description: '恭喜完成全部场景游览'
|
||
},
|
||
{
|
||
id: 'dongzhimen',
|
||
name: '东直门商圈',
|
||
description: '京城东部的交通枢纽和商业中心',
|
||
image: 'https://picsum.photos/id/1019/750/1600',
|
||
thumbnail: 'https://picsum.photos/id/1019/200/100',
|
||
interactionTip: '移动烤鸭,领取团圆福筷',
|
||
collectedItem: '团圆福筷'
|
||
},
|
||
{
|
||
id: 'longfusi',
|
||
name: '隆福寺商圈',
|
||
description: '传统文化与现代艺术融合的潮流地标',
|
||
image: 'https://picsum.photos/id/1018/750/1600',
|
||
thumbnail: 'https://picsum.photos/id/1018/200/100',
|
||
interactionTip: '点击文创物品,点亮文化福灯',
|
||
collectedItem: '文化福灯'
|
||
},
|
||
{
|
||
id: 'wangfujing',
|
||
name: '王府井商圈',
|
||
description: '北京最繁华的商业中心之一',
|
||
image: 'https://picsum.photos/id/1018/750/1600',
|
||
thumbnail: 'https://picsum.photos/id/1018/200/100',
|
||
interactionTip: '双指放大,收集金袋福卡',
|
||
collectedItem: '金袋福卡'
|
||
},
|
||
{
|
||
id: 'chongwen',
|
||
name: '崇文门商圈',
|
||
description: '融合传统文化与现代商业的活力区域',
|
||
image: 'https://picsum.photos/id/1016/750/1600',
|
||
thumbnail: 'https://picsum.photos/id/1016/200/100',
|
||
interactionTip: '滑动探索商圈,收集国潮福字',
|
||
collectedItem: '国潮福字'
|
||
},
|
||
{
|
||
id: 'qianmen',
|
||
name: '前门商圈',
|
||
description: '北京最具历史文化底蕴的商圈之一',
|
||
image: 'https://picsum.photos/id/1015/750/1600',
|
||
thumbnail: 'https://picsum.photos/id/1015/200/100',
|
||
interactionTip: '点击京韵大鼓,收集非遗福印',
|
||
collectedItem: '非遗福印'
|
||
},
|
||
{
|
||
id: 'home',
|
||
name: '首页',
|
||
description: '马年新春2026'
|
||
}
|
||
])
|
||
|
||
// 交互状态
|
||
const sceneInteractiveStates = ref(scenes.value.map(() => false))
|
||
// 福印收集状态
|
||
const sealCollectedStates = ref(scenes.value.map(() => 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)
|
||
})
|
||
|
||
// 组件挂载后初始化
|
||
onMounted(() => {
|
||
// 检查并初始化场景交互状态
|
||
scenes.value.forEach((scene, index) => {
|
||
if (index > 0 && index < scenes.value.length) {
|
||
const isCollected = collectionStore.isSceneSealCollected(scene.id)
|
||
sealCollectedStates.value[index] = isCollected
|
||
}
|
||
})
|
||
|
||
// 添加滚动事件监听 - 使用uniapp的scroll-view事件绑定
|
||
// scroll-view 组件通过 @scroll 事件监听滚动,无需手动添加事件监听器
|
||
|
||
|
||
// 页面加载时滚动到最底部(首页)
|
||
const scrollToHomePage = () => {
|
||
if (!scrollContainer.value) {
|
||
console.log('scrollContainer 尚未初始化')
|
||
return
|
||
}
|
||
|
||
// 保存当前容器引用,避免异步回调中丢失
|
||
const container = scrollContainer.value
|
||
|
||
console.log('scrollContainer 已初始化:', {
|
||
element: container,
|
||
scrollHeight: container.scrollHeight,
|
||
clientHeight: container.clientHeight,
|
||
scrollTop: container.scrollTop,
|
||
classList: container.classList
|
||
})
|
||
|
||
// 如果scrollHeight为0,说明内容还未渲染
|
||
if (container.scrollHeight === 0) {
|
||
console.log('内容尚未渲染,延迟重试...')
|
||
setTimeout(scrollToHomePage, 100)
|
||
return
|
||
}
|
||
|
||
// 首页是最后一个场景,索引为 scenes.value.length - 1
|
||
// 应该位于页面最底部
|
||
const homeIndex = scenes.value.length - 1
|
||
|
||
// 方法1: 使用 scroll-into-view(uniapp 原生支持)
|
||
console.log('使用 scroll-into-view 滚动到首页')
|
||
homeSectionId.value = 'home-section'
|
||
|
||
// 设置活动场景为首页
|
||
activeSceneIndex.value = homeIndex
|
||
|
||
// 设置活动场景为首页
|
||
activeSceneIndex.value = homeIndex
|
||
|
||
// 验证滚动是否成功,如果不成功,延迟后重试
|
||
setTimeout(() => {
|
||
if (!container) {
|
||
console.log('scrollContainer 已卸载')
|
||
return
|
||
}
|
||
|
||
console.log('验证滚动结果:', {
|
||
currentScrollTop: container.scrollTop,
|
||
scrollHeight: container.scrollHeight,
|
||
clientHeight: container.clientHeight,
|
||
isAtBottom: Math.abs(container.scrollTop - (container.scrollHeight - container.clientHeight)) < 50
|
||
})
|
||
|
||
// 检查是否已经滚动到底部
|
||
const isAtBottom = Math.abs(container.scrollTop - (container.scrollHeight - container.clientHeight)) < 50
|
||
|
||
if (!isAtBottom) {
|
||
console.log('滚动验证失败,使用备用方法重试...')
|
||
// 方法2: 使用 scrollTo 或 scrollTop
|
||
try {
|
||
const targetScrollTop = container.scrollHeight - container.clientHeight
|
||
if (typeof container.scrollTo === 'function') {
|
||
container.scrollTo({
|
||
top: targetScrollTop,
|
||
duration: 0
|
||
})
|
||
} else {
|
||
container.scrollTop = targetScrollTop
|
||
}
|
||
} catch (error) {
|
||
console.log('备用方法失败:', error)
|
||
container.scrollTop = container.scrollHeight - container.clientHeight
|
||
}
|
||
|
||
// 再次验证后显示容器
|
||
setTimeout(() => {
|
||
// 标记页面准备就绪,隐藏加载提示并显示页面
|
||
isPageReady.value = true
|
||
console.log('设置 isPageReady = true,准备显示页面')
|
||
}, 50)
|
||
} else {
|
||
console.log('成功滚动到首页')
|
||
// 滚动成功后显示容器
|
||
// 标记页面准备就绪,隐藏加载提示并显示页面
|
||
isPageReady.value = true
|
||
console.log('设置 isPageReady = true,准备显示页面')
|
||
}
|
||
}, 300)
|
||
}
|
||
|
||
// 初始滚动到首页 - 延迟更长时间确保内容渲染
|
||
setTimeout(scrollToHomePage, 800)
|
||
|
||
// 额外的确保措施,等待更长时间后再次尝试
|
||
setTimeout(scrollToHomePage, 1500)
|
||
})
|
||
|
||
// 处理滚动事件(从下往上滑动)
|
||
const handleScroll = (event) => {
|
||
if (isScrolling.value) return
|
||
|
||
// 在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)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 滚动到指定场景(从下往上滑动逻辑)
|
||
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 handleSceneInteraction = (index) => {
|
||
if (sceneInteractiveStates.value[index]) return
|
||
|
||
sceneInteractiveStates.value[index] = true
|
||
|
||
// 收集福印
|
||
setTimeout(() => {
|
||
collectSeal(index)
|
||
}, 500)
|
||
}
|
||
|
||
// 收集福印
|
||
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 submitLotteryForm = () => {
|
||
// 验证表单
|
||
if (!lotteryForm.value.name || !lotteryForm.value.phone || !lotteryForm.value.address) {
|
||
uni.showToast({
|
||
title: '请填写完整信息',
|
||
icon: 'none',
|
||
duration: 2000
|
||
})
|
||
return
|
||
}
|
||
|
||
// 验证手机号
|
||
const phoneRegex = /^1[3-9]\d{9}$/
|
||
if (!phoneRegex.test(lotteryForm.value.phone)) {
|
||
uni.showToast({
|
||
title: '请输入正确的手机号',
|
||
icon: 'none',
|
||
duration: 2000
|
||
})
|
||
return
|
||
}
|
||
|
||
// 模拟提交
|
||
uni.showLoading({
|
||
title: '提交中...',
|
||
mask: true
|
||
})
|
||
|
||
setTimeout(() => {
|
||
uni.hideLoading()
|
||
uni.showToast({
|
||
title: '报名成功!我们将在活动结束后通知您抽奖结果',
|
||
duration: 2000
|
||
})
|
||
showLotteryForm.value = false
|
||
// 重置表单
|
||
lotteryForm.value = {
|
||
name: '',
|
||
phone: '',
|
||
address: ''
|
||
}
|
||
}, 1500)
|
||
}
|
||
|
||
// 打开AI春联生成弹窗
|
||
const openAICoupletForm = () => {
|
||
showAICoupletForm.value = true
|
||
}
|
||
|
||
// 选择推荐关键词
|
||
const selectRecommendedKeyword = (word) => {
|
||
coupletKeyword.value = word
|
||
}
|
||
|
||
// 生成春联
|
||
const generateCouplet = () => {
|
||
if (!coupletKeyword.value || coupletKeyword.value.length < 2) {
|
||
uni.showToast({
|
||
title: '请输入两个字的关键词',
|
||
icon: 'none',
|
||
duration: 2000
|
||
})
|
||
return
|
||
}
|
||
|
||
uni.showLoading({
|
||
title: '生成中...',
|
||
mask: true
|
||
})
|
||
|
||
// 模拟AI生成春联
|
||
setTimeout(() => {
|
||
uni.hideLoading()
|
||
|
||
// 生成春联(这里使用预设模板)
|
||
const coupletTemplates = [
|
||
{
|
||
top: `${coupletKeyword.value.charAt(0)}气盈门添福寿`,
|
||
bottom: `${coupletKeyword.value.charAt(1)}光满院报平安`,
|
||
横批: '新春大吉'
|
||
},
|
||
{
|
||
top: `${coupletKeyword.value.charAt(0)}祥如意年年好`,
|
||
bottom: `${coupletKeyword.value.charAt(1)}事顺心步步高`,
|
||
横批: '万事如意'
|
||
},
|
||
{
|
||
top: `${coupletKeyword.value.charAt(0)}星高照家昌盛`,
|
||
bottom: `${coupletKeyword.value.charAt(1)}运亨通福满堂`,
|
||
横批: '吉星高照'
|
||
}
|
||
]
|
||
|
||
generatedCouplet.value = coupletTemplates[Math.floor(Math.random() * coupletTemplates.length)]
|
||
showCoupletDisplay.value = true
|
||
showAICoupletForm.value = false
|
||
}, 1500)
|
||
}
|
||
|
||
// 返回顶部
|
||
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'))
|
||
}
|
||
|
||
// 使用scroll-view组件后,不再需要手动处理触摸事件
|
||
// scroll-view会自动处理触摸滚动
|
||
|
||
// 组件卸载前清理资源
|
||
onUnmounted(() => {
|
||
// 无需手动移除事件监听器,uniapp会自动处理
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div class="single-page-wrapper">
|
||
<!-- 加载提示 -->
|
||
<div class="loading-hint" v-if="!isPageReady">
|
||
<div class="loading-spinner"></div>
|
||
<p>正在加载...</p>
|
||
</div>
|
||
|
||
<scroll-view
|
||
class="single-page-container"
|
||
:class="{ 'visible': isPageReady }"
|
||
ref="scrollContainer"
|
||
scroll-y="true"
|
||
scroll-with-animation="true"
|
||
enhanced="true"
|
||
refresher-enabled="false"
|
||
show-scrollbar="false"
|
||
@scroll="handleScroll"
|
||
:scroll-into-view="homeSectionId"
|
||
>
|
||
<!-- 结束页 -->
|
||
<section class="scene-section end-section" :class="{ 'active': activeSceneIndex === 0 }">
|
||
<div class="end-page-content">
|
||
<h1 class="end-title"><EFBFBD><EFBFBD> {{ scenes[0].description }} <EFBFBD><EFBFBD></h1>
|
||
<div class="seal-collection-section">
|
||
<h2 class="section-title">福印收集成果</h2>
|
||
<div class="collection-progress">
|
||
<div class="progress-bar">
|
||
<div class="progress-fill" :style="{ width: `${collectionProgress}%` }"></div>
|
||
</div>
|
||
<p class="progress-text">已收集 {{ collectedItems.length }} / {{ scenes.length - 1 }} 个福印</p>
|
||
</div>
|
||
</div>
|
||
<div class="end-buttons">
|
||
<button class="function-btn lottery-btn" @click="openLotteryForm"><EFBFBD><EFBFBD> 参与抽奖</button>
|
||
<button class="function-btn couplet-btn" @click="openAICoupletForm">✍️ AI春联</button>
|
||
</div>
|
||
<div class="restart-btn-container">
|
||
<button class="restart-btn" @click="scrollToTop"><EFBFBD><EFBFBD> 重新游览</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 东直门商圈 -->
|
||
<DongzhimenScene
|
||
:active="activeSceneIndex === 1"
|
||
:scroll-position="scrollContainer && scrollContainer.value ? scrollContainer.value.scrollTop : 0"
|
||
@collect-seal="collectSeal(1)"
|
||
@play-drum="handleSceneInteraction(1)"
|
||
/>
|
||
|
||
<!-- 隆福寺商圈 -->
|
||
<LongfusiScene
|
||
:active="activeSceneIndex === 2"
|
||
:scroll-position="scrollContainer && scrollContainer.value ? scrollContainer.value.scrollTop : 0"
|
||
@collect-seal="collectSeal(2)"
|
||
@play-drum="handleSceneInteraction(2)"
|
||
/>
|
||
|
||
<!-- 王府井商圈 -->
|
||
<WangfujingScene
|
||
:active="activeSceneIndex === 3"
|
||
:scroll-position="scrollContainer?.value?.scrollTop || 0"
|
||
@collect-seal="collectSeal(3)"
|
||
@play-drum="handleSceneInteraction(3)"
|
||
/>
|
||
|
||
<!-- 崇文门商圈 -->
|
||
<ChongwenScene
|
||
:active="activeSceneIndex === 4"
|
||
:scroll-position="scrollContainer?.value?.scrollTop || 0"
|
||
@collect-seal="collectSeal(4)"
|
||
@play-drum="handleSceneInteraction(4)"
|
||
/>
|
||
|
||
<!-- 前门商圈 -->
|
||
<QianmenScene
|
||
:active="activeSceneIndex === 5"
|
||
:scroll-position="scrollContainer?.value?.scrollTop || 0"
|
||
@collect-seal="collectSeal(5)"
|
||
@play-drum="handleSceneInteraction(5)"
|
||
@height-changed="handleQianmenHeightChanged"
|
||
/>
|
||
|
||
<!-- 首页 -->
|
||
<section id="home-section" class="scene-section home-section" :class="{ 'active': activeSceneIndex === 6 }">
|
||
<!-- 背景图片层 -->
|
||
<div class="home-bg">
|
||
<img
|
||
src="/static/bg/bg1.jpg"
|
||
alt="2026新春东城商圈"
|
||
class="bg-image"
|
||
@error="handleImageError"
|
||
/>
|
||
</div>
|
||
<!-- 内容层 -->
|
||
<div class="scene-content">
|
||
<h1 class="title">{{ scenes[6].description }}</h1>
|
||
<p class="subtitle">探索东城五大商圈</p>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 向上滑动提示 -->
|
||
<!-- <div class="scroll-tip-bottom">
|
||
<div class="tip-icon"><EFBFBD><EFBFBD></div>
|
||
<p>向上滑动探索商圈</p>
|
||
</div> -->
|
||
|
||
<!-- 抽奖留资弹窗 -->
|
||
<div class="form-modal" v-if="showLotteryForm">
|
||
<div class="modal-overlay" @click="showLotteryForm = false"></div>
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h3>参与抽奖</h3>
|
||
<button class="close-btn" @click="showLotteryForm = false">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<p class="modal-description">请填写以下信息,参与线下抽奖活动</p>
|
||
<div class="form-group">
|
||
<label>姓名</label>
|
||
<input v-model="lotteryForm.name" type="text" placeholder="请输入您的姓名" class="form-input" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>联系电话</label>
|
||
<input v-model="lotteryForm.phone" type="tel" placeholder="请输入您的手机号" class="form-input" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>邮寄地址</label>
|
||
<textarea v-model="lotteryForm.address" placeholder="请输入您的邮寄地址" class="form-textarea" rows="3"></textarea>
|
||
</div>
|
||
<div class="form-actions">
|
||
<button class="submit-btn" @click="submitLotteryForm">提交报名</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- AI春联生成弹窗 -->
|
||
<div class="form-modal" v-if="showAICoupletForm">
|
||
<div class="modal-overlay" @click="showAICoupletForm = false"></div>
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h3>AI春联生成</h3>
|
||
<button class="close-btn" @click="showAICoupletForm = false">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<p class="modal-description">输入两个字的关键词,生成个性化春联</p>
|
||
<div class="form-group">
|
||
<input v-model="coupletKeyword" type="text" placeholder="请输入关键词(如:吉祥、如意)" maxlength="2" class="keyword-input" />
|
||
</div>
|
||
<div class="recommended-keywords">
|
||
<span v-for="word in recommendedKeywords" :key="word" @click="selectRecommendedKeyword(word)" class="keyword-tag">{{ word }}</span>
|
||
</div>
|
||
<div class="form-actions">
|
||
<button class="submit-btn" :disabled="!coupletKeyword || coupletKeyword.length < 2" @click="generateCouplet">生成春联</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 春联展示页面 -->
|
||
<div class="couplet-display-modal" v-if="showCoupletDisplay">
|
||
<div class="modal-overlay" @click="showCoupletDisplay = false"></div>
|
||
<div class="modal-content couplet-content">
|
||
<div class="modal-header">
|
||
<h3>您的专属春联</h3>
|
||
<button class="close-btn" @click="showCoupletDisplay = false">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="couplet-display">
|
||
<div class="couplet-item top-couplet">{{ generatedCouplet.top }}</div>
|
||
<div class="couplet-item bottom-couplet">{{ generatedCouplet.bottom }}</div>
|
||
<div class="couplet-item横批">{{ generatedCouplet.横批 }}</div>
|
||
</div>
|
||
<div class="collected-items-section" v-if="collectedItems.length > 0">
|
||
<h4 class="items-title">您收集的福印:</h4>
|
||
<div class="collected-items">
|
||
<span v-for="(item, index) in collectedItems" :key="index" class="item-tag"><3E><> {{ item }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="couplet-actions">
|
||
<button class="share-btn" @click="uni.showToast({title: '分享功能开发中...', icon: 'none', duration: 2000})"><3E><> 分享春联</button>
|
||
<button class="close-couplet-btn" @click="showCoupletDisplay = false">✅ 完成</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</scroll-view>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.single-page-wrapper {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 100vh;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* 加载提示样式 */
|
||
.loading-hint {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
background: rgba(253, 233, 223, 0.95);
|
||
padding: 30px 40px;
|
||
border-radius: 15px;
|
||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.loading-spinner {
|
||
width: 40px;
|
||
height: 40px;
|
||
border: 3px solid #f3f3f3;
|
||
border-top: 3px solid #FF6B35;
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.loading-hint p {
|
||
color: #FF6B35;
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
|
||
.single-page-container {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 100%;
|
||
overflow-y: auto;
|
||
overflow-x: hidden;
|
||
-webkit-overflow-scrolling: touch; /* 启用iOS平滑滚动 */
|
||
scrollbar-width: thin; /* Firefox滚动条样式 */
|
||
/* 使用 visibility 而不是 opacity,确保完全不可见且不可交互 */
|
||
visibility: hidden;
|
||
touch-action: pan-y; /* 允许垂直方向的触摸滚动 */
|
||
user-select: none; /* 防止触摸时文本被选中 */
|
||
}
|
||
|
||
.single-page-container.visible {
|
||
visibility: visible;
|
||
/* 添加淡入效果 */
|
||
animation: fadeIn 0.4s ease-in-out;
|
||
}
|
||
|
||
@keyframes fadeIn {
|
||
from { opacity: 0; }
|
||
to { opacity: 1; }
|
||
}
|
||
|
||
/* 滚动条样式 */
|
||
.single-page-container::-webkit-scrollbar {
|
||
width: 6px;
|
||
}
|
||
|
||
.single-page-container::-webkit-scrollbar-track {
|
||
background: rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.single-page-container::-webkit-scrollbar-thumb {
|
||
background: rgba(0, 0, 0, 0.3);
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.single-page-container::-webkit-scrollbar-thumb:hover {
|
||
background: rgba(0, 0, 0, 0.5);
|
||
}
|
||
|
||
/* 场景部分样式 */
|
||
.scene-section {
|
||
position: relative;
|
||
width: 100%;
|
||
height: var(--scene-height, 100vh);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background-color: #f5f5f5;
|
||
}
|
||
|
||
.scene-section.active {
|
||
/* 当前活动场景样式 */
|
||
}
|
||
|
||
.scene-content {
|
||
width: 100%;
|
||
max-width: 640px;
|
||
padding: 20px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.scene-title {
|
||
font-size: 28px;
|
||
color: #333;
|
||
margin-bottom: 10px;
|
||
text-align: center;
|
||
}
|
||
|
||
.scene-description {
|
||
font-size: 16px;
|
||
color: #666;
|
||
margin-bottom: 20px;
|
||
text-align: center;
|
||
}
|
||
|
||
.long-image-container {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 50vh;
|
||
margin-bottom: 20px;
|
||
background-color: #000;
|
||
border-radius: 10px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.interaction-tip {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background-color: rgba(0, 0, 0, 0.7);
|
||
color: #fff;
|
||
padding: 15px 25px;
|
||
border-radius: 10px;
|
||
z-index: 10;
|
||
animation: pulse 1.5s infinite;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0% {
|
||
transform: translate(-50%, -50%) scale(1);
|
||
}
|
||
50% {
|
||
transform: translate(-50%, -50%) scale(1.1);
|
||
}
|
||
100% {
|
||
transform: translate(-50%, -50%) scale(1);
|
||
}
|
||
}
|
||
|
||
.tip-icon {
|
||
font-size: 36px;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.seal-collect-tip {
|
||
position: absolute;
|
||
top: 20px;
|
||
right: 20px;
|
||
display: flex;
|
||
align-items: center;
|
||
background-color: rgba(255, 107, 53, 0.9);
|
||
color: #fff;
|
||
padding: 8px 15px;
|
||
border-radius: 20px;
|
||
font-size: 14px;
|
||
z-index: 10;
|
||
}
|
||
|
||
.seal-icon {
|
||
font-size: 24px;
|
||
margin-right: 5px;
|
||
}
|
||
|
||
.media-section {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
/* 结束页样式 */
|
||
.end-section {
|
||
background-color: #FDE9DF;
|
||
height: var(--scene-height, 100vh);
|
||
}
|
||
|
||
.end-page-content {
|
||
width: 100%;
|
||
max-width: 640px;
|
||
padding: 20px;
|
||
box-sizing: border-box;
|
||
text-align: center;
|
||
}
|
||
|
||
.end-title {
|
||
font-size: 32px;
|
||
color: #FF6B35;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.seal-collection-section {
|
||
margin-bottom: 40px;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 24px;
|
||
color: #333;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.collection-progress {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.progress-bar {
|
||
width: 100%;
|
||
height: 10px;
|
||
background-color: #ddd;
|
||
border-radius: 5px;
|
||
overflow: hidden;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.progress-fill {
|
||
height: 100%;
|
||
background-color: #FF6B35;
|
||
transition: width 0.5s ease;
|
||
}
|
||
|
||
.progress-text {
|
||
font-size: 16px;
|
||
color: #666;
|
||
margin: 0;
|
||
}
|
||
|
||
.end-buttons {
|
||
display: flex;
|
||
gap: 20px;
|
||
justify-content: center;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.function-btn {
|
||
padding: 15px 30px;
|
||
font-size: 18px;
|
||
border: none;
|
||
border-radius: 25px;
|
||
cursor: pointer;
|
||
font-weight: bold;
|
||
transition: all 0.3s ease;
|
||
min-width: 150px;
|
||
}
|
||
|
||
.lottery-btn {
|
||
background-color: #FFD700;
|
||
color: #333;
|
||
}
|
||
|
||
.lottery-btn:hover {
|
||
background-color: #FFC107;
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.couplet-btn {
|
||
background-color: #4CAF50;
|
||
color: white;
|
||
}
|
||
|
||
.couplet-btn:hover {
|
||
background-color: #45a049;
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.restart-btn-container {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.restart-btn {
|
||
padding: 10px 20px;
|
||
font-size: 16px;
|
||
background-color: #999;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 20px;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.restart-btn:hover {
|
||
background-color: #777;
|
||
}
|
||
|
||
/* 弹窗样式 */
|
||
.form-modal,
|
||
.couplet-display-modal {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
z-index: 1000;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.modal-overlay {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background-color: rgba(0, 0, 0, 0.7);
|
||
}
|
||
|
||
.modal-content {
|
||
position: relative;
|
||
background-color: white;
|
||
border-radius: 15px;
|
||
padding: 20px;
|
||
width: 90%;
|
||
max-width: 500px;
|
||
max-height: 80vh;
|
||
overflow-y: auto;
|
||
z-index: 1001;
|
||
}
|
||
|
||
.close-btn {
|
||
position: absolute;
|
||
top: 10px;
|
||
right: 15px;
|
||
background: none;
|
||
border: none;
|
||
font-size: 24px;
|
||
cursor: pointer;
|
||
color: #666;
|
||
}
|
||
|
||
.modal-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
border-bottom: 1px solid #eee;
|
||
padding-bottom: 10px;
|
||
}
|
||
|
||
.modal-header h3 {
|
||
margin: 0;
|
||
font-size: 20px;
|
||
color: #333;
|
||
}
|
||
|
||
.modal-body {
|
||
padding-bottom: 10px;
|
||
}
|
||
|
||
.modal-description {
|
||
font-size: 16px;
|
||
color: #666;
|
||
margin-bottom: 20px;
|
||
text-align: center;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.form-input,
|
||
.keyword-input,
|
||
.form-textarea {
|
||
width: 100%;
|
||
padding: 12px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 8px;
|
||
font-size: 16px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.form-textarea {
|
||
resize: vertical;
|
||
min-height: 80px;
|
||
}
|
||
|
||
.recommended-keywords {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
margin-bottom: 20px;
|
||
justify-content: center;
|
||
}
|
||
|
||
.keyword-tag {
|
||
padding: 8px 15px;
|
||
background-color: #f0f0f0;
|
||
border-radius: 20px;
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.keyword-tag:hover {
|
||
background-color: #e0e0e0;
|
||
}
|
||
|
||
.form-actions {
|
||
text-align: center;
|
||
}
|
||
|
||
.submit-btn {
|
||
padding: 12px 30px;
|
||
font-size: 16px;
|
||
background-color: #FF6B35;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 25px;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.submit-btn:hover:not(:disabled) {
|
||
background-color: #E65A2B;
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.submit-btn:disabled {
|
||
background-color: #ccc;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.modal-video {
|
||
width: 100%;
|
||
max-height: 70vh;
|
||
border-radius: 10px;
|
||
}
|
||
|
||
/* 春联展示样式 */
|
||
.couplet-content {
|
||
max-width: 600px;
|
||
}
|
||
|
||
.couplet-display {
|
||
text-align: center;
|
||
margin-bottom: 30px;
|
||
padding: 20px;
|
||
background-color: #f9f9f9;
|
||
border-radius: 15px;
|
||
}
|
||
|
||
.couplet-item {
|
||
font-family: 'SimSun', 'STSong', serif;
|
||
font-size: 24px;
|
||
line-height: 1.8;
|
||
margin: 10px 0;
|
||
}
|
||
|
||
.top-couplet {
|
||
transform: rotate(90deg);
|
||
transform-origin: right center;
|
||
display: inline-block;
|
||
margin-right: 40px;
|
||
}
|
||
|
||
.bottom-couplet {
|
||
transform: rotate(-90deg);
|
||
transform-origin: left center;
|
||
display: inline-block;
|
||
margin-left: 40px;
|
||
}
|
||
|
||
.横批 {
|
||
font-size: 28px;
|
||
font-weight: bold;
|
||
color: #FF6B35;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.collected-items-section {
|
||
margin-top: 20px;
|
||
padding-top: 20px;
|
||
border-top: 1px solid #eee;
|
||
}
|
||
|
||
.items-title {
|
||
font-size: 16px;
|
||
color: #333;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.collected-items {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
justify-content: center;
|
||
}
|
||
|
||
.item-tag {
|
||
padding: 8px 15px;
|
||
background-color: #fff3e0;
|
||
border: 1px solid #ffb74d;
|
||
border-radius: 20px;
|
||
font-size: 14px;
|
||
color: #e65100;
|
||
}
|
||
|
||
.couplet-actions {
|
||
display: flex;
|
||
gap: 15px;
|
||
justify-content: center;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.share-btn {
|
||
padding: 10px 20px;
|
||
background-color: #2196F3;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 20px;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.share-btn:hover {
|
||
background-color: #1976D2;
|
||
}
|
||
|
||
.close-couplet-btn {
|
||
padding: 10px 20px;
|
||
background-color: #9E9E9E;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 20px;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.close-couplet-btn:hover {
|
||
background-color: #757575;
|
||
}
|
||
|
||
/* 首页样式 */
|
||
.home-section {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
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;
|
||
}
|
||
|
||
.home-section .scene-content {
|
||
position: relative;
|
||
z-index: 2;
|
||
text-align: center;
|
||
padding: 20px;
|
||
background: rgba(255, 255, 255, 0.85);
|
||
border-radius: 15px;
|
||
backdrop-filter: blur(5px);
|
||
max-width: 90%;
|
||
}
|
||
|
||
/* 向上滑动提示样式 */
|
||
.scroll-tip-bottom {
|
||
position: fixed;
|
||
bottom: 50px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
animation: bounce 1.5s infinite;
|
||
z-index: 100;
|
||
}
|
||
|
||
@keyframes bounce {
|
||
0%, 20%, 50%, 80%, 100% {
|
||
transform: translateX(-50%) translateY(0);
|
||
}
|
||
40% {
|
||
transform: translateX(-50%) translateY(-10px);
|
||
}
|
||
60% {
|
||
transform: translateX(-50%) translateY(-5px);
|
||
}
|
||
}
|
||
|
||
.home-section .title {
|
||
font-size: 36px;
|
||
color: #FF6B35;
|
||
margin-bottom: 15px;
|
||
text-align: center;
|
||
}
|
||
|
||
.home-section .subtitle {
|
||
font-size: 18px;
|
||
color: #666;
|
||
text-align: center;
|
||
}
|
||
|
||
.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);
|
||
}
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@media (max-width: 480px) {
|
||
.scene-title {
|
||
font-size: 24px;
|
||
}
|
||
|
||
.scene-description {
|
||
font-size: 14px;
|
||
}
|
||
|
||
.end-title {
|
||
font-size: 28px;
|
||
}
|
||
|
||
.end-buttons {
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
|
||
.function-btn {
|
||
width: 200px;
|
||
}
|
||
|
||
.couplet-item {
|
||
font-size: 20px;
|
||
}
|
||
|
||
.top-couplet, .bottom-couplet {
|
||
margin-right: 20px;
|
||
margin-left: 20px;
|
||
}
|
||
}
|
||
</style> |