diff --git a/apps/web/src/pages/editing-planner/components/TimelinePanel.tsx b/apps/web/src/pages/editing-planner/components/TimelinePanel.tsx index cc418e343..bc59991a4 100755 --- a/apps/web/src/pages/editing-planner/components/TimelinePanel.tsx +++ b/apps/web/src/pages/editing-planner/components/TimelinePanel.tsx @@ -3,7 +3,13 @@ * 时间标尺(20px) + 水平片段卡片轨道(100x100) + HTML5拖拽排序 * "+" 卡片 → 类型+时长选择器 */ -import React, { useState, useRef, useCallback, useEffect } from "react"; +import React, { + useState, + useRef, + useCallback, + useEffect, + useLayoutEffect, +} from "react"; import type { ClipData, ClipType } from "../types"; interface TimelinePanelProps { @@ -60,32 +66,24 @@ const TimelinePanel: React.FC = ({ ? ["pip"] : ["voice", "pip"]; // voice_pip 或默认 - /* ── 面板尺寸估算(用于边界检测) ── */ - const PICKER_H = 200; // 面板预估高度(padding + 标题 + 类型行 + 时长行 + 按钮 + 间距) + /* ── 面板尺寸(宽度固定,高度由 useLayoutEffect 实测) ── */ const PICKER_W = 240; // 面板宽度(与 CSS 一致) const GAP = 6; // 面板与"+"卡片的间距 - /* ── 计算 picker 弹出位置(fixed 定位 + 视口边界检测) ── */ + /* ── 计算 picker 初始位置(默认从"+"按钮上方弹出) ── */ const updatePickerPosition = useCallback(() => { if (!addCardRef.current) return; const rect = addCardRef.current.getBoundingClientRect(); const vw = window.innerWidth; - const vh = window.innerHeight; - /* 垂直方向:优先向下弹出;空间不足则向上翻转 */ - let top: number; - if (rect.bottom + GAP + PICKER_H > vh) { - // 下方空间不够 → 向上弹出 - top = rect.top - GAP - PICKER_H; - if (top < 8) top = 8; // 极端情况: clamp 到视口顶部 - } else { - top = rect.bottom + GAP; - } + /* 垂直方向:默认向上弹出(上方空间永远比下方大) */ + const roughHeight = 180; // 粗略估算,useLayoutEffect 会用实际高度校正 + let top = rect.top - GAP - roughHeight; + if (top < 8) top = 8; /* 水平方向:右对齐"+"卡片;太靠右超出视口则左移 */ let right = vw - rect.right; if (rect.right - PICKER_W < 8) { - // 左边界保护 right = vw - PICKER_W - 8; } @@ -99,36 +97,36 @@ const TimelinePanel: React.FC = ({ setShowAddPicker((v) => !v); }; - /* ── 渲染后用实际面板高度做精确边界校正 ── */ - useEffect(() => { - if (!showAddPicker || !pickerRef.current) return; - const pickerRect = pickerRef.current.getBoundingClientRect(); + /* ── 渲染后用实际 offsetHeight 做精确边界校正(useLayoutEffect 确保 paint 前完成) ── */ + useLayoutEffect(() => { + if (!showAddPicker || !pickerRef.current || !addCardRef.current) return; + const pickerEl = pickerRef.current; + const addRect = addCardRef.current.getBoundingClientRect(); + const pickerH = pickerEl.offsetHeight; // 实际高度,不用硬编码 const vh = window.innerHeight; const vw = window.innerWidth; - let adjustTop: number | null = null; - let adjustRight: number | null = null; + /* 默认:面板在"+"按钮上方 */ + let top = addRect.top - GAP - pickerH; - // 底部溢出 → 上移 - if (pickerRect.bottom > vh - 8) { - adjustTop = vh - 8 - pickerRect.height; - if (adjustTop < 8) adjustTop = 8; + /* 上方空间也不够(极端情况)→ 翻转到下方 */ + if (top < 8) { + top = addRect.bottom + GAP; + /* 下方也溢出 → clamp */ + if (top + pickerH > vh - 8) { + top = vh - 8 - pickerH; + if (top < 8) top = 8; + } } - // 顶部溢出 → 下移 - if (pickerRect.top < 8) { - adjustTop = 8; - } - // 左侧溢出 → 左移(right 增大) + + /* 水平方向:右对齐"+"卡片;左侧溢出保护 */ + let right = vw - addRect.right; + const pickerRect = pickerEl.getBoundingClientRect(); if (pickerRect.left < 8) { - adjustRight = vw - PICKER_W - 8; + right = vw - PICKER_W - 8; } - if (adjustTop !== null || adjustRight !== null) { - setPickerPos((prev) => ({ - top: adjustTop ?? prev.top, - right: adjustRight ?? prev.right, - })); - } + setPickerPos({ top, right }); }, [showAddPicker]); /* ── 点击外部关闭添加面板 ── */