"use client"; import { useRef, useState, useEffect, useCallback } from 'react'; import { format } from 'date-fns'; interface Segment { start: number; end: number; active: boolean; } interface Marker { time: number; label?: string; active?: boolean; } interface TimelineSliderProps { minTime: number; maxTime: number; value: number; mode?: 'default' | 'segments' | 'ticks'; segments?: Segment[]; markers?: Marker[]; onChange: (value: number) => void; } export default function TimelineSlider({ minTime, maxTime, value, mode = 'default', segments = [], markers = [], onChange }: TimelineSliderProps) { const sliderRef = useRef(null); const [dragging, setDragging] = useState(false); const [internalValue, setInternalValue] = useState(value); const [showTooltip, setShowTooltip] = useState(false); // 同步外部value到内部状态(只在非拖拽状态下) useEffect(() => { if (!dragging) { setInternalValue(value); } }, [value, dragging]); const formattedValue = format(new Date(internalValue), 'HH:mm:ss'); const percentage = ((internalValue - minTime) / (maxTime - minTime)) * 100; const getSnapValue = useCallback((val: number): number => { if (mode === 'ticks' && markers.length > 0) { let closest = markers[0].time; let minDiff = Math.abs(val - markers[0].time); for (const marker of markers) { const diff = Math.abs(val - marker.time); if (diff < minDiff) { minDiff = diff; closest = marker.time; } } return closest; } else if (mode === 'segments' && segments.length > 0) { let closestMidpoint: number | null = null; let minDiff = Infinity; for (const segment of segments) { if (segment.active) { const midpoint = (segment.start + segment.end) / 2; const diff = Math.abs(val - midpoint); if (diff < minDiff) { minDiff = diff; closestMidpoint = midpoint; } } } if (closestMidpoint !== null) { return closestMidpoint; } } return val; }, [mode, markers, segments]); // 使用ref来跟踪上次通知的值,避免频繁调用onChange const lastNotifiedValue = useRef(value); // 指针移动处理函数 const pointerMoveHandler = useCallback((event: PointerEvent) => { if (!sliderRef.current) return; const rect = sliderRef.current.getBoundingClientRect(); let x = event.clientX - rect.left; x = Math.max(0, Math.min(x, rect.width)); const newValue = minTime + (x / rect.width) * (maxTime - minTime); setInternalValue(newValue); // 节流调用onChange,避免过于频繁的更新 const snapped = getSnapValue(newValue); if (Math.abs(snapped - lastNotifiedValue.current) > 100) { // 100ms的节流 lastNotifiedValue.current = snapped; onChange(snapped); } }, [minTime, maxTime, getSnapValue, onChange]); // 指针释放处理函数 const pointerUpHandler = useCallback(() => { setDragging(false); setShowTooltip(false); window.removeEventListener('pointermove', pointerMoveHandler); window.removeEventListener('pointerup', pointerUpHandler); }, [getSnapValue, onChange, pointerMoveHandler]); // 指针按下处理函数 const onPointerDown = (event: React.PointerEvent) => { event.stopPropagation(); event.preventDefault(); if (!sliderRef.current) return; const rect = sliderRef.current.getBoundingClientRect(); let x = event.clientX - rect.left; x = Math.max(0, Math.min(x, rect.width)); const newValue = minTime + (x / rect.width) * (maxTime - minTime); setDragging(true); setShowTooltip(true); setInternalValue(newValue); // 立即通知父组件值的变化 const snapped = getSnapValue(newValue); lastNotifiedValue.current = snapped; onChange(snapped); window.addEventListener('pointermove', pointerMoveHandler); window.addEventListener('pointerup', pointerUpHandler); }; const getSegmentStyle = (segment: Segment) => { const startPercent = ((segment.start - minTime) / (maxTime - minTime)) * 100; const endPercent = ((segment.end - minTime) / (maxTime - minTime)) * 100; const widthPercent = endPercent - startPercent; return { position: 'absolute' as const, left: `${startPercent}%`, width: `${widthPercent}%`, height: '100%', backgroundColor: segment.active ? '#4ADE80' : 'transparent', borderLeft: '1px solid #E5E7EB', borderRight: '1px solid #E5E7EB', }; }; const getMarkerStyle = (marker: Marker) => { const pos = ((marker.time - minTime) / (maxTime - minTime)) * 100; return { position: 'absolute' as const, left: `${pos}%`, top: '50%', width: '2px', height: '100%', backgroundColor: marker.active ? '#4ADE80' : '#9CA3AF', transform: 'translate(-50%, -50%)' }; }; return (
{/* 底部轨道 */}
{/* Segments 模式 */} {mode === 'segments' && segments.map((segment, index) => (
))} {/* Ticks 模式 */} {mode === 'ticks' && markers.map((marker, index) => (
))}
{/* 可拖动的滑块 */}
{/* Tooltip */} {showTooltip && (
{formattedValue}
)}
); }