话不多说,直接上代码
<script setup>
import { useProductStore } from '@/stores/product'
import { computed, defineEmits, onMounted, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import Loader from './loader.vue'
const route = useRoute()
const emit = defineEmits(['setAudioTime'])
const props = defineProps({
currentTime: {
type: Number
}
})
// Available colors
const availableColors = [
'#475ff5',
'#999966',
'#0066CC',
'#993399',
'#CCCC33',
'#99CC33',
'#CCCCFF',
'#99CC99'
]
const speakerColors = ref({})
const productStore = useProductStore()
const lsubtitles = computed(() => productStore.detailMap.get(route.query.doc_id)?.fullText)
const entryRefs = ref([]) // 用于存储每个条目的引用
const lastScrolledIndex = ref(-1) // 记录上次滚动的条目索引
const containerRef = ref(null) // 用于存储滚动容器的引用
// 设置条目引用的函数
const setEntryRef = (el, index) => {
if (el) {
entryRefs.value[index] = el // 使用 index 确保正确存储每个条目的引用
}
}
onMounted(() => {
productStore.getFullText({ doc_id: route.query.doc_id })
})
// 监听 currentTime 的变化,并滚动到当前条目
watch(
() => props.currentTime,
(newTime) => {
if (!lsubtitles.value?.length) return
const currentIndex = lsubtitles.value.findIndex((entry, index) => isCurrentEntry(index))
if (
currentIndex !== -1 &&
entryRefs.value[currentIndex] &&
currentIndex !== lastScrolledIndex.value
) {
smoothScroll(entryRefs.value[currentIndex], 300) // 自定义滚动时长为500毫秒
lastScrolledIndex.value = currentIndex // 更新最后滚动的条目索引
}
}
)
// 自定义的平滑滚动函数
function smoothScroll(target, duration) {
const container = containerRef.value
const targetPosition = target.offsetTop - container.offsetTop
const startPosition = container.scrollTop
const distance = targetPosition - startPosition
let startTime = null
function animation(currentTime) {
if (startTime === null) startTime = currentTime
const timeElapsed = currentTime - startTime
const run = ease(timeElapsed, startPosition, distance, duration)
container.scrollTop = run
if (timeElapsed < duration) requestAnimationFrame(animation)
}
function ease(t, b, c, d) {
t /= d / 2
if (t < 1) return (c / 2) * t * t + b
t--
return (-c / 2) * (t * (t - 2) - 1) + b
}
requestAnimationFrame(animation)
}
// Function to convert hex color to rgba
const hexToRgba = (hex, alpha) => {
let r = 0,
g = 0,
b = 0
if (hex.length === 4) {
r = parseInt(hex[1] + hex[1], 16)
g = parseInt(hex[2] + hex[2], 16)
b = parseInt(hex[3] + hex[3], 16)
} else if (hex.length === 7) {
r = parseInt(hex[1] + hex[2], 16)
g = parseInt(hex[3] + hex[4], 16)
b = parseInt(hex[5] + hex[6], 16)
}
return `rgba(${r}, ${g}, ${b}, ${alpha})`
}
// Function to get or assign speaker color
const getSpeakerColor = (speakerId) => {
if (!speakerColors.value[speakerId]) {
const colorIndex = Object.keys(speakerColors.value).length % availableColors.length
speakerColors.value[speakerId] = availableColors[colorIndex]
}
return speakerColors.value[speakerId]
}
// Function to get entry style
const getStyle = (speakerId) => {
const color = getSpeakerColor(speakerId)
return {
backgroundColor: hexToRgba(color, 0.1),
color
}
}
// 检查条目是否是当前条目的函数
const isCurrentEntry = (index) => {
const entry = lsubtitles.value[index]
const nextEntry = lsubtitles.value[index + 1]
const { begin_time } = entry
const nextBeginTime = nextEntry ? nextEntry.begin_time : Infinity
return props.currentTime >= begin_time && props.currentTime < nextBeginTime
}
// Function to get speaker name
const getSpeakerName = (speakerId) => `发言人${speakerId}`
const shouldShowSpeaker = (index) => {
if (index === 0) return true
return lsubtitles.value[index].speaker_id !== lsubtitles.value[index - 1].speaker_id
}
const formatTime = (time) => {
// 单独定义小时、分钟和秒
const hours = Math.floor(time / 3600)
const minutes = Math.floor((time % 3600) / 60)
const seconds = time % 60
// 将时间单位转换为字符串,并确保每个单位至少两位数
return [hours, minutes, seconds].map((unit) => unit.toString().padStart(2, '0')).join(':')
}
const setAudioTime = (time) => {
emit('setAudioTime', time)
}
</script>
<template>
<div ref="containerRef" class="subtitles">
<template v-if="lsubtitles?.length">
<template v-for="(item, index) in lsubtitles" :key="index">
<div
:ref="(el) => setEntryRef(el, index)"
@click="setAudioTime(item.begin_time)"
class="item"
>
<div v-if="shouldShowSpeaker(index)" class="speaker" :style="getStyle(item.speaker_id)">
<van-icon name="contact" />
<span>{{ getSpeakerName(item.speaker_id) }}</span>
</div>
<div class="content" :class="{ active: isCurrentEntry(index) }">
<div class="time">
{{ formatTime(item.begin_time) }}
</div>
<div class="text">{{ item.text }}</div>
</div>
</div>
</template>
</template>
<Loader v-else />
</div>
</template>
<style lang="scss" scoped>
.subtitles {
display: flex;
flex-direction: column;
}
.item {
.speaker {
width: fit-content;
padding: 6px 12px;
display: flex;
align-items: center;
gap: 4px;
font-size: 14px;
line-height: 12px;
border-radius: 99px;
span {
font-size: 12px;
}
}
&:not(:first-child) .speaker {
margin-top: 24px;
}
.content {
display: flex;
margin-top: 10px;
gap: 18px;
padding: 6px;
border-radius: 10px;
font-size: 14px;
transition: all 0.3s;
&.active {
font-weight: 700;
background-color: #f2edfc;
.time {
color: black;
}
}
.time {
color: #999;
}
}
}
</style>