From 8507729ca9cf6229332ac228a102afffa6568a96 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Thu, 6 Aug 2026 17:11:45 +0800 Subject: [PATCH 1/7] =?UTF-8?q?refactor:=20=E5=88=A0=E9=99=A4no-video?= =?UTF-8?q?=E6=A0=87=E9=A2=98=E9=A2=84=E8=A7=88=20+=20Canvas=E5=B1=85?= =?UTF-8?q?=E4=B8=AD=E6=94=B9=E7=94=A8getBoundingClientRect?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除 noVideoCanvasRef/noVideoContainerRef 声明 - 删除 drawNoVideoTitle 回调 - 删除 no-video ResizeObserver effect - 字体加载 effect 移除 drawNoVideoTitle 引用 - 删除 no-video JSX 块 - drawVideoTitle 改用 videoEl.getBoundingClientRect() 获取实际渲染尺寸 - 用 containerRect 计算 offsetX/offsetY 精确补偿视频偏移 --- .../generate/components/PreviewVideoPanel.tsx | 105 ++++-------------- 1 file changed, 19 insertions(+), 86 deletions(-) diff --git a/apps/web/src/pages/generate/components/PreviewVideoPanel.tsx b/apps/web/src/pages/generate/components/PreviewVideoPanel.tsx index d02fd88a3..723a44545 100644 --- a/apps/web/src/pages/generate/components/PreviewVideoPanel.tsx +++ b/apps/web/src/pages/generate/components/PreviewVideoPanel.tsx @@ -53,9 +53,8 @@ function wrapText(ctx: CanvasRenderingContext2D, text: string, maxWidth: number) * @param paddingX 左右边距(px),与 ASS 的 MarginL/MarginR 对应 * @param position "top" | "center" | "bottom" * @param topOffset 顶部/底部偏移量 - * @param fontSizeScale 字号缩放(no-video 模式下缩小) - * @param offsetX 绘制区域左边缘相对于 canvas 的偏移(用于 video 居中时补偿 offsetLeft) - * @param offsetY 绘制区域上边缘相对于 canvas 的偏移(用于 video 居中时补偿 offsetTop) + * @param offsetX 绘制区域左边缘相对于 canvas 的偏移(用于 video 居中时补偿) + * @param offsetY 绘制区域上边缘相对于 canvas 的偏移(用于 video 居中时补偿) */ function drawTitleOnCanvas( ctx: CanvasRenderingContext2D, @@ -66,7 +65,6 @@ function drawTitleOnCanvas( paddingX: number, position: string, topOffset: number, - fontSizeScale = 1, offsetX = 0, offsetY = 0, ) { @@ -87,7 +85,7 @@ function drawTitleOnCanvas( if (availableWidth <= 0) return // 字体设置 - const fontSize = Math.round(Math.min(settings.size * fontSizeScale, 36)) + const fontSize = Math.round(Math.min(settings.size, 36)) const fontWeight = settings.bold ? "bold" : "normal" const fontStyle = settings.italic ? "italic" : "normal" ctx.font = `${fontStyle} ${fontWeight} ${fontSize}px "${settings.font}"` @@ -170,9 +168,6 @@ export const PreviewVideoPanel: React.FC = ({ const canvasRef = useRef(null) const containerRef = useRef(null) const videoRef = useRef(null) - // no-video 模式 refs - const noVideoCanvasRef = useRef(null) - const noVideoContainerRef = useRef(null) // 字体加载状态(ref 供 draw 回调同步读取,无需 state 避免触发不必要的重渲染) const fontLoadedRef = useRef(false) @@ -187,16 +182,22 @@ export const PreviewVideoPanel: React.FC = ({ const ctx = canvas.getContext("2d") if (!ctx) return - const rect = container.getBoundingClientRect() - if (rect.width <= 0 || rect.height <= 0) return + const containerRect = container.getBoundingClientRect() + if (containerRect.width <= 0 || containerRect.height <= 0) return - // 优先使用 video 元素实际渲染尺寸,确保标题居中于视频画面 + // 使用 video 元素的 getBoundingClientRect 获取实际渲染尺寸和位置 const videoEl = videoRef.current - let drawW = rect.width - let drawH = rect.height + let drawW = containerRect.width + let drawH = containerRect.height + let offsetX = 0 + let offsetY = 0 + if (videoEl && videoEl.clientWidth > 0 && videoEl.clientHeight > 0) { - drawW = videoEl.clientWidth - drawH = videoEl.clientHeight + const videoRect = videoEl.getBoundingClientRect() + drawW = videoRect.width + drawH = videoRect.height + offsetX = videoRect.left - containerRect.left + offsetY = videoRect.top - containerRect.top } drawTitleOnCanvas( @@ -208,35 +209,8 @@ export const PreviewVideoPanel: React.FC = ({ 40, titleSettings.position, 60, - 1, - videoEl ? videoEl.offsetLeft : 0, - videoEl ? videoEl.offsetTop : 0, - ) - }, [titleText, titleSettings]) - - /** 在 no-video canvas 上绘制标题 */ - const drawNoVideoTitle = useCallback(() => { - if (!fontLoadedRef.current) return - const canvas = noVideoCanvasRef.current - const container = noVideoContainerRef.current - if (!canvas || !container || !titleSettings) return - const ctx = canvas.getContext("2d") - if (!ctx) return - - const rect = container.getBoundingClientRect() - if (rect.width <= 0 || rect.height <= 0) return - - // no-video 容器较窄,字号缩小 + 减少边距 - drawTitleOnCanvas( - ctx, - rect.width, - rect.height, - titleText || "", - titleSettings, - 20, - titleSettings.position, - 30, - 0.75, + offsetX, + offsetY, ) }, [titleText, titleSettings]) @@ -255,21 +229,6 @@ export const PreviewVideoPanel: React.FC = ({ return () => observer.disconnect() }, [showTitlePreview, hasPreview, drawVideoTitle]) - // no-video 模式:ResizeObserver 监听容器尺寸变化 → 重绘 - useEffect(() => { - if (!showTitlePreview || hasPreview || isError) return - const container = noVideoContainerRef.current - if (!container) return - - const observer = new ResizeObserver(() => { - drawNoVideoTitle() - }) - observer.observe(container) - requestAnimationFrame(drawNoVideoTitle) - - return () => observer.disconnect() - }, [showTitlePreview, hasPreview, isError, drawNoVideoTitle]) - // 字体加载检测:字体变更时重新检测,确保 measureText 使用正确字体 useEffect(() => { if (!showTitlePreview || !titleSettings) { @@ -291,7 +250,6 @@ export const PreviewVideoPanel: React.FC = ({ requestAnimationFrame(() => { if (!cancelled) { drawVideoTitle() - drawNoVideoTitle() } }) } @@ -311,7 +269,7 @@ export const PreviewVideoPanel: React.FC = ({ return () => { cancelled = true } - }, [showTitlePreview, titleSettings, drawVideoTitle, drawNoVideoTitle]) + }, [showTitlePreview, titleSettings, drawVideoTitle]) // video 加载完成后重绘 const handleVideoLoaded = useCallback(() => { @@ -399,31 +357,6 @@ export const PreviewVideoPanel: React.FC = ({ )} - {/* 无视频时的标题预览(idle/loading 状态下显示,error 状态下隐藏) */} - {!hasPreview && !isError && showTitlePreview && ( -
- -
- )} - {/* 预览信息 */} {hasPreview && previewResult && (
-- 2.54.0 From c411b732bda434e15d06cb756399ced4c192232b Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Thu, 6 Aug 2026 17:11:55 +0800 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20=E8=A7=86=E9=A2=91=E7=B4=A0?= =?UTF-8?q?=E6=9D=90=E7=BC=A9=E7=95=A5=E5=9B=BE=E6=94=B9=E4=B8=BA=E5=8F=AF?= =?UTF-8?q?=E6=92=AD=E6=94=BEvideo=E9=A2=84=E8=A7=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 视频素材(isVideo+file_url)使用video标签自动循环播放 - 无file_url时fallback到thumbnail_url静态图 - 非视频素材保持原有img/fallback逻辑 --- .../material/ManualMaterialList.tsx | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/apps/web/src/pages/generate/components/material/ManualMaterialList.tsx b/apps/web/src/pages/generate/components/material/ManualMaterialList.tsx index 3fa20a904..af82ee7ea 100644 --- a/apps/web/src/pages/generate/components/material/ManualMaterialList.tsx +++ b/apps/web/src/pages/generate/components/material/ManualMaterialList.tsx @@ -34,7 +34,6 @@ const ManualMaterialList: React.FC = ({ const checked = selectedMaterials.includes(m.id) const isVideo = m.mime_type?.startsWith("video/") ?? false // thumbnail_url 是静态截图,可直接用于 - // file_url 是视频/音频文件本身, 无法渲染,不能作为 src 回退 const thumbSrc = m.thumbnail_url || undefined return (