feat: 优化播放控制器中的时间同步逻辑,增强时间轴点击交互,确保选择器与视窗一致

This commit is contained in:
xudan 2025-10-09 17:11:22 +08:00
parent ab2cd0d346
commit 7794c91eca
2 changed files with 49 additions and 24 deletions

View File

@ -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<number | null>(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<HTMLElement | null>(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;
}
};
</script>
<template>
@ -190,7 +215,6 @@ const handlePlayPause = () => {
position: relative;
padding-top: 20px; // Space for tick labels
margin-bottom: 5px;
cursor: pointer;
}
.playhead-marker {

View File

@ -42,7 +42,8 @@ export function useTimelineTicks(viewStartTime: Ref<number>, viewEndTime: Ref<nu
result.push({
position: `${(relativeTime / viewDuration) * 100}%`,
label: isMajor ? formatLabelTime(time) : null,
// 使用相对视窗的时间展示,避免绝对时间导致的大数/负数
label: isMajor ? formatLabelTime(relativeTime) : null,
isMajor,
});
}