From 7e63bf7e269307e727a626df9a90bfe128b87807 Mon Sep 17 00:00:00 2001 From: SaaS Frontend Date: Sun, 26 Jul 2026 09:52:10 +0800 Subject: [PATCH 1/5] refactor(timeline-panel): Phase 1 - extract constants and utils --- .../components/TimelinePanel.tsx | 30 ++++++---- .../editing-planner/constants/timeline.ts | 56 +++++++++++++++++++ .../pages/editing-planner/utils/timeline.ts | 20 +++++++ 3 files changed, 94 insertions(+), 12 deletions(-) create mode 100644 apps/web/src/pages/editing-planner/constants/timeline.ts create mode 100644 apps/web/src/pages/editing-planner/utils/timeline.ts diff --git a/apps/web/src/pages/editing-planner/components/TimelinePanel.tsx b/apps/web/src/pages/editing-planner/components/TimelinePanel.tsx index 64d3f961a..de3acf8c1 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" diff --git a/apps/web/src/pages/editing-planner/constants/timeline.ts b/apps/web/src/pages/editing-planner/constants/timeline.ts new file mode 100644 index 000000000..c93de9812 --- /dev/null +++ b/apps/web/src/pages/editing-planner/constants/timeline.ts @@ -0,0 +1,56 @@ +import type { ClipType } from "../types" + +/** 片段类型图标 */ +export const CLIP_TYPE_ICONS: Record = { + voice: "🎙️", + pip: "🖼️", +} + +/** 片段类型标签 */ +export const CLIP_TYPE_LABELS: Record = { + voice: "口播", + pip: "混剪", +} + +/** 默认缩放:每秒像素数 */ +export const DEFAULT_PIXELS_PER_SECOND = 40 + +/** 最小缩放 */ +export const MIN_PIXELS_PER_SECOND = 10 + +/** 最大缩放 */ +export const MAX_PIXELS_PER_SECOND = 120 + +/** 缩放步长 */ +export const ZOOM_STEP = 10 + +/** 片段卡片最小宽度(px) */ +export const MIN_CLIP_WIDTH = 60 + +/** 添加面板宽度(px) */ +export const ADD_PICKER_WIDTH = 240 + +/** 轨道间距(px) */ +export const TRACK_GAP = 6 + +/** 最小裁剪时长(秒) */ +export const MIN_TRIM_DURATION = 1 + +/** 默认添加时长(秒) */ +export const DEFAULT_ADD_DURATION = 5 + +/** 最小添加时长(秒) */ +export const MIN_ADD_DURATION = 1 + +/** 最大添加时长(秒) */ +export const MAX_ADD_DURATION = 120 + +/** 轨道最小宽度(px) */ +export const MIN_TRACK_WIDTH = 300 + +/** 时间标尺刻度计算:根据总时长返回刻度步长(秒) */ +export const getRulerStep = (totalDuration: number): number => { + if (totalDuration <= 30) return 5 + if (totalDuration <= 60) return 10 + return 15 +} diff --git a/apps/web/src/pages/editing-planner/utils/timeline.ts b/apps/web/src/pages/editing-planner/utils/timeline.ts new file mode 100644 index 000000000..d0bd0b97c --- /dev/null +++ b/apps/web/src/pages/editing-planner/utils/timeline.ts @@ -0,0 +1,20 @@ +/** 格式化时间为 mm:ss */ +export const formatTime = (sec: number): string => { + const m = Math.floor(sec / 60) + const s = Math.floor(sec % 60) + return `${m}:${s.toString().padStart(2, "0")}` +} + +/** 格式化裁剪时间(精确到 0.1 秒) */ +export const formatTrimTime = (sec: number): string => { + return `${sec.toFixed(1)}s` +} + +/** 生成时间标尺刻度 */ +export const generateRulerMarks = (totalDuration: number, step: number): number[] => { + const marks: number[] = [] + for (let t = 0; t <= totalDuration + step; t += step) { + marks.push(t) + } + return marks +} -- 2.54.0 From c71352c2b72622690c37b935938e8743b924e735 Mon Sep 17 00:00:00 2001 From: SaaS Frontend Date: Sun, 26 Jul 2026 11:05:40 +0800 Subject: [PATCH 2/5] fix(phase1): remove duplicate local definitions, use imported utils --- .../components/TimelinePanel.tsx | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/apps/web/src/pages/editing-planner/components/TimelinePanel.tsx b/apps/web/src/pages/editing-planner/components/TimelinePanel.tsx index de3acf8c1..a62a783fe 100644 --- a/apps/web/src/pages/editing-planner/components/TimelinePanel.tsx +++ b/apps/web/src/pages/editing-planner/components/TimelinePanel.tsx @@ -436,22 +436,8 @@ const TimelinePanel: React.FC = ({ /* ── 时间标尺 ── */ 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 step = getRulerStep(totalDuration) + const rulerMarks = generateRulerMarks(totalDuration, step) return (
-- 2.54.0 From c704f9b844508d7681ba7cb586cf8d4ecb527d67 Mon Sep 17 00:00:00 2001 From: SaaS Frontend Date: Sun, 26 Jul 2026 11:13:30 +0800 Subject: [PATCH 3/5] fix(phase1): replace all magic numbers with named constants - Replace hardcoded values with imported constants - Fix unused import errors (TS6133) - MIN/MAX pixels per second, zoom step, clip width, trim duration, add duration, track width, picker width, track gap --- .../components/TimelinePanel.tsx | 46 ++++++++----------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/apps/web/src/pages/editing-planner/components/TimelinePanel.tsx b/apps/web/src/pages/editing-planner/components/TimelinePanel.tsx index a62a783fe..a29731b27 100644 --- a/apps/web/src/pages/editing-planner/components/TimelinePanel.tsx +++ b/apps/web/src/pages/editing-planner/components/TimelinePanel.tsx @@ -141,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(() => { @@ -150,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 }) }, []) @@ -187,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 @@ -198,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]) @@ -230,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 ── */ @@ -350,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 @@ -365,10 +359,10 @@ 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 @@ -435,7 +429,7 @@ const TimelinePanel: React.FC = ({ }, [contextMenu, onClipRemove]) /* ── 时间标尺 ── */ - const trackWidth = Math.max(totalDuration * pps, 300) + const trackWidth = Math.max(totalDuration * pps, MIN_TRACK_WIDTH) const step = getRulerStep(totalDuration) const rulerMarks = generateRulerMarks(totalDuration, step) @@ -452,7 +446,7 @@ const TimelinePanel: React.FC = ({