From 7794c91eca1945ff26c822629d2907ef0145f33f Mon Sep 17 00:00:00 2001 From: xudan Date: Thu, 9 Oct 2025 17:11:22 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E6=92=AD=E6=94=BE?= =?UTF-8?q?=E6=8E=A7=E5=88=B6=E5=99=A8=E4=B8=AD=E7=9A=84=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=E9=80=BB=E8=BE=91=EF=BC=8C=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E6=97=B6=E9=97=B4=E8=BD=B4=E7=82=B9=E5=87=BB=E4=BA=A4=E4=BA=92?= =?UTF-8?q?=EF=BC=8C=E7=A1=AE=E4=BF=9D=E9=80=89=E6=8B=A9=E5=99=A8=E4=B8=8E?= =?UTF-8?q?=E8=A7=86=E7=AA=97=E4=B8=80=E8=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/PlaybackController.vue | 70 ++++++++++++++++++--------- src/hooks/useTimelineTicks.ts | 3 +- 2 files changed, 49 insertions(+), 24 deletions(-) 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; + } +};