时间线组件优化

- 时间线缩放滑块的数值提示限定小数点后两位
- 时间线缩放时,如果播放头在时间线范围内,以播放头为中心缩放,如果看不到播放头,则以时间线组件中心缩放
- 时间线在加载时自适应最小缩放值
This commit is contained in:
yueliuli 2026-04-22 11:51:26 +08:00
parent cdff0950d9
commit aaeb0c4f4f
2 changed files with 77 additions and 14 deletions

View File

@ -39,8 +39,8 @@ export function useTimeline(
) {
const FPS = 30
const pxPerFrame = ref(2)
const minPxPerFrame = 0.5
const maxPxPerFrame = 3
const minPxPerFrame = ref(0.5)
const maxPxPerFrame = ref(3)
const timelineContainer: Ref<HTMLElement | null> = ref(null)
const isDragging = ref(false)
const isPlayheadDragging = ref(false)
@ -58,15 +58,54 @@ export function useTimeline(
})
function updatePxPerFrame(): void {
if (!timelineContainer.value)
if (!timelineContainer.value || props.totalFrames === 0)
return
const containerWidth = timelineContainer.value.clientWidth - 20
const calculatedPxPerFrame = containerWidth / props.totalFrames
pxPerFrame.value = Math.max(minPxPerFrame, Math.min(calculatedPxPerFrame, maxPxPerFrame))
// 计算最小缩放倍数,确保能看到视频全长
const newMinPxPerFrame = Math.max(0.1, containerWidth / props.totalFrames)
// 只在必要时更新,避免无限循环
if (Math.abs(newMinPxPerFrame - minPxPerFrame.value) > 0.01) {
minPxPerFrame.value = newMinPxPerFrame
}
// 最大值固定为3不修改
pxPerFrame.value = Math.max(minPxPerFrame.value, Math.min(calculatedPxPerFrame, maxPxPerFrame.value))
}
function setZoom(value: number): void {
pxPerFrame.value = Math.max(minPxPerFrame, Math.min(value, maxPxPerFrame))
const oldPxPerFrame = pxPerFrame.value
const newPxPerFrame = Math.max(minPxPerFrame.value, Math.min(value, maxPxPerFrame.value))
if (oldPxPerFrame === newPxPerFrame || !timelineContainer.value)
return
const container = timelineContainer.value
const containerWidth = container.clientWidth
const containerScrollLeft = container.scrollLeft
const playheadPosition = props.currentFrame * oldPxPerFrame
// 检查播放头是否在可见范围内
const playheadVisible = playheadPosition >= containerScrollLeft
&& playheadPosition <= containerScrollLeft + containerWidth
// 计算缩放前后的位置偏移
if (playheadVisible) {
// 以播放头为中心缩放
const playheadX = playheadPosition - containerScrollLeft
const newPlayheadPosition = props.currentFrame * newPxPerFrame
const newScrollLeft = newPlayheadPosition - playheadX
container.scrollLeft = newScrollLeft
}
else {
// 以容器中心为中心缩放
const centerX = containerWidth / 2
const centerPosition = containerScrollLeft + centerX
const newCenterPosition = centerPosition * (newPxPerFrame / oldPxPerFrame)
container.scrollLeft = newCenterPosition - centerX
}
// 最后更新缩放值
pxPerFrame.value = newPxPerFrame
}
const timeTicks: ComputedRef<TimeTick[]> = computed(() => {
@ -105,10 +144,9 @@ export function useTimeline(
}
const playheadPos: ComputedRef<number> = computed(() => {
if (isPlayheadDragging.value) {
return localCurrentFrame.value * pxPerFrame.value
}
return props.currentFrame * pxPerFrame.value
const frame = isPlayheadDragging.value ? localCurrentFrame.value : props.currentFrame
const clampedFrame = Math.max(0, Math.min(frame, props.totalFrames - 1))
return clampedFrame * pxPerFrame.value
})
const trackRows: ComputedRef<HazardRow[][]> = computed(() => {
@ -281,7 +319,10 @@ export function useTimeline(
if (!progressContainer)
return
// 使用 nextTick 确保容器已渲染
nextTick(() => {
updatePxPerFrame()
})
progressContainer.addEventListener('wheel', handleWheel, { passive: false })
progressContainer.addEventListener('mousedown', handleMouseDown)
@ -291,10 +332,31 @@ export function useTimeline(
}
watch(() => props.totalFrames, () => {
nextTick(() => {
// 使用 setTimeout 避免频繁更新
setTimeout(() => {
updatePxPerFrame()
}, 100)
})
})
// 移除 resize 监听,避免无限循环
// 监听容器宽度变化
// let resizeObserver: ResizeObserver | null = null
// function handleResize(): void {
// updatePxPerFrame()
// }
// watch(() => timelineContainer.value, (newVal, oldVal) => {
// if (newVal && !oldVal) {
// // 使用 ResizeObserver 代替 window resize 监听
// resizeObserver = new ResizeObserver(handleResize)
// resizeObserver.observe(newVal)
// }
// else if (!newVal && oldVal && resizeObserver) {
// resizeObserver.disconnect()
// resizeObserver = null
// }
// })
function initUnmounted(): void {
window.removeEventListener('mousemove', handleMouseMove)

View File

@ -1,4 +1,5 @@
<script setup lang="ts">
import type { Arrayable } from '@vueuse/core'
import type { TimelineEmits, TimelineProps } from './timeline'
import { ElScrollbar, ElSlider } from 'element-plus'
import { computed, onMounted, onUnmounted } from 'vue'
@ -55,9 +56,9 @@ onUnmounted(() => {
:max="maxPxPerFrame"
:step="0.5"
:show-tooltip="true"
:format-tooltip="(val: number) => `缩放: ${val}`"
:format-tooltip="(val: number) => `缩放: ${val.toFixed(2)}`"
class="zoom-slider"
@change="(val: number | number[]) => setZoom(Array.isArray(val) ? val[0] : val)"
@update:model-value="(val: Arrayable<number>) => setZoom(Array.isArray(val) ? val[0] : val)"
/>
</div>
</div>