mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-05-17 02:07:29 +08:00
feat: 逐字歌词
This commit is contained in:
@@ -119,7 +119,22 @@
|
||||
:class="{ 'now-text': index === nowIndex, 'hover-text': item.text }"
|
||||
@click="setAudioTime(index)"
|
||||
>
|
||||
<span :style="getLrcStyle(index)">{{ item.text }}</span>
|
||||
<!-- 逐字歌词显示 -->
|
||||
<div
|
||||
v-if="item.hasWordByWord && item.words && item.words.length > 0"
|
||||
class="word-by-word-lyric"
|
||||
>
|
||||
<span
|
||||
v-for="(word, wordIndex) in item.words"
|
||||
:key="wordIndex"
|
||||
class="lyric-word"
|
||||
:style="getWordStyle(index, wordIndex, word)"
|
||||
>
|
||||
{{ word.text }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- 普通歌词显示 -->
|
||||
<span v-else :style="getLrcStyle(index)">{{ item.text }}</span>
|
||||
<div v-show="config.showTranslation" class="music-lrc-text-tr">
|
||||
{{ item.trText }}
|
||||
</div>
|
||||
@@ -144,7 +159,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useDebounceFn } from '@vueuse/core';
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import Cover3D from '@/components/cover/Cover3D.vue';
|
||||
@@ -157,6 +172,7 @@ import {
|
||||
correctionTime,
|
||||
lrcArray,
|
||||
nowIndex,
|
||||
nowTime,
|
||||
playMusic,
|
||||
setAudioTime,
|
||||
textColors,
|
||||
@@ -179,6 +195,7 @@ const animationFrame = ref<number | null>(null);
|
||||
const isDark = ref(false);
|
||||
const showStickyHeader = ref(false);
|
||||
const lyricSettingsRef = ref<InstanceType<typeof LyricSettings>>();
|
||||
const isSongChanging = ref(false);
|
||||
|
||||
const config = ref<LyricConfig>({ ...DEFAULT_LYRIC_CONFIG });
|
||||
|
||||
@@ -274,6 +291,8 @@ const mouseLeaveLayout = () => {
|
||||
};
|
||||
|
||||
watch(nowIndex, () => {
|
||||
// 歌曲切换时不自动滚动
|
||||
if (isSongChanging.value) return;
|
||||
debouncedLrcScroll();
|
||||
});
|
||||
|
||||
@@ -337,25 +356,30 @@ watch(
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 修改 useLyricProgress 的使用方式
|
||||
const { getLrcStyle: originalLrcStyle } = useLyricProgress();
|
||||
|
||||
// 修改 getLrcStyle 函数
|
||||
const getLrcStyle = (index: number) => {
|
||||
const colors = textColors.value || getTextColors;
|
||||
const colors = textColors.value || getTextColors();
|
||||
const originalStyle = originalLrcStyle(index);
|
||||
|
||||
if (index === nowIndex.value) {
|
||||
// 当前播放的歌词,使用渐变效果
|
||||
return {
|
||||
...originalStyle,
|
||||
backgroundImage: originalStyle.backgroundImage
|
||||
?.replace(/#ffffff/g, colors.active)
|
||||
.replace(/#ffffff8a/g, `${colors.primary}`),
|
||||
backgroundClip: 'text',
|
||||
WebkitBackgroundClip: 'text',
|
||||
color: 'transparent'
|
||||
};
|
||||
// 当前播放的歌词
|
||||
if (originalStyle.backgroundImage) {
|
||||
// 有渐变进度时,使用渐变效果
|
||||
return {
|
||||
...originalStyle,
|
||||
backgroundImage: originalStyle.backgroundImage
|
||||
.replace(/#ffffff/g, colors.active)
|
||||
.replace(/#ffffff8a/g, `${colors.primary}`),
|
||||
backgroundClip: 'text',
|
||||
WebkitBackgroundClip: 'text',
|
||||
color: 'transparent'
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
color: colors.primary
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 非当前播放的歌词,使用普通颜色
|
||||
@@ -364,6 +388,57 @@ const getLrcStyle = (index: number) => {
|
||||
};
|
||||
};
|
||||
|
||||
// 逐字歌词样式函数
|
||||
const getWordStyle = (lineIndex: number, _wordIndex: number, word: any) => {
|
||||
const colors = textColors.value || getTextColors();
|
||||
// 如果不是当前行,返回普通样式
|
||||
if (lineIndex !== nowIndex.value) {
|
||||
return {
|
||||
color: colors.primary,
|
||||
transition: 'color 0.3s ease',
|
||||
// 重置背景相关属性
|
||||
backgroundImage: 'none',
|
||||
WebkitTextFillColor: 'initial'
|
||||
};
|
||||
}
|
||||
|
||||
// 当前行的逐字效果,应用歌词矫正时间
|
||||
const currentTime = (nowTime.value + correctionTime.value) * 1000; // 转换为毫秒,确保与word时间单位一致
|
||||
|
||||
// 直接使用绝对时间比较
|
||||
const wordStartTime = word.startTime; // 单词开始的绝对时间(毫秒)
|
||||
const wordEndTime = word.startTime + word.duration;
|
||||
|
||||
if (currentTime >= wordStartTime && currentTime < wordEndTime) {
|
||||
// 当前正在播放的单词 - 使用渐变进度效果
|
||||
const progress = Math.min((currentTime - wordStartTime) / word.duration, 1);
|
||||
const progressPercent = Math.round(progress * 100);
|
||||
|
||||
return {
|
||||
backgroundImage: `linear-gradient(to right, ${colors.active} 0%, ${colors.active} ${progressPercent}%, ${colors.primary} ${progressPercent}%, ${colors.primary} 100%)`,
|
||||
backgroundClip: 'text',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
textShadow: `0 0 8px ${colors.active}40`,
|
||||
transition: 'all 0.1s ease'
|
||||
};
|
||||
} else if (currentTime >= wordEndTime) {
|
||||
// 已经播放过的单词 - 纯色显示
|
||||
return {
|
||||
color: colors.active,
|
||||
WebkitTextFillColor: 'initial',
|
||||
transition: 'none'
|
||||
};
|
||||
} else {
|
||||
// 还未播放的单词 - 普通状态
|
||||
return {
|
||||
color: colors.primary,
|
||||
WebkitTextFillColor: 'initial',
|
||||
transition: 'none'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// 组件卸载时清理动画
|
||||
onBeforeUnmount(() => {
|
||||
if (animationFrame.value) {
|
||||
@@ -502,12 +577,24 @@ onMounted(() => {
|
||||
}
|
||||
});
|
||||
|
||||
// 添加对 playMusic 的监听
|
||||
watch(playMusic, () => {
|
||||
nextTick(() => {
|
||||
lrcScroll('instant', true);
|
||||
});
|
||||
});
|
||||
// 添加对 playMusic.id 的监听,歌曲切换时滚动到顶部
|
||||
watch(
|
||||
() => playMusic.value.id,
|
||||
(newId, oldId) => {
|
||||
// 只在歌曲真正切换时滚动到顶部
|
||||
if (newId !== oldId && newId) {
|
||||
isSongChanging.value = true;
|
||||
// 延迟滚动,确保 nowIndex 已重置
|
||||
setTimeout(() => {
|
||||
lrcScroll('instant', true);
|
||||
// 延迟恢复自动滚动,等待歌词数据更新
|
||||
setTimeout(() => {
|
||||
isSongChanging.value = false;
|
||||
}, 300);
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
defineExpose({
|
||||
lrcScroll,
|
||||
@@ -621,6 +708,9 @@ defineExpose({
|
||||
|
||||
.music-lrc-container {
|
||||
padding-top: 30vh;
|
||||
.music-lrc-text:last-child {
|
||||
margin-bottom: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.music-lrc {
|
||||
@@ -670,6 +760,28 @@ defineExpose({
|
||||
opacity: 0.7;
|
||||
color: var(--text-color-primary);
|
||||
}
|
||||
|
||||
// 逐字歌词样式
|
||||
.word-by-word-lyric {
|
||||
@apply flex flex-wrap;
|
||||
|
||||
.lyric-word {
|
||||
@apply inline-block;
|
||||
margin-right: 4px;
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
font-weight: inherit;
|
||||
font-size: inherit;
|
||||
letter-spacing: inherit;
|
||||
line-height: inherit;
|
||||
cursor: inherit;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hover-text {
|
||||
|
||||
@@ -57,7 +57,22 @@
|
||||
:class="{ 'now-text': index === nowIndex, 'hover-text': item.text }"
|
||||
@click="jumpToLyricTime(index)"
|
||||
>
|
||||
<span :style="getLrcStyle(index)">{{ item.text }}</span>
|
||||
<!-- 逐字歌词显示 -->
|
||||
<div
|
||||
v-if="item.hasWordByWord && item.words && item.words.length > 0"
|
||||
class="word-by-word-lyric"
|
||||
>
|
||||
<span
|
||||
v-for="(word, wordIndex) in item.words"
|
||||
:key="wordIndex"
|
||||
class="lyric-word"
|
||||
:style="getWordStyle(index, wordIndex, word)"
|
||||
>
|
||||
{{ word.text }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- 普通歌词显示 -->
|
||||
<span v-else :style="getLrcStyle(index)">{{ item.text }}</span>
|
||||
<div v-if="config.showTranslation && item.trText" class="translation">
|
||||
{{ item.trText }}
|
||||
</div>
|
||||
@@ -119,7 +134,22 @@
|
||||
<div class="lyrics-container" v-if="!config.hideLyrics" @click="showFullLyricScreen">
|
||||
<div v-if="lrcArray.length > 0" class="lyrics-wrapper">
|
||||
<div v-for="(line, idx) in visibleLyrics" :key="idx" class="lyric-line">
|
||||
{{ line.text }}
|
||||
<!-- 逐字歌词显示 -->
|
||||
<div
|
||||
v-if="line.hasWordByWord && line.words && line.words.length > 0"
|
||||
class="word-by-word-lyric"
|
||||
>
|
||||
<span
|
||||
v-for="(word, wordIndex) in line.words"
|
||||
:key="wordIndex"
|
||||
class="lyric-word"
|
||||
:style="getWordStyle(line.originalIndex, wordIndex, word)"
|
||||
>
|
||||
{{ word.text }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- 普通歌词显示 -->
|
||||
<span v-else>{{ line.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-lyrics">
|
||||
@@ -225,7 +255,22 @@
|
||||
:class="{ 'now-text': index === nowIndex, 'hover-text': item.text }"
|
||||
@click="jumpToLyricTime(index)"
|
||||
>
|
||||
<span :style="getLrcStyle(index)">{{ item.text }}</span>
|
||||
<!-- 逐字歌词显示 -->
|
||||
<div
|
||||
v-if="item.hasWordByWord && item.words && item.words.length > 0"
|
||||
class="word-by-word-lyric"
|
||||
>
|
||||
<span
|
||||
v-for="(word, wordIndex) in item.words"
|
||||
:key="wordIndex"
|
||||
class="lyric-word"
|
||||
:style="getWordStyle(index, wordIndex, word)"
|
||||
>
|
||||
{{ word.text }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- 普通歌词显示 -->
|
||||
<span v-else :style="getLrcStyle(index)">{{ item.text }}</span>
|
||||
<div v-if="config.showTranslation && item.trText" class="translation">
|
||||
{{ item.trText }}
|
||||
</div>
|
||||
@@ -770,7 +815,11 @@ const visibleLyrics = computed(() => {
|
||||
startIdx = Math.max(0, endIdx - numLines + 1);
|
||||
}
|
||||
|
||||
return lrcArray.value.slice(startIdx, endIdx + 1);
|
||||
// 返回带有原始索引的歌词数组
|
||||
return lrcArray.value.slice(startIdx, endIdx + 1).map((item, idx) => ({
|
||||
...item,
|
||||
originalIndex: startIdx + idx
|
||||
}));
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
@@ -1020,6 +1069,57 @@ const getLrcStyle = (index: number) => {
|
||||
color: colors.primary
|
||||
};
|
||||
};
|
||||
|
||||
// 逐字歌词样式函数
|
||||
const getWordStyle = (lineIndex: number, _wordIndex: number, word: any) => {
|
||||
const colors = textColors.value || getTextColors();
|
||||
// 如果不是当前行,返回普通样式
|
||||
if (lineIndex !== nowIndex.value) {
|
||||
return {
|
||||
color: colors.primary,
|
||||
transition: 'color 0.3s ease',
|
||||
// 重置背景相关属性
|
||||
backgroundImage: 'none',
|
||||
WebkitTextFillColor: 'initial'
|
||||
};
|
||||
}
|
||||
|
||||
// 当前行的逐字效果
|
||||
const currentTime = nowTime.value * 1000; // 转换为毫秒,确保与word时间单位一致
|
||||
|
||||
// 直接使用绝对时间比较
|
||||
const wordStartTime = word.startTime; // 单词开始的绝对时间(毫秒)
|
||||
const wordEndTime = word.startTime + word.duration;
|
||||
|
||||
if (currentTime >= wordStartTime && currentTime < wordEndTime) {
|
||||
// 当前正在播放的单词 - 使用渐变进度效果
|
||||
const progress = Math.min((currentTime - wordStartTime) / word.duration, 1);
|
||||
const progressPercent = Math.round(progress * 100);
|
||||
|
||||
return {
|
||||
backgroundImage: `linear-gradient(to right, ${colors.active} 0%, ${colors.active} ${progressPercent}%, ${colors.primary} ${progressPercent}%, ${colors.primary} 100%)`,
|
||||
backgroundClip: 'text',
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
textShadow: `0 0 8px ${colors.active}40`,
|
||||
transition: 'all 0.1s ease'
|
||||
};
|
||||
} else if (currentTime >= wordEndTime) {
|
||||
// 已经播放过的单词 - 纯色显示
|
||||
return {
|
||||
color: colors.active,
|
||||
WebkitTextFillColor: 'initial',
|
||||
transition: 'none'
|
||||
};
|
||||
} else {
|
||||
// 还未播放的单词 - 普通状态
|
||||
return {
|
||||
color: colors.primary,
|
||||
WebkitTextFillColor: 'initial',
|
||||
transition: 'none'
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@@ -1574,7 +1674,7 @@ const getLrcStyle = (index: number) => {
|
||||
|
||||
// 通用歌词样式
|
||||
.lyric-line {
|
||||
@apply cursor-pointer transition-all duration-300;
|
||||
@apply cursor-pointer transition-all duration-300 font-medium;
|
||||
font-weight: 500;
|
||||
letter-spacing: var(--lyric-letter-spacing, 0);
|
||||
line-height: var(--lyric-line-height, 1.6);
|
||||
@@ -1587,7 +1687,7 @@ const getLrcStyle = (index: number) => {
|
||||
}
|
||||
|
||||
&.now-text {
|
||||
@apply font-bold py-4;
|
||||
@apply font-medium py-4;
|
||||
color: var(--text-color-active);
|
||||
opacity: 1;
|
||||
}
|
||||
@@ -1599,6 +1699,25 @@ const getLrcStyle = (index: number) => {
|
||||
.translation {
|
||||
@apply font-normal opacity-70 mt-1 text-base;
|
||||
}
|
||||
|
||||
// 逐字歌词样式
|
||||
.word-by-word-lyric {
|
||||
@apply flex flex-wrap justify-center;
|
||||
|
||||
.lyric-word {
|
||||
@apply inline-block;
|
||||
font-weight: inherit;
|
||||
font-size: inherit;
|
||||
letter-spacing: inherit;
|
||||
line-height: inherit;
|
||||
cursor: inherit;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 全屏歌词相关样式
|
||||
@@ -1765,6 +1884,10 @@ const getLrcStyle = (index: number) => {
|
||||
}
|
||||
}
|
||||
|
||||
.lyric-word {
|
||||
@apply px-[2px];
|
||||
}
|
||||
|
||||
.no-lyrics {
|
||||
@apply text-center text-base opacity-60;
|
||||
color: var(--text-color-primary);
|
||||
|
||||
@@ -202,7 +202,7 @@ watch(
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mobile-play-bar {
|
||||
@apply fixed bottom-[56px] left-0 w-full flex flex-col;
|
||||
@apply fixed bottom-[76px] left-0 w-full flex flex-col;
|
||||
z-index: 10000;
|
||||
animation-duration: 0.3s !important;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
Reference in New Issue
Block a user