diff --git a/src/components/PlaybackController.vue b/src/components/PlaybackController.vue index 908afbd..f230f8a 100644 --- a/src/components/PlaybackController.vue +++ b/src/components/PlaybackController.vue @@ -22,6 +22,7 @@ const HOUR_IN_MS = 3600 * 1000; // --- Internal State --- const selectedHour = ref(Math.floor(props.currentTime / HOUR_IN_MS)); +const isUserInteracting = ref(false); // 标志位,防止 props 更新与用户操作冲突 // --- Computed View Range --- const viewStartTime = computed(() => selectedHour.value * HOUR_IN_MS); @@ -34,6 +35,11 @@ const { ticks } = useTimelineTicks(viewStartTime, viewEndTime); watch( () => props.currentTime, (newValue) => { + // 如果用户正在主动交互(如拖动、点击),则暂时不根据外部时间更新小时选择 + if (isUserInteracting.value) { + return; + } + // 仅在有效范围内才同步小时;避免负数/异常 currentTime 破坏选择器 if (Number.isFinite(newValue) && newValue >= 0 && props.totalDuration > 0) { const maxHour = Math.max(Math.ceil(props.totalDuration / HOUR_IN_MS) - 1, 0); @@ -56,7 +62,7 @@ const formatTime = (ms: number): string => { return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; }; -const currentTimeFormatted = computed(() => formatTime(props.currentTime)); +const currentTimeFormatted = computed(() => formatTime(pendingSeekTime.value ?? props.currentTime)); const totalDurationFormatted = computed(() => formatTime(props.totalDuration)); // 本地“乐观显示”的时间:在 seek 后先行展示,待实际时间变更后清空 @@ -70,14 +76,6 @@ watch( }, ); -const sliderValue = computed({ - get: () => pendingSeekTime.value ?? props.currentTime, - set: (val: number) => { - pendingSeekTime.value = val; - emit('seek', val); - }, -}); - const playheadPosition = computed(() => { const viewDuration = viewEndTime.value - viewStartTime.value; if (viewDuration <= 0) return '0%'; @@ -101,6 +99,26 @@ const hourOptions = computed(() => { // --- Event Handlers --- const timelineContainerRef = ref(null); +let interactionTimer: number | undefined; + +// 设置一个短暂的用户交互锁,以避免外部时间更新覆盖用户的即时选择 +const setUserInteraction = () => { + isUserInteracting.value = true; + clearTimeout(interactionTimer); + interactionTimer = window.setTimeout(() => { + isUserInteracting.value = false; + }, 2000); // 2秒内,用户的操作优先 +}; + +const handleHourChange = (newHour: number) => { + setUserInteraction(); + selectedHour.value = newHour; + const newTime = newHour * HOUR_IN_MS; + // 确保搜寻时间不超过总时长 + const clampedTime = Math.min(newTime, props.totalDuration); + pendingSeekTime.value = clampedTime; + emit('seek', clampedTime); +}; const handlePlayPause = () => { if (props.isPlaying) { @@ -112,6 +130,7 @@ const handlePlayPause = () => { // 根据点击时间轴位置计算对应时间并触发 seek const handleTimelineClick = (event: MouseEvent) => { + setUserInteraction(); const container = timelineContainerRef.value; if (!container) return; @@ -155,7 +174,7 @@ const handleTimelineClick = (event: MouseEvent) => { - + @@ -173,13 +192,6 @@ const handleTimelineClick = (event: MouseEvent) => { {{ tick.label }} - @@ -215,6 +227,20 @@ const handleTimelineClick = (event: MouseEvent) => { position: relative; padding-top: 20px; // Space for tick labels margin-bottom: 5px; + height: 32px; + cursor: pointer; + + &::after { + content: ''; + position: absolute; + bottom: 8px; + left: 8px; + right: 8px; + height: 2px; + background-color: #f0f0f0; + border-radius: 1px; + pointer-events: none; + } } .playhead-marker { @@ -270,5 +296,8 @@ body[data-theme='dark'] { .tick-label { color: #aaa; } + .timeline-container::after { + background-color: #434343; + } } diff --git a/src/hooks/useTimelineTicks.ts b/src/hooks/useTimelineTicks.ts index cffb849..f8c6d39 100644 --- a/src/hooks/useTimelineTicks.ts +++ b/src/hooks/useTimelineTicks.ts @@ -42,8 +42,8 @@ export function useTimelineTicks(viewStartTime: Ref, viewEndTime: Ref