feat: 在播放控制器中添加用户交互锁,优化时间选择逻辑,确保用户操作优先于外部时间更新
This commit is contained in:
parent
7794c91eca
commit
c5410bb3f5
@ -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>
|
||||||
|
|||||||
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user