1、移除无效页面
2、修改全景链接地址
This commit is contained in:
Wenzhe 2026-02-01 11:22:55 +08:00
parent b2ccf76fd0
commit 1f42e5d713
8 changed files with 1 additions and 3080 deletions

View File

@ -1,405 +0,0 @@
<template>
<div class="spring-couple-container">
<!-- 背景图 -->
<div class="background">
<img
v-lazy="backgroundImage"
alt="四合院背景"
@error="handleImageError"
/>
</div>
<!-- 春联展示区 -->
<div class="couplet-display" v-if="couplet">
<div class="couplet-item top-couplet">{{ couplet.top }}</div>
<div class="couplet-item bottom-couplet">{{ couplet.bottom }}</div>
<div class="couplet-item横批">{{ couplet.横批 }}</div>
</div>
<!-- 输入区域 -->
<div class="input-section">
<h2 class="title">AI春联生成</h2>
<p class="subtitle">输入两个字的关键词生成个性化春联</p>
<div class="input-container">
<input
v-model="keyword"
type="text"
placeholder="请输入关键词(如:吉祥、如意)"
maxlength="2"
@input="handleKeywordInput"
class="keyword-input"
/>
<button
@click="generateCouple"
:disabled="loading || keyword.length < 2"
class="generate-btn"
>
<span v-if="loading">生成中...</span>
<span v-else>生成春联</span>
</button>
</div>
<!-- 推荐关键词 -->
<div class="recommended-keywords">
<span
v-for="word in recommendedWords"
:key="word"
@click="selectKeyword(word)"
class="keyword-tag"
>
{{ word }}
</span>
</div>
</div>
<!-- 分享按钮 -->
<div class="share-section" v-if="couplet">
<button @click="shareCouple" class="share-btn">
📤 分享春联
</button>
</div>
<!-- 加载动画 -->
<div v-if="loading" class="loading-overlay">
<uni-spinner type="circle" size="48" color="#fff"></uni-spinner>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue';
import { generateSpringCouple } from '../api/ai';
import { share } from '../utils/share';
//
const keyword = ref('');
const couplet = ref(null);
const loading = ref(false);
const backgroundImage = ref('https://placeholder.pics/svg/640x1136/E8F5E9/4CAF50/四合院背景');
//
const recommendedWords = [
'吉祥', '如意', '幸福', '安康', '财源', '富贵', '团圆', '快乐',
'平安', '健康', '顺利', '成功', '美满', '和谐', '兴旺', '发达'
];
//
const handleKeywordInput = () => {
// 2
if (keyword.value.length > 2) {
keyword.value = keyword.value.slice(0, 2);
}
};
//
const selectKeyword = (word) => {
keyword.value = word;
generateCouple();
};
//
const generateCouple = async () => {
if (!keyword.value || keyword.value.length < 2) {
uni.uni.showToast({title: '请输入两个字的关键词', duration: 2000});
return;
}
loading.value = true;
try {
const result = await generateSpringCouple(keyword.value);
if (result.code === 200) {
couplet.value = result.data;
uni.uni.showToast({title: '春联生成成功', duration: 2000});
//
setTimeout(() => {
const coupletElements = document.querySelectorAll('.couplet-item');
coupletElements.forEach((el, index) => {
el.style.opacity = '0';
el.style.transform = 'translateY(20px)';
setTimeout(() => {
el.style.transition = 'opacity 0.5s ease, transform 0.5s ease';
el.style.opacity = '1';
el.style.transform = 'translateY(0)';
}, index * 200);
});
}, 100);
} else {
uni.uni.showToast({title: '生成失败,请重试', duration: 2000});
}
} catch (error) {
console.error('生成春联失败:', error);
uni.uni.showToast({title: '网络异常,请稍后重试', duration: 2000});
} finally {
loading.value = false;
}
};
//
const shareCouple = () => {
if (!couplet.value) {
uni.uni.showToast({title: '请先生成春联', duration: 2000});
return;
}
const shareText = `
我用AI生成了一副春联
${couplet.value.top}
${couplet.value.bottom}
${couplet.value.横批}
#2026新春东城商圈#
`;
//
share({
title: 'AI春联生成',
text: shareText,
image: backgroundImage.value
});
};
//
const handleImageError = (event) => {
event.target.src = 'https://placeholder.pics/svg/640x1136/E8F5E9/4CAF50/四合院背景';
};
</script>
<style scoped>
.spring-couple-container {
position: relative;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
overflow: hidden;
}
.background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
overflow: hidden;
}
.background img {
width: 100%;
height: 100%;
object-fit: cover;
}
.couplet-display {
position: relative;
width: 100%;
max-width: 600px;
margin: 40px 0;
padding: 60px 20px;
display: flex;
justify-content: space-between;
align-items: center;
z-index: 1;
}
.couplet-item {
font-family: 'STKaiti', 'Kaiti SC', serif;
font-size: 24px;
font-weight: bold;
color: #e67e22;
text-align: center;
line-height: 1.8;
writing-mode: vertical-rl;
text-orientation: upright;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
opacity: 0;
}
.top-couplet,
.bottom-couplet {
width: 60px;
height: 240px;
display: flex;
align-items: center;
justify-content: center;
}
.横批 {
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
writing-mode: horizontal-tb;
font-size: 32px;
width: 200px;
}
.input-section {
background: rgba(255, 255, 255, 0.9);
border-radius: 20px;
padding: 30px;
width: 100%;
max-width: 500px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
z-index: 2;
margin-top: auto;
}
.title {
font-size: 28px;
color: #e67e22;
margin-bottom: 10px;
text-align: center;
font-weight: bold;
}
.subtitle {
font-size: 16px;
color: #666;
margin-bottom: 25px;
text-align: center;
}
.input-container {
display: flex;
gap: 10px;
margin-bottom: 25px;
}
.keyword-input {
flex: 1;
padding: 15px;
border: 2px solid #ddd;
border-radius: 10px;
font-size: 18px;
text-align: center;
transition: border-color 0.3s ease;
}
.keyword-input:focus {
outline: none;
border-color: #e67e22;
}
.generate-btn {
padding: 15px 25px;
background: linear-gradient(135deg, #e67e22, #d35400);
color: white;
border: none;
border-radius: 10px;
font-size: 18px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
}
.generate-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(230, 126, 34, 0.4);
}
.generate-btn:disabled {
background: #ddd;
cursor: not-allowed;
}
.recommended-keywords {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
}
.keyword-tag {
padding: 8px 16px;
background: #f39c12;
color: white;
border-radius: 20px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
}
.keyword-tag:hover {
background: #e67e22;
transform: translateY(-2px);
}
.share-section {
margin-top: 20px;
margin-bottom: 20px;
z-index: 2;
}
.share-btn {
padding: 15px 30px;
background: linear-gradient(135deg, #3498db, #2980b9);
color: white;
border: none;
border-radius: 25px;
font-size: 18px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
}
.share-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(52, 152, 219, 0.4);
}
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 999;
}
/* 响应式设计 */
@media (max-width: 480px) {
.spring-couple-container {
padding: 10px;
}
.couplet-item {
font-size: 20px;
}
.top-couplet,
.bottom-couplet {
height: 200px;
}
.input-section {
padding: 20px;
}
.title {
font-size: 24px;
}
.keyword-input,
.generate-btn {
font-size: 16px;
padding: 12px;
}
.input-container {
flex-direction: column;
}
}
</style>

View File

@ -1,437 +0,0 @@
<script setup>
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useSceneStore } from '../../src/store/scene'
import { useCollectionStore } from '../../src/store/collection'
import LongImageViewer from '../components/LongImageViewer.vue'
import MediaPlayer from '../components/MediaPlayer.vue'
const router = useRouter()
const sceneStore = useSceneStore()
const collectionStore = useCollectionStore()
//
const sceneData = ref({
id: 'chongwen',
name: '崇文商圈',
description: '融合传统文化与现代商业的活力区域',
image: 'https://picsum.photos/id/1016/750/1600', //
thumbnail: 'https://picsum.photos/id/1016/200/100',
location: '北京市东城区崇文门外大街',
history: '崇文商圈历史悠久,曾是明清时期的文化中心,如今成为现代化的商业区域。',
attractions: [
{ name: '北京新世界百货', description: '大型综合性购物中心' },
{ name: '红桥市场', description: '著名的珍珠和工艺品市场' },
{ name: '天坛公园', description: '世界文化遗产,明清皇帝祭天的场所' }
],
audio: 'https://example.com/audio/chongwen.mp3', //
video: 'https://example.com/video/chongwen.mp4' //
})
//
const isInteractive = ref(false)
const sealCollected = ref(false)
const showInfoPanel = ref(false)
//
const sealStatus = computed(() => {
return collectionStore.isSceneSealCollected(sceneData.value.id)
})
//
onMounted(() => {
//
sceneStore.activateScene(sceneData.value.id)
//
const sceneIndex = sceneStore.scenes.findIndex(scene => scene.id === sceneData.value.id)
if (sceneIndex !== -1) {
sceneStore.setCurrentScene(sceneIndex)
}
//
sealCollected.value = sealStatus.value
})
//
const handleSceneInteraction = () => {
if (!isInteractive.value) {
isInteractive.value = true
//
setTimeout(() => {
collectSeal()
}, 1000)
}
}
//
const collectSeal = () => {
if (!sealCollected.value) {
const success = collectionStore.collectSealBySceneId(sceneData.value.id)
if (success) {
sealCollected.value = true
showToast({
message: '恭喜获得崇文门福印!',
icon: 'success',
duration: 2000
})
} else {
showFailToast({
message: '福印收集失败',
duration: 2000
})
}
} else {
showToast({
message: '福印已收集',
duration: 1500
})
}
}
//
const showSceneInfo = () => {
showInfoPanel.value = true
}
//
const closeInfoPanel = () => {
showInfoPanel.value = false
}
//
const goToNextScene = () => {
router.push('/wangfujing')
}
//
const goToPreviousScene = () => {
router.push('/qianmen')
}
</script>
<template>
<div class="scene-page">
<!-- 场景标题 -->
<div class="scene-header">
<h1 class="scene-title">{{ sceneData.name }}</h1>
<p class="scene-description">{{ sceneData.description }}</p>
</div>
<!-- 长图查看器 -->
<div class="long-image-container">
<LongImageViewer
:image-url="sceneData.image"
:description="sceneData.name"
@click="handleSceneInteraction"
/>
</div>
<!-- 交互提示 -->
<div class="interaction-tip" v-if="!isInteractive">
<div class="tip-icon">👆</div>
<p>点击图片查看详情</p>
</div>
<!-- 福印收集提示 -->
<div class="seal-collect-tip" v-if="sealCollected">
<div class="seal-icon">🏮</div>
<p>已收集崇文福印</p>
</div>
<!-- 场景信息面板 -->
<div class="info-panel" v-if="showInfoPanel">
<div class="info-panel-content">
<div class="info-panel-header">
<h2>场景信息</h2>
<button class="close-btn" @click="closeInfoPanel">×</button>
</div>
<div class="info-panel-body">
<div class="info-item">
<label>名称</label>
<span>{{ sceneData.name }}</span>
</div>
<div class="info-item">
<label>位置</label>
<span>{{ sceneData.location }}</span>
</div>
<div class="info-item">
<label>历史</label>
<span>{{ sceneData.history }}</span>
</div>
<div class="attractions-section">
<h3>主要景点</h3>
<ul class="attractions-list">
<li v-for="(attraction, index) in sceneData.attractions" :key="index">
<div class="attraction-name">{{ attraction.name }}</div>
<div class="attraction-description">{{ attraction.description }}</div>
</li>
</ul>
</div>
</div>
</div>
</div>
<!-- 媒体播放器 -->
<div class="media-player-container">
<MediaPlayer
:audio-url="sceneData.audio"
:video-url="sceneData.video"
:scene-name="sceneData.name"
/>
</div>
<!-- 场景导航 -->
<div class="scene-navigation">
<button class="nav-btn prev-btn" @click="goToPreviousScene">
上一个
</button>
<button class="nav-btn info-btn" @click="showSceneInfo">
场景信息
</button>
<button class="nav-btn next-btn" @click="goToNextScene">
下一个
</button>
</div>
</div>
</template>
<style scoped>
.scene-page {
width: 100%;
min-height: 100vh;
background-color: #f5f5f5;
padding-top: 0.88rem;
padding-bottom: 1.2rem;
}
.scene-header {
padding: 0.2rem;
text-align: center;
background-color: #fff;
margin-bottom: 0.1rem;
}
.scene-title {
font-size: 0.4rem;
font-weight: bold;
color: #333;
margin-bottom: 0.1rem;
}
.scene-description {
font-size: 0.28rem;
color: #666;
}
.long-image-container {
width: 100%;
height: 5rem;
overflow: hidden;
background-color: #000;
position: relative;
}
.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: 0.3rem 0.5rem;
border-radius: 0.2rem;
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: 0.6rem;
margin-bottom: 0.1rem;
}
.seal-collect-tip {
position: absolute;
top: 1rem;
right: 0.2rem;
display: flex;
align-items: center;
background-color: rgba(255, 107, 53, 0.9);
color: #fff;
padding: 0.15rem 0.3rem;
border-radius: 0.2rem;
font-size: 0.28rem;
z-index: 10;
}
.seal-icon {
font-size: 0.4rem;
margin-right: 0.1rem;
}
.info-panel {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.info-panel-content {
width: 90%;
max-width: 500px;
background-color: #fff;
border-radius: 0.2rem;
overflow: hidden;
max-height: 80vh;
overflow-y: auto;
}
.info-panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.3rem;
background-color: #ff6b35;
color: #fff;
}
.info-panel-header h2 {
font-size: 0.36rem;
margin: 0;
}
.close-btn {
background: none;
border: none;
color: #fff;
font-size: 0.4rem;
cursor: pointer;
}
.info-panel-body {
padding: 0.3rem;
}
.info-item {
margin-bottom: 0.2rem;
font-size: 0.28rem;
}
.info-item label {
font-weight: bold;
color: #333;
}
.info-item span {
color: #666;
}
.attractions-section {
margin-top: 0.3rem;
}
.attractions-section h3 {
font-size: 0.32rem;
margin-bottom: 0.2rem;
color: #333;
}
.attractions-list {
list-style: none;
padding: 0;
margin: 0;
}
.attractions-list li {
margin-bottom: 0.2rem;
padding: 0.2rem;
background-color: #f9f9f9;
border-radius: 0.1rem;
}
.attraction-name {
font-weight: bold;
color: #333;
margin-bottom: 0.05rem;
}
.attraction-description {
color: #666;
font-size: 0.26rem;
}
.media-player-container {
margin: 0.2rem;
background-color: #fff;
border-radius: 0.1rem;
padding: 0.2rem;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.scene-navigation {
display: flex;
justify-content: space-between;
padding: 0.2rem;
margin-top: 0.2rem;
}
.nav-btn {
flex: 1;
padding: 0.2rem;
margin: 0 0.1rem;
font-size: 0.3rem;
border: none;
border-radius: 0.15rem;
cursor: pointer;
transition: background-color 0.3s ease;
}
.prev-btn {
background-color: #2196f3;
color: #fff;
}
.prev-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.info-btn {
background-color: #4caf50;
color: #fff;
}
.next-btn {
background-color: #ff9800;
color: #fff;
}
.nav-btn:hover:not(:disabled) {
opacity: 0.8;
}
</style>

View File

@ -1,437 +0,0 @@
<script setup>
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useSceneStore } from '../../src/store/scene'
import { useCollectionStore } from '../../src/store/collection'
import LongImageViewer from '../components/LongImageViewer.vue'
import MediaPlayer from '../components/MediaPlayer.vue'
const router = useRouter()
const sceneStore = useSceneStore()
const collectionStore = useCollectionStore()
//
const sceneData = ref({
id: 'dongzhimen',
name: '东直门商圈',
description: '国际化的交通枢纽与商业中心',
image: 'https://picsum.photos/id/1019/750/1600', //
thumbnail: 'https://picsum.photos/id/1019/200/100',
location: '北京市东城区东直门内大街',
history: '东直门商圈是北京重要的交通枢纽,连接着多条地铁线路和长途汽车站,如今成为国际化的商业中心。',
attractions: [
{ name: '东直门交通枢纽', description: '大型综合交通枢纽' },
{ name: '东方银座', description: '高端商务中心' },
{ name: '簋街', description: '著名的美食街' }
],
audio: 'https://example.com/audio/dongzhimen.mp3', //
video: 'https://example.com/video/dongzhimen.mp4' //
})
//
const isInteractive = ref(false)
const sealCollected = ref(false)
const showInfoPanel = ref(false)
//
const sealStatus = computed(() => {
return collectionStore.isSceneSealCollected(sceneData.value.id)
})
//
onMounted(() => {
//
sceneStore.activateScene(sceneData.value.id)
//
const sceneIndex = sceneStore.scenes.findIndex(scene => scene.id === sceneData.value.id)
if (sceneIndex !== -1) {
sceneStore.setCurrentScene(sceneIndex)
}
//
sealCollected.value = sealStatus.value
})
//
const handleSceneInteraction = () => {
if (!isInteractive.value) {
isInteractive.value = true
//
setTimeout(() => {
collectSeal()
}, 1000)
}
}
//
const collectSeal = () => {
if (!sealCollected.value) {
const success = collectionStore.collectSealBySceneId(sceneData.value.id)
if (success) {
sealCollected.value = true
showToast({
message: '恭喜获得东直门福印!',
icon: 'success',
duration: 2000
})
} else {
showFailToast({
message: '福印收集失败',
duration: 2000
})
}
} else {
showToast({
message: '福印已收集',
duration: 1500
})
}
}
//
const showSceneInfo = () => {
showInfoPanel.value = true
}
//
const closeInfoPanel = () => {
showInfoPanel.value = false
}
//
const goToNextScene = () => {
router.push('/ai-spring')
}
//
const goToPreviousScene = () => {
router.push('/longfusi')
}
</script>
<template>
<div class="scene-page">
<!-- 场景标题 -->
<div class="scene-header">
<h1 class="scene-title">{{ sceneData.name }}</h1>
<p class="scene-description">{{ sceneData.description }}</p>
</div>
<!-- 长图查看器 -->
<div class="long-image-container">
<LongImageViewer
:image-url="sceneData.image"
:description="sceneData.name"
@click="handleSceneInteraction"
/>
</div>
<!-- 交互提示 -->
<div class="interaction-tip" v-if="!isInteractive">
<div class="tip-icon">👆</div>
<p>点击图片查看详情</p>
</div>
<!-- 福印收集提示 -->
<div class="seal-collect-tip" v-if="sealCollected">
<div class="seal-icon">🏮</div>
<p>已收集东直门福印</p>
</div>
<!-- 场景信息面板 -->
<div class="info-panel" v-if="showInfoPanel">
<div class="info-panel-content">
<div class="info-panel-header">
<h2>场景信息</h2>
<button class="close-btn" @click="closeInfoPanel">×</button>
</div>
<div class="info-panel-body">
<div class="info-item">
<label>名称</label>
<span>{{ sceneData.name }}</span>
</div>
<div class="info-item">
<label>位置</label>
<span>{{ sceneData.location }}</span>
</div>
<div class="info-item">
<label>历史</label>
<span>{{ sceneData.history }}</span>
</div>
<div class="attractions-section">
<h3>主要景点</h3>
<ul class="attractions-list">
<li v-for="(attraction, index) in sceneData.attractions" :key="index">
<div class="attraction-name">{{ attraction.name }}</div>
<div class="attraction-description">{{ attraction.description }}</div>
</li>
</ul>
</div>
</div>
</div>
</div>
<!-- 媒体播放器 -->
<div class="media-player-container">
<MediaPlayer
:audio-url="sceneData.audio"
:video-url="sceneData.video"
:scene-name="sceneData.name"
/>
</div>
<!-- 场景导航 -->
<div class="scene-navigation">
<button class="nav-btn prev-btn" @click="goToPreviousScene">
上一个
</button>
<button class="nav-btn info-btn" @click="showSceneInfo">
场景信息
</button>
<button class="nav-btn next-btn" @click="goToNextScene">
下一个
</button>
</div>
</div>
</template>
<style scoped>
.scene-page {
width: 100%;
min-height: 100vh;
background-color: #f5f5f5;
padding-top: 0.88rem;
padding-bottom: 1.2rem;
}
.scene-header {
padding: 0.2rem;
text-align: center;
background-color: #fff;
margin-bottom: 0.1rem;
}
.scene-title {
font-size: 0.4rem;
font-weight: bold;
color: #333;
margin-bottom: 0.1rem;
}
.scene-description {
font-size: 0.28rem;
color: #666;
}
.long-image-container {
width: 100%;
height: 5rem;
overflow: hidden;
background-color: #000;
position: relative;
}
.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: 0.3rem 0.5rem;
border-radius: 0.2rem;
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: 0.6rem;
margin-bottom: 0.1rem;
}
.seal-collect-tip {
position: absolute;
top: 1rem;
right: 0.2rem;
display: flex;
align-items: center;
background-color: rgba(255, 107, 53, 0.9);
color: #fff;
padding: 0.15rem 0.3rem;
border-radius: 0.2rem;
font-size: 0.28rem;
z-index: 10;
}
.seal-icon {
font-size: 0.4rem;
margin-right: 0.1rem;
}
.info-panel {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.info-panel-content {
width: 90%;
max-width: 500px;
background-color: #fff;
border-radius: 0.2rem;
overflow: hidden;
max-height: 80vh;
overflow-y: auto;
}
.info-panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.3rem;
background-color: #ff6b35;
color: #fff;
}
.info-panel-header h2 {
font-size: 0.36rem;
margin: 0;
}
.close-btn {
background: none;
border: none;
color: #fff;
font-size: 0.4rem;
cursor: pointer;
}
.info-panel-body {
padding: 0.3rem;
}
.info-item {
margin-bottom: 0.2rem;
font-size: 0.28rem;
}
.info-item label {
font-weight: bold;
color: #333;
}
.info-item span {
color: #666;
}
.attractions-section {
margin-top: 0.3rem;
}
.attractions-section h3 {
font-size: 0.32rem;
margin-bottom: 0.2rem;
color: #333;
}
.attractions-list {
list-style: none;
padding: 0;
margin: 0;
}
.attractions-list li {
margin-bottom: 0.2rem;
padding: 0.2rem;
background-color: #f9f9f9;
border-radius: 0.1rem;
}
.attraction-name {
font-weight: bold;
color: #333;
margin-bottom: 0.05rem;
}
.attraction-description {
color: #666;
font-size: 0.26rem;
}
.media-player-container {
margin: 0.2rem;
background-color: #fff;
border-radius: 0.1rem;
padding: 0.2rem;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.scene-navigation {
display: flex;
justify-content: space-between;
padding: 0.2rem;
margin-top: 0.2rem;
}
.nav-btn {
flex: 1;
padding: 0.2rem;
margin: 0 0.1rem;
font-size: 0.3rem;
border: none;
border-radius: 0.15rem;
cursor: pointer;
transition: background-color 0.3s ease;
}
.prev-btn {
background-color: #f44336;
color: #fff;
}
.prev-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.info-btn {
background-color: #4caf50;
color: #fff;
}
.next-btn {
background-color: #2196f3;
color: #fff;
}
.nav-btn:hover:not(:disabled) {
opacity: 0.8;
}
</style>

View File

@ -1,489 +0,0 @@
<template>
<div class="end-page-container">
<!-- 背景图 -->
<div class="background">
<img
v-lazy="backgroundImage"
alt="结束页背景"
@error="handleImageError"
/>
</div>
<!-- 标题 -->
<h1 class="title">🎉 恭喜完成全部场景游览 🎉</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: `${progressPercentage}%` }"
></div>
</div>
<p class="progress-text">
已收集 {{ collectedSeals.length }} / {{ totalSeals }} 个福印
</p>
</div>
<!-- 福印列表 -->
<div class="seal-grid">
<div
v-for="seal in allSeals"
:key="seal.id"
class="seal-item"
:class="{ 'collected': seal.collected }"
>
<div class="seal-image">
<img
v-lazy="seal.collected ? seal.image : uncollectedImage"
:alt="seal.name"
/>
</div>
<div class="seal-name">{{ seal.name }}</div>
<div class="seal-status" :class="{ 'collected-status': seal.collected }">
{{ seal.collected ? '已收集' : '未收集' }}
</div>
</div>
</div>
<!-- 全部收集奖励 -->
<div class="full-collection-reward" v-if="allCollected">
<div class="reward-content">
<div class="reward-icon">🏆</div>
<h3>恭喜集齐全部福印</h3>
<p>您已完成所有商圈的探索获得了完整的福印收集成就</p>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="action-buttons">
<button @click="restartTour" class="btn restart-btn">
重新游览
</button>
<button @click="generateCouple" class="btn couple-btn">
生成AI春联
</button>
<button @click="participateLottery" class="btn lottery-btn">
参与抽奖
</button>
</div>
<!-- 分享按钮 -->
<div class="share-section">
<button @click="shareResult" class="share-btn">
📤 分享我的成果
</button>
</div>
<!-- 加载动画 -->
<div v-if="loading" class="loading-overlay">
<uni-spinner type="circle" size="48" color="#fff"></uni-spinner>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { useCollectionStore } from '../../src/store/collection';
import { share } from '../utils/share';
const router = useRouter();
const collectionStore = useCollectionStore();
//
const loading = ref(false);
const backgroundImage = ref('/static/bg/bg_finish.jpg');
const uncollectedImage = ref('https://placeholder.pics/svg/100x100/CCCCCC/999999/未收集');
//
const allSeals = computed(() => {
return collectionStore.seals;
});
const collectedSeals = computed(() => {
return collectionStore.collectedSeals;
});
const totalSeals = computed(() => {
return allSeals.value.length;
});
const progressPercentage = computed(() => {
return collectionStore.getCollectionProgress;
});
const allCollected = computed(() => {
return collectionStore.isAllCollected;
});
//
onMounted(() => {
//
if (allCollected.value) {
uni.showToast({
title: '恭喜集齐全部福印!',
icon: 'success',
duration: 3000
});
}
});
//
const restartTour = () => {
//
collectionStore.resetCollection();
//
router.push('/');
};
// AI
const generateCouple = () => {
router.push('/ai-spring');
};
//
const participateLottery = () => {
router.push('/');
uni.showToast({
title: '抽奖功能请在首页参与',
icon: 'none',
duration: 2000
});
};
//
const shareResult = () => {
const shareText = `
我在2026新春东城商圈H5中完成了全部场景游览
已收集 ${collectedSeals.value.length} / ${totalSeals.value} 个福印
#2026新春东城商圈#
`;
//
share(
'新春商圈游览成果',
shareText,
backgroundImage.value
);
};
//
const handleImageError = (event) => {
event.target.src = 'https://placeholder.pics/svg/640x1136/FDF2E9/F39C12/结束页背景';
};
</script>
<style scoped>
.end-page-container {
position: relative;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
overflow: hidden;
}
.background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
overflow: hidden;
}
.background img {
width: 100%;
height: 100%;
object-fit: cover;
}
.title {
font-size: 28px;
color: #f39c12;
text-align: center;
margin: 40px 0 30px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}
.seal-collection-section {
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
padding: 25px;
width: 100%;
max-width: 600px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
margin-bottom: 30px;
}
.section-title {
font-size: 24px;
color: #e67e22;
text-align: center;
margin-bottom: 20px;
font-weight: bold;
}
.collection-progress {
margin-bottom: 30px;
}
.progress-bar {
width: 100%;
height: 15px;
background: #e0e0e0;
border-radius: 10px;
overflow: hidden;
margin-bottom: 10px;
}
.progress-fill {
height: 100%;
background: linear-gradient(135deg, #f39c12, #e67e22);
border-radius: 10px;
transition: width 0.5s ease;
}
.progress-text {
text-align: center;
font-size: 16px;
color: #666;
margin: 0;
}
.seal-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
margin-bottom: 30px;
}
.seal-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 15px;
background: #f9f9f9;
border-radius: 15px;
transition: all 0.3s ease;
border: 2px solid #ddd;
}
.seal-item.collected {
background: linear-gradient(135deg, #fff9c4, #fff59d);
border-color: #ffd740;
transform: scale(1.05);
}
.seal-image {
width: 80px;
height: 80px;
border-radius: 50%;
overflow: hidden;
margin-bottom: 10px;
background: #fff;
border: 3px solid #e67e22;
display: flex;
justify-content: center;
align-items: center;
}
.seal-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.seal-name {
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 5px;
}
.seal-status {
font-size: 14px;
color: #999;
padding: 3px 8px;
border-radius: 10px;
background: #eee;
}
.seal-status.collected-status {
color: #4caf50;
background: #e8f5e9;
}
.full-collection-reward {
background: linear-gradient(135deg, #ffeb3b, #ffc107);
border-radius: 15px;
padding: 20px;
text-align: center;
box-shadow: 0 4px 12px rgba(255, 193, 7, 0.3);
animation: glow 2s infinite;
}
@keyframes glow {
0%, 100% {
box-shadow: 0 4px 12px rgba(255, 193, 7, 0.3);
}
50% {
box-shadow: 0 6px 18px rgba(255, 193, 7, 0.6);
}
}
.reward-content {
display: flex;
flex-direction: column;
align-items: center;
}
.reward-icon {
font-size: 64px;
margin-bottom: 10px;
}
.reward-content h3 {
font-size: 22px;
color: #e65100;
margin-bottom: 10px;
}
.reward-content p {
font-size: 16px;
color: #8d6e63;
margin: 0;
}
.action-buttons {
display: flex;
flex-direction: column;
gap: 15px;
width: 100%;
max-width: 400px;
margin-bottom: 20px;
}
.btn {
padding: 15px 25px;
border: none;
border-radius: 25px;
font-size: 18px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
justify-content: center;
align-items: center;
}
.restart-btn {
background: linear-gradient(135deg, #95a5a6, #7f8c8d);
color: white;
}
.restart-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(149, 165, 166, 0.4);
}
.couple-btn {
background: linear-gradient(135deg, #e67e22, #d35400);
color: white;
}
.couple-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(230, 126, 34, 0.4);
}
.lottery-btn {
background: linear-gradient(135deg, #e74c3c, #c0392b);
color: white;
}
.lottery-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(231, 76, 60, 0.4);
}
.share-section {
margin-bottom: 30px;
}
.share-btn {
padding: 15px 30px;
background: linear-gradient(135deg, #3498db, #2980b9);
color: white;
border: none;
border-radius: 25px;
font-size: 18px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
}
.share-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(52, 152, 219, 0.4);
}
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 999;
}
/* 响应式设计 */
@media (max-width: 480px) {
.title {
font-size: 24px;
}
.seal-collection-section {
padding: 20px;
}
.section-title {
font-size: 20px;
}
.seal-grid {
grid-template-columns: repeat(2, 1fr);
gap: 15px;
}
.seal-image {
width: 70px;
height: 70px;
}
.seal-name {
font-size: 14px;
}
.btn {
font-size: 16px;
padding: 12px 20px;
}
}
</style>

View File

@ -1,437 +0,0 @@
<script setup>
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useSceneStore } from '../../src/store/scene'
import { useCollectionStore } from '../../src/store/collection'
import LongImageViewer from '../components/LongImageViewer.vue'
import MediaPlayer from '../components/MediaPlayer.vue'
const router = useRouter()
const sceneStore = useSceneStore()
const collectionStore = useCollectionStore()
//
const sceneData = ref({
id: 'longfusi',
name: '隆福寺商圈',
description: '融合传统文化与现代艺术的新兴商圈',
image: 'https://picsum.photos/id/1018/750/1600', //
thumbnail: 'https://picsum.photos/id/1018/200/100',
location: '北京市东城区隆福寺街',
history: '隆福寺商圈历史悠久,曾是明清时期的重要庙会场所,如今成为融合传统文化与现代艺术的新兴商业区域。',
attractions: [
{ name: '隆福寺', description: '历史悠久的佛教寺院' },
{ name: '隆福文化中心', description: '现代化的文化艺术中心' },
{ name: '隆福广场', description: '时尚潮流的购物中心' }
],
audio: 'https://example.com/audio/longfusi.mp3', //
video: 'https://example.com/video/longfusi.mp4' //
})
//
const isInteractive = ref(false)
const sealCollected = ref(false)
const showInfoPanel = ref(false)
//
const sealStatus = computed(() => {
return collectionStore.isSceneSealCollected(sceneData.value.id)
})
//
onMounted(() => {
//
sceneStore.activateScene(sceneData.value.id)
//
const sceneIndex = sceneStore.scenes.findIndex(scene => scene.id === sceneData.value.id)
if (sceneIndex !== -1) {
sceneStore.setCurrentScene(sceneIndex)
}
//
sealCollected.value = sealStatus.value
})
//
const handleSceneInteraction = () => {
if (!isInteractive.value) {
isInteractive.value = true
//
setTimeout(() => {
collectSeal()
}, 1000)
}
}
//
const collectSeal = () => {
if (!sealCollected.value) {
const success = collectionStore.collectSealBySceneId(sceneData.value.id)
if (success) {
sealCollected.value = true
showToast({
message: '恭喜获得隆福寺福印!',
icon: 'success',
duration: 2000
})
} else {
showFailToast({
message: '福印收集失败',
duration: 2000
})
}
} else {
showToast({
message: '福印已收集',
duration: 1500
})
}
}
//
const showSceneInfo = () => {
showInfoPanel.value = true
}
//
const closeInfoPanel = () => {
showInfoPanel.value = false
}
//
const goToNextScene = () => {
router.push('/dongzhimen')
}
//
const goToPreviousScene = () => {
router.push('/wangfujing')
}
</script>
<template>
<div class="scene-page">
<!-- 场景标题 -->
<div class="scene-header">
<h1 class="scene-title">{{ sceneData.name }}</h1>
<p class="scene-description">{{ sceneData.description }}</p>
</div>
<!-- 长图查看器 -->
<div class="long-image-container">
<LongImageViewer
:image-url="sceneData.image"
:description="sceneData.name"
@click="handleSceneInteraction"
/>
</div>
<!-- 交互提示 -->
<div class="interaction-tip" v-if="!isInteractive">
<div class="tip-icon"><EFBFBD><EFBFBD></div>
<p>点击图片查看详情</p>
</div>
<!-- 福印收集提示 -->
<div class="seal-collect-tip" v-if="sealCollected">
<div class="seal-icon"><EFBFBD><EFBFBD></div>
<p>已收集隆福寺福印</p>
</div>
<!-- 场景信息面板 -->
<div class="info-panel" v-if="showInfoPanel">
<div class="info-panel-content">
<div class="info-panel-header">
<h2>场景信息</h2>
<button class="close-btn" @click="closeInfoPanel">×</button>
</div>
<div class="info-panel-body">
<div class="info-item">
<label>名称</label>
<span>{{ sceneData.name }}</span>
</div>
<div class="info-item">
<label>位置</label>
<span>{{ sceneData.location }}</span>
</div>
<div class="info-item">
<label>历史</label>
<span>{{ sceneData.history }}</span>
</div>
<div class="attractions-section">
<h3>主要景点</h3>
<ul class="attractions-list">
<li v-for="(attraction, index) in sceneData.attractions" :key="index">
<div class="attraction-name">{{ attraction.name }}</div>
<div class="attraction-description">{{ attraction.description }}</div>
</li>
</ul>
</div>
</div>
</div>
</div>
<!-- 媒体播放器 -->
<div class="media-player-container">
<MediaPlayer
:audio-url="sceneData.audio"
:video-url="sceneData.video"
:scene-name="sceneData.name"
/>
</div>
<!-- 场景导航 -->
<div class="scene-navigation">
<button class="nav-btn prev-btn" @click="goToPreviousScene">
上一个
</button>
<button class="nav-btn info-btn" @click="showSceneInfo">
场景信息
</button>
<button class="nav-btn next-btn" @click="goToNextScene">
下一个
</button>
</div>
</div>
</template>
<style scoped>
.scene-page {
width: 100%;
min-height: 100vh;
background-color: #f5f5f5;
padding-top: 0.88rem;
padding-bottom: 1.2rem;
}
.scene-header {
padding: 0.2rem;
text-align: center;
background-color: #fff;
margin-bottom: 0.1rem;
}
.scene-title {
font-size: 0.4rem;
font-weight: bold;
color: #333;
margin-bottom: 0.1rem;
}
.scene-description {
font-size: 0.28rem;
color: #666;
}
.long-image-container {
width: 100%;
height: 5rem;
overflow: hidden;
background-color: #000;
position: relative;
}
.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: 0.3rem 0.5rem;
border-radius: 0.2rem;
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: 0.6rem;
margin-bottom: 0.1rem;
}
.seal-collect-tip {
position: absolute;
top: 1rem;
right: 0.2rem;
display: flex;
align-items: center;
background-color: rgba(255, 107, 53, 0.9);
color: #fff;
padding: 0.15rem 0.3rem;
border-radius: 0.2rem;
font-size: 0.28rem;
z-index: 10;
}
.seal-icon {
font-size: 0.4rem;
margin-right: 0.1rem;
}
.info-panel {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.info-panel-content {
width: 90%;
max-width: 500px;
background-color: #fff;
border-radius: 0.2rem;
overflow: hidden;
max-height: 80vh;
overflow-y: auto;
}
.info-panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.3rem;
background-color: #ff6b35;
color: #fff;
}
.info-panel-header h2 {
font-size: 0.36rem;
margin: 0;
}
.close-btn {
background: none;
border: none;
color: #fff;
font-size: 0.4rem;
cursor: pointer;
}
.info-panel-body {
padding: 0.3rem;
}
.info-item {
margin-bottom: 0.2rem;
font-size: 0.28rem;
}
.info-item label {
font-weight: bold;
color: #333;
}
.info-item span {
color: #666;
}
.attractions-section {
margin-top: 0.3rem;
}
.attractions-section h3 {
font-size: 0.32rem;
margin-bottom: 0.2rem;
color: #333;
}
.attractions-list {
list-style: none;
padding: 0;
margin: 0;
}
.attractions-list li {
margin-bottom: 0.2rem;
padding: 0.2rem;
background-color: #f9f9f9;
border-radius: 0.1rem;
}
.attraction-name {
font-weight: bold;
color: #333;
margin-bottom: 0.05rem;
}
.attraction-description {
color: #666;
font-size: 0.26rem;
}
.media-player-container {
margin: 0.2rem;
background-color: #fff;
border-radius: 0.1rem;
padding: 0.2rem;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.scene-navigation {
display: flex;
justify-content: space-between;
padding: 0.2rem;
margin-top: 0.2rem;
}
.nav-btn {
flex: 1;
padding: 0.2rem;
margin: 0 0.1rem;
font-size: 0.3rem;
border: none;
border-radius: 0.15rem;
cursor: pointer;
transition: background-color 0.3s ease;
}
.prev-btn {
background-color: #9c27b0;
color: #fff;
}
.prev-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.info-btn {
background-color: #4caf50;
color: #fff;
}
.next-btn {
background-color: #f44336;
color: #fff;
}
.nav-btn:hover:not(:disabled) {
opacity: 0.8;
}
</style>

View File

@ -1,437 +0,0 @@
<script setup>
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useSceneStore } from '../../src/store/scene'
import { useCollectionStore } from '../../src/store/collection'
import LongImageViewer from '../components/LongImageViewer.vue'
import MediaPlayer from '../components/MediaPlayer.vue'
const router = useRouter()
const sceneStore = useSceneStore()
const collectionStore = useCollectionStore()
//
const sceneData = ref({
id: 'qianmen',
name: '前门商圈',
description: '北京最具历史文化底蕴的商圈之一',
image: 'https://picsum.photos/id/1015/750/1600', //
thumbnail: 'https://picsum.photos/id/1015/200/100',
location: '北京市东城区前门大街',
history: '前门大街形成于明代已有600多年历史是北京最古老的商业街之一。',
attractions: [
{ name: '前门大街', description: '北京著名的商业街,汇集众多老字号' },
{ name: '大栅栏', description: '明清时期的商业中心,保留大量传统建筑' },
{ name: '天安门广场', description: '世界最大的城市广场,中国的象征' }
],
audio: 'https://example.com/audio/qianmen.mp3', //
video: 'https://example.com/video/qianmen.mp4' //
})
//
const isInteractive = ref(false)
const sealCollected = ref(false)
const showInfoPanel = ref(false)
//
const sealStatus = computed(() => {
return collectionStore.isSceneSealCollected(sceneData.value.id)
})
//
onMounted(() => {
//
sceneStore.activateScene(sceneData.value.id)
//
const sceneIndex = sceneStore.scenes.findIndex(scene => scene.id === sceneData.value.id)
if (sceneIndex !== -1) {
sceneStore.setCurrentScene(sceneIndex)
}
//
sealCollected.value = sealStatus.value
})
//
const handleSceneInteraction = () => {
if (!isInteractive.value) {
isInteractive.value = true
//
setTimeout(() => {
collectSeal()
}, 1000)
}
}
//
const collectSeal = () => {
if (!sealCollected.value) {
const success = collectionStore.collectSealBySceneId(sceneData.value.id)
if (success) {
sealCollected.value = true
showToast({
message: '恭喜获得前门福印!',
icon: 'success',
duration: 2000
})
} else {
showFailToast({
message: '福印收集失败',
duration: 2000
})
}
} else {
showToast({
message: '福印已收集',
duration: 1500
})
}
}
//
const showSceneInfo = () => {
showInfoPanel.value = true
}
//
const closeInfoPanel = () => {
showInfoPanel.value = false
}
//
const goToNextScene = () => {
router.push('/chongwen')
}
//
const goToPreviousScene = () => {
Toast('已经是第一个场景了')
}
</script>
<template>
<div class="scene-page">
<!-- 场景标题 -->
<div class="scene-header">
<h1 class="scene-title">{{ sceneData.name }}</h1>
<p class="scene-description">{{ sceneData.description }}</p>
</div>
<!-- 长图查看器 -->
<div class="long-image-container">
<LongImageViewer
:image-url="sceneData.image"
:description="sceneData.name"
@click="handleSceneInteraction"
/>
</div>
<!-- 交互提示 -->
<div class="interaction-tip" v-if="!isInteractive">
<div class="tip-icon">👆</div>
<p>点击图片查看详情</p>
</div>
<!-- 福印收集提示 -->
<div class="seal-collect-tip" v-if="sealCollected">
<div class="seal-icon">🏮</div>
<p>已收集前门福印</p>
</div>
<!-- 场景信息面板 -->
<div class="info-panel" v-if="showInfoPanel">
<div class="info-panel-content">
<div class="info-panel-header">
<h2>场景信息</h2>
<button class="close-btn" @click="closeInfoPanel">×</button>
</div>
<div class="info-panel-body">
<div class="info-item">
<label>名称</label>
<span>{{ sceneData.name }}</span>
</div>
<div class="info-item">
<label>位置</label>
<span>{{ sceneData.location }}</span>
</div>
<div class="info-item">
<label>历史</label>
<span>{{ sceneData.history }}</span>
</div>
<div class="attractions-section">
<h3>主要景点</h3>
<ul class="attractions-list">
<li v-for="(attraction, index) in sceneData.attractions" :key="index">
<div class="attraction-name">{{ attraction.name }}</div>
<div class="attraction-description">{{ attraction.description }}</div>
</li>
</ul>
</div>
</div>
</div>
</div>
<!-- 媒体播放器 -->
<div class="media-player-container">
<MediaPlayer
:audio-url="sceneData.audio"
:video-url="sceneData.video"
:scene-name="sceneData.name"
/>
</div>
<!-- 场景导航 -->
<div class="scene-navigation">
<button class="nav-btn prev-btn" @click="goToPreviousScene">
上一个
</button>
<button class="nav-btn info-btn" @click="showSceneInfo">
场景信息
</button>
<button class="nav-btn next-btn" @click="goToNextScene">
下一个
</button>
</div>
</div>
</template>
<style scoped>
.scene-page {
width: 100%;
min-height: 100vh;
background-color: #f5f5f5;
padding-top: 0.88rem;
padding-bottom: 1.2rem;
}
.scene-header {
padding: 0.2rem;
text-align: center;
background-color: #fff;
margin-bottom: 0.1rem;
}
.scene-title {
font-size: 0.4rem;
font-weight: bold;
color: #333;
margin-bottom: 0.1rem;
}
.scene-description {
font-size: 0.28rem;
color: #666;
}
.long-image-container {
width: 100%;
height: 5rem;
overflow: hidden;
background-color: #000;
position: relative;
}
.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: 0.3rem 0.5rem;
border-radius: 0.2rem;
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: 0.6rem;
margin-bottom: 0.1rem;
}
.seal-collect-tip {
position: absolute;
top: 1rem;
right: 0.2rem;
display: flex;
align-items: center;
background-color: rgba(255, 107, 53, 0.9);
color: #fff;
padding: 0.15rem 0.3rem;
border-radius: 0.2rem;
font-size: 0.28rem;
z-index: 10;
}
.seal-icon {
font-size: 0.4rem;
margin-right: 0.1rem;
}
.info-panel {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.info-panel-content {
width: 90%;
max-width: 500px;
background-color: #fff;
border-radius: 0.2rem;
overflow: hidden;
max-height: 80vh;
overflow-y: auto;
}
.info-panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.3rem;
background-color: #ff6b35;
color: #fff;
}
.info-panel-header h2 {
font-size: 0.36rem;
margin: 0;
}
.close-btn {
background: none;
border: none;
color: #fff;
font-size: 0.4rem;
cursor: pointer;
}
.info-panel-body {
padding: 0.3rem;
}
.info-item {
margin-bottom: 0.2rem;
font-size: 0.28rem;
}
.info-item label {
font-weight: bold;
color: #333;
}
.info-item span {
color: #666;
}
.attractions-section {
margin-top: 0.3rem;
}
.attractions-section h3 {
font-size: 0.32rem;
margin-bottom: 0.2rem;
color: #333;
}
.attractions-list {
list-style: none;
padding: 0;
margin: 0;
}
.attractions-list li {
margin-bottom: 0.2rem;
padding: 0.2rem;
background-color: #f9f9f9;
border-radius: 0.1rem;
}
.attraction-name {
font-weight: bold;
color: #333;
margin-bottom: 0.05rem;
}
.attraction-description {
color: #666;
font-size: 0.26rem;
}
.media-player-container {
margin: 0.2rem;
background-color: #fff;
border-radius: 0.1rem;
padding: 0.2rem;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.scene-navigation {
display: flex;
justify-content: space-between;
padding: 0.2rem;
margin-top: 0.2rem;
}
.nav-btn {
flex: 1;
padding: 0.2rem;
margin: 0 0.1rem;
font-size: 0.3rem;
border: none;
border-radius: 0.15rem;
cursor: pointer;
transition: background-color 0.3s ease;
}
.prev-btn {
background-color: #e0e0e0;
color: #666;
}
.prev-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.info-btn {
background-color: #4caf50;
color: #fff;
}
.next-btn {
background-color: #2196f3;
color: #fff;
}
.nav-btn:hover:not(:disabled) {
opacity: 0.8;
}
</style>

View File

@ -1,437 +0,0 @@
<script setup>
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { useSceneStore } from '../../src/store/scene'
import { useCollectionStore } from '../../src/store/collection'
import LongImageViewer from '../components/LongImageViewer.vue'
import MediaPlayer from '../components/MediaPlayer.vue'
const router = useRouter()
const sceneStore = useSceneStore()
const collectionStore = useCollectionStore()
//
const sceneData = ref({
id: 'wangfujing',
name: '王府井商圈',
description: '北京最繁华的商业中心之一',
image: 'https://picsum.photos/id/1018/750/1600', //
thumbnail: 'https://picsum.photos/id/1018/200/100',
location: '北京市东城区王府井大街',
history: '王府井商圈已有百年历史,是北京著名的商业步行街,汇聚了众多国内外知名品牌。',
attractions: [
{ name: '王府井步行街', description: '北京最著名的商业步行街' },
{ name: '东方新天地', description: '高端购物中心' },
{ name: '北京apm', description: '时尚潮流购物中心' }
],
audio: 'https://example.com/audio/wangfujing.mp3', //
video: 'https://example.com/video/wangfujing.mp4' //
})
//
const isInteractive = ref(false)
const sealCollected = ref(false)
const showInfoPanel = ref(false)
//
const sealStatus = computed(() => {
return collectionStore.isSceneSealCollected(sceneData.value.id)
})
//
onMounted(() => {
//
sceneStore.activateScene(sceneData.value.id)
//
const sceneIndex = sceneStore.scenes.findIndex(scene => scene.id === sceneData.value.id)
if (sceneIndex !== -1) {
sceneStore.setCurrentScene(sceneIndex)
}
//
sealCollected.value = sealStatus.value
})
//
const handleSceneInteraction = () => {
if (!isInteractive.value) {
isInteractive.value = true
//
setTimeout(() => {
collectSeal()
}, 1000)
}
}
//
const collectSeal = () => {
if (!sealCollected.value) {
const success = collectionStore.collectSealBySceneId(sceneData.value.id)
if (success) {
sealCollected.value = true
showToast({
message: '恭喜获得王府井福印!',
icon: 'success',
duration: 2000
})
} else {
showFailToast({
message: '福印收集失败',
duration: 2000
})
}
} else {
showToast({
message: '福印已收集',
duration: 1500
})
}
}
//
const showSceneInfo = () => {
showInfoPanel.value = true
}
//
const closeInfoPanel = () => {
showInfoPanel.value = false
}
//
const goToNextScene = () => {
router.push('/longfusi')
}
//
const goToPreviousScene = () => {
router.push('/chongwen')
}
</script>
<template>
<div class="scene-page">
<!-- 场景标题 -->
<div class="scene-header">
<h1 class="scene-title">{{ sceneData.name }}</h1>
<p class="scene-description">{{ sceneData.description }}</p>
</div>
<!-- 长图查看器 -->
<div class="long-image-container">
<LongImageViewer
:image-url="sceneData.image"
:description="sceneData.name"
@click="handleSceneInteraction"
/>
</div>
<!-- 交互提示 -->
<div class="interaction-tip" v-if="!isInteractive">
<div class="tip-icon"><EFBFBD><EFBFBD></div>
<p>点击图片查看详情</p>
</div>
<!-- 福印收集提示 -->
<div class="seal-collect-tip" v-if="sealCollected">
<div class="seal-icon"><EFBFBD><EFBFBD></div>
<p>已收集王府井福印</p>
</div>
<!-- 场景信息面板 -->
<div class="info-panel" v-if="showInfoPanel">
<div class="info-panel-content">
<div class="info-panel-header">
<h2>场景信息</h2>
<button class="close-btn" @click="closeInfoPanel">×</button>
</div>
<div class="info-panel-body">
<div class="info-item">
<label>名称</label>
<span>{{ sceneData.name }}</span>
</div>
<div class="info-item">
<label>位置</label>
<span>{{ sceneData.location }}</span>
</div>
<div class="info-item">
<label>历史</label>
<span>{{ sceneData.history }}</span>
</div>
<div class="attractions-section">
<h3>主要景点</h3>
<ul class="attractions-list">
<li v-for="(attraction, index) in sceneData.attractions" :key="index">
<div class="attraction-name">{{ attraction.name }}</div>
<div class="attraction-description">{{ attraction.description }}</div>
</li>
</ul>
</div>
</div>
</div>
</div>
<!-- 媒体播放器 -->
<div class="media-player-container">
<MediaPlayer
:audio-url="sceneData.audio"
:video-url="sceneData.video"
:scene-name="sceneData.name"
/>
</div>
<!-- 场景导航 -->
<div class="scene-navigation">
<button class="nav-btn prev-btn" @click="goToPreviousScene">
上一个
</button>
<button class="nav-btn info-btn" @click="showSceneInfo">
场景信息
</button>
<button class="nav-btn next-btn" @click="goToNextScene">
下一个
</button>
</div>
</div>
</template>
<style scoped>
.scene-page {
width: 100%;
min-height: 100vh;
background-color: #f5f5f5;
padding-top: 0.88rem;
padding-bottom: 1.2rem;
}
.scene-header {
padding: 0.2rem;
text-align: center;
background-color: #fff;
margin-bottom: 0.1rem;
}
.scene-title {
font-size: 0.4rem;
font-weight: bold;
color: #333;
margin-bottom: 0.1rem;
}
.scene-description {
font-size: 0.28rem;
color: #666;
}
.long-image-container {
width: 100%;
height: 5rem;
overflow: hidden;
background-color: #000;
position: relative;
}
.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: 0.3rem 0.5rem;
border-radius: 0.2rem;
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: 0.6rem;
margin-bottom: 0.1rem;
}
.seal-collect-tip {
position: absolute;
top: 1rem;
right: 0.2rem;
display: flex;
align-items: center;
background-color: rgba(255, 107, 53, 0.9);
color: #fff;
padding: 0.15rem 0.3rem;
border-radius: 0.2rem;
font-size: 0.28rem;
z-index: 10;
}
.seal-icon {
font-size: 0.4rem;
margin-right: 0.1rem;
}
.info-panel {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.info-panel-content {
width: 90%;
max-width: 500px;
background-color: #fff;
border-radius: 0.2rem;
overflow: hidden;
max-height: 80vh;
overflow-y: auto;
}
.info-panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.3rem;
background-color: #ff6b35;
color: #fff;
}
.info-panel-header h2 {
font-size: 0.36rem;
margin: 0;
}
.close-btn {
background: none;
border: none;
color: #fff;
font-size: 0.4rem;
cursor: pointer;
}
.info-panel-body {
padding: 0.3rem;
}
.info-item {
margin-bottom: 0.2rem;
font-size: 0.28rem;
}
.info-item label {
font-weight: bold;
color: #333;
}
.info-item span {
color: #666;
}
.attractions-section {
margin-top: 0.3rem;
}
.attractions-section h3 {
font-size: 0.32rem;
margin-bottom: 0.2rem;
color: #333;
}
.attractions-list {
list-style: none;
padding: 0;
margin: 0;
}
.attractions-list li {
margin-bottom: 0.2rem;
padding: 0.2rem;
background-color: #f9f9f9;
border-radius: 0.1rem;
}
.attraction-name {
font-weight: bold;
color: #333;
margin-bottom: 0.05rem;
}
.attraction-description {
color: #666;
font-size: 0.26rem;
}
.media-player-container {
margin: 0.2rem;
background-color: #fff;
border-radius: 0.1rem;
padding: 0.2rem;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.scene-navigation {
display: flex;
justify-content: space-between;
padding: 0.2rem;
margin-top: 0.2rem;
}
.nav-btn {
flex: 1;
padding: 0.2rem;
margin: 0 0.1rem;
font-size: 0.3rem;
border: none;
border-radius: 0.15rem;
cursor: pointer;
transition: background-color 0.3s ease;
}
.prev-btn {
background-color: #ff9800;
color: #fff;
}
.prev-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.info-btn {
background-color: #4caf50;
color: #fff;
}
.next-btn {
background-color: #9c27b0;
color: #fff;
}
.nav-btn:hover:not(:disabled) {
opacity: 0.8;
}
</style>

View File

@ -17,7 +17,7 @@ onLoad((options) => {
console.log('Webview loading URL:', url.value)
} else {
// URL
url.value = 'https://www.720yun.com/t/1dvktq8b0fl?scene_id=74010726'
url.value = 'https://www.720yun.com/vr/cd4jtOyusw4'
}
})