refactor(TimelinePanel): 主组件精简,抽Header/ClipTrack/PlayheadDrag

This commit is contained in:
2026-07-30 07:14:09 +08:00
parent 0687b36fb8
commit 421bf9e2e1
@@ -1,22 +1,32 @@
/**
* 水平轨道时间线 — 支持裁剪手柄、分割、右键菜单
* 时间标尺(20px) + 水平片段卡片轨道(100x100) + HTML5拖拽排序
* "+" 卡片 → 类型+时长选择器
* 时间标尺 + 片段轨道 + HTML5拖拽排序
*
* 裁剪交互:
* - 鼠标悬停片段两端显示拖拽手柄,拖动调整入点/出点
* - 拖动时实时显示裁剪预览(入点/出点/时长)
* - 右键片段弹出菜单:分割 / 恢复原始长度 / 删除
* 子组件(timeline/):
* - TimeRuler - 时间标尺
* - ClipCard - 片段卡片
* - ClipTrack - 片段轨道(播放头+片段列表+添加卡片)
* - TimelineHeader - 时间线头部(标题+缩放+操作按钮)
* - AddClipPicker - 添加片段选择器
* - TrimPreview - 裁剪预览 tooltip
* - ContextMenu - 右键菜单
*
* Hooks:
* - useClipDrag - 片段拖拽排序
* - useTrimDrag - 裁剪拖拽
* - useTimelineMenus - 菜单 & 面板
* - usePlayheadDrag - 播放头拖拽
*/
import React, { useState, useRef, useCallback, useEffect } from "react"
import React from "react"
import type { ClipData, ClipType, TrimConfig } from "../types"
import { DEFAULT_PIXELS_PER_SECOND } from "../constants/timeline"
import { formatTime } from "../utils/timeline"
import { useClipDrag } from "../hooks/useClipDrag"
import { useTrimDrag } from "../hooks/useTrimDrag"
import { useTimelineMenus } from "../hooks/useTimelineMenus"
import { ClipCard } from "./timeline/ClipCard"
import { usePlayheadDrag } from "./timeline/hooks/usePlayheadDrag"
import { TimeRuler } from "./timeline/TimeRuler"
import { ClipTrack } from "./timeline/ClipTrack"
import { TimelineHeader } from "./timeline/TimelineHeader"
import { AddClipPicker } from "./timeline/AddClipPicker"
import { TrimPreview } from "./timeline/TrimPreview"
import { ContextMenu } from "./timeline/ContextMenu"
@@ -105,106 +115,32 @@ const TimelinePanel: React.FC<TimelinePanelProps> = ({
setHoveredClipId,
} = useTimelineMenus(clips, currentMode, onAddClip, onClipSplit, onClipResetTrim, onClipRemove)
/* ── 播放头拖拽状态 ── */
const [playheadDragging, setPlayheadDragging] = useState(false)
const trackRef = useRef<HTMLDivElement>(null)
/* ── 播放头拖拽 ── */
const { trackRef, handlePlayheadMouseDown, handleRulerClick } = usePlayheadDrag({
currentTime,
pps,
totalDuration,
onSeek,
})
/* ── 播放头拖拽全局 mousemove/mouseup ── */
useEffect(() => {
if (!playheadDragging) return
const handleMouseMove = (e: MouseEvent) => {
const trackEl = trackRef.current
if (!trackEl) return
const rect = trackEl.getBoundingClientRect()
const x = e.clientX - rect.left + trackEl.scrollLeft
const time = Math.max(0, Math.min(x / pps, totalDuration))
onSeek?.(Math.round(time * 10) / 10)
/* ── 撤销最后一个片段 ── */
const handleUndoClip = () => {
if (clips.length > 1) {
const last = clips[clips.length - 1]
onClipRemove(last.id)
}
const handleMouseUp = () => {
setPlayheadDragging(false)
}
document.addEventListener("mousemove", handleMouseMove)
document.addEventListener("mouseup", handleMouseUp)
return () => {
document.removeEventListener("mousemove", handleMouseMove)
document.removeEventListener("mouseup", handleMouseUp)
}
}, [playheadDragging, pps, totalDuration, onSeek])
/* ── 标尺点击跳转播放头 ── */
const handleRulerClick = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
const rect = e.currentTarget.getBoundingClientRect()
const x = e.clientX - rect.left
const time = Math.max(0, Math.min(x / pps, totalDuration))
onSeek?.(Math.round(time * 10) / 10)
},
[pps, totalDuration, onSeek],
)
/* ── 播放头拖拽开始 ── */
const handlePlayheadMouseDown = useCallback((e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
setPlayheadDragging(true)
}, [])
}
return (
<div className="ep-timeline-area">
{/* 时间线头部 */}
<div className="ep-timeline-header">
<div className="ep-timeline-title">
<span>🎬 时间线</span>
<span className="ep-timeline-duration">总时长: {formatTime(totalDuration)}</span>
</div>
<div className="ep-timeline-actions">
{/* 缩放控件 */}
<div className="ep-timeline-zoom">
<button
className="ep-zoom-btn"
onClick={() => onZoomChange?.(Math.max(10, pps - 10))}
title="缩小"
>
−
</button>
<input
type="range"
className="ep-zoom-slider"
min={10}
max={120}
step={5}
value={pps}
onChange={(e) => onZoomChange?.(Number(e.target.value))}
/>
<button
className="ep-zoom-btn"
onClick={() => onZoomChange?.(Math.min(120, pps + 10))}
title="放大"
>
+
</button>
<span className="ep-zoom-label">{pps}px/s</span>
</div>
<button
className="ep-timeline-action-btn"
onClick={() => {
if (clips.length > 1) {
const last = clips[clips.length - 1]
onClipRemove(last.id)
}
}}
title="删除最后一个片段"
>
↩
</button>
<button className="ep-timeline-action-btn" title="重做">
↪
</button>
</div>
</div>
<TimelineHeader
totalDuration={totalDuration}
pps={pps}
onZoomChange={onZoomChange}
onUndoClip={handleUndoClip}
clipsCount={clips.length}
/>
{/* 一镜到底提示 */}
{currentMode === "one_take" && <div className="ep-one-take-hint">🎥 一镜到底模式无片段</div>}
@@ -216,61 +152,33 @@ const TimelinePanel: React.FC<TimelinePanelProps> = ({
{/* 水平片段轨道 */}
{currentMode !== "one_take" && (
<div className="ep-clip-track" ref={trackRef} onDragOver={handleEmptyDragOver}>
{/* 播放头 */}
{currentMode !== "one_take" && totalDuration > 0 && (
<div
className="ep-playhead"
style={{ left: currentTime * pps }}
onMouseDown={handlePlayheadMouseDown}
>
<div className="ep-playhead-handle" />
</div>
)}
{clips.length === 0 ? (
<div className="ep-track-empty">
<div className="ep-track-empty-icon">🎬</div>
<div className="ep-track-empty-text">点击右侧 + 添加片段</div>
</div>
) : (
clips.map((clip, idx) => (
<ClipCard
key={clip.id}
clip={clip}
idx={idx}
isSelected={selectedClipId === clip.id}
isDragging={dragIdx === idx}
isDragOver={dragOverIdx === idx}
isHovered={hoveredClipId === clip.id}
pps={pps}
trimDragActive={!!trimDrag}
showTrimHandles={!!onClipTrim}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
onDrop={handleDrop}
onSelect={onClipSelect}
onContextMenu={handleContextMenu}
onMouseEnter={() => setHoveredClipId(clip.id)}
onMouseLeave={() => setHoveredClipId(null)}
onTrimHandleMouseDown={handleTrimHandleMouseDown}
onRemove={onClipRemove}
/>
))
)}
{/* ── 轨道末尾 "+" 添加卡片 ── */}
<div className="ep-track-add-card-wrapper">
<div
ref={addCardRef}
className="ep-track-add-card"
onClick={handleTogglePicker}
title="添加片段"
>
+
</div>
</div>
</div>
<ClipTrack
clips={clips}
selectedClipId={selectedClipId}
pps={pps}
currentTime={currentTime}
totalDuration={totalDuration}
dragIdx={dragIdx}
dragOverIdx={dragOverIdx}
hoveredClipId={hoveredClipId}
trimDragActive={!!trimDrag}
showTrimHandles={!!onClipTrim}
trackRef={trackRef}
addCardRef={addCardRef}
onPlayheadMouseDown={handlePlayheadMouseDown}
onClipSelect={onClipSelect}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
onDrop={handleDrop}
onEmptyDragOver={handleEmptyDragOver}
onContextMenu={handleContextMenu}
onClipMouseEnter={setHoveredClipId}
onClipMouseLeave={() => setHoveredClipId(null)}
onTrimHandleMouseDown={handleTrimHandleMouseDown}
onClipRemove={onClipRemove}
onTogglePicker={handleTogglePicker}
/>
)}
{/* 裁剪预览 tooltip */}
@@ -315,3 +223,4 @@ const TimelinePanel: React.FC<TimelinePanelProps> = ({
}
export default TimelinePanel