|
|
@ -1,27 +1,23 @@ |
|
|
|
<template> |
|
|
|
<view class="container"> |
|
|
|
<!-- 歌词显示区域 --> |
|
|
|
<scroll-view |
|
|
|
class="lyrics-container" |
|
|
|
:show-scrollbar="false" |
|
|
|
enhanced |
|
|
|
scroll-y |
|
|
|
:scroll-top="scrollTop" |
|
|
|
scroll-with-animation |
|
|
|
:style="{ height: scrollViewHeight + 'px' }" |
|
|
|
> |
|
|
|
<view class="lyrics-container"> |
|
|
|
<view class="lyrics-content"> |
|
|
|
<text |
|
|
|
v-for="(sentence, index) in currentChapter.sentences" |
|
|
|
:key="index" |
|
|
|
:id="`sentence-${index}`" |
|
|
|
class="lyric-line" |
|
|
|
:class="{ active: index === activeSentenceIndex }" |
|
|
|
> |
|
|
|
{{ sentence.FinalSentence }} |
|
|
|
</text> |
|
|
|
<!-- 当前句子显示 --> |
|
|
|
<view class="current-sentence"> |
|
|
|
<text class="lyric-line active"> |
|
|
|
{{ currentSentence ? currentSentence.FinalSentence : "" }} |
|
|
|
</text> |
|
|
|
</view> |
|
|
|
|
|
|
|
<!-- 句子指示器 --> |
|
|
|
<view class="sentence-indicator"> |
|
|
|
<text class="sentence-text"> |
|
|
|
{{ currentSentenceIndex + 1 }}/{{ totalSentences }} |
|
|
|
</text> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
</scroll-view> |
|
|
|
</view> |
|
|
|
|
|
|
|
<!-- 播放控制区域 --> |
|
|
|
<view class="player-controls"> |
|
|
@ -54,6 +50,29 @@ |
|
|
|
</view> |
|
|
|
<text class="time-text">{{ formatTime(duration) }}</text> |
|
|
|
</view> |
|
|
|
<!-- 句子控制按钮 --> |
|
|
|
<view class="sentence-controls"> |
|
|
|
<view |
|
|
|
class="control-btn" |
|
|
|
@click="prevSentence" |
|
|
|
:disabled="currentSentenceIndex === 0" |
|
|
|
> |
|
|
|
<text class="control-icon-text">◀</text> |
|
|
|
</view> |
|
|
|
<view class="sentence-info"> |
|
|
|
<text class="sentence-info-text" |
|
|
|
>{{ currentSentenceIndex + 1 }}/{{ totalSentences }}</text |
|
|
|
> |
|
|
|
</view> |
|
|
|
<view |
|
|
|
class="control-btn" |
|
|
|
@click="nextSentence" |
|
|
|
:disabled="currentSentenceIndex >= totalSentences - 1" |
|
|
|
> |
|
|
|
<text class="control-icon-text">▶</text> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
|
|
|
|
<!-- 底部控制按钮 --> |
|
|
|
<view class="bottom-controls"> |
|
|
|
<view class="control-btn"> |
|
|
@ -114,6 +133,8 @@ export default { |
|
|
|
activeSentenceIndex: -1, |
|
|
|
scrollTop: 0, |
|
|
|
scrollViewHeight: 0, |
|
|
|
// 句子相关数据 |
|
|
|
currentSentenceIndex: 0, |
|
|
|
}; |
|
|
|
}, |
|
|
|
mounted() { |
|
|
@ -153,6 +174,13 @@ export default { |
|
|
|
durationFormatted() { |
|
|
|
return this.formatTime(this.duration); |
|
|
|
}, |
|
|
|
// 句子相关计算属性 |
|
|
|
totalSentences() { |
|
|
|
return this.currentChapter.sentences.length; |
|
|
|
}, |
|
|
|
currentSentence() { |
|
|
|
return this.currentChapter.sentences[this.currentSentenceIndex] || null; |
|
|
|
}, |
|
|
|
}, |
|
|
|
methods: { |
|
|
|
togglePlay() { |
|
|
@ -181,7 +209,7 @@ export default { |
|
|
|
this.currentTime = 0; |
|
|
|
this.duration = 0; |
|
|
|
this.activeSentenceIndex = -1; |
|
|
|
this.scrollTop = 0; |
|
|
|
this.currentSentenceIndex = 0; |
|
|
|
|
|
|
|
// 更新背景音频信息 |
|
|
|
const currentChapter = this.chapters[this.currentChapterIndex]; |
|
|
@ -244,7 +272,7 @@ export default { |
|
|
|
.exec(); |
|
|
|
}, |
|
|
|
updateSentenceHighlight(currentTimeSeconds) { |
|
|
|
// 根据指定时间找到对应的句子并更新高亮和滚动位置 |
|
|
|
// 根据指定时间找到对应的句子并更新当前句子 |
|
|
|
const sentences = this.currentChapter.sentences; |
|
|
|
let targetIndex = -1; |
|
|
|
|
|
|
@ -278,51 +306,10 @@ export default { |
|
|
|
|
|
|
|
// 只有当索引真正发生变化时才更新 |
|
|
|
if (targetIndex >= 0 && targetIndex !== this.activeSentenceIndex) { |
|
|
|
const previousIndex = this.activeSentenceIndex; |
|
|
|
this.activeSentenceIndex = targetIndex; |
|
|
|
|
|
|
|
// 使用$nextTick确保DOM更新后再计算滚动位置 |
|
|
|
this.$nextTick(() => { |
|
|
|
this.scrollToSentence(targetIndex); |
|
|
|
}); |
|
|
|
this.currentSentenceIndex = targetIndex; |
|
|
|
} |
|
|
|
}, |
|
|
|
scrollToSentence(index) { |
|
|
|
// 动态获取句子位置并滚动到可视区域 |
|
|
|
const query = uni.createSelectorQuery().in(this); |
|
|
|
|
|
|
|
// 获取目标句子的位置信息 |
|
|
|
query.select(`#sentence-${index}`).boundingClientRect(); |
|
|
|
// 获取滚动容器的位置信息 |
|
|
|
query.select(".lyrics-content").boundingClientRect(); |
|
|
|
|
|
|
|
query.exec((res) => { |
|
|
|
if (res && res[0] && res[1]) { |
|
|
|
const sentenceRect = res[0]; |
|
|
|
const containerRect = res[1]; |
|
|
|
|
|
|
|
// 计算句子相对于容器顶部的位置 |
|
|
|
const sentenceTop = sentenceRect.top - containerRect.top; |
|
|
|
|
|
|
|
// 计算滚动位置,让句子显示在可视区域中央 |
|
|
|
const scrollPosition = Math.max( |
|
|
|
0, |
|
|
|
sentenceTop - this.scrollViewHeight / 2 + sentenceRect.height / 2 |
|
|
|
); |
|
|
|
|
|
|
|
// 更新滚动位置 |
|
|
|
this.scrollTop = scrollPosition; |
|
|
|
} else { |
|
|
|
// 如果无法获取元素位置,使用估算方式 |
|
|
|
const estimatedHeight = 60; |
|
|
|
const scrollPosition = Math.max( |
|
|
|
0, |
|
|
|
index * estimatedHeight - this.scrollViewHeight / 2 |
|
|
|
); |
|
|
|
this.scrollTop = scrollPosition; |
|
|
|
} |
|
|
|
}); |
|
|
|
}, |
|
|
|
formatTime(seconds) { |
|
|
|
const mins = Math.floor(seconds / 60); |
|
|
|
const secs = Math.floor(seconds % 60); |
|
|
@ -370,6 +357,31 @@ export default { |
|
|
|
// 计算内容区域可用高度 |
|
|
|
this.scrollViewHeight = windowHeight - controlsHeight - 60; // 60px为额外边距 |
|
|
|
}, |
|
|
|
// 句子控制方法 |
|
|
|
prevSentence() { |
|
|
|
if (this.currentSentenceIndex > 0) { |
|
|
|
this.currentSentenceIndex--; |
|
|
|
this.activeSentenceIndex = this.currentSentenceIndex; |
|
|
|
// 跳转到当前句子的开始时间 |
|
|
|
const sentence = |
|
|
|
this.currentChapter.sentences[this.currentSentenceIndex]; |
|
|
|
if (sentence && this.$refs.audioPlayer) { |
|
|
|
this.$refs.audioPlayer.seek(sentence.startTimeInSeconds); |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
nextSentence() { |
|
|
|
if (this.currentSentenceIndex < this.totalSentences - 1) { |
|
|
|
this.currentSentenceIndex++; |
|
|
|
this.activeSentenceIndex = this.currentSentenceIndex; |
|
|
|
// 跳转到当前句子的开始时间 |
|
|
|
const sentence = |
|
|
|
this.currentChapter.sentences[this.currentSentenceIndex]; |
|
|
|
if (sentence && this.$refs.audioPlayer) { |
|
|
|
this.$refs.audioPlayer.seek(sentence.startTimeInSeconds); |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
}, |
|
|
|
}; |
|
|
|
</script> |
|
|
@ -423,26 +435,41 @@ export default { |
|
|
|
/* 歌词容器样式 */ |
|
|
|
.lyrics-container { |
|
|
|
flex: 1; |
|
|
|
padding: 0 20rpx; |
|
|
|
padding: 0 60rpx; |
|
|
|
margin-bottom: 20rpx; |
|
|
|
width: 710rpx; |
|
|
|
display: flex; |
|
|
|
flex-direction: column; |
|
|
|
justify-content: center; |
|
|
|
} |
|
|
|
|
|
|
|
.lyrics-content { |
|
|
|
display: flex; |
|
|
|
flex-direction: column; |
|
|
|
align-items: center; |
|
|
|
padding: 40rpx 0; |
|
|
|
justify-content: center; |
|
|
|
height: 100%; |
|
|
|
position: relative; |
|
|
|
} |
|
|
|
|
|
|
|
.current-sentence { |
|
|
|
display: flex; |
|
|
|
flex-direction: column; |
|
|
|
align-items: center; |
|
|
|
justify-content: center; |
|
|
|
width: 100%; |
|
|
|
height: 100%; |
|
|
|
} |
|
|
|
|
|
|
|
.lyric-line { |
|
|
|
display: block; |
|
|
|
text-align: center; |
|
|
|
font-size: 32rpx; |
|
|
|
color: #999; |
|
|
|
line-height: 1.6; |
|
|
|
margin: 16rpx 0; |
|
|
|
font-size: 36rpx; |
|
|
|
color: #333; |
|
|
|
line-height: 1.8; |
|
|
|
margin: 0; |
|
|
|
transition: all 0.3s ease; |
|
|
|
padding: 20rpx; |
|
|
|
font-weight: 600; |
|
|
|
} |
|
|
|
|
|
|
|
.lyric-line.active { |
|
|
@ -451,6 +478,21 @@ export default { |
|
|
|
font-size: 36rpx; |
|
|
|
} |
|
|
|
|
|
|
|
.sentence-indicator { |
|
|
|
position: absolute; |
|
|
|
bottom: 20rpx; |
|
|
|
right: 20rpx; |
|
|
|
background: rgba(0, 0, 0, 0.1); |
|
|
|
padding: 8rpx 16rpx; |
|
|
|
border-radius: 20rpx; |
|
|
|
} |
|
|
|
|
|
|
|
.sentence-text { |
|
|
|
font-size: 24rpx; |
|
|
|
color: #666; |
|
|
|
font-weight: 500; |
|
|
|
} |
|
|
|
|
|
|
|
/* 播放控制区域 */ |
|
|
|
.player-controls { |
|
|
|
background: white; |
|
|
@ -548,6 +590,31 @@ export default { |
|
|
|
color: #999; |
|
|
|
} |
|
|
|
|
|
|
|
/* 句子控制按钮 */ |
|
|
|
.sentence-controls { |
|
|
|
display: flex; |
|
|
|
justify-content: center; |
|
|
|
align-items: center; |
|
|
|
gap: 40rpx; |
|
|
|
margin-bottom: 30rpx; |
|
|
|
padding: 20rpx 0; |
|
|
|
border-top: 1rpx solid #f0f0f0; |
|
|
|
} |
|
|
|
|
|
|
|
.sentence-info { |
|
|
|
background: #f8f8f8; |
|
|
|
padding: 12rpx 24rpx; |
|
|
|
border-radius: 20rpx; |
|
|
|
min-width: 120rpx; |
|
|
|
text-align: center; |
|
|
|
} |
|
|
|
|
|
|
|
.sentence-info-text { |
|
|
|
font-size: 28rpx; |
|
|
|
color: #666; |
|
|
|
font-weight: 500; |
|
|
|
} |
|
|
|
|
|
|
|
/* 底部控制按钮 */ |
|
|
|
.bottom-controls { |
|
|
|
display: flex; |
|
|
@ -587,4 +654,13 @@ export default { |
|
|
|
.control-btn:active { |
|
|
|
transform: scale(0.95); |
|
|
|
} |
|
|
|
|
|
|
|
.control-btn:disabled { |
|
|
|
opacity: 0.3; |
|
|
|
cursor: not-allowed; |
|
|
|
} |
|
|
|
|
|
|
|
.control-btn:disabled:active { |
|
|
|
transform: none; |
|
|
|
} |
|
|
|
</style> |
|
|
|