feat: 在播放控制器中添加用户交互锁,优化时间选择逻辑,确保用户操作优先于外部时间更新

This commit is contained in:
xudan 2025-10-09 17:31:03 +08:00
parent 7794c91eca
commit c5410bb3f5
2 changed files with 48 additions and 19 deletions

View File

@ -22,6 +22,7 @@ const HOUR_IN_MS = 3600 * 1000;
// --- Internal State --- // --- Internal State ---
const selectedHour = ref(Math.floor(props.currentTime / HOUR_IN_MS)); const selectedHour = ref(Math.floor(props.currentTime / HOUR_IN_MS));
const isUserInteracting = ref(false); // props
// --- Computed View Range --- // --- Computed View Range ---
const viewStartTime = computed(() => selectedHour.value * HOUR_IN_MS); const viewStartTime = computed(() => selectedHour.value * HOUR_IN_MS);
@ -34,6 +35,11 @@ const { ticks } = useTimelineTicks(viewStartTime, viewEndTime);
watch( watch(
() => props.currentTime, () => props.currentTime,
(newValue) => { (newValue) => {
//
if (isUserInteracting.value) {
return;
}
// / currentTime // / currentTime
if (Number.isFinite(newValue) && newValue >= 0 && props.totalDuration > 0) { if (Number.isFinite(newValue) && newValue >= 0 && props.totalDuration > 0) {
const maxHour = Math.max(Math.ceil(props.totalDuration / HOUR_IN_MS) - 1, 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')}`; 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)); const totalDurationFormatted = computed(() => formatTime(props.totalDuration));
// seek // 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 playheadPosition = computed(() => {
const viewDuration = viewEndTime.value - viewStartTime.value; const viewDuration = viewEndTime.value - viewStartTime.value;
if (viewDuration <= 0) return '0%'; if (viewDuration <= 0) return '0%';
@ -101,6 +99,26 @@ const hourOptions = computed(() => {
// --- Event Handlers --- // --- Event Handlers ---
const timelineContainerRef = ref<HTMLElement | null>(null); const timelineContainerRef = ref<HTMLElement | null>(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 = () => { const handlePlayPause = () => {
if (props.isPlaying) { if (props.isPlaying) {
@ -112,6 +130,7 @@ const handlePlayPause = () => {
// seek // seek
const handleTimelineClick = (event: MouseEvent) => { const handleTimelineClick = (event: MouseEvent) => {
setUserInteraction();
const container = timelineContainerRef.value; const container = timelineContainerRef.value;
if (!container) return; if (!container) return;
@ -155,7 +174,7 @@ const handleTimelineClick = (event: MouseEvent) => {
<!-- Hour Selector --> <!-- Hour Selector -->
<a-col> <a-col>
<a-select v-model:value="selectedHour" style="width: 150px" :options="hourOptions" /> <a-select :value="selectedHour" style="width: 150px" :options="hourOptions" @change="handleHourChange" />
</a-col> </a-col>
<!-- Timeline Slider --> <!-- Timeline Slider -->
@ -173,13 +192,6 @@ const handleTimelineClick = (event: MouseEvent) => {
<span v-if="tick.label" class="tick-label">{{ tick.label }}</span> <span v-if="tick.label" class="tick-label">{{ tick.label }}</span>
</span> </span>
</div> </div>
<a-slider
v-model:value="sliderValue"
:min="viewStartTime"
:max="viewEndTime"
:tip-formatter="formatTime as any"
:tooltip-open="false"
/>
</div> </div>
</a-col> </a-col>
</a-row> </a-row>
@ -215,6 +227,20 @@ const handleTimelineClick = (event: MouseEvent) => {
position: relative; position: relative;
padding-top: 20px; // Space for tick labels padding-top: 20px; // Space for tick labels
margin-bottom: 5px; 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 { .playhead-marker {
@ -270,5 +296,8 @@ body[data-theme='dark'] {
.tick-label { .tick-label {
color: #aaa; color: #aaa;
} }
.timeline-container::after {
background-color: #434343;
}
} }
</style> </style>

View File

@ -42,8 +42,8 @@ export function useTimelineTicks(viewStartTime: Ref<number>, viewEndTime: Ref<nu
result.push({ result.push({
position: `${(relativeTime / viewDuration) * 100}%`, position: `${(relativeTime / viewDuration) * 100}%`,
// 使用相对视窗的时间展示,避免绝对时间导致的大数/负数 // 使用绝对时间来展示刻度标签,确保其在切换小时后能正确反映当前时间
label: isMajor ? formatLabelTime(relativeTime) : null, label: isMajor ? formatLabelTime(time) : null,
isMajor, isMajor,
}); });
} }