fix(editing-planner): 修复添加片段面板被裁剪 — 默认向上弹出+useLayoutEffect精确校正

- 面板默认从+按钮上方弹出,上方不够时翻转到下方
- 用useLayoutEffect+实际offsetHeight替代硬编码PICKER_H
- fixed定位脱离父容器overflow裁剪

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
灵应
2026-07-06 22:03:33 +08:00
parent edf6a5501b
commit 7ec8e4355e
@@ -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<TimelinePanelProps> = ({
? ["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<TimelinePanelProps> = ({
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]);
/* ── 点击外部关闭添加面板 ── */