diff --git a/apps/web/src/pages/editing-planner/components/TimelinePanel.tsx b/apps/web/src/pages/editing-planner/components/TimelinePanel.tsx index 64d3f961a..2a2f6f87f 100644 --- a/apps/web/src/pages/editing-planner/components/TimelinePanel.tsx +++ b/apps/web/src/pages/editing-planner/components/TimelinePanel.tsx @@ -11,6 +11,24 @@ import React, { useState, useRef, useCallback, useEffect, useLayoutEffect, useMemo } from "react" import type { ClipData, ClipType, TrimConfig } from "../types" import { TRANSITION_OPTIONS } from "@/api/template-editor" +import { + CLIP_TYPE_ICONS, + CLIP_TYPE_LABELS, + DEFAULT_PIXELS_PER_SECOND, + MIN_PIXELS_PER_SECOND, + MAX_PIXELS_PER_SECOND, + ZOOM_STEP, + MIN_CLIP_WIDTH, + ADD_PICKER_WIDTH, + TRACK_GAP, + MIN_TRIM_DURATION, + DEFAULT_ADD_DURATION, + MIN_ADD_DURATION, + MAX_ADD_DURATION, + MIN_TRACK_WIDTH, + getRulerStep, +} from "../constants/timeline" +import { formatTime, formatTrimTime, generateRulerMarks } from "../utils/timeline" interface TimelinePanelProps { clips: ClipData[] @@ -38,18 +56,6 @@ interface TimelinePanelProps { totalDuration?: number } -/** 片段类型图标 */ -const CLIP_TYPE_ICONS: Record = { - voice: "🎙️", - pip: "🖼️", -} - -/** 片段类型标签 */ -const CLIP_TYPE_LABELS: Record = { - voice: "口播", - pip: "混剪", -} - /** 裁剪拖拽方向 */ type TrimDirection = "left" | "right" @@ -135,7 +141,7 @@ const TimelinePanel: React.FC = ({ /* ── "+" 卡片:类型+时长选择状态 ── */ const [addType, setAddType] = useState(defaultAddType) - const [addDuration, setAddDuration] = useState(5) + const [addDuration, setAddDuration] = useState(DEFAULT_ADD_DURATION) /* ── 模式切换时自动同步默认添加类型 ── */ useEffect(() => { @@ -144,21 +150,17 @@ const TimelinePanel: React.FC = ({ } }, [currentMode, addType, availableTypes, defaultAddType]) - /* ── 面板尺寸 ── */ - const PICKER_W = 240 - const GAP = 6 - /* ── 计算 picker 初始位置 ── */ const updatePickerPosition = useCallback(() => { if (!addCardRef.current) return const rect = addCardRef.current.getBoundingClientRect() const vw = window.innerWidth const roughHeight = 180 - let top = rect.top - GAP - roughHeight + let top = rect.top - TRACK_GAP - roughHeight if (top < 8) top = 8 let right = vw - rect.right - if (rect.right - PICKER_W < 8) { - right = vw - PICKER_W - 8 + if (rect.right - ADD_PICKER_WIDTH < 8) { + right = vw - ADD_PICKER_WIDTH - 8 } setPickerPos({ top, right }) }, []) @@ -181,9 +183,9 @@ const TimelinePanel: React.FC = ({ const pickerH = pickerEl.offsetHeight const vh = window.innerHeight const vw = window.innerWidth - let top = addRect.top - GAP - pickerH + let top = addRect.top - TRACK_GAP - pickerH if (top < 8) { - top = addRect.bottom + GAP + top = addRect.bottom + TRACK_GAP if (top + pickerH > vh - 8) { top = vh - 8 - pickerH if (top < 8) top = 8 @@ -192,7 +194,7 @@ const TimelinePanel: React.FC = ({ let right = vw - addRect.right const pickerRect = pickerEl.getBoundingClientRect() if (pickerRect.left < 8) { - right = vw - PICKER_W - 8 + right = vw - ADD_PICKER_WIDTH - 8 } setPickerPos({ top, right }) }, [showAddPicker]) @@ -224,7 +226,7 @@ const TimelinePanel: React.FC = ({ }, [contextMenu]) /* ── 缩放 & 时长 ── */ - const pps = pixelsPerSecond ?? 40 + const pps = pixelsPerSecond ?? DEFAULT_PIXELS_PER_SECOND const totalDuration = totalDurationProp ?? clips.reduce((s, c) => s + c.duration, 0) /* ── 播放头拖拽全局 mousemove/mouseup ── */ @@ -344,11 +346,9 @@ const TimelinePanel: React.FC = ({ useEffect(() => { if (!trimDrag) return - const PX_PER_SECOND = pixelsPerSecond ?? 40 // 与缩放级别同步 - const handleMouseMove = (e: MouseEvent) => { const dx = e.clientX - trimDrag.startX - const dtSec = dx / PX_PER_SECOND + const dtSec = dx / pps const clip = clips.find((c) => c.id === trimDrag.clipId) if (!clip) return @@ -359,10 +359,13 @@ const TimelinePanel: React.FC = ({ if (trimDrag.direction === "left") { // 左手柄:调整入点 - newStart = Math.max(0, Math.min(origTrim.start_time + dtSec, newEnd - 1)) + newStart = Math.max(0, Math.min(origTrim.start_time + dtSec, newEnd - MIN_TRIM_DURATION)) } else { // 右手柄:调整出点 - newEnd = Math.max(origTrim.start_time + 1, Math.min(origTrim.end_time + dtSec, origDur)) + newEnd = Math.max( + origTrim.start_time + MIN_TRIM_DURATION, + Math.min(origTrim.end_time + dtSec, origDur), + ) } const newDuration = Math.round((newEnd - newStart) * 10) / 10 @@ -396,7 +399,7 @@ const TimelinePanel: React.FC = ({ document.removeEventListener("mousemove", handleMouseMove) document.removeEventListener("mouseup", handleMouseUp) } - }, [trimDrag, trimPreview, clips, onClipTrim, pixelsPerSecond]) + }, [trimDrag, trimPreview, clips, onClipTrim, pps]) /* ── 右键菜单 ── */ const handleContextMenu = useCallback((e: React.MouseEvent, clipId: string) => { @@ -429,23 +432,9 @@ const TimelinePanel: React.FC = ({ }, [contextMenu, onClipRemove]) /* ── 时间标尺 ── */ - const trackWidth = Math.max(totalDuration * pps, 300) - const rulerMarks: number[] = [] - const step = totalDuration <= 30 ? 5 : totalDuration <= 60 ? 10 : 15 - for (let t = 0; t <= totalDuration + step; t += step) { - rulerMarks.push(t) - } - - const formatTime = (sec: number) => { - const m = Math.floor(sec / 60) - const s = Math.floor(sec % 60) - return `${m}:${s.toString().padStart(2, "0")}` - } - - /** 格式化裁剪时间(精确到0.1秒) */ - const formatTrimTime = (sec: number) => { - return `${sec.toFixed(1)}s` - } + const trackWidth = Math.max(totalDuration * pps, MIN_TRACK_WIDTH) + const step = getRulerStep(totalDuration) + const rulerMarks = generateRulerMarks(totalDuration, step) return (
@@ -460,7 +449,7 @@ const TimelinePanel: React.FC = ({