Browse Source
# Conflicts: # App.vue # bmzm/home/home.vue # pages.json # pages/index/readingBody.vue # pig/home/home.vue # xrcc/home/home.vue # xxdf/home/home.vuedev_des
61 changed files with 4788 additions and 1001 deletions
@ -1,147 +1,263 @@ |
|||
<template> |
|||
<view> |
|||
<swiper class="swiper" :current="currentIndex" :vertical="true" @change="handleSwiperChange"> |
|||
<swiper-item v-for="(item, index) in swiperItems" :key="index" v-if="index < indexShow"> |
|||
<view :class="['swiper-item',{'swiper-item1-10': item.images}]" :style="{ backgroundImage: `url(${item.imageUrl})` }" @click="changeIndex(index)"> |
|||
<!-- 如果是第一章的第10个swiper-item,显示图片并绑定点击事件 --> |
|||
<template v-if="item.images"> |
|||
<image v-for="(image, imgIndex) in item.images" :key="imgIndex" :src="image.src" |
|||
mode="aspectFill" @click="setStorage(imgIndex);gotoPath(item.link.replace('{index}', imgIndex + item.linkIndex))"></image> |
|||
</template> |
|||
<view> |
|||
<swiper class="swiper" :current="currentIndex" :vertical="true" @change="handleSwiperChange"> |
|||
<swiper-item v-for="(item, index) in swiperItems" :key="index" v-if="index < indexShow"> |
|||
<view :class="['swiper-item',{'swiper-item1-10': item.images}]" |
|||
:style="{ backgroundImage: `url(${item.imageUrl})` }" @click="changeIndex(index)"> |
|||
<!-- 如果是第一章的第10个swiper-item,显示图片并绑定点击事件 --> |
|||
<template v-if="item.images"> |
|||
<image v-for="(image, imgIndex) in item.images" :key="imgIndex" :src="image.src" |
|||
mode="aspectFill" |
|||
@click="setStorage(imgIndex);gotoPath(item.link.replace('{index}', imgIndex + item.linkIndex))"> |
|||
</image> |
|||
</template> |
|||
<!-- 视频 --> |
|||
<template v-if="index == 3"> |
|||
<video src="https://static.ticket.sz-trip.com/epicSoul/bmzm.mp4" style="width: 100vw;height: 30vh;" objectFit="cover"></video> |
|||
<video src="https://static.ticket.sz-trip.com/epicSoul/bmzm.mp4" @play="handleVideoPlay" |
|||
@pause="handleVideoPause" @ended="handleVideoEnded" style="width: 100vw;height: 30vh;" |
|||
objectFit="cover"></video> |
|||
</template> |
|||
<!-- 动图 --> |
|||
<template v-if="index == 8"> |
|||
<image src="https://static.ticket.sz-trip.com/epicSoul/bmzm/chapter1/img7s.gif" mode="widthFix" style="width: 100vw;"></image> |
|||
<image src="https://static.ticket.sz-trip.com/epicSoul/bmzm/chapter1/img7s.gif" mode="widthFix" |
|||
style="width: 100vw;"></image> |
|||
</template> |
|||
</view> |
|||
</swiper-item> |
|||
</swiper> |
|||
</view> |
|||
</swiper-item> |
|||
</swiper> |
|||
<MusicControl /> |
|||
<NavMenu :nav-index="0" @jump-to-page="handleJumpToPage" /> |
|||
</view> |
|||
<AudioControl audioSrc="https://des.js-dyyj.com/data/2025/09/05/9875a62d-14ef-481e-b88f-19c061478ce6.MP3" /> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import MusicControl from '@/components/MusicControl.vue'; |
|||
import NavMenu from '../components/NavMenu.vue'; |
|||
export default { |
|||
components: { |
|||
MusicControl, |
|||
NavMenu |
|||
}, |
|||
data() { |
|||
return { |
|||
currentIndex: 0, |
|||
// 禁止滑动到的索引 |
|||
forbiddenIndex: 2, |
|||
initialIndex: 1, |
|||
popupIndex: 1, |
|||
// 抽象出swiper-item的数据 |
|||
swiperItems: [ |
|||
{ |
|||
imageUrl: `https://static.ticket.sz-trip.com/epicSoul/bmzm/index/index1.png` |
|||
}, |
|||
{ |
|||
imageUrl: 'https://static.ticket.sz-trip.com/epicSoul/bmzm/index/index5.png' |
|||
}, |
|||
// 第一章 |
|||
...Array.from({ length: 9 }, (_, i) => ({ |
|||
imageUrl: `https://static.ticket.sz-trip.com/epicSoul/bmzm/chapter1/img${i + 1}.png` |
|||
})), |
|||
{ |
|||
imageUrl: 'https://static.ticket.sz-trip.com/epicSoul/bmzm/chapter1/img10.png', |
|||
images: Array.from({ length: 4 }, (_, i) => ({ |
|||
src: `https://static.ticket.sz-trip.com/epicSoul/bmzm/chapter1/img10-${i + 1}.png` |
|||
})), |
|||
link: '/bmzm/chapter2/index?index={index}', |
|||
linkIndex: 11 |
|||
} |
|||
], |
|||
indexShow: 3 |
|||
}; |
|||
}, |
|||
onLoad(option) { |
|||
this.initialIndex = option.index; |
|||
// 更新第一个swiper-item的图片路径 |
|||
this.swiperItems[0].imageUrl = `https://static.ticket.sz-trip.com/epicSoul/bmzm/index/index${this.initialIndex}.png`; |
|||
}, |
|||
methods: { |
|||
touchmove() { |
|||
return this.currentIndex == 2 ? true : false |
|||
import AudioControl from '@/components/AudioControl.vue'; |
|||
import MusicControl from '@/components/MusicControl.vue'; |
|||
import NavMenu from '../components/NavMenu.vue'; |
|||
export default { |
|||
components: { |
|||
MusicControl, |
|||
NavMenu, |
|||
AudioControl |
|||
}, |
|||
data() { |
|||
return { |
|||
currentIndex: 0, |
|||
// 禁止滑动到的索引 |
|||
forbiddenIndex: 2, |
|||
initialIndex: 1, |
|||
popupIndex: 1, |
|||
// 视频播放前的状态记录 |
|||
beforeVideoState: { |
|||
audioWasPlaying: false, |
|||
bgMusicWasPlaying: false |
|||
}, |
|||
// 抽象出swiper-item的数据 |
|||
swiperItems: [{ |
|||
imageUrl: `https://static.ticket.sz-trip.com/epicSoul/bmzm/index/index1.png` |
|||
}, |
|||
{ |
|||
imageUrl: 'https://static.ticket.sz-trip.com/epicSoul/bmzm/index/index5.png' |
|||
}, |
|||
// 第一章 |
|||
...Array.from({ |
|||
length: 9 |
|||
}, (_, i) => ({ |
|||
imageUrl: `https://static.ticket.sz-trip.com/epicSoul/bmzm/chapter1/img${i + 1}.png` |
|||
})), |
|||
{ |
|||
imageUrl: 'https://static.ticket.sz-trip.com/epicSoul/bmzm/chapter1/img10.png', |
|||
images: Array.from({ |
|||
length: 4 |
|||
}, (_, i) => ({ |
|||
src: `https://static.ticket.sz-trip.com/epicSoul/bmzm/chapter1/img10-${i + 1}.png` |
|||
})), |
|||
link: '/bmzm/chapter2/index?index={index}', |
|||
linkIndex: 11 |
|||
} |
|||
], |
|||
indexShow: 3 |
|||
}; |
|||
}, |
|||
changeIndex(index) { |
|||
if(index == 2) this.currentIndex = 3 |
|||
this.indexShow = 100 |
|||
onLoad(option) { |
|||
this.initialIndex = option.index; |
|||
// 更新第一个swiper-item的图片路径 |
|||
this.swiperItems[0].imageUrl = |
|||
`https://static.ticket.sz-trip.com/epicSoul/bmzm/index/index${this.initialIndex}.png`; |
|||
}, |
|||
handleSwiperChange(e) { |
|||
this.currentIndex = e.detail.current; |
|||
if(this.currentIndex == 2) this.indexShow = 3 |
|||
}, |
|||
// 存储答案,供后面使用 |
|||
setStorage(i) { |
|||
let text = '' |
|||
switch (i){ |
|||
case 0: |
|||
text = '灵感的捕捉者' |
|||
break; |
|||
case 1: |
|||
text = '众智的编织者' |
|||
break; |
|||
case 2: |
|||
text = '智慧的开启者' |
|||
break; |
|||
case 3: |
|||
text = '自然的尊重者' |
|||
break; |
|||
default: |
|||
break; |
|||
} |
|||
this.appendToStorage('answerObj', { answer2: text }); |
|||
methods: { |
|||
touchmove() { |
|||
return this.currentIndex == 2 ? true : false |
|||
}, |
|||
changeIndex(index) { |
|||
if (index == 2) this.currentIndex = 3 |
|||
this.indexShow = 100 |
|||
}, |
|||
handleSwiperChange(e) { |
|||
this.currentIndex = e.detail.current; |
|||
if (this.currentIndex == 2) this.indexShow = 3 |
|||
}, |
|||
// 存储答案,供后面使用 |
|||
setStorage(i) { |
|||
let text = '' |
|||
switch (i) { |
|||
case 0: |
|||
text = '灵感的捕捉者' |
|||
break; |
|||
case 1: |
|||
text = '众智的编织者' |
|||
break; |
|||
case 2: |
|||
text = '智慧的开启者' |
|||
break; |
|||
case 3: |
|||
text = '自然的尊重者' |
|||
break; |
|||
default: |
|||
break; |
|||
} |
|||
this.appendToStorage('answerObj', { |
|||
answer2: text |
|||
}); |
|||
}, |
|||
// 视频开始播放 |
|||
handleVideoPlay() { |
|||
console.log('视频开始播放'); |
|||
|
|||
// 记录当前状态 |
|||
this.recordCurrentState(); |
|||
|
|||
// 暂停音频和背景音乐 |
|||
this.pauseAllAudio(); |
|||
}, |
|||
|
|||
// 视频暂停 |
|||
handleVideoPause() { |
|||
console.log('视频暂停'); |
|||
// 恢复之前的状态 |
|||
this.restorePreviousState(); |
|||
}, |
|||
|
|||
// 视频播放结束 |
|||
handleVideoEnded() { |
|||
console.log('视频播放结束'); |
|||
// 恢复之前的状态 |
|||
this.restorePreviousState(); |
|||
}, |
|||
|
|||
// 记录当前音频和背景音乐状态 |
|||
recordCurrentState() { |
|||
try { |
|||
// 检查音频状态 |
|||
if (uni.$globalAudio && uni.$globalAudio.isAudioPlaying()) { |
|||
this.beforeVideoState.audioWasPlaying = true; |
|||
console.log('bmzm chapter1: 记录:音频正在播放'); |
|||
} else { |
|||
this.beforeVideoState.audioWasPlaying = false; |
|||
} |
|||
|
|||
// 检查背景音乐状态 |
|||
const app = getApp(); |
|||
if (app && app.globalData && app.globalData.isMusicPlaying) { |
|||
this.beforeVideoState.bgMusicWasPlaying = true; |
|||
console.log('bmzm chapter1: 记录:背景音乐正在播放'); |
|||
} else { |
|||
this.beforeVideoState.bgMusicWasPlaying = false; |
|||
} |
|||
} catch (error) { |
|||
console.error('bmzm chapter1: 记录状态失败:', error); |
|||
} |
|||
}, |
|||
|
|||
// 暂停所有音频 |
|||
pauseAllAudio() { |
|||
try { |
|||
// 暂停音频 |
|||
if (this.beforeVideoState.audioWasPlaying && uni.$globalAudio) { |
|||
uni.$globalAudio.pauseCurrentAudio(); |
|||
console.log('bmzm chapter1: 暂停音频'); |
|||
} |
|||
|
|||
// 暂停背景音乐 |
|||
if (this.beforeVideoState.bgMusicWasPlaying) { |
|||
const app = getApp(); |
|||
if (app && app.globalData && app.globalData.bgMusic) { |
|||
app.globalData.bgMusic.pause(); |
|||
console.log('bmzm chapter1: 暂停背景音乐'); |
|||
} |
|||
} |
|||
} catch (error) { |
|||
console.error('bmzm chapter1: 暂停音频失败:', error); |
|||
} |
|||
}, |
|||
|
|||
// 恢复之前的状态 |
|||
restorePreviousState() { |
|||
try { |
|||
// 恢复音频 |
|||
if (this.beforeVideoState.audioWasPlaying && uni.$globalAudio) { |
|||
uni.$globalAudio.playCurrentAudio(); |
|||
console.log('bmzm chapter1: 恢复音频播放'); |
|||
} |
|||
|
|||
// 恢复背景音乐 |
|||
if (this.beforeVideoState.bgMusicWasPlaying) { |
|||
const app = getApp(); |
|||
if (app && app.globalData && app.globalData.bgMusic) { |
|||
app.globalData.bgMusic.play(); |
|||
console.log('bmzm chapter1: 恢复背景音乐播放'); |
|||
} |
|||
} |
|||
|
|||
// 重置状态记录 |
|||
this.beforeVideoState.audioWasPlaying = false; |
|||
this.beforeVideoState.bgMusicWasPlaying = false; |
|||
} catch (error) { |
|||
console.error('bmzm chapter1: 恢复状态失败:', error); |
|||
} |
|||
}, |
|||
} |
|||
} |
|||
}; |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.swiper { |
|||
width: 100vw; |
|||
height: 100vh; |
|||
} |
|||
|
|||
.swiper-item { |
|||
width: 100vw; |
|||
height: 100vh; |
|||
background-size: 100% 100%; |
|||
} |
|||
|
|||
.swiper-item1-10 { |
|||
padding: 506rpx 65rpx 370rpx; |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
justify-content: space-between; |
|||
|
|||
&>image:nth-child(n+1) { |
|||
width: 310rpx; |
|||
height: 316.58rpx; |
|||
} |
|||
|
|||
&>image:nth-child(n+2) { |
|||
width: 291.61rpx; |
|||
height: 334.55rpx; |
|||
} |
|||
|
|||
&>image:nth-child(n+3) { |
|||
width: 292.61rpx; |
|||
height: 334.55rpx; |
|||
} |
|||
|
|||
&>image:nth-child(n+4) { |
|||
width: 309.59rpx; |
|||
height: 317.58rpx; |
|||
margin-top: 15rpx; |
|||
} |
|||
} |
|||
.swiper { |
|||
width: 100vw; |
|||
height: 100vh; |
|||
} |
|||
|
|||
.swiper-item { |
|||
width: 100vw; |
|||
height: 100vh; |
|||
background-size: 100% 100%; |
|||
} |
|||
|
|||
.swiper-item1-10 { |
|||
padding: 506rpx 65rpx 370rpx; |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
justify-content: space-between; |
|||
|
|||
&>image:nth-child(n+1) { |
|||
width: 310rpx; |
|||
height: 316.58rpx; |
|||
} |
|||
|
|||
&>image:nth-child(n+2) { |
|||
width: 291.61rpx; |
|||
height: 334.55rpx; |
|||
} |
|||
|
|||
&>image:nth-child(n+3) { |
|||
width: 292.61rpx; |
|||
height: 334.55rpx; |
|||
} |
|||
|
|||
&>image:nth-child(n+4) { |
|||
width: 309.59rpx; |
|||
height: 317.58rpx; |
|||
margin-top: 15rpx; |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,289 @@ |
|||
<template> |
|||
<view class="audio-control" :style="{top:top}" @click.stop="toggleAudio"> |
|||
<image v-if="isAudioPlaying" :src="showImg('/uploads/20250904/c7ddc330d25a0916885a31f1d28f6665.png')" mode="" style="width: 80rpx;height: 80rpx;"></image> |
|||
<image v-else :src="showImg('/uploads/20250904/057669bd710740650db1cafd414c08c9.png')" mode="" style="width: 80rpx;height: 80rpx;"></image> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: 'AudioControl', |
|||
inheritAttrs: false, |
|||
props: { |
|||
// 音频文件路径 |
|||
audioSrc: { |
|||
type: String, |
|||
required: true |
|||
}, |
|||
top:{ |
|||
type:String, |
|||
default:'180rpx' |
|||
}, |
|||
// 是否显示组件 |
|||
visible: { |
|||
type: Boolean, |
|||
default: true |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
isAudioPlaying: false, |
|||
audioContext: null, |
|||
backgroundMusicWasPlaying: false, // 记录背景音乐之前的状态 |
|||
audioInstanceId: null // 音频实例标识 |
|||
} |
|||
}, |
|||
mounted() { |
|||
this.initAudio(); |
|||
|
|||
// 监听背景音乐切换事件 |
|||
uni.$on('backgroundMusicToggle', this.handleBackgroundMusicToggle); |
|||
|
|||
// 页面加载时检查是否有全局音频在播放 |
|||
this.checkGlobalAudioState(); |
|||
}, |
|||
beforeUnmount() { |
|||
this.destroyAudio(); |
|||
|
|||
// 移除事件监听 |
|||
uni.$off('backgroundMusicToggle', this.handleBackgroundMusicToggle); |
|||
}, |
|||
methods: { |
|||
initAudio() { |
|||
try { |
|||
// 检查是否已有全局音频实例 |
|||
const app = getApp(); |
|||
if (app && app.globalData && app.globalData.currentAudio) { |
|||
// 如果有全局音频且URL匹配,则复用 |
|||
if (app.globalData.currentAudio.src === this.audioSrc) { |
|||
this.audioContext = app.globalData.currentAudio; |
|||
this.isAudioPlaying = !this.audioContext.paused; |
|||
this.setupAudioEvents(); |
|||
return; |
|||
} else { |
|||
// 如果URL不匹配,停止之前的音频 |
|||
app.globalData.currentAudio.stop(); |
|||
app.globalData.currentAudio.destroy(); |
|||
} |
|||
} |
|||
|
|||
// 创建新的音频上下文 |
|||
this.audioContext = uni.createInnerAudioContext(); |
|||
this.audioContext.src = this.audioSrc; |
|||
|
|||
// 保存到全局 |
|||
if (app && app.globalData) { |
|||
app.globalData.currentAudio = this.audioContext; |
|||
} |
|||
|
|||
this.setupAudioEvents(); |
|||
|
|||
} catch (error) { |
|||
console.error('初始化音频失败:', error); |
|||
} |
|||
}, |
|||
|
|||
setupAudioEvents() { |
|||
if (!this.audioContext) return; |
|||
|
|||
// 监听音频事件 |
|||
this.audioContext.onPlay(() => { |
|||
this.isAudioPlaying = true; |
|||
console.log('音频开始播放'); |
|||
}); |
|||
|
|||
this.audioContext.onPause(() => { |
|||
this.isAudioPlaying = false; |
|||
console.log('音频暂停'); |
|||
}); |
|||
|
|||
this.audioContext.onStop(() => { |
|||
this.isAudioPlaying = false; |
|||
console.log('音频停止'); |
|||
}); |
|||
|
|||
this.audioContext.onEnded(() => { |
|||
this.isAudioPlaying = false; |
|||
// 音频播放结束后恢复背景音乐 |
|||
this.restoreBackgroundMusic(); |
|||
console.log('音频播放结束'); |
|||
}); |
|||
|
|||
this.audioContext.onError((err) => { |
|||
console.error('音频播放错误:', err); |
|||
this.isAudioPlaying = false; |
|||
// 发生错误时也恢复背景音乐 |
|||
this.restoreBackgroundMusic(); |
|||
}); |
|||
}, |
|||
|
|||
checkGlobalAudioState() { |
|||
try { |
|||
const app = getApp(); |
|||
if (app && app.globalData && app.globalData.currentAudio) { |
|||
const globalAudio = app.globalData.currentAudio; |
|||
// 如果全局音频的URL与当前组件的URL匹配 |
|||
if (globalAudio.src === this.audioSrc) { |
|||
this.isAudioPlaying = !globalAudio.paused; |
|||
console.log('同步全局音频状态:', this.isAudioPlaying); |
|||
} |
|||
} |
|||
} catch (error) { |
|||
console.error('检查全局音频状态失败:', error); |
|||
} |
|||
}, |
|||
|
|||
toggleAudio() { |
|||
if (!this.audioContext) { |
|||
console.error('音频上下文未初始化'); |
|||
return; |
|||
} |
|||
|
|||
if (this.isAudioPlaying) { |
|||
// 暂停音频,恢复背景音乐 |
|||
this.pauseAudio(); |
|||
} else { |
|||
// 播放音频,暂停背景音乐 |
|||
this.playAudio(); |
|||
} |
|||
}, |
|||
|
|||
playAudio() { |
|||
try { |
|||
// 记录背景音乐当前状态并暂停 |
|||
this.pauseBackgroundMusic(); |
|||
|
|||
// 播放音频 |
|||
this.audioContext.play(); |
|||
|
|||
// 发送音频播放事件 |
|||
uni.$emit('audioPlaying', true); |
|||
} catch (error) { |
|||
console.error('播放音频失败:', error); |
|||
} |
|||
}, |
|||
|
|||
pauseAudio() { |
|||
try { |
|||
// 暂停音频 |
|||
this.audioContext.pause(); |
|||
|
|||
// 恢复背景音乐 |
|||
this.restoreBackgroundMusic(); |
|||
|
|||
// 发送音频停止事件 |
|||
uni.$emit('audioPlaying', false); |
|||
} catch (error) { |
|||
console.error('暂停音频失败:', error); |
|||
} |
|||
}, |
|||
|
|||
pauseBackgroundMusic() { |
|||
try { |
|||
const app = getApp(); |
|||
if (app && app.globalData && app.globalData.bgMusic) { |
|||
// 记录背景音乐当前状态 |
|||
this.backgroundMusicWasPlaying = app.globalData.isMusicPlaying; |
|||
|
|||
// 如果背景音乐正在播放,则暂停 |
|||
if (this.backgroundMusicWasPlaying) { |
|||
app.globalData.bgMusic.pause(); |
|||
console.log('背景音乐已暂停'); |
|||
} |
|||
} |
|||
} catch (error) { |
|||
console.error('暂停背景音乐失败:', error); |
|||
} |
|||
}, |
|||
|
|||
restoreBackgroundMusic() { |
|||
try { |
|||
const app = getApp(); |
|||
if (app && app.globalData && app.globalData.bgMusic && this.backgroundMusicWasPlaying) { |
|||
// 恢复背景音乐播放 |
|||
app.globalData.bgMusic.play(); |
|||
console.log('背景音乐已恢复'); |
|||
} |
|||
} catch (error) { |
|||
console.error('恢复背景音乐失败:', error); |
|||
} |
|||
}, |
|||
|
|||
destroyAudio() { |
|||
// 注意:不销毁全局音频实例,只是断开当前组件的连接 |
|||
console.log('AudioControl组件销毁,但保持全局音频实例'); |
|||
// 清空当前组件的引用,但不销毁全局音频 |
|||
this.audioContext = null; |
|||
}, |
|||
|
|||
// 处理背景音乐切换事件 |
|||
handleBackgroundMusicToggle() { |
|||
// 如果音频正在播放,则暂停音频(但不恢复背景音乐) |
|||
if (this.isAudioPlaying) { |
|||
try { |
|||
// 只暂停音频,不恢复背景音乐 |
|||
this.audioContext.pause(); |
|||
|
|||
// 发送音频停止事件 |
|||
uni.$emit('audioPlaying', false); |
|||
|
|||
console.log('背景音乐切换时暂停音频(不恢复背景音乐)'); |
|||
} catch (error) { |
|||
console.error('暂停音频失败:', error); |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
watch: { |
|||
// 监听音频源变化 |
|||
audioSrc: { |
|||
handler(newSrc) { |
|||
if (this.audioContext && newSrc) { |
|||
this.audioContext.src = newSrc; |
|||
} |
|||
}, |
|||
immediate: true |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.audio-control { |
|||
position: absolute; |
|||
top: 180rpx; |
|||
right: 30rpx; |
|||
width: 80rpx; |
|||
height: 80rpx; |
|||
border-radius: 50%; |
|||
// background-color: rgba(0, 0, 0, 0.5); |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
z-index: 998; |
|||
transition: all 0.3s ease; |
|||
|
|||
&:active { |
|||
transform: scale(0.95); |
|||
} |
|||
} |
|||
|
|||
.audio-icon { |
|||
font-size: 36rpx; |
|||
color: #fff; |
|||
transition: all 0.3s ease; |
|||
} |
|||
|
|||
.playing { |
|||
animation: pulse 1.5s ease-in-out infinite; |
|||
} |
|||
|
|||
@keyframes pulse { |
|||
0%, 100% { |
|||
transform: scale(1); |
|||
} |
|||
50% { |
|||
transform: scale(1.1); |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,155 @@ |
|||
# AudioControl 音频控制组件使用文档 |
|||
|
|||
## 组件功能 |
|||
- 在父组件右上角显示音频控制图标 |
|||
- 点击图标播放指定音频,同时暂停背景音乐 |
|||
- 再次点击暂停音频,恢复背景音乐 |
|||
- 音频播放结束后自动恢复背景音乐 |
|||
|
|||
## 组件属性 (Props) |
|||
|
|||
| 属性名 | 类型 | 必填 | 默认值 | 说明 | |
|||
|--------|------|------|--------|------| |
|||
| audioSrc | String | 是 | - | 音频文件路径 | |
|||
| visible | Boolean | 否 | true | 是否显示组件 | |
|||
|
|||
## 使用方法 |
|||
|
|||
### 1. 在父组件中引入和注册组件 |
|||
|
|||
```vue |
|||
<template> |
|||
<view class="parent-container"> |
|||
<!-- 父组件内容 --> |
|||
<view class="content"> |
|||
<!-- 你的页面内容 --> |
|||
</view> |
|||
|
|||
<!-- 音频控制组件 --> |
|||
<AudioControl |
|||
:audioSrc="audioUrl" |
|||
:visible="showAudio" |
|||
/> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import AudioControl from '@/components/AudioControl.vue'; |
|||
|
|||
export default { |
|||
components: { |
|||
AudioControl |
|||
}, |
|||
data() { |
|||
return { |
|||
audioUrl: 'https://your-domain.com/audio/sample.mp3', // 替换为你的音频URL |
|||
showAudio: true |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style> |
|||
.parent-container { |
|||
position: relative; /* 重要:确保AudioControl能正确定位 */ |
|||
width: 100vw; |
|||
height: 100vh; |
|||
} |
|||
</style> |
|||
``` |
|||
|
|||
### 2. 使用项目中的showImg方法(推荐) |
|||
|
|||
如果你的音频文件也存储在项目服务器上,可以使用项目的showImg方法: |
|||
|
|||
```vue |
|||
<script> |
|||
export default { |
|||
data() { |
|||
return { |
|||
audioUrl: this.showImg('/uploads/audio/your-audio-file.mp3'), |
|||
showAudio: true |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
``` |
|||
|
|||
### 3. 动态控制音频源 |
|||
|
|||
你可以根据不同的页面或条件播放不同的音频: |
|||
|
|||
```vue |
|||
<script> |
|||
export default { |
|||
data() { |
|||
return { |
|||
audioUrl: '', |
|||
showAudio: true |
|||
} |
|||
}, |
|||
mounted() { |
|||
// 根据页面设置不同的音频 |
|||
this.setAudioForCurrentPage(); |
|||
}, |
|||
methods: { |
|||
setAudioForCurrentPage() { |
|||
const currentRoute = this.$route.path; // 假设使用vue-router |
|||
|
|||
switch(currentRoute) { |
|||
case '/chapter1': |
|||
this.audioUrl = this.showImg('/uploads/audio/chapter1.mp3'); |
|||
break; |
|||
case '/chapter2': |
|||
this.audioUrl = this.showImg('/uploads/audio/chapter2.mp3'); |
|||
break; |
|||
default: |
|||
this.audioUrl = this.showImg('/uploads/audio/default.mp3'); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
``` |
|||
|
|||
## 样式说明 |
|||
|
|||
组件默认定位在父组件的右上角(top: 30rpx, right: 30rpx),如果需要调整位置,可以在父组件中覆盖样式: |
|||
|
|||
```vue |
|||
<style> |
|||
/* 调整音频控制组件位置 */ |
|||
.parent-container ::v-deep .audio-control { |
|||
top: 50rpx !important; |
|||
right: 50rpx !important; |
|||
} |
|||
</style> |
|||
``` |
|||
|
|||
## 注意事项 |
|||
|
|||
1. **父组件样式**:确保父组件设置了 `position: relative`,这样AudioControl组件才能正确定位 |
|||
2. **音频格式**:建议使用 mp3 格式的音频文件,兼容性最好 |
|||
3. **音频路径**:确保音频文件路径正确且可访问 |
|||
4. **背景音乐**:组件会自动处理与MusicControl组件的交互,无需额外配置 |
|||
|
|||
## 图标说明 |
|||
|
|||
- 🔊:音频未播放状态 |
|||
- 🎧:音频播放中状态,带有脉动动画效果 |
|||
|
|||
## 事件处理 |
|||
|
|||
组件内部已处理所有音频播放逻辑,包括: |
|||
- 播放音频时自动暂停背景音乐 |
|||
- 暂停音频时自动恢复背景音乐 |
|||
- 音频播放结束时自动恢复背景音乐 |
|||
- 组件销毁时自动清理资源 |
|||
|
|||
## 示例场景 |
|||
|
|||
适用于以下场景: |
|||
- 章节页面播放对应的音频解说 |
|||
- 展示页面播放介绍音频 |
|||
- 互动页面播放提示音频 |
|||
- 任何需要临时播放音频并暂停背景音乐的场景 |
@ -0,0 +1,187 @@ |
|||
<template> |
|||
<view |
|||
class="swipe-to-next" |
|||
@touchstart="handleTouchStart" |
|||
@touchend="handleTouchEnd" |
|||
> |
|||
<slot></slot> |
|||
<!-- 提示文字 --> |
|||
<view v-if="showTip && shouldShowTip" class="bottom-tip"> |
|||
<text>{{ tipText }}</text> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: 'SwipeToNext', |
|||
props: { |
|||
// 是否在最后一页 |
|||
isLastSlide: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 跳转的目标路径 |
|||
targetPath: { |
|||
type: String, |
|||
required: true |
|||
}, |
|||
// 是否显示提示文字 |
|||
showTip: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 提示文字内容 |
|||
tipText: { |
|||
type: String, |
|||
default: '上滑进入下一章节' |
|||
}, |
|||
// 滑动阈值(px) |
|||
swipeThreshold: { |
|||
type: Number, |
|||
default: 100 |
|||
}, |
|||
// 延迟时间(ms) |
|||
delayTime: { |
|||
type: Number, |
|||
default: 500 |
|||
}, |
|||
// 是否启用延迟机制 |
|||
enableDelay: { |
|||
type: Boolean, |
|||
default: true |
|||
}, |
|||
// 是否总是启用跳转(忽略isLastSlide状态) |
|||
alwaysEnable: { |
|||
type: Boolean, |
|||
default: false |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
touchStartY: 0, // 触摸开始Y坐标 |
|||
touchEndY: 0, // 触摸结束Y坐标 |
|||
canJump: false, // 是否允许跳转 |
|||
delayTimer: null // 延迟定时器 |
|||
} |
|||
}, |
|||
computed: { |
|||
// 是否应该显示提示 |
|||
shouldShowTip() { |
|||
return this.alwaysEnable || this.isLastSlide; |
|||
}, |
|||
// 是否应该允许触摸检测 |
|||
shouldEnableTouch() { |
|||
return this.alwaysEnable || this.isLastSlide; |
|||
} |
|||
}, |
|||
watch: { |
|||
isLastSlide(newVal) { |
|||
this.handleSlideChange(newVal); |
|||
}, |
|||
alwaysEnable: { |
|||
handler(newVal) { |
|||
if (newVal) { |
|||
// 如果启用总是跳转,立即处理 |
|||
this.handleSlideChange(true); |
|||
} |
|||
}, |
|||
immediate: true |
|||
} |
|||
}, |
|||
beforeDestroy() { |
|||
// 清理定时器 |
|||
if (this.delayTimer) { |
|||
clearTimeout(this.delayTimer); |
|||
} |
|||
}, |
|||
methods: { |
|||
// 处理滑动状态变化 |
|||
handleSlideChange(isActive) { |
|||
console.log('ppsls'); |
|||
if (isActive || this.alwaysEnable) { |
|||
// 到达最后一页或总是启用 |
|||
this.canJump = false; |
|||
if (this.enableDelay && !this.alwaysEnable) { |
|||
// 启用延迟机制(仅当不是总是启用时) |
|||
this.delayTimer = setTimeout(() => { |
|||
if (this.shouldEnableTouch) { |
|||
this.canJump = true; |
|||
} |
|||
}, this.delayTime); |
|||
} else { |
|||
// 不启用延迟或总是启用,立即允许跳转 |
|||
this.canJump = true; |
|||
} |
|||
} else { |
|||
// 离开最后一页且不是总是启用 |
|||
this.canJump = false; |
|||
if (this.delayTimer) { |
|||
clearTimeout(this.delayTimer); |
|||
this.delayTimer = null; |
|||
} |
|||
} |
|||
}, |
|||
// 触摸开始 |
|||
handleTouchStart(e) { |
|||
console.log('。。。。。。。。。。///////////'); |
|||
// 检查是否应该允许触摸 |
|||
if (this.shouldEnableTouch && (this.canJump || !this.enableDelay || this.alwaysEnable)) { |
|||
this.touchStartY = e.touches[0].clientY; |
|||
} |
|||
}, |
|||
// 触摸结束 |
|||
handleTouchEnd(e) { |
|||
// 检查是否满足跳转条件 |
|||
if (!this.shouldEnableTouch || !this.touchStartY) { |
|||
return; |
|||
} |
|||
|
|||
// 如果启用延迟机制但还不能跳转,且不是总是启用模式,则返回 |
|||
if (this.enableDelay && !this.canJump && !this.alwaysEnable) { |
|||
return; |
|||
} |
|||
|
|||
this.touchEndY = e.changedTouches[0].clientY; |
|||
const deltaY = this.touchStartY - this.touchEndY; |
|||
|
|||
// 向上滑动且滑动距离大于阈值才跳转 |
|||
if (deltaY > this.swipeThreshold) { |
|||
console.log('向上滑动触发跳转,目标路径:', this.targetPath); |
|||
this.$emit('swipe-to-next', this.targetPath); |
|||
uni.navigateTo({ |
|||
url:this.targetPath |
|||
}); |
|||
} |
|||
|
|||
// 重置触摸位置 |
|||
this.touchStartY = 0; |
|||
this.touchEndY = 0; |
|||
}, |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.swipe-to-next { |
|||
width: 100%; |
|||
height: 100%; |
|||
position: relative; |
|||
} |
|||
|
|||
.bottom-tip { |
|||
position: absolute; |
|||
bottom: 50rpx; |
|||
left: 50%; |
|||
transform: translateX(-50%); |
|||
background: rgba(0, 0, 0, 0.6); |
|||
padding: 10rpx 20rpx; |
|||
border-radius: 50rpx; |
|||
z-index: 999; |
|||
|
|||
text { |
|||
color: #fff; |
|||
font-size: 24rpx; |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,293 @@ |
|||
# SwipeToNext 组件使用文档 |
|||
|
|||
## 组件介绍 |
|||
|
|||
`SwipeToNext` 是一个通用的触底跳转组件,封装了手势检测、延迟防抖、提示文字等功能,可以在任何需要滑动跳转的页面中使用。 |
|||
|
|||
## 组件特性 |
|||
|
|||
- ✅ 手势滑动检测 |
|||
- ✅ 防误触发机制(延迟允许跳转) |
|||
- ✅ 可配置的滑动阈值 |
|||
- ✅ 自定义提示文字 |
|||
- ✅ 支持事件监听 |
|||
- ✅ 完全可配置的参数 |
|||
|
|||
## 使用方法 |
|||
|
|||
### 1. 引入组件 |
|||
|
|||
```vue |
|||
<script> |
|||
import SwipeToNext from '@/components/SwipeToNext.vue'; |
|||
|
|||
export default { |
|||
components: { |
|||
SwipeToNext |
|||
} |
|||
} |
|||
</script> |
|||
``` |
|||
|
|||
### 2. 基础使用 |
|||
|
|||
```vue |
|||
<template> |
|||
<SwipeToNext |
|||
:is-last-slide="isLastSlide" |
|||
:target-path="'/next/page'" |
|||
@swipe-to-next="handleSwipeToNext" |
|||
> |
|||
<!-- 你的页面内容 --> |
|||
<view class="content"> |
|||
<!-- 轮播图或其他内容 --> |
|||
</view> |
|||
</SwipeToNext> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
data() { |
|||
return { |
|||
isLastSlide: false |
|||
} |
|||
}, |
|||
methods: { |
|||
// 处理swiper切换或其他逻辑 |
|||
handlePageChange() { |
|||
// 根据你的逻辑设置 isLastSlide |
|||
this.isLastSlide = true; // 当到达最后一页时 |
|||
}, |
|||
handleSwipeToNext(targetPath) { |
|||
console.log('即将跳转到:', targetPath); |
|||
// 可以在这里添加额外的逻辑,如数据统计 |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
``` |
|||
|
|||
### 3. 完整配置使用 |
|||
|
|||
```vue |
|||
<template> |
|||
<SwipeToNext |
|||
:is-last-slide="isLastSlide" |
|||
:target-path="'/next/page'" |
|||
:show-tip="true" |
|||
tip-text="向上滑动查看更多内容" |
|||
:swipe-threshold="100" |
|||
:delay-time="800" |
|||
:enable-delay="true" |
|||
@swipe-to-next="handleSwipeToNext" |
|||
> |
|||
<!-- 你的页面内容 --> |
|||
<view class="content"> |
|||
<!-- 内容区域 --> |
|||
</view> |
|||
</SwipeToNext> |
|||
</template> |
|||
``` |
|||
|
|||
## Props 参数 |
|||
|
|||
| 参数名 | 类型 | 默认值 | 必填 | 说明 | |
|||
|--------|------|--------|------|------| |
|||
| `isLastSlide` | Boolean | `false` | ✅ | 是否在最后一页/最后一个状态 | |
|||
| `targetPath` | String | - | ✅ | 跳转的目标路径 | |
|||
| `showTip` | Boolean | `true` | ❌ | 是否显示提示文字 | |
|||
| `tipText` | String | `'继续向上滑动进入下一章节'` | ❌ | 提示文字内容 | |
|||
| `swipeThreshold` | Number | `80` | ❌ | 滑动阈值(像素) | |
|||
| `delayTime` | Number | `500` | ❌ | 延迟允许跳转的时间(毫秒) | |
|||
| `enableDelay` | Boolean | `true` | ❌ | 是否启用延迟机制 | |
|||
| `alwaysEnable` | Boolean | `false` | ❌ | 是否总是启用跳转(忽略isLastSlide状态) | |
|||
|
|||
## Events 事件 |
|||
|
|||
| 事件名 | 参数 | 说明 | |
|||
|--------|------|------| |
|||
| `swipe-to-next` | `targetPath` | 触发跳转时的回调事件 | |
|||
|
|||
## 使用场景示例 |
|||
|
|||
### 场景1:图片轮播页面 |
|||
|
|||
```vue |
|||
<template> |
|||
<SwipeToNext |
|||
:is-last-slide="currentIndex === images.length - 1" |
|||
:target-path="'/gallery/next'" |
|||
> |
|||
<swiper @change="handleSwiperChange"> |
|||
<swiper-item v-for="(img, index) in images" :key="index"> |
|||
<image :src="img" mode="aspectFit" /> |
|||
</swiper-item> |
|||
</swiper> |
|||
</SwipeToNext> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
data() { |
|||
return { |
|||
currentIndex: 0, |
|||
images: ['img1.jpg', 'img2.jpg', 'img3.jpg'] |
|||
} |
|||
}, |
|||
methods: { |
|||
handleSwiperChange(e) { |
|||
this.currentIndex = e.detail.current; |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
``` |
|||
|
|||
### 场景2:文章阅读页面 |
|||
|
|||
```vue |
|||
<template> |
|||
<SwipeToNext |
|||
:is-last-slide="isReadComplete" |
|||
:target-path="'/article/next'" |
|||
tip-text="继续滑动阅读下一篇文章" |
|||
:swipe-threshold="60" |
|||
> |
|||
<scroll-view @scrolltolower="handleScrollToBottom"> |
|||
<view class="article-content"> |
|||
<!-- 文章内容 --> |
|||
</view> |
|||
</scroll-view> |
|||
</SwipeToNext> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
data() { |
|||
return { |
|||
isReadComplete: false |
|||
} |
|||
}, |
|||
methods: { |
|||
handleScrollToBottom() { |
|||
// 滚动到底部时认为阅读完成 |
|||
this.isReadComplete = true; |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
``` |
|||
|
|||
### 场景4:单张图片或总是启用触底跳转 |
|||
|
|||
```vue |
|||
<template> |
|||
<SwipeToNext |
|||
:always-enable="true" |
|||
:target-path="'/next/chapter'" |
|||
tip-text="向上滑动查看下一内容" |
|||
:enable-delay="false" |
|||
> |
|||
<view class="single-image"> |
|||
<image :src="imageUrl" mode="aspectFit" /> |
|||
</view> |
|||
</SwipeToNext> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
data() { |
|||
return { |
|||
imageUrl: 'single-image.jpg' |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
``` |
|||
|
|||
```vue |
|||
<template> |
|||
<SwipeToNext |
|||
:is-last-slide="currentStep === totalSteps - 1" |
|||
:target-path="'/guide/complete'" |
|||
tip-text="向上滑动完成引导" |
|||
> |
|||
<view class="guide-step"> |
|||
<view class="step-content"> |
|||
步骤 {{ currentStep + 1 }} / {{ totalSteps }} |
|||
</view> |
|||
<button @click="nextStep">下一步</button> |
|||
</view> |
|||
</SwipeToNext> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
data() { |
|||
return { |
|||
currentStep: 0, |
|||
totalSteps: 5 |
|||
} |
|||
}, |
|||
methods: { |
|||
nextStep() { |
|||
if (this.currentStep < this.totalSteps - 1) { |
|||
this.currentStep++; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
``` |
|||
|
|||
## 特殊情况处理 |
|||
|
|||
### 单张图片问题 |
|||
|
|||
当只有一张图片时,传统的 `isLastSlide` 逻辑不适用。这时可以使用 `alwaysEnable` 参数: |
|||
|
|||
```vue |
|||
<!-- 单张图片的解决方案 --> |
|||
<SwipeToNext |
|||
:always-enable="true" |
|||
:target-path="'/next/page'" |
|||
:enable-delay="false" |
|||
> |
|||
<image src="single-image.jpg" /> |
|||
</SwipeToNext> |
|||
``` |
|||
|
|||
### 参数优先级 |
|||
|
|||
当 `alwaysEnable="true"` 时: |
|||
- 忽略 `isLastSlide` 的值 |
|||
- 总是显示提示文字 |
|||
- 总是允许触底跳转 |
|||
- 建议设置 `enableDelay="false"` 以获得更好的响应速度 |
|||
|
|||
|
|||
|
|||
1. **确保正确设置 `isLastSlide`**:这是控制是否允许跳转的关键属性 |
|||
2. **路径格式**:`targetPath` 需要是有效的 uni-app 路由路径 |
|||
3. **性能考虑**:如果不需要延迟机制,可以设置 `enableDelay: false` 来提高响应速度 |
|||
4. **样式覆盖**:组件内的提示文字样式可以通过全局样式覆盖 |
|||
5. **事件监听**:建议监听 `swipe-to-next` 事件进行数据统计或其他操作 |
|||
|
|||
## 自定义样式 |
|||
|
|||
如果需要自定义提示文字的样式,可以在页面中添加: |
|||
|
|||
```scss |
|||
// 覆盖组件样式 |
|||
.swipe-to-next .bottom-tip { |
|||
bottom: 200rpx !important; // 调整位置 |
|||
background: rgba(255, 255, 255, 0.9) !important; // 改变背景色 |
|||
|
|||
text { |
|||
color: #333 !important; // 改变文字颜色 |
|||
font-size: 32rpx !important; // 改变字体大小 |
|||
} |
|||
} |
|||
``` |
|||
|
|||
这个组件极大地简化了触底跳转功能的实现,让你可以专注于业务逻辑而不用重复编写相同的手势检测代码。 |
@ -0,0 +1,225 @@ |
|||
# 跨页面音频控制解决方案 |
|||
|
|||
## 进一步优化:解决跨页面状态同步问题 |
|||
|
|||
### 问题描述 |
|||
在跨页面场景下,当音频正在播放时跳转到新页面,新页面的MusicControl组件不知道有音频在播放,点击背景音乐按钮时会直接播放背景音乐,导致音频和背景音乐同时播放。 |
|||
|
|||
### 解决方案 |
|||
|
|||
#### 1. MusicControl组件增强检测 |
|||
```javascript |
|||
// 在mounted生命周期中添加全局音频状态检测 |
|||
mounted() { |
|||
this.syncMusicState(); |
|||
this.checkGlobalAudioState(); // 新增:检查全局音频状态 |
|||
|
|||
// 定时器也要检查全局音频状态 |
|||
this.timer = setInterval(() => { |
|||
this.syncMusicState(); |
|||
this.checkGlobalAudioState(); |
|||
}, 1000); |
|||
} |
|||
|
|||
// 新增方法:检查全局音频状态 |
|||
checkGlobalAudioState() { |
|||
const app = getApp(); |
|||
if (app && app.globalData && app.globalData.currentAudio) { |
|||
const globalAudio = app.globalData.currentAudio; |
|||
this.isAudioPlaying = !globalAudio.paused; |
|||
} else { |
|||
this.isAudioPlaying = false; |
|||
} |
|||
} |
|||
``` |
|||
|
|||
#### 2. 全局音频管理工具优化 |
|||
```javascript |
|||
// 在音频状态变化时发送全局事件 |
|||
pauseCurrentAudio() { |
|||
const audio = this.getCurrentAudio(); |
|||
if (audio && !audio.paused) { |
|||
audio.pause(); |
|||
this.notifyAudioStateChange(false); // 通知状态变化 |
|||
return true; |
|||
} |
|||
return false; |
|||
} |
|||
|
|||
// 新增:通知音频状态变化 |
|||
notifyAudioStateChange(isPlaying) { |
|||
if (typeof uni !== 'undefined') { |
|||
uni.$emit('audioPlaying', isPlaying); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### 修复后的交互流程 |
|||
```mermaid |
|||
graph TD |
|||
A[跨页面跳转] --> B[MusicControl组件加载] |
|||
B --> C[检查全局音频状态] |
|||
C --> D{有音频在播放?} |
|||
D -->|是| E[设置isAudioPlaying=true] |
|||
D -->|否| F[设置isAudioPlaying=false] |
|||
E --> G[点击背景音乐按钮] |
|||
F --> G |
|||
G --> H{检查isAudioPlaying} |
|||
H -->|有音频| I[先暂停音频再播放背景音乐] |
|||
H -->|无音频| J[直接播放背景音乐] |
|||
``` |
|||
|
|||
## 问题描述 |
|||
|
|||
AudioControl组件在页面跳转时会出现以下问题: |
|||
1. 组件状态重置,图标显示不正确 |
|||
2. 音频实例丢失连接,但音频可能仍在播放 |
|||
3. 无法在其他页面控制正在播放的音频 |
|||
|
|||
## 解决方案 |
|||
|
|||
### 1. 全局音频实例管理 |
|||
|
|||
在`App.vue`的`globalData`中添加`currentAudio`属性,用于保存当前的音频实例: |
|||
|
|||
```javascript |
|||
globalData: { |
|||
// ... 其他属性 |
|||
currentAudio: null // 全局音频实例 |
|||
} |
|||
``` |
|||
|
|||
### 2. AudioControl组件优化 |
|||
|
|||
#### 状态同步机制 |
|||
- 组件挂载时检查全局音频状态 |
|||
- 复用已存在的音频实例(如果URL匹配) |
|||
- 组件销毁时不销毁全局音频实例 |
|||
|
|||
#### 核心方法改进 |
|||
```javascript |
|||
// 检查全局音频状态 |
|||
checkGlobalAudioState() { |
|||
const app = getApp(); |
|||
if (app && app.globalData && app.globalData.currentAudio) { |
|||
const globalAudio = app.globalData.currentAudio; |
|||
if (globalAudio.src === this.audioSrc) { |
|||
this.isAudioPlaying = !globalAudio.paused; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 初始化音频时复用全局实例 |
|||
initAudio() { |
|||
const app = getApp(); |
|||
if (app && app.globalData && app.globalData.currentAudio) { |
|||
if (app.globalData.currentAudio.src === this.audioSrc) { |
|||
// 复用现有实例 |
|||
this.audioContext = app.globalData.currentAudio; |
|||
this.isAudioPlaying = !this.audioContext.paused; |
|||
return; |
|||
} |
|||
} |
|||
// 创建新实例... |
|||
} |
|||
``` |
|||
|
|||
### 3. 全局音频管理工具 |
|||
|
|||
创建了`utils/globalAudioManager.js`工具类,提供统一的音频控制接口: |
|||
|
|||
```javascript |
|||
// 在任何页面或组件中使用 |
|||
uni.$globalAudio.pauseCurrentAudio(); // 暂停当前音频 |
|||
uni.$globalAudio.playCurrentAudio(); // 播放当前音频 |
|||
uni.$globalAudio.isAudioPlaying(); // 检查播放状态 |
|||
uni.$globalAudio.getCurrentAudioSrc(); // 获取当前音频源 |
|||
``` |
|||
|
|||
## 使用示例 |
|||
|
|||
### 在页面中控制音频 |
|||
|
|||
```vue |
|||
<template> |
|||
<view> |
|||
<!-- 音频控制组件 --> |
|||
<AudioControl :audioSrc="audioUrl" /> |
|||
|
|||
<!-- 手动控制按钮 --> |
|||
<button @click="toggleAudio">切换音频</button> |
|||
<button @click="stopAudio">停止音频</button> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import AudioControl from '@/components/AudioControl.vue'; |
|||
|
|||
export default { |
|||
components: { AudioControl }, |
|||
data() { |
|||
return { |
|||
audioUrl: this.showImg('/uploads/audio/chapter1.mp3') |
|||
} |
|||
}, |
|||
methods: { |
|||
toggleAudio() { |
|||
if (uni.$globalAudio.isAudioPlaying()) { |
|||
uni.$globalAudio.pauseCurrentAudio(); |
|||
} else { |
|||
uni.$globalAudio.playCurrentAudio(); |
|||
} |
|||
}, |
|||
|
|||
stopAudio() { |
|||
uni.$globalAudio.stopCurrentAudio(); |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
``` |
|||
|
|||
### 在其他页面检查音频状态 |
|||
|
|||
```javascript |
|||
// 在任何页面的onShow生命周期中 |
|||
onShow() { |
|||
// 检查是否有音频在播放 |
|||
if (uni.$globalAudio.isAudioPlaying()) { |
|||
console.log('有音频正在播放:', uni.$globalAudio.getCurrentAudioSrc()); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## 技术优势 |
|||
|
|||
### ✅ **状态持久化** |
|||
- 音频实例在页面跳转时不会丢失 |
|||
- 组件状态能够正确同步全局音频状态 |
|||
|
|||
### ✅ **跨页面控制** |
|||
- 在任何页面都可以控制当前播放的音频 |
|||
- 提供统一的音频管理接口 |
|||
|
|||
### ✅ **资源优化** |
|||
- 避免创建多个音频实例 |
|||
- 自动清理无用的音频资源 |
|||
|
|||
### ✅ **用户体验** |
|||
- 页面跳转时音频播放不中断 |
|||
- 图标状态显示正确 |
|||
- 音频控制逻辑一致 |
|||
|
|||
## 注意事项 |
|||
|
|||
1. **页面生命周期**:音频实例与页面生命周期解耦,需要手动管理 |
|||
2. **内存管理**:确保在应用退出时正确清理音频资源 |
|||
3. **状态同步**:多个AudioControl组件需要监听相同的全局状态 |
|||
4. **错误处理**:增强错误处理机制,确保音频异常时的状态恢复 |
|||
|
|||
## 兼容性 |
|||
|
|||
- ✅ uni-app |
|||
- ✅ 小程序环境 |
|||
- ✅ H5环境 |
|||
- ✅ APP环境 |
@ -0,0 +1,144 @@ |
|||
# 音频与背景音乐交互功能说明 |
|||
|
|||
## Bug修复记录 |
|||
|
|||
### 问题描述 |
|||
当音频播放时点击背景音乐按钮,背景音乐暂停音频并开始播放。此时再点击关闭背景音乐,图标显示关闭状态但背景音乐仍在播放。 |
|||
|
|||
### 问题原因 |
|||
1. 点击背景音乐按钮时会发送事件暂停音频 |
|||
2. 音频暂停时会调用restoreBackgroundMusic恢复背景音乐 |
|||
3. 然后MusicControl再执行自己的切换逻辑 |
|||
4. 导致背景音乐被恢复后又被操作,状态混乱 |
|||
|
|||
### 解决方案 |
|||
1. **MusicControl组件优化**: |
|||
- 检测是否有音频在播放 |
|||
- 如有音频,先暂停音频,延迟执行背景音乐切换 |
|||
- 如无音频,直接切换背景音乐状态 |
|||
|
|||
2. **AudioControl组件优化**: |
|||
- 在handleBackgroundMusicToggle中只暂停音频 |
|||
- 不自动恢复背景音乐,让MusicControl自己控制 |
|||
|
|||
### 修复后的交互流程 |
|||
```mermaid |
|||
graph TD |
|||
A[点击背景音乐按钮] --> B{是否有音频播放?} |
|||
B -->|是| C[发送暂停音频事件] |
|||
C --> D[AudioControl暂停音频\n不恢复背景音乐] |
|||
D --> E[延迟100ms后切换背景音乐] |
|||
B -->|否| F[直接切换背景音乐状态] |
|||
``` |
|||
|
|||
## 实现的功能 |
|||
|
|||
### 🎵 **背景音乐控制音频** |
|||
当点击背景音乐控制按钮时: |
|||
- 如果有音频正在播放,会自动暂停音频 |
|||
- 然后正常切换背景音乐的播放/暂停状态 |
|||
|
|||
### 🎧 **音频控制背景音乐** |
|||
当点击音频控制按钮时: |
|||
- 播放音频时自动暂停背景音乐 |
|||
- 暂停音频时自动恢复背景音乐 |
|||
- 音频播放结束时自动恢复背景音乐 |
|||
|
|||
## 技术实现 |
|||
|
|||
### 事件通信机制 |
|||
使用uni-app的全局事件机制实现组件间通信: |
|||
|
|||
```javascript |
|||
// AudioControl组件发送事件 |
|||
uni.$emit('audioPlaying', true/false); |
|||
|
|||
// MusicControl组件发送事件 |
|||
uni.$emit('backgroundMusicToggle'); |
|||
|
|||
// 组件监听事件 |
|||
uni.$on('eventName', this.handlerFunction); |
|||
``` |
|||
|
|||
### 交互流程 |
|||
|
|||
#### 点击背景音乐按钮: |
|||
```mermaid |
|||
graph TD |
|||
A[点击背景音乐按钮] --> B[发送backgroundMusicToggle事件] |
|||
B --> C[AudioControl收到事件] |
|||
C --> D{音频是否在播放?} |
|||
D -->|是| E[暂停音频] |
|||
D -->|否| F[继续背景音乐操作] |
|||
E --> G[恢复背景音乐] |
|||
F --> H[切换背景音乐状态] |
|||
``` |
|||
|
|||
#### 点击音频按钮: |
|||
```mermaid |
|||
graph TD |
|||
A[点击音频按钮] --> B{当前音频状态?} |
|||
B -->|未播放| C[暂停背景音乐] |
|||
C --> D[播放音频] |
|||
D --> E[发送audioPlaying:true事件] |
|||
B -->|正在播放| F[暂停音频] |
|||
F --> G[恢复背景音乐] |
|||
G --> H[发送audioPlaying:false事件] |
|||
``` |
|||
|
|||
## 使用方法 |
|||
|
|||
在页面中同时使用两个组件: |
|||
|
|||
```vue |
|||
<template> |
|||
<view class="page-container"> |
|||
<!-- 页面内容 --> |
|||
|
|||
<!-- 音频控制组件 --> |
|||
<AudioControl :audioSrc="audioUrl" /> |
|||
|
|||
<!-- 背景音乐控制组件 --> |
|||
<MusicControl /> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import AudioControl from '@/components/AudioControl.vue'; |
|||
import MusicControl from '@/components/MusicControl.vue'; |
|||
|
|||
export default { |
|||
components: { |
|||
AudioControl, |
|||
MusicControl |
|||
}, |
|||
data() { |
|||
return { |
|||
audioUrl: this.showImg('/uploads/audio/your-audio.mp3') |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
``` |
|||
|
|||
## 优势特点 |
|||
|
|||
### ✅ **智能交互** |
|||
- 两个组件能够智能感知对方的状态 |
|||
- 避免同时播放音频和背景音乐造成的冲突 |
|||
- 提供良好的用户体验 |
|||
|
|||
### ✅ **解耦设计** |
|||
- 组件间通过事件通信,保持松耦合 |
|||
- 每个组件都能独立工作 |
|||
- 易于维护和扩展 |
|||
|
|||
### ✅ **状态同步** |
|||
- 实时同步音频和背景音乐的播放状态 |
|||
- 确保状态的一致性和准确性 |
|||
|
|||
## 注意事项 |
|||
|
|||
1. **事件监听清理**:组件销毁时会自动清理事件监听,避免内存泄漏 |
|||
2. **状态管理**:两个组件都维护各自的状态,通过事件保持同步 |
|||
3. **错误处理**:包含完善的错误处理机制,确保功能稳定运行 |
File diff suppressed because it is too large
@ -0,0 +1,25 @@ |
|||
{ |
|||
"setting": { |
|||
"es6": true, |
|||
"postcss": true, |
|||
"minified": true, |
|||
"uglifyFileName": false, |
|||
"enhance": true, |
|||
"packNpmRelationList": [], |
|||
"babelSetting": { |
|||
"ignore": [], |
|||
"disablePlugins": [], |
|||
"outputPath": "" |
|||
}, |
|||
"useCompilerPlugins": false, |
|||
"minifyWXML": true |
|||
}, |
|||
"compileType": "miniprogram", |
|||
"simulatorPluginLibVersion": {}, |
|||
"packOptions": { |
|||
"ignore": [], |
|||
"include": [] |
|||
}, |
|||
"appid": "wx8954209bb3ad489e", |
|||
"editorSetting": {} |
|||
} |
@ -0,0 +1,14 @@ |
|||
{ |
|||
"libVersion": "3.10.0", |
|||
"projectname": "EpicSoul", |
|||
"setting": { |
|||
"urlCheck": true, |
|||
"coverView": true, |
|||
"lazyloadPlaceholderEnable": false, |
|||
"skylineRenderEnable": false, |
|||
"preloadBackgroundData": false, |
|||
"autoAudits": false, |
|||
"showShadowRootInWxmlPanel": true, |
|||
"compileHotReLoad": true |
|||
} |
|||
} |
@ -0,0 +1,92 @@ |
|||
// 全局音频管理工具
|
|||
export const GlobalAudioManager = { |
|||
// 获取当前全局音频实例
|
|||
getCurrentAudio() { |
|||
const app = getApp(); |
|||
return app && app.globalData ? app.globalData.currentAudio : null; |
|||
}, |
|||
|
|||
// 设置全局音频实例
|
|||
setCurrentAudio(audioContext) { |
|||
const app = getApp(); |
|||
if (app && app.globalData) { |
|||
app.globalData.currentAudio = audioContext; |
|||
} |
|||
}, |
|||
|
|||
// 清除全局音频实例
|
|||
clearCurrentAudio() { |
|||
const app = getApp(); |
|||
if (app && app.globalData) { |
|||
if (app.globalData.currentAudio) { |
|||
try { |
|||
app.globalData.currentAudio.stop(); |
|||
app.globalData.currentAudio.destroy(); |
|||
} catch (error) { |
|||
console.error('销毁全局音频失败:', error); |
|||
} |
|||
} |
|||
app.globalData.currentAudio = null; |
|||
} |
|||
}, |
|||
|
|||
// 检查是否有音频在播放
|
|||
isAudioPlaying() { |
|||
const audio = this.getCurrentAudio(); |
|||
return audio && !audio.paused; |
|||
}, |
|||
|
|||
// 暂停当前音频
|
|||
pauseCurrentAudio() { |
|||
const audio = this.getCurrentAudio(); |
|||
if (audio && !audio.paused) { |
|||
audio.pause(); |
|||
// 通知状态变化
|
|||
this.notifyAudioStateChange(false); |
|||
return true; |
|||
} |
|||
return false; |
|||
}, |
|||
|
|||
// 播放当前音频
|
|||
playCurrentAudio() { |
|||
const audio = this.getCurrentAudio(); |
|||
if (audio && audio.paused) { |
|||
audio.play(); |
|||
// 通知状态变化
|
|||
this.notifyAudioStateChange(true); |
|||
return true; |
|||
} |
|||
return false; |
|||
}, |
|||
|
|||
// 停止当前音频
|
|||
stopCurrentAudio() { |
|||
const audio = this.getCurrentAudio(); |
|||
if (audio) { |
|||
audio.stop(); |
|||
// 通知状态变化
|
|||
this.notifyAudioStateChange(false); |
|||
return true; |
|||
} |
|||
return false; |
|||
}, |
|||
|
|||
// 通知音频状态变化
|
|||
notifyAudioStateChange(isPlaying) { |
|||
if (typeof uni !== 'undefined') { |
|||
uni.$emit('audioPlaying', isPlaying); |
|||
} |
|||
}, |
|||
|
|||
// 获取当前音频的src
|
|||
getCurrentAudioSrc() { |
|||
const audio = this.getCurrentAudio(); |
|||
return audio ? audio.src : null; |
|||
} |
|||
}; |
|||
|
|||
// 将工具挂载到全局
|
|||
if (typeof uni !== 'undefined') { |
|||
uni.$globalAudio = GlobalAudioManager; |
|||
} |
@ -0,0 +1,348 @@ |
|||
<template> |
|||
<view style="width: 100vw;position: relative;"> |
|||
<swiper class="swiper" :current="currentIndex" :vertical="true" @change="handleSwiperChange"> |
|||
<swiper-item v-for="(image, index) in swiperImages" :key="index"> |
|||
<view class="swiper-item" :style="{ backgroundImage: `url(${image})` }"> |
|||
<template v-if="index == 2"> |
|||
<video :src="showImg('/uploads/20250905/c47451404cc87d89205b1003e3a0b589.mp4')" |
|||
style="width: 100vw;height: 30vh;" objectFit="cover" @play="handleVideoPlay" |
|||
@pause="handleVideoPause" @ended="handleVideoEnded"></video> |
|||
<image @click="gotoPath('/xqk/chapter2/index')" v-if="currentIndex == swiperImages.length-1" |
|||
:src="showImg('/uploads/20250905/692abbf32b38257ffb2153651f468a63.png')" class="imgJump" |
|||
mode=""></image> |
|||
</template> |
|||
|
|||
</view> |
|||
</swiper-item> |
|||
</swiper> |
|||
<AudioControl audioSrc="https://des.js-dyyj.com/data/2025/09/04/fbc13519-cfe5-4088-89b2-59f138bc23cb.MP3" /> |
|||
<NavMenu :nav-index="navIndex" @jump-to-page="handleJumpToPage" /> |
|||
<MusicControl /> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import AudioControl from '@/components/AudioControl.vue'; |
|||
import MusicControl from '@/components/MusicControl.vue'; |
|||
import NavMenu from '../components/NavMenu.vue'; |
|||
export default { |
|||
components: { |
|||
MusicControl, |
|||
NavMenu, |
|||
AudioControl |
|||
}, |
|||
data() { |
|||
return { |
|||
currentIndex: 0, |
|||
navIndex: 1, |
|||
// 视频播放前的状态记录 |
|||
beforeVideoState: { |
|||
audioWasPlaying: false, |
|||
bgMusicWasPlaying: false |
|||
}, |
|||
swiperImages: [ |
|||
this.showImg('/uploads/20250903/24303e4b7218eaf3d857c846417eb490.png'), |
|||
this.showImg('/uploads/20250903/17495ef65648c64c31920d312301e991.png'), |
|||
this.showImg('/uploads/20250903/92d6f1c6f8f7de040f3c31c8faf98927.png'), |
|||
], |
|||
animationConfig: { |
|||
delay: 0.5, |
|||
duration: 3, |
|||
keyframes: { |
|||
start: 1, |
|||
first: 0.8, |
|||
second: 1.2, |
|||
third: 0.9, |
|||
end: 1.1 |
|||
} |
|||
}, |
|||
popupIndex: 1, |
|||
} |
|||
}, |
|||
onLoad(option) { |
|||
this.currentIndex = option.currentIndex || 0 |
|||
if (this.currentIndex == this.swiperImages.length - 1) this.navIndex = 1; |
|||
}, |
|||
onHide() { |
|||
// 页面隐藏时不做任何音频处理(保持xqk分包内音频播放) |
|||
console.log('xqk chapter1: 页面隐藏,保持音频状态'); |
|||
}, |
|||
onUnload() { |
|||
// 页面销毁时检查是否需要暂停xqk音频 |
|||
this.handleXqkAudioOnLeave(); |
|||
}, |
|||
methods: { |
|||
handleJumpToPage(idx) { |
|||
}, |
|||
handleSwiperChange(e) { |
|||
this.currentIndex = e.detail.current; |
|||
}, |
|||
// 第二页气泡弹框 |
|||
openPopup(i) { |
|||
this.popupIndex = i |
|||
this.$refs.chapterPopup.open(); |
|||
}, |
|||
// 视频开始播放 |
|||
handleVideoPlay() { |
|||
console.log('视频开始播放'); |
|||
|
|||
// 记录当前状态 |
|||
this.recordCurrentState(); |
|||
|
|||
// 暂停音频和背景音乐 |
|||
this.pauseAllAudio(); |
|||
}, |
|||
|
|||
// 视频暂停 |
|||
handleVideoPause() { |
|||
console.log('视频暂停'); |
|||
// 恢复之前的状态 |
|||
this.restorePreviousState(); |
|||
}, |
|||
|
|||
// 视频播放结束 |
|||
handleVideoEnded() { |
|||
console.log('视频播放结束'); |
|||
// 恢复之前的状态 |
|||
this.restorePreviousState(); |
|||
}, |
|||
|
|||
// 记录当前音频和背景音乐状态 |
|||
recordCurrentState() { |
|||
try { |
|||
// 检查音频状态 |
|||
if (uni.$globalAudio && uni.$globalAudio.isAudioPlaying()) { |
|||
this.beforeVideoState.audioWasPlaying = true; |
|||
console.log('记录:音频正在播放'); |
|||
} else { |
|||
this.beforeVideoState.audioWasPlaying = false; |
|||
} |
|||
|
|||
// 检查背景音乐状态 |
|||
const app = getApp(); |
|||
if (app && app.globalData && app.globalData.isMusicPlaying) { |
|||
this.beforeVideoState.bgMusicWasPlaying = true; |
|||
console.log('记录:背景音乐正在播放'); |
|||
} else { |
|||
this.beforeVideoState.bgMusicWasPlaying = false; |
|||
} |
|||
} catch (error) { |
|||
console.error('记录状态失败:', error); |
|||
} |
|||
}, |
|||
|
|||
// 暂停所有音频 |
|||
pauseAllAudio() { |
|||
try { |
|||
// 暂停音频 |
|||
if (this.beforeVideoState.audioWasPlaying && uni.$globalAudio) { |
|||
uni.$globalAudio.pauseCurrentAudio(); |
|||
console.log('暂停音频'); |
|||
} |
|||
|
|||
// 暂停背景音乐 |
|||
if (this.beforeVideoState.bgMusicWasPlaying) { |
|||
const app = getApp(); |
|||
if (app && app.globalData && app.globalData.bgMusic) { |
|||
app.globalData.bgMusic.pause(); |
|||
console.log('暂停背景音乐'); |
|||
} |
|||
} |
|||
} catch (error) { |
|||
console.error('暂停音频失败:', error); |
|||
} |
|||
}, |
|||
|
|||
// 恢复之前的状态 |
|||
restorePreviousState() { |
|||
try { |
|||
// 恢复音频 |
|||
if (this.beforeVideoState.audioWasPlaying && uni.$globalAudio) { |
|||
uni.$globalAudio.playCurrentAudio(); |
|||
console.log('恢复音频播放'); |
|||
} |
|||
|
|||
// 恢复背景音乐 |
|||
if (this.beforeVideoState.bgMusicWasPlaying) { |
|||
const app = getApp(); |
|||
if (app && app.globalData && app.globalData.bgMusic) { |
|||
app.globalData.bgMusic.play(); |
|||
console.log('恢复背景音乐播放'); |
|||
} |
|||
} |
|||
|
|||
// 重置状态记录 |
|||
this.beforeVideoState.audioWasPlaying = false; |
|||
this.beforeVideoState.bgMusicWasPlaying = false; |
|||
} catch (error) { |
|||
console.error('恢复状态失败:', error); |
|||
} |
|||
}, |
|||
// 生成图片完整 URL |
|||
getImageUrl(path) { |
|||
if (typeof path === 'object') { |
|||
path = path.url; |
|||
} |
|||
return `https://static.ticket.sz-trip.com/epicSoul/xrcc/chapter1/${path}`; |
|||
}, |
|||
// 处理xqk音频管理(页面离开时) |
|||
handleXqkAudioOnLeave() { |
|||
try { |
|||
// 获取当前路径 |
|||
const currentPath = uni.$xqkAudio.getCurrentPath(); |
|||
|
|||
// 检查下一个页面的路径(通过页面栈获取) |
|||
const pages = getCurrentPages(); |
|||
let shouldPause = true; // 默认暂停 |
|||
|
|||
// 如果页面栈还有其他页面,检查上一个页面是否在xqk分包内 |
|||
if (pages.length > 1) { |
|||
const previousPage = pages[pages.length - 2]; |
|||
if (previousPage && previousPage.route) { |
|||
// 如果返回到的页面仍在xqk分包内,不暂停音频 |
|||
if (uni.$xqkAudio.isInXqkPackage(previousPage.route)) { |
|||
shouldPause = false; |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (shouldPause) { |
|||
// 离开xqk分包,暂停音频 |
|||
uni.$xqkAudio.pauseXqkAudio(); |
|||
console.log('xqk chapter1: 离开xqk分包,已暂停音频'); |
|||
} else { |
|||
console.log('xqk chapter1: 仍在xqk分包内,保持音频播放'); |
|||
} |
|||
} catch (error) { |
|||
console.error('xqk chapter1: 音频管理失败:', error); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.swiper { |
|||
width: 100vw; |
|||
height: 100vh; |
|||
} |
|||
.imgJump{ |
|||
position: absolute; |
|||
bottom:53rpx; |
|||
right:0; |
|||
width: 273rpx; |
|||
height: 85rpx; |
|||
opacity:0.9; |
|||
z-index: 999999; |
|||
} |
|||
|
|||
.swiper-item { |
|||
width: 100vw; |
|||
height: 100vh; |
|||
background-size: 100% 100%; |
|||
background-color: #000; |
|||
background-repeat: no-repeat; |
|||
position: relative; |
|||
|
|||
.module1 { |
|||
position: absolute; |
|||
width: 52.1rpx; |
|||
top: 1020rpx; |
|||
left: 235rpx; |
|||
animation: breath1 3s ease-in-out infinite; |
|||
} |
|||
|
|||
.module2 { |
|||
position: absolute; |
|||
width: 52.1rpx; |
|||
top: 760rpx; |
|||
left: 317rpx; |
|||
animation: breath2 4s ease-in-out infinite; |
|||
} |
|||
|
|||
.module3 { |
|||
position: absolute; |
|||
width: 52.1rpx; |
|||
top: 700rpx; |
|||
left: 498rpx; |
|||
animation: breath3 5s ease-in-out infinite; |
|||
} |
|||
|
|||
// 呼吸效果动画 - 不同频率 |
|||
@keyframes breath1 { |
|||
|
|||
0%, |
|||
100% { |
|||
transform: scale(1); |
|||
} |
|||
|
|||
50% { |
|||
transform: scale(1.2); |
|||
} |
|||
} |
|||
|
|||
@keyframes breath2 { |
|||
|
|||
0%, |
|||
100% { |
|||
transform: scale(1); |
|||
} |
|||
|
|||
50% { |
|||
transform: scale(1.2); |
|||
} |
|||
} |
|||
|
|||
@keyframes breath3 { |
|||
|
|||
0%, |
|||
100% { |
|||
transform: scale(1); |
|||
} |
|||
|
|||
50% { |
|||
transform: scale(1.2); |
|||
} |
|||
} |
|||
|
|||
.img4-text { |
|||
width: 428.43rpx; |
|||
position: absolute; |
|||
top: 170rpx; |
|||
left: 100rpx; |
|||
} |
|||
|
|||
.btn-img { |
|||
position: absolute; |
|||
width: 149.8rpx; |
|||
bottom: 290rpx; |
|||
left: 100rpx; |
|||
} |
|||
} |
|||
|
|||
.swiper-img { |
|||
width: 100vw; |
|||
height: 100vh; |
|||
} |
|||
|
|||
.img2-1 { |
|||
width: 267.37rpx; |
|||
position: fixed; |
|||
top: 395rpx; |
|||
left: 70rpx; |
|||
} |
|||
|
|||
.img2-2 { |
|||
width: 332.24rpx; |
|||
position: fixed; |
|||
top: 210rpx; |
|||
left: 360rpx; |
|||
} |
|||
|
|||
.img2-3 { |
|||
width: 600.59rpx; |
|||
position: fixed; |
|||
bottom: 215rpx; |
|||
right: 40rpx; |
|||
} |
|||
</style> |
@ -0,0 +1,173 @@ |
|||
<template> |
|||
<view style="width: 100vw;"> |
|||
<swiper class="swiper" :current="currentIndex" :vertical="true" @change="handleSwiperChange"> |
|||
<swiper-item v-for="(image, index) in swiperImages" :key="index"> |
|||
<view class="swiper-item" :style="{ backgroundImage: `url(${image})` }"> |
|||
<template v-if="index == 1" > |
|||
<image @click="gotoPath('/xqk/chapter5/index')" :src="showImg('/uploads/20250903/dcfd8b8a708f4f2d43edf35a906f75ba.png')" mode="widthFix" class="img1-text"></image> |
|||
</template> |
|||
</view> |
|||
</swiper-item> |
|||
</swiper> |
|||
<AudioControl audioSrc="https://des.js-dyyj.com/data/2025/09/04/fbc13519-cfe5-4088-89b2-59f138bc23cb.MP3" /> |
|||
<NavMenu :nav-index="navIndex" @jump-to-page="handleJumpToPage" /> |
|||
<MusicControl /> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import AudioControl from '@/components/AudioControl.vue'; |
|||
import MusicControl from '@/components/MusicControl.vue'; |
|||
import NavMenu from '../components/NavMenu.vue'; |
|||
export default { |
|||
components: { |
|||
MusicControl, |
|||
NavMenu, |
|||
AudioControl |
|||
}, |
|||
data() { |
|||
return { |
|||
currentIndex: 0, |
|||
navIndex: 2, |
|||
swiperImages: [ |
|||
this.showImg('/uploads/20250903/3bd4fe43f2a6a8806799f06a548f9477.png'), |
|||
this.showImg('/uploads/20250903/8fe8d66210edd96a9f322a661b4d9ba4.png'), |
|||
|
|||
], |
|||
// 控制图片显示的变量 |
|||
showImg7_1: false, |
|||
showImg7_2: false, |
|||
showImg7_3: false, |
|||
// 存储定时器 |
|||
timers: [], |
|||
popupIndex: 1 |
|||
} |
|||
}, |
|||
onLoad(option) { |
|||
this.currentIndex = option.currentIndex || 0 |
|||
// if (this.currentIndex == this.swiperImages.length - 1) this.navIndex = 1; |
|||
}, |
|||
onUnload() { |
|||
// 清除定时器 |
|||
this.timers.forEach(timer => clearTimeout(timer)) |
|||
}, |
|||
methods: { |
|||
handleJumpToPage(idx) { |
|||
}, |
|||
handleSwiperChange(e) { |
|||
this.currentIndex = e.detail.current; |
|||
}, |
|||
// 第八页气泡弹框 |
|||
openPopup(i) { |
|||
this.popupIndex = i |
|||
this.$refs.chapterPopup.open(); |
|||
}, |
|||
getImageUrl(path) { |
|||
if (typeof path === 'object') { |
|||
path = path.url; |
|||
} |
|||
return `https://static.ticket.sz-trip.com/epicSoul/xrcc/chapter2/${path}`; |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.swiper { |
|||
width: 100vw; |
|||
height: 100vh; |
|||
} |
|||
|
|||
.swiper-item { |
|||
width: 100vw; |
|||
height: 100vh; |
|||
background-size: 100% 100%; |
|||
background-color: #000; |
|||
background-repeat: no-repeat; |
|||
position: relative; |
|||
.img1-text{ |
|||
width: 484rpx; |
|||
position: absolute; |
|||
bottom: 100rpx; |
|||
left: 250rpx; |
|||
} |
|||
.img10-text { |
|||
width: 484rpx; |
|||
position: absolute; |
|||
top: 170rpx; |
|||
left: 100rpx; |
|||
} |
|||
|
|||
.btn-img { |
|||
position: absolute; |
|||
width: 149.8rpx; |
|||
bottom: 290rpx; |
|||
left: 100rpx; |
|||
} |
|||
|
|||
.module-img { |
|||
position: absolute; |
|||
left: 0; |
|||
right: 0; |
|||
margin: 0 auto; |
|||
width: 564.25rpx; |
|||
} |
|||
.module1 { |
|||
top: 630rpx; |
|||
} |
|||
.module2 { |
|||
top: 780rpx; |
|||
} |
|||
.module3 { |
|||
top: 930rpx; |
|||
} |
|||
.module4 { |
|||
top: 1080rpx; |
|||
} |
|||
} |
|||
|
|||
.swiper-img { |
|||
width: 100vw; |
|||
height: 100vh; |
|||
} |
|||
|
|||
/* 渐入动画样式 */ |
|||
.fade-in-image { |
|||
/* 初始状态:透明 */ |
|||
opacity: 0; |
|||
/* 添加过渡动画:1秒内透明度从0到1 */ |
|||
animation: fadeIn 1s ease-out forwards; |
|||
/* 根据需要调整图片的定位 */ |
|||
position: absolute; |
|||
} |
|||
|
|||
/* 渐入动画关键帧 */ |
|||
@keyframes fadeIn { |
|||
from { |
|||
opacity: 0; |
|||
transform: translateY(20rpx); /* 可选:添加轻微上移动画 */ |
|||
} |
|||
to { |
|||
opacity: 1; |
|||
transform: translateY(0); |
|||
} |
|||
} |
|||
|
|||
.fade-in-image:nth-child(1) { |
|||
width: 22.97rpx; |
|||
top: 825rpx; |
|||
right: 191rpx; |
|||
} |
|||
|
|||
.fade-in-image:nth-child(2) { |
|||
width: 34.95rpx; |
|||
top: 790rpx; |
|||
right: 170rpx; |
|||
} |
|||
|
|||
.fade-in-image:nth-child(3) { |
|||
width: 144.81rpx; |
|||
top: 680rpx; |
|||
right: 71rpx; |
|||
} |
|||
</style> |
@ -0,0 +1,200 @@ |
|||
<template> |
|||
<view style="width: 100vw; position: relative;"> |
|||
<swiper class="swiper" :current="currentIndex" :vertical="true" @change="handleSwiperChange"> |
|||
<swiper-item v-for="(image, index) in swiperImages" :key="index"> |
|||
<view class="swiper-item" :style="{ backgroundImage: `url(${image})` }"> |
|||
<!-- 第5页内容 --> |
|||
<template v-if="index === 2"> |
|||
<image :src="showImg('/uploads/20250904/3728c0eb6f25e433d539b0c0781039d0.png')" |
|||
mode="widthFix" class="img2-text"></image> |
|||
<image @click="gotoPath('/xqk/chapter4/index')" v-if="currentIndex == swiperImages.length-1" |
|||
:src="showImg('/uploads/20250905/692abbf32b38257ffb2153651f468a63.png')" class="imgJump" |
|||
mode=""></image> |
|||
</template> |
|||
|
|||
</view> |
|||
</swiper-item> |
|||
</swiper> |
|||
<AudioControl audioSrc="https://des.js-dyyj.com/data/2025/09/04/fbc13519-cfe5-4088-89b2-59f138bc23cb.MP3" /> |
|||
<NavMenu :nav-index="navIndex" @jump-to-page="handleJumpToPage" /> |
|||
<MusicControl /> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import AudioControl from '@/components/AudioControl.vue'; |
|||
import MusicControl from '@/components/MusicControl.vue'; |
|||
import NavMenu from '../components/NavMenu.vue'; |
|||
export default { |
|||
components: { |
|||
MusicControl, |
|||
NavMenu, |
|||
AudioControl |
|||
}, |
|||
data() { |
|||
return { |
|||
currentIndex: 0, |
|||
navIndex: 3, |
|||
swiperImages: [ |
|||
this.showImg('/uploads/20250904/c07ff8c707b5368df0faaf5b425e32c3.png'), |
|||
this.showImg('/uploads/20250904/4c2b9c8b647b736f7cb20a42aa5f8fb3.png'), |
|||
this.showImg('/uploads/20250904/c5c2622499089799e6eaf6a704508a07.gif'), |
|||
], |
|||
inputValue: '', |
|||
swipeDirection: '' |
|||
} |
|||
}, |
|||
onLoad(option) { |
|||
this.currentIndex = option.currentIndex || 0 |
|||
if (this.currentIndex == this.swiperImages.length - 1) this.navIndex = 1; |
|||
}, |
|||
methods: { |
|||
handleJumpToPage(idx) { |
|||
this.navIndex = idx |
|||
if (idx == this.swiperImages.length - 1) this.navIndex = idx + 1 |
|||
}, |
|||
handleTouchStart(e) { |
|||
this.startY = e.touches[0].clientY; |
|||
}, |
|||
handleTouchMove(e) { |
|||
const moveY = e.touches[0].clientY; |
|||
this.swipeDirection = moveY < this.startY ? 'down' : 'up'; |
|||
}, |
|||
handleSwiperChange(e) { |
|||
const newIndex = e.detail.current; |
|||
this.currentIndex = newIndex; |
|||
}, |
|||
submit() { |
|||
if (!this.inputValue.trim()) return; |
|||
this.$refs.customPopup.close() |
|||
this.currentIndex = 6; |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
/* 原有样式保持不变 */ |
|||
.swiper { |
|||
width: 100vw; |
|||
height: 100vh; |
|||
} |
|||
.imgJump{ |
|||
position: absolute; |
|||
bottom:100rpx; |
|||
left:247rpx; |
|||
width: 273rpx; |
|||
height: 85rpx; |
|||
opacity: 1; |
|||
z-index: 999999; |
|||
} |
|||
.swiper-item { |
|||
width: 100vw; |
|||
height: 100vh; |
|||
background-size: 100% 100%; |
|||
background-color: #000; |
|||
background-repeat: no-repeat; |
|||
position: relative; |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
.img2-text { |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
width: 407rpx; |
|||
position: absolute; |
|||
top: 500rpx; |
|||
left: 0; |
|||
right: 0; |
|||
margin: 0 auto; |
|||
} |
|||
|
|||
// .img6-btn { |
|||
// position: absolute; |
|||
// width: 520.31rpx; |
|||
// bottom: 210rpx; |
|||
// left: 0; |
|||
// right: 0; |
|||
// margin: 0 auto; |
|||
// } |
|||
|
|||
/* 其他样式保持不变 */ |
|||
.img7-box { |
|||
position: relative; |
|||
top: 170rpx; |
|||
margin: 0 auto; |
|||
} |
|||
|
|||
.img7-textBg { |
|||
background-image: url('https://static.ticket.sz-trip.com/epicSoul/xrcc/chapter3/img7-textBg.png'); |
|||
background-size: 100% 100%; |
|||
padding: 30rpx 50rpx; |
|||
font-size: 50rpx; |
|||
text-align: center; |
|||
color: #fff; |
|||
width: 520.31rpx; |
|||
margin: 0 auto; |
|||
} |
|||
|
|||
.img7-text { |
|||
display: block; |
|||
margin: 60rpx auto 0; |
|||
width: 447.4rpx; |
|||
} |
|||
|
|||
.img8-text { |
|||
width: 379.49rpx; |
|||
position: absolute; |
|||
top: 170rpx; |
|||
left: 100rpx; |
|||
} |
|||
|
|||
.btn-img { |
|||
position: absolute; |
|||
width: 149.8rpx; |
|||
bottom: 290rpx; |
|||
left: 100rpx; |
|||
} |
|||
} |
|||
|
|||
.swipe-blocker { |
|||
position: absolute; |
|||
top: 0; |
|||
left: 0; |
|||
width: 100vw; |
|||
height: 100vh; |
|||
z-index: 9; |
|||
} |
|||
|
|||
.popup-content { |
|||
width: 85vw; |
|||
background-color: #fff; |
|||
border-radius: 16rpx; |
|||
padding: 40rpx 30rpx; |
|||
box-sizing: border-box; |
|||
|
|||
.input-area { |
|||
width: 100%; |
|||
min-height: 180rpx; |
|||
padding: 20rpx; |
|||
border: 2rpx solid #eee; |
|||
border-radius: 8rpx; |
|||
font-size: 28rpx; |
|||
resize: none; |
|||
box-sizing: border-box; |
|||
margin-bottom: 15rpx; |
|||
} |
|||
|
|||
.word-count { |
|||
text-align: right; |
|||
font-size: 24rpx; |
|||
color: #999; |
|||
margin-bottom: 35rpx; |
|||
} |
|||
|
|||
.confirm-btn { |
|||
width: 100%; |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,201 @@ |
|||
<template> |
|||
<view style="width: 100vw;"> |
|||
<swiper class="swiper" :current="currentIndex" :vertical="true" @change="handleSwiperChange"> |
|||
<swiper-item v-for="(image, index) in swiperImages" :key="index"> |
|||
<view class="swiper-item" :style="{ backgroundImage: `url(${image})` }"> |
|||
<template v-if="index === swiperImages.length - 1"> |
|||
<!-- 所有maskImage,每个占一行 --> |
|||
<view class="mask-container"> |
|||
<view class="mask-item" v-for="(maskItem, maskIndex) in maskImage" |
|||
:key="'mask-' + maskIndex" :class="maskIndex == 2?'topImg':''"> |
|||
<image :src="maskItem.main" mode="aspectFill" class="main-image"></image> |
|||
<image v-if="!hiddenMasks[maskIndex]" :src="maskItem.mask" mode="aspectFill" |
|||
class="mask-image" @click="hideMask(maskIndex)" |
|||
:style="maskIndex == 1?'opacity: 0.8':''"></image> |
|||
</view> |
|||
</view> |
|||
<image @click="gotoPath('/xqk/chapter6/index')" v-if="currentIndex == swiperImages.length-1" |
|||
:src="showImg('/uploads/20250905/692abbf32b38257ffb2153651f468a63.png')" class="imgJump" |
|||
mode=""></image> |
|||
</template> |
|||
</view> |
|||
</swiper-item> |
|||
</swiper> |
|||
<AudioControl audioSrc="https://des.js-dyyj.com/data/2025/09/04/fbc13519-cfe5-4088-89b2-59f138bc23cb.MP3" /> |
|||
<NavMenu :nav-index="navIndex" @jump-to-page="handleJumpToPage" /> |
|||
<MusicControl /> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import AudioControl from '@/components/AudioControl.vue'; |
|||
import MusicControl from '@/components/MusicControl.vue'; |
|||
import NavMenu from '../components/NavMenu.vue'; |
|||
export default { |
|||
components: { |
|||
MusicControl, |
|||
NavMenu, |
|||
AudioControl |
|||
}, |
|||
data() { |
|||
return { |
|||
currentIndex: 0, |
|||
navIndex: 4, |
|||
hiddenMasks: [false, false, false, false], // 控制每个遮罩层的显示状态 |
|||
maskImage: [{ |
|||
main: this.showImg('/uploads/20250904/35745646266436c060f56067d96a3e99.png'), |
|||
mask: this.showImg('/uploads/20250904/d6d95bbbc38b1626021d127ccf425091.png'), |
|||
}, |
|||
{ |
|||
main: this.showImg('/uploads/20250904/5cb81451599707b7ff65178b0cc74e7b.png'), |
|||
mask: this.showImg('/uploads/20250904/32caadcddd64aa911270f7c67960df10.png'), |
|||
}, |
|||
{ |
|||
main: this.showImg('/uploads/20250904/c768952bdb4ae59506685fad6416adf0.png'), |
|||
mask: this.showImg('/uploads/20250904/5e37bb76dffb86caa495361503e68cf0.png'), |
|||
}, |
|||
{ |
|||
main: this.showImg('/uploads/20250904/09792d96ceb8503d8ea01fcc1c083399.png'), |
|||
mask: this.showImg('/uploads/20250904/6f3062a2ed9f35aec241b2b0b3d15a58.png'), |
|||
}, |
|||
], |
|||
swiperImages: [ |
|||
this.showImg('/uploads/20250904/49ed0f441175de9fba8ed644962c10a2.png'), |
|||
this.showImg('/uploads/20250904/c9e6fa6112b9803202b6d50591e7b986.png'), |
|||
this.showImg('/uploads/20250904/48c55f56d649a6d1c92d8ed16fb4df08.png'), |
|||
this.showImg('/uploads/20250904/645920955d2dbda9f86422e39f167da1.png'), |
|||
this.showImg('/uploads/20250904/ec18a2d2b6a0836d2b284bf116728441.png'), |
|||
], |
|||
popupIndex: 1 |
|||
} |
|||
}, |
|||
onLoad(option) { |
|||
this.currentIndex = option.currentIndex || 0 |
|||
}, |
|||
methods: { |
|||
handleJumpToPage(idx) { |
|||
|
|||
}, |
|||
handleSwiperChange(e) { |
|||
this.currentIndex = e.detail.current; |
|||
}, |
|||
openPopup(i) { |
|||
this.popupIndex = i |
|||
this.$refs.chapterPopup.open(); |
|||
}, |
|||
hideMask(index) { |
|||
// 隐藏指定索引的遮罩层 |
|||
this.$set(this.hiddenMasks, index, true); |
|||
}, |
|||
getImageUrl(path) { |
|||
if (typeof path === 'object') { |
|||
path = path.url; |
|||
} |
|||
return `https://static.ticket.sz-trip.com/epicSoul/xrcc/chapter4/${path}`; |
|||
} |
|||
}, |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.swiper { |
|||
width: 100vw; |
|||
height: 100vh; |
|||
} |
|||
.imgJump{ |
|||
position: absolute; |
|||
bottom:53rpx; |
|||
right:0; |
|||
width: 273rpx; |
|||
height: 85rpx; |
|||
opacity: 1; |
|||
z-index: 999999; |
|||
} |
|||
.swiper-item { |
|||
width: 100vw; |
|||
height: 100vh; |
|||
background-size: 100% 100%; |
|||
background-color: #000; |
|||
background-repeat: no-repeat; |
|||
position: relative; |
|||
|
|||
.img5-text { |
|||
width: 576.23rpx; |
|||
position: absolute; |
|||
top: 170rpx; |
|||
left: 100rpx; |
|||
} |
|||
|
|||
.btn-img { |
|||
position: absolute; |
|||
width: 149.8rpx; |
|||
bottom: 290rpx; |
|||
left: 100rpx; |
|||
} |
|||
|
|||
.module-box { |
|||
position: absolute; |
|||
top: 460rpx; |
|||
text-align: center; |
|||
|
|||
image { |
|||
width: 650rpx; |
|||
margin-bottom: 80rpx; |
|||
} |
|||
} |
|||
|
|||
// maskImage容器样式 |
|||
.mask-container { |
|||
position: absolute; |
|||
left: 50%; |
|||
top: 50%; |
|||
transform: translate(-50%, -50%); |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
gap: 20rpx; |
|||
padding-top: 20rpx; |
|||
|
|||
.topImg { |
|||
margin-top: 211rpx; |
|||
} |
|||
|
|||
.mask-item { |
|||
position: relative; |
|||
width: 693rpx; |
|||
height: 300rpx; |
|||
border-radius: 10rpx; |
|||
overflow: hidden; |
|||
|
|||
.main-image { |
|||
width: 100%; |
|||
height: 100%; |
|||
position: absolute; |
|||
top: 0; |
|||
left: 0; |
|||
z-index: 1; |
|||
} |
|||
|
|||
.mask-image { |
|||
width: 100%; |
|||
height: 100%; |
|||
position: absolute; |
|||
top: 0; |
|||
left: 0; |
|||
z-index: 2; |
|||
cursor: pointer; |
|||
transition: opacity 0.3s ease; |
|||
|
|||
&:active { |
|||
opacity: 0.8; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.swiper-img { |
|||
width: 100vw; |
|||
height: 100vh; |
|||
} |
|||
</style> |
@ -0,0 +1,180 @@ |
|||
<template> |
|||
<view style="width: 100vw;position: relative;"> |
|||
<!-- <SwipeToNext :is-last-slide="isLastSlide" :always-enable="swiperImages.length === 1" --> |
|||
<!-- :target-path="'/xqk/chapter3/index'" :enable-delay="swiperImages.length > 1"> --> |
|||
<swiper class="swiper" :current="currentIndex" :vertical="true" @change="handleSwiperChange"> |
|||
<swiper-item v-for="(image, index) in swiperImages" :key="index"> |
|||
<view class="swiper-item" :style="{ backgroundImage: `url(${image})` }"> |
|||
|
|||
</view> |
|||
</swiper-item> |
|||
</swiper> |
|||
<image @click="gotoPath('/xqk/chapter3/index')" v-if="currentIndex == swiperImages.length-1" |
|||
:src="showImg('/uploads/20250905/692abbf32b38257ffb2153651f468a63.png')" class="imgJump" |
|||
mode=""></image> |
|||
<!-- </SwipeToNext> --> |
|||
<AudioControl audioSrc="https://des.js-dyyj.com/data/2025/09/04/fbc13519-cfe5-4088-89b2-59f138bc23cb.MP3" /> |
|||
<NavMenu :nav-index="navIndex" @jump-to-page="handleJumpToPage" /> |
|||
<MusicControl /> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import AudioControl from '@/components/AudioControl.vue'; |
|||
import MusicControl from '@/components/MusicControl.vue'; |
|||
import NavMenu from '../components/NavMenu.vue'; |
|||
// import SwipeToNext from '@/components/SwipeToNext.vue'; |
|||
export default { |
|||
components: { |
|||
MusicControl, |
|||
NavMenu, |
|||
// SwipeToNext, |
|||
AudioControl |
|||
}, |
|||
data() { |
|||
return { |
|||
currentIndex: 0, |
|||
navIndex: 2, |
|||
swiperImages: [ |
|||
this.showImg('/uploads/20250903/dd5b260002da55d4c3d56b338451bc11.gif'), |
|||
], |
|||
animateShow: false, |
|||
isLastSlide: false // 是否在最后一页 |
|||
} |
|||
}, |
|||
onLoad(option) { |
|||
this.currentIndex = option.currentIndex || 0 |
|||
// 对于单张图片或初始在最后一页的情况 |
|||
if (this.currentIndex == this.swiperImages.length - 1) { |
|||
this.navIndex = 2; |
|||
this.isLastSlide = true; |
|||
} |
|||
// 如果只有一张图片,也认为是最后一页 |
|||
if (this.swiperImages.length === 1) { |
|||
this.isLastSlide = true; |
|||
} |
|||
}, |
|||
methods: { |
|||
handleJumpToPage(idx) { |
|||
this.navIndex = idx |
|||
if (idx == this.swiperImages.length - 1) this.navIndex = idx + 1 |
|||
}, |
|||
handleSwiperChange(e) { |
|||
this.currentIndex = e.detail.current; |
|||
if (this.currentIndex == this.swiperImages.length - 1) { |
|||
// 判断是否切换到最后一页 |
|||
this.isLastSlide = true; |
|||
} else { |
|||
// 判断是否切换到最后一页 |
|||
this.isLastSlide = false; |
|||
} |
|||
}, |
|||
}, |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.swiper { |
|||
width: 100vw; |
|||
height: 100vh; |
|||
} |
|||
.imgJump{ |
|||
position: absolute; |
|||
bottom:100rpx; |
|||
left:250rpx; |
|||
width: 273rpx; |
|||
height: 85rpx; |
|||
opacity: 0.8; |
|||
z-index: 999999; |
|||
} |
|||
.swiper-item { |
|||
width: 100vw; |
|||
height: 100vh; |
|||
background-size: 100% 100%; |
|||
background-color: #000; |
|||
background-repeat: no-repeat; |
|||
position: relative; |
|||
|
|||
.img2s { |
|||
position: absolute; |
|||
top: 380rpx; |
|||
right: 25rpx; |
|||
width: 358rpx; |
|||
transform: translateX(100%); |
|||
opacity: 0; |
|||
} |
|||
|
|||
.img2-btn { |
|||
width: 558rpx; |
|||
position: absolute; |
|||
left: 0; |
|||
right: 0; |
|||
margin: 0 auto; |
|||
bottom: 70rpx; |
|||
} |
|||
|
|||
.img3-btn { |
|||
width: 558rpx; |
|||
line-height: 72rpx; |
|||
text-align: center; |
|||
border-radius: 20rpx; |
|||
border: 2rpx solid; |
|||
font-size: 30rpx; |
|||
color: #fff; |
|||
position: absolute; |
|||
left: 0; |
|||
right: 0; |
|||
margin: 0 auto; |
|||
bottom: 180rpx; |
|||
} |
|||
|
|||
.flex-column { |
|||
position: absolute; |
|||
bottom: 280rpx; |
|||
width: 100%; |
|||
align-items: center; |
|||
|
|||
.img5-text { |
|||
width: 100%; |
|||
} |
|||
|
|||
.img5-btn { |
|||
width: 230rpx; |
|||
margin-top: 99rpx; |
|||
} |
|||
} |
|||
|
|||
.img6-text { |
|||
width: 408.46rpx; |
|||
position: absolute; |
|||
left: 0; |
|||
right: 0; |
|||
margin: 0 auto; |
|||
bottom: 365rpx |
|||
} |
|||
} |
|||
|
|||
/* 从右往左进入的动画 */ |
|||
.animate-enter-from-right { |
|||
animation: enterFromRight 2s ease-out forwards; |
|||
} |
|||
|
|||
@keyframes enterFromRight { |
|||
0% { |
|||
/* 起始位置:右侧外部 */ |
|||
transform: translateX(100%); |
|||
opacity: 0; |
|||
} |
|||
|
|||
100% { |
|||
/* 结束位置:正常位置 */ |
|||
transform: translateX(0); |
|||
opacity: 1; |
|||
} |
|||
} |
|||
|
|||
.swiper-img { |
|||
width: 100vw; |
|||
height: 100vh; |
|||
} |
|||
</style> |
@ -0,0 +1,190 @@ |
|||
<template> |
|||
<view style="width: 100vw;"> |
|||
<swiper class="swiper" :current="currentIndex" :vertical="true" @change="handleSwiperChange"> |
|||
<swiper-item v-for="(image, index) in swiperImages" :key="index"> |
|||
<view class="swiper-item" :style="{ backgroundImage: `url(${image})` }"> |
|||
<template v-if="index == 2"> |
|||
<view class="item-box"> |
|||
<view class="box-two"> |
|||
<image @click="gotoPath('/subPackages/techan/detail?id=40')" :src="showImg('/uploads/20250904/0bfd974c3fc411811463e066c96f3d35.png')" |
|||
mode="widthFix" class="img2-text"></image> |
|||
<image @click="gotoPath('/subPackages/techan/detail?id=40')" style="margin-left: 39rpx;" :src="showImg('/uploads/20250904/262814b745a934c3d5bdd114109ff564.png')" |
|||
mode="widthFix" class="img2-text"></image> |
|||
</view> |
|||
<view class="box-two" style="margin-top: 46rpx;"> |
|||
<image @click="gotoPath('/subPackages/techan/detail?id=40')" :src="showImg('/uploads/20250904/425a5716993c38bf8e4a6a66882d8685.png')" |
|||
mode="widthFix" class="img2-text"></image> |
|||
<image @click="gotoPath('/subPackages/techan/detail?id=40')" style="margin-left: 39rpx;" :src="showImg('/uploads/20250904/728878284ea30f148bc87376d85684d9.png')" |
|||
mode="widthFix" class="img2-text"></image> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
<template v-if="index== 3"> |
|||
<image style="width: 100vw;height: 100vh;padding-top: env(safe-area-inset-top);padding-bottom: env(safe-area-inset-bottom);" :src="showImg('/uploads/20250904/f5926e170dc0dc886a33422a334dae56.png')" :show-menu-by-longpress="true" mode="widthFix"></image> |
|||
</template> |
|||
</view> |
|||
</swiper-item> |
|||
</swiper> |
|||
<NavMenu :nav-index="navIndex" @jump-to-page="handleJumpToPage" /> |
|||
<AudioControl audioSrc="https://des.js-dyyj.com/data/2025/09/04/fbc13519-cfe5-4088-89b2-59f138bc23cb.MP3" /> |
|||
<MusicControl /> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import AudioControl from '@/components/AudioControl.vue'; |
|||
import MusicControl from '@/components/MusicControl.vue'; |
|||
import NavMenu from '../components/NavMenu.vue'; |
|||
export default { |
|||
components: { |
|||
MusicControl, |
|||
NavMenu, |
|||
AudioControl |
|||
}, |
|||
data() { |
|||
return { |
|||
currentIndex: 0, |
|||
navIndex: 5, |
|||
swiperImages: [ |
|||
this.showImg('/uploads/20250904/07e27812a048efbef08593649c9e6504.png'), |
|||
this.showImg('/uploads/20250904/5c32fa0bc571085d2c4f2b03fd6291a5.png'), |
|||
this.showImg('/uploads/20250904/c282ee859a5da1e1eb0cc1ded809dd09.png'), |
|||
, |
|||
], |
|||
inputValue: '', |
|||
inputValues: '', |
|||
} |
|||
}, |
|||
methods: { |
|||
handleJumpToPage(idx) { |
|||
|
|||
}, |
|||
handleSwiperChange(e) { |
|||
this.currentIndex = e.detail.current; |
|||
}, |
|||
}, |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.swiper { |
|||
width: 100vw; |
|||
height: 100vh; |
|||
} |
|||
|
|||
.swiper-item { |
|||
width: 100vw; |
|||
height: 100vh; |
|||
background-size: 100% 100%; |
|||
background-color: #000; |
|||
background-repeat: no-repeat; |
|||
position: relative; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
.item-box{ |
|||
.box-two{ |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
|
|||
} |
|||
position: absolute; |
|||
top:289rpx; |
|||
left:99rpx; |
|||
.img2-text{ |
|||
width: 257rpx; |
|||
} |
|||
} |
|||
.btn-img { |
|||
position: absolute; |
|||
width: 149.8rpx; |
|||
bottom: 290rpx; |
|||
left: 84rpx; |
|||
} |
|||
|
|||
.img7-1, |
|||
.img7-2 { |
|||
width: 437.67rpx; |
|||
position: absolute; |
|||
left: 0; |
|||
right: 0; |
|||
margin: 0 auto; |
|||
top: 365rpx; |
|||
} |
|||
|
|||
.bgm-box { |
|||
width: 437.67rpx; |
|||
position: absolute; |
|||
left: 0; |
|||
right: 0; |
|||
bottom: 650rpx; |
|||
margin: 0 auto; |
|||
flex-wrap: wrap; |
|||
|
|||
view { |
|||
width: 207.89rpx; |
|||
line-height: 42.77rpx; |
|||
border-radius: 2rpx; |
|||
text-align: center; |
|||
color: #fff; |
|||
font-size: 20rpx; |
|||
border: 1rpx solid #fff; |
|||
} |
|||
|
|||
view:nth-child(n+3) { |
|||
margin-top: 17rpx; |
|||
} |
|||
|
|||
.bgm-active { |
|||
border-color: #00C48C; |
|||
color: #00C48C; |
|||
} |
|||
} |
|||
|
|||
.img7-btn { |
|||
width: 439.66rpx; |
|||
position: absolute; |
|||
left: 0; |
|||
right: 0; |
|||
bottom: 244rpx; |
|||
margin: 0 auto; |
|||
} |
|||
} |
|||
|
|||
.swiper-img { |
|||
width: 100vw; |
|||
height: 100vh; |
|||
} |
|||
|
|||
.popup-content { |
|||
width: 85vw; |
|||
background-color: #fff; |
|||
border-radius: 16rpx; |
|||
padding: 40rpx 30rpx; |
|||
box-sizing: border-box; |
|||
|
|||
.input-area { |
|||
width: 100%; |
|||
min-height: 180rpx; |
|||
padding: 20rpx; |
|||
border: 2rpx solid #747c8e; |
|||
border-radius: 8rpx; |
|||
font-size: 28rpx; |
|||
resize: none; |
|||
box-sizing: border-box; |
|||
margin-bottom: 15rpx; |
|||
} |
|||
|
|||
.word-count { |
|||
text-align: right; |
|||
font-size: 24rpx; |
|||
color: #999; |
|||
margin-bottom: 35rpx; |
|||
} |
|||
|
|||
.confirm-btn { |
|||
width: 100%; |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,248 @@ |
|||
<template> |
|||
<view> |
|||
<view class="overlay" v-if="showMenu" @click="onCloseMenu"></view> |
|||
<view class="fixed-nav" :class="{'hidden': showMenu}" @click="onShowMenu"> |
|||
<image class="nav-icon" :class="{'rotated': iconRotated, 'bounce-back': iconBounceBack}" :src="navIconSrc" |
|||
mode="aspectFill"></image> |
|||
</view> |
|||
<view class="nav-menu" :class="{'show': showMenu}"> |
|||
<view class="nav-item" :class="{'item-active': isItemActive(item)}" v-for="item in menuItems" |
|||
:key="item.targetIndex" @click="() => onJumpToPage(item)"> |
|||
<view v-if="item.text.includes('#Chapter')" class="chapter-text"> |
|||
<text class="chapter-title">#Chapter</text> |
|||
<text :class="{'active': isItemActive(item)}" class="chapter-number"> |
|||
{{ item.text.replace('#Chapter', '') }} |
|||
</text> |
|||
</view> |
|||
<text v-else :class="{'active': isItemActive(item)}">{{ item.text }}</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
props: { |
|||
// 当前激活的索引 |
|||
navIndex: { |
|||
type: Number, |
|||
required: true |
|||
}, |
|||
// 导航图标地址 |
|||
navIconSrc: { |
|||
type: String, |
|||
default: 'https://static.ticket.sz-trip.com/epicSoul/taozi/nav-icon.png' |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
showMenu: false, |
|||
iconRotated: false, |
|||
iconBounceBack: false, |
|||
menuItems: [{ |
|||
text: 'INTRO序曲', |
|||
targetIndex: 0, |
|||
path: "/xqk/home/home" |
|||
}, |
|||
{ |
|||
text: '01 青壳初生', |
|||
targetIndex: 1, |
|||
path: "/xqk/chapter1/index" |
|||
}, |
|||
{ |
|||
text: '02 负海志 向湖生', |
|||
targetIndex: 2, |
|||
path: "/xqk/chapter2/index" |
|||
}, |
|||
{ |
|||
text: '03 名曰江湖', |
|||
targetIndex: 3, |
|||
path: "/xqk/chapter3/index" |
|||
}, |
|||
{ |
|||
text: '04 蟹在人间', |
|||
targetIndex: 4, |
|||
path: "/xqk/chapter4/index" |
|||
}, |
|||
{ |
|||
text: '05 共济', |
|||
targetIndex: 5, |
|||
path: "/xqk/chapter6/index" |
|||
}, |
|||
{ |
|||
text: '有感商品', |
|||
targetIndex: 6, |
|||
path: "/subPackages/techan/detail?id=40" |
|||
}, |
|||
{ |
|||
text: '购物车', |
|||
targetIndex: 7, |
|||
path: "/subPackages/user/gwc" |
|||
} |
|||
], |
|||
}; |
|||
}, |
|||
watch: { |
|||
navIndex(newVal) { |
|||
console.log(newVal) |
|||
if(newVal) this.navIndex = newVal |
|||
} |
|||
}, |
|||
methods: { |
|||
onShowMenu() { |
|||
this.iconRotated = true; |
|||
setTimeout(() => { |
|||
this.showMenu = true; |
|||
this.$emit('menu-show'); |
|||
}, 300); |
|||
}, |
|||
onCloseMenu() { |
|||
this.showMenu = false; |
|||
setTimeout(() => { |
|||
this.iconBounceBack = true; |
|||
this.iconRotated = false; |
|||
setTimeout(() => { |
|||
this.iconBounceBack = false; |
|||
}, 500); |
|||
}, 300); |
|||
this.$emit('menu-hide'); |
|||
}, |
|||
onJumpToPage(item) { |
|||
console.log(this.navIndex,item.targetIndex,item.path) |
|||
if(item.path && item.targetIndex != this.navIndex) this.gotoPath(item.path) |
|||
this.onCloseMenu(); |
|||
}, |
|||
isItemActive(item) { |
|||
return this.navIndex === item.targetIndex; |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.overlay { |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
background-color: rgba(0, 0, 0, 0.3); |
|||
z-index: 100; |
|||
} |
|||
|
|||
.fixed-nav { |
|||
width: 80rpx; |
|||
height: 80rpx; |
|||
background-color: rgb(0 0 0 / 0.7); |
|||
border-radius: 10rpx 0 0 10rpx; |
|||
position: fixed; |
|||
right: 0; |
|||
top: 0; |
|||
bottom: 0; |
|||
margin: auto 0; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
z-index: 9; |
|||
transition: transform 0.3s ease, opacity 0.3s ease; |
|||
} |
|||
|
|||
.fixed-nav.hidden { |
|||
transform: translateX(100%); |
|||
opacity: 0; |
|||
pointer-events: none; |
|||
} |
|||
|
|||
.nav-icon { |
|||
width: 35rpx; |
|||
height: 35rpx; |
|||
transition: transform 0.3s ease; |
|||
} |
|||
|
|||
.nav-icon.rotated { |
|||
transform: rotate(180deg); |
|||
} |
|||
|
|||
.nav-icon.bounce-back { |
|||
animation: bounceRotation 0.5s ease; |
|||
} |
|||
|
|||
@keyframes bounceRotation { |
|||
0% { |
|||
transform: rotate(180deg); |
|||
} |
|||
|
|||
50% { |
|||
transform: rotate(-20deg); |
|||
} |
|||
|
|||
75% { |
|||
transform: rotate(10deg); |
|||
} |
|||
|
|||
100% { |
|||
transform: rotate(0deg); |
|||
} |
|||
} |
|||
|
|||
.nav-menu { |
|||
position: fixed; |
|||
top: 50%; |
|||
right: 0; |
|||
transform: translate(100%, -50%); |
|||
z-index: 999; |
|||
background-color: rgba(255, 255, 255, 0.95); |
|||
border-radius: 16rpx 0 0 16rpx; |
|||
box-shadow: -4px 0 15px rgba(0, 0, 0, 0.1); |
|||
transition: transform 0.3s ease; |
|||
} |
|||
|
|||
.nav-menu.show { |
|||
transform: translate(0, -50%); |
|||
} |
|||
|
|||
.nav-item { |
|||
padding: 20rpx; |
|||
text-align: center; |
|||
|
|||
text { |
|||
color: #333; |
|||
opacity: 0.7; |
|||
font-size: 28rpx; |
|||
} |
|||
} |
|||
|
|||
.item-active { |
|||
background-color: rgba(0, 0, 0, 0.1); |
|||
} |
|||
|
|||
.nav-item .active { |
|||
color: #333; |
|||
opacity: 1; |
|||
} |
|||
|
|||
.chapter-text { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
line-height: 1.3; |
|||
} |
|||
|
|||
.chapter-title { |
|||
color: #fff; |
|||
opacity: 0.7; |
|||
font-size: 24rpx; |
|||
} |
|||
|
|||
.chapter-number { |
|||
color: #fff; |
|||
opacity: 0.7; |
|||
font-size: 28rpx; |
|||
margin-top: 8rpx; |
|||
} |
|||
|
|||
.item-active .chapter-title, |
|||
.item-active .chapter-number.active { |
|||
opacity: 1; |
|||
} |
|||
</style> |
@ -0,0 +1,95 @@ |
|||
<template> |
|||
<view class="gif-container"> |
|||
<view class="dynamic-container"> |
|||
<image |
|||
:src="gifSrc" |
|||
mode="widthFix" |
|||
class="gif-image" |
|||
:style="{ display: isPlaying ? 'block' : 'none' }" |
|||
@load="startGifPlay" |
|||
></image> |
|||
<image |
|||
:src="staticCover" |
|||
mode="widthFix" |
|||
class="gif-image" |
|||
:style="{ display: isPlaying ? 'none' : 'block' }" |
|||
></image> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
props: { |
|||
// GIF图片路径 |
|||
gifSrc: { |
|||
type: String, |
|||
required: true |
|||
}, |
|||
// 静态封面图 |
|||
staticCover: { |
|||
type: String, |
|||
default: '' |
|||
}, |
|||
// GIF播放时长(毫秒) |
|||
duration: { |
|||
type: Number, |
|||
default: 2000 |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
isPlaying: false, |
|||
playTimer: null |
|||
} |
|||
}, |
|||
methods: { |
|||
// 开始播放GIF |
|||
startGifPlay() { |
|||
this.isPlaying = true; |
|||
|
|||
// 清除之前的定时器 |
|||
if (this.playTimer) { |
|||
clearTimeout(this.playTimer); |
|||
} |
|||
|
|||
// 播放完成后显示静态封面 |
|||
this.playTimer = setTimeout(() => { |
|||
this.isPlaying = false; |
|||
}, this.duration); |
|||
} |
|||
}, |
|||
onUnload() { |
|||
// 页面卸载时清除定时器 |
|||
if (this.playTimer) { |
|||
clearTimeout(this.playTimer); |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.gif-container { |
|||
width: 100vw; |
|||
display: flex; |
|||
justify-content: center; |
|||
} |
|||
|
|||
.gif-image { |
|||
width: 100vw; |
|||
height: auto; |
|||
} |
|||
|
|||
.dynamic-container { |
|||
position: relative; |
|||
width: 100vw; |
|||
display: flex; |
|||
justify-content: center; |
|||
} |
|||
|
|||
.dynamic-container image { |
|||
position: absolute; |
|||
top: 0; |
|||
left: 0; |
|||
} |
|||
</style> |
@ -0,0 +1,183 @@ |
|||
<template> |
|||
<view style="width: 100vw;position: relative;"> |
|||
<!-- <SinglePlayGif |
|||
gifSrc="https://static.ticket.sz-trip.com/epicSoul/xrcc/home/img1.gif" |
|||
staticCover="https://static.ticket.sz-trip.com/epicSoul/xrcc/home/img1.png" |
|||
duration="5000" |
|||
/> --> |
|||
<!-- 触底方法跳转页面组件 --> |
|||
<!-- <SwipeToNext :is-last-slide="isLastSlide" :always-enable="swiperImages.length === 1" |
|||
:target-path="'/xqk/chapter1/index'" :enable-delay="swiperImages.length > 1" |
|||
@swipe-to-next="handleSwipeToNext"> --> |
|||
<swiper class="swiper" :current="currentIndex" :vertical="true" @change="handleSwiperChange"> |
|||
<swiper-item v-for="(image, index) in swiperImages" :key="index"> |
|||
<view class="swiper-item" :style="{ backgroundImage: `url(${image})` }"> |
|||
<template v-if="index == 0" > |
|||
<image :src="showImg('/uploads/20250905/75a01f24cf88d71499d8ae1bf256e913.png')" style="width: 678rpx;" mode="widthFix" ></image> |
|||
</template> |
|||
</view> |
|||
</swiper-item> |
|||
</swiper> |
|||
<!-- </SwipeToNext> --> |
|||
<image @click="gotoPath('/xqk/chapter1/index')" :src="showImg('/uploads/20250905/692abbf32b38257ffb2153651f468a63.png')" class="imgJump" mode=""></image> |
|||
<NavMenu :nav-index="navIndex" @jump-to-page="handleJumpToPage" /> |
|||
<AudioControl audioSrc="https://des.js-dyyj.com/data/2025/09/04/fbc13519-cfe5-4088-89b2-59f138bc23cb.MP3" /> |
|||
<MusicControl /> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import AudioControl from '@/components/AudioControl.vue'; |
|||
import MusicControl from '@/components/MusicControl.vue'; |
|||
import SinglePlayGif from '../components/SinglePlayGif.vue'; |
|||
import NavMenu from '../components/NavMenu.vue'; |
|||
// import SwipeToNext from '@/components/SwipeToNext.vue'; |
|||
export default { |
|||
components: { |
|||
MusicControl, |
|||
SinglePlayGif, |
|||
NavMenu, |
|||
// SwipeToNext, |
|||
AudioControl |
|||
}, |
|||
data() { |
|||
return { |
|||
isPlaying: false, |
|||
playTimer: null, |
|||
duration: 5000, |
|||
currentIndex: 0, |
|||
navIndex: 0, |
|||
audioUrl: 'https://des.js-dyyj.com/data/2025/09/04/fbc13519-cfe5-4088-89b2-59f138bc23cb.MP3', |
|||
swiperImages: [ |
|||
this.showImg('/uploads/20250904/e734717a187c357b63d64d6214de0ca1.gif'), |
|||
// this.showImg('/uploads/20250903/24303e4b7218eaf3d857c846417eb490.png'), |
|||
// this.showImg('/uploads/20250903/17495ef65648c64c31920d312301e991.png'), |
|||
// this.showImg('/uploads/20250903/92d6f1c6f8f7de040f3c31c8faf98927.png'), |
|||
], |
|||
isLastSlide: false // 是否在最后一页 |
|||
} |
|||
}, |
|||
onLoad(option) { |
|||
this.currentIndex = option.currentIndex || 0 |
|||
// 对于单张图片或初始在最后一页的情况 |
|||
if (this.currentIndex == this.swiperImages.length - 1) { |
|||
// this.navIndex = 1; |
|||
this.isLastSlide = true; |
|||
} |
|||
// 如果只有一张图片,也认为是最后一页 |
|||
if (this.swiperImages.length === 1) { |
|||
this.isLastSlide = true; |
|||
} |
|||
}, |
|||
onShow() { |
|||
const app = getApp(); |
|||
app.updateMusicSrc('https://des.js-dyyj.com/data/2025/09/04/bb2921f6-eeac-4f21-b2b7-11c1e3138976.mp3'); |
|||
app.initBackgroundMusic(); // 初始化背景音乐 |
|||
uni.$bgMusic.play(); // 播放音乐 |
|||
}, |
|||
methods: { |
|||
handleJumpToPage(idx) { |
|||
this.navIndex = idx |
|||
if (idx == this.swiperImages.length - 1) this.navIndex = idx + 1 |
|||
}, |
|||
handleSwiperChange(e) { |
|||
console.log(e); |
|||
this.currentIndex = e.detail.current; |
|||
if (this.currentIndex == this.swiperImages.length - 1) { |
|||
this.navIndex = 1; |
|||
// 判断是否切换到最后一页 |
|||
this.isLastSlide = true; |
|||
} else { |
|||
this.navIndex = 0; |
|||
this.isLastSlide = false; |
|||
} |
|||
}, |
|||
// 处理滑动跳转事件 |
|||
handleSwipeToNext(targetPath) { |
|||
console.log('收到滑动跳转事件,目标路径:', targetPath); |
|||
// 可以在这里添加额外的逻辑,比如数据统计等 |
|||
}, |
|||
// <!--微信分享配置-- > |
|||
// #ifdef MP-WEIXIN |
|||
onShareAppMessage() { |
|||
return { |
|||
title: '一只蟹的生命远征|「Epic Soul」阅读体 issue06', |
|||
mpId: 'wx8954209bb3ad489e', |
|||
path: '/xqk/home/home', |
|||
imageUrl: this.showImg('/uploads/20250903/66ff1f3cd63ea776a0203e8e0dd92dda.jpg') |
|||
}; |
|||
}, |
|||
onShareTimeline() { |
|||
return { |
|||
title: '一只蟹的生命远征|「Epic Soul」阅读体 issue06', |
|||
query: '', |
|||
imageUrl: this.showImg('/uploads/20250903/66ff1f3cd63ea776a0203e8e0dd92dda.jpg') |
|||
}; |
|||
} |
|||
// #endif |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.swiper { |
|||
width: 100vw; |
|||
height: 100vh; |
|||
} |
|||
.imgJump{ |
|||
position: absolute; |
|||
bottom:120rpx; |
|||
left:247rpx; |
|||
width: 273rpx; |
|||
height: 85rpx; |
|||
opacity: 1; |
|||
z-index: 999999; |
|||
} |
|||
.swiper-item { |
|||
/* 新增安全区域适配 */ |
|||
padding-top: env(safe-area-inset-top); |
|||
/* 顶部安全距离 */ |
|||
padding-bottom: env(safe-area-inset-bottom); |
|||
/* 底部安全距离 */ |
|||
box-sizing: border-box; |
|||
/* 修改背景尺寸为覆盖模式 */ |
|||
background-size: cover; |
|||
width: 100vw; |
|||
height: 100vh; |
|||
// background-size: 100% auto; |
|||
background-position: center center; |
|||
background-color: #000; |
|||
background-repeat: no-repeat; |
|||
position: relative; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
.img1-text { |
|||
position: absolute; |
|||
width: 632.16rpx; |
|||
top: 170rpx; |
|||
left: 0; |
|||
right: 0; |
|||
margin: 0 auto; |
|||
} |
|||
|
|||
.img4-text { |
|||
position: absolute; |
|||
width: 476.36rpx; |
|||
top: 170rpx; |
|||
left: 84rpx; |
|||
} |
|||
|
|||
.btn-img { |
|||
position: absolute; |
|||
width: 149.8rpx; |
|||
bottom: 290rpx; |
|||
left: 84rpx; |
|||
} |
|||
} |
|||
|
|||
.swiper-img { |
|||
width: 100vw; |
|||
height: 100vh; |
|||
} |
|||
</style> |
Loading…
Reference in new issue