diff --git a/src/components/PlaybackController.vue b/src/components/PlaybackController.vue index 7b7f668..908afbd 100644 --- a/src/components/PlaybackController.vue +++ b/src/components/PlaybackController.vue @@ -34,10 +34,13 @@ const { ticks } = useTimelineTicks(viewStartTime, viewEndTime); watch( () => props.currentTime, (newValue) => { - // Only update from prop if not playing - const newHour = Math.floor(newValue / HOUR_IN_MS); - if (newHour !== selectedHour.value) { - selectedHour.value = newHour; + // 仅在有效范围内才同步小时;避免负数/异常 currentTime 破坏选择器 + if (Number.isFinite(newValue) && newValue >= 0 && props.totalDuration > 0) { + const maxHour = Math.max(Math.ceil(props.totalDuration / HOUR_IN_MS) - 1, 0); + const newHour = Math.min(Math.max(Math.floor(newValue / HOUR_IN_MS), 0), maxHour); + if (newHour !== selectedHour.value) { + selectedHour.value = newHour; + } } }, { immediate: true }, @@ -56,18 +59,32 @@ const formatTime = (ms: number): string => { const currentTimeFormatted = computed(() => formatTime(props.currentTime)); const totalDurationFormatted = computed(() => formatTime(props.totalDuration)); +// 本地“乐观显示”的时间:在 seek 后先行展示,待实际时间变更后清空 +const pendingSeekTime = ref(null); + +// 同步清理:当外部 currentTime 改变时,移除乐观显示 +watch( + () => props.currentTime, + () => { + pendingSeekTime.value = null; + }, +); + const sliderValue = computed({ - get: () => props.currentTime, + get: () => pendingSeekTime.value ?? props.currentTime, set: (val: number) => { + pendingSeekTime.value = val; emit('seek', val); }, }); const playheadPosition = computed(() => { - if (!props.totalDuration) return '0%'; - const relativeTime = props.currentTime - viewStartTime.value; const viewDuration = viewEndTime.value - viewStartTime.value; - return `${(relativeTime / viewDuration) * 100}%`; + if (viewDuration <= 0) return '0%'; + const current = pendingSeekTime.value ?? props.currentTime; + const relativeTime = current - viewStartTime.value; + const clamped = Math.min(Math.max(relativeTime, 0), viewDuration); + return `${(clamped / viewDuration) * 100}%`; }); const hourOptions = computed(() => { @@ -85,20 +102,6 @@ const hourOptions = computed(() => { // --- Event Handlers --- const timelineContainerRef = ref(null); -const handleTimelineClick = (event: MouseEvent) => { - if (!timelineContainerRef.value || !props.totalDuration) return; - - const rect = timelineContainerRef.value.getBoundingClientRect(); - const effectiveWidth = rect.width - SLIDER_PADDING * 2; - const clickX = event.clientX - rect.left - SLIDER_PADDING; - - const percentage = Math.max(0, Math.min(1, clickX / effectiveWidth)); - const newTimeInView = percentage * HOUR_IN_MS; - const newAbsoluteTime = viewStartTime.value + newTimeInView; - - emit('seek', newAbsoluteTime); -}; - const handlePlayPause = () => { if (props.isPlaying) { emit('pause'); @@ -106,6 +109,28 @@ const handlePlayPause = () => { emit('play'); } }; + +// 根据点击时间轴位置计算对应时间并触发 seek +const handleTimelineClick = (event: MouseEvent) => { + const container = timelineContainerRef.value; + if (!container) return; + + const rect = container.getBoundingClientRect(); + const effectiveWidth = Math.max(rect.width - SLIDER_PADDING * 2, 1); + const clampedX = Math.min(Math.max(event.clientX - rect.left - SLIDER_PADDING, 0), effectiveWidth); + const ratio = clampedX / effectiveWidth; + const viewDuration = viewEndTime.value - viewStartTime.value; + const targetTime = Math.floor(viewStartTime.value + ratio * viewDuration); + pendingSeekTime.value = targetTime; + emit('seek', targetTime); + // 点击发生后,立即更新所选小时,保持左侧小时选择与视窗一致 + const clickedHour = Math.floor(targetTime / HOUR_IN_MS); + const maxHour = Math.max(Math.ceil(props.totalDuration / HOUR_IN_MS) - 1, 0); + const clampedHour = Math.min(Math.max(clickedHour, 0), maxHour); + if (clampedHour !== selectedHour.value) { + selectedHour.value = clampedHour; + } +};