Merge pull request 'refactor(timeline-panel): Phase 1 - extract constants and utils' (#920) from refactor/timeline-panel-phase1-pr into develop
CI/CD Pipeline / Frontend Lint (push) Successful in 38s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m28s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m28s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 2m12s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m0s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m5s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m5s
CI/CD Pipeline / Integration Tests (push) Successful in 2m58s
CI/CD Pipeline / Unit Tests (push) Failing after 6m35s
CI/CD Pipeline / Build Staging API Image (push) Successful in 12m16s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m41s
CI/CD Pipeline / ACR Image Cleanup (push) Failing after 14s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 3m5s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m14s
CI/CD Pipeline / Canary Release to Production (push) Failing after 1251h35m38s
CI/CD Pipeline / Production Browser E2E (push) Failing after 1251h53m4s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 1251h53m30s
CI/CD Pipeline / Build Production Web Image (push) Failing after 1251h53m32s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 1251h56m1s
CI/CD Pipeline / PR Build Web Image (push) Failing after 1251h56m3s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 1251h56m26s
CI/CD Pipeline / Build Production API Image (push) Failing after 1252h26m12s
CI/CD Pipeline / PR Build API Image (push) Failing after 1252h28m43s
CI/CD Pipeline / Deploy Production (push) Failing after 1252h25m46s
CI/CD Pipeline / Frontend Lint (push) Successful in 38s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m28s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m28s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 2m12s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m0s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m5s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m5s
CI/CD Pipeline / Integration Tests (push) Successful in 2m58s
CI/CD Pipeline / Unit Tests (push) Failing after 6m35s
CI/CD Pipeline / Build Staging API Image (push) Successful in 12m16s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m41s
CI/CD Pipeline / ACR Image Cleanup (push) Failing after 14s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 3m5s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m14s
CI/CD Pipeline / Canary Release to Production (push) Failing after 1251h35m38s
CI/CD Pipeline / Production Browser E2E (push) Failing after 1251h53m4s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 1251h53m30s
CI/CD Pipeline / Build Production Web Image (push) Failing after 1251h53m32s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 1251h56m1s
CI/CD Pipeline / PR Build Web Image (push) Failing after 1251h56m3s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 1251h56m26s
CI/CD Pipeline / Build Production API Image (push) Failing after 1252h26m12s
CI/CD Pipeline / PR Build API Image (push) Failing after 1252h28m43s
CI/CD Pipeline / Deploy Production (push) Failing after 1252h25m46s
This commit was merged in pull request #920.
This commit is contained in:
@@ -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<ClipType, string> = {
|
||||
voice: "🎙️",
|
||||
pip: "🖼️",
|
||||
}
|
||||
|
||||
/** 片段类型标签 */
|
||||
const CLIP_TYPE_LABELS: Record<ClipType, string> = {
|
||||
voice: "口播",
|
||||
pip: "混剪",
|
||||
}
|
||||
|
||||
/** 裁剪拖拽方向 */
|
||||
type TrimDirection = "left" | "right"
|
||||
|
||||
@@ -135,7 +141,7 @@ const TimelinePanel: React.FC<TimelinePanelProps> = ({
|
||||
|
||||
/* ── "+" 卡片:类型+时长选择状态 ── */
|
||||
const [addType, setAddType] = useState<ClipType>(defaultAddType)
|
||||
const [addDuration, setAddDuration] = useState<number>(5)
|
||||
const [addDuration, setAddDuration] = useState<number>(DEFAULT_ADD_DURATION)
|
||||
|
||||
/* ── 模式切换时自动同步默认添加类型 ── */
|
||||
useEffect(() => {
|
||||
@@ -144,21 +150,17 @@ const TimelinePanel: React.FC<TimelinePanelProps> = ({
|
||||
}
|
||||
}, [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<TimelinePanelProps> = ({
|
||||
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<TimelinePanelProps> = ({
|
||||
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<TimelinePanelProps> = ({
|
||||
}, [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<TimelinePanelProps> = ({
|
||||
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<TimelinePanelProps> = ({
|
||||
|
||||
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<TimelinePanelProps> = ({
|
||||
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<TimelinePanelProps> = ({
|
||||
}, [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 (
|
||||
<div className="ep-timeline-area">
|
||||
@@ -460,7 +449,7 @@ const TimelinePanel: React.FC<TimelinePanelProps> = ({
|
||||
<div className="ep-timeline-zoom">
|
||||
<button
|
||||
className="ep-zoom-btn"
|
||||
onClick={() => onZoomChange?.(Math.max(10, pps - 10))}
|
||||
onClick={() => onZoomChange?.(Math.max(MIN_PIXELS_PER_SECOND, pps - ZOOM_STEP))}
|
||||
title="缩小"
|
||||
>
|
||||
−
|
||||
@@ -468,15 +457,15 @@ const TimelinePanel: React.FC<TimelinePanelProps> = ({
|
||||
<input
|
||||
type="range"
|
||||
className="ep-zoom-slider"
|
||||
min={10}
|
||||
max={120}
|
||||
min={MIN_PIXELS_PER_SECOND}
|
||||
max={MAX_PIXELS_PER_SECOND}
|
||||
step={5}
|
||||
value={pps}
|
||||
onChange={(e) => onZoomChange?.(Number(e.target.value))}
|
||||
/>
|
||||
<button
|
||||
className="ep-zoom-btn"
|
||||
onClick={() => onZoomChange?.(Math.min(120, pps + 10))}
|
||||
onClick={() => onZoomChange?.(Math.min(MAX_PIXELS_PER_SECOND, pps + ZOOM_STEP))}
|
||||
title="放大"
|
||||
>
|
||||
+
|
||||
@@ -567,7 +556,7 @@ const TimelinePanel: React.FC<TimelinePanelProps> = ({
|
||||
|
||||
<div
|
||||
className={`ep-clip-card ${selectedClipId === clip.id ? "selected" : ""} ${dragIdx === idx ? "dragging" : ""} ${dragOverIdx === idx ? "drag-over" : ""} ${hasTrim ? "trimmed" : ""}`}
|
||||
style={{ width: Math.max(clip.duration * pps, 60) }}
|
||||
style={{ width: Math.max(clip.duration * pps, MIN_CLIP_WIDTH) }}
|
||||
draggable={!trimDrag}
|
||||
onDragStart={(e) => handleDragStart(e, idx)}
|
||||
onDragOver={(e) => handleDragOver(e, idx)}
|
||||
@@ -751,11 +740,16 @@ const TimelinePanel: React.FC<TimelinePanelProps> = ({
|
||||
<input
|
||||
type="number"
|
||||
className="ep-duration-input"
|
||||
min={1}
|
||||
max={120}
|
||||
min={MIN_ADD_DURATION}
|
||||
max={MAX_ADD_DURATION}
|
||||
value={addDuration}
|
||||
onChange={(e) =>
|
||||
setAddDuration(Math.max(1, Math.min(120, Number(e.target.value) || 1)))
|
||||
setAddDuration(
|
||||
Math.max(
|
||||
MIN_ADD_DURATION,
|
||||
Math.min(MAX_ADD_DURATION, Number(e.target.value) || MIN_ADD_DURATION),
|
||||
),
|
||||
)
|
||||
}
|
||||
/>
|
||||
<span className="ep-add-clip-duration-unit">秒</span>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { ClipType } from "../types"
|
||||
|
||||
/** 片段类型图标 */
|
||||
export const CLIP_TYPE_ICONS: Record<ClipType, string> = {
|
||||
voice: "🎙️",
|
||||
pip: "🖼️",
|
||||
}
|
||||
|
||||
/** 片段类型标签 */
|
||||
export const CLIP_TYPE_LABELS: Record<ClipType, string> = {
|
||||
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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -10,9 +10,11 @@ import "@/pages/editing-planner/EditingPlanner"
|
||||
|
||||
// 常量
|
||||
import "@/pages/editing-planner/constants"
|
||||
import "@/pages/editing-planner/constants/timeline"
|
||||
|
||||
// 工具函数
|
||||
import "@/pages/editing-planner/utils/selectors"
|
||||
import "@/pages/editing-planner/utils/timeline"
|
||||
|
||||
// 子组件
|
||||
import "@/pages/editing-planner/components/BgmSelector"
|
||||
|
||||
Reference in New Issue
Block a user