fix: 修复视频多实例播放问题+ref回调改为poster属性
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 39s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 40s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m28s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m42s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m53s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m20s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 1m30s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m10s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m33s
AI Code Review / AI Code Review (pull_request) Failing after 2m15s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 5m7s
CI/CD Pipeline / CI Gate (pull_request) Failing after 6s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 982h0m32s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 982h0m32s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 982h0m33s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 982h0m33s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 982h0m33s
CI/CD Pipeline / PR Build Worker Image (pull_request) Failing after 982h4m59s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 982h5m1s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 982h5m37s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 982h5m40s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 982h5m37s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 982h5m40s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 982h5m41s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 982h5m41s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 982h33m50s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 982h33m51s
CI/CD Pipeline / PR Build API Image (pull_request) Failing after 982h38m18s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 982h38m56s

- 新增 activeVideoRef 追踪当前播放视频,播放新视频前暂停其他视频
- 移除内联 ref 回调,改用 poster 属性直接设置封面
- mouseLeave 时同步清理 activeVideoRef
This commit is contained in:
2026-08-06 17:21:36 +08:00
parent 57c181ce97
commit 0d3c3e81ac
@@ -20,22 +20,45 @@ const ManualMaterialList: React.FC<ManualMaterialListProps> = ({
selectedMaterials,
onToggle,
}) => {
const handleVideoMouseEnter = useCallback((e: React.MouseEvent<HTMLVideoElement>) => {
e.currentTarget.play().catch(() => {})
}, [])
// 追踪当前正在播放的视频元素,确保同时只有一个视频播放
const activeVideoRef = useRef<HTMLVideoElement | null>(null)
const handleVideoMouseLeave = useCallback((e: React.MouseEvent<HTMLVideoElement>) => {
const video = e.currentTarget
video.pause()
video.currentTime = 0
}, [])
const handleVideoMouseEnter = useCallback(
(e: React.MouseEvent<HTMLVideoElement>) => {
const video = e.currentTarget
// 暂停之前正在播放的视频
if (activeVideoRef.current && activeVideoRef.current !== video) {
activeVideoRef.current.pause()
activeVideoRef.current.currentTime = 0
}
activeVideoRef.current = video
video.play().catch(() => {})
},
[],
)
const handleVideoMouseLeave = useCallback(
(e: React.MouseEvent<HTMLVideoElement>) => {
const video = e.currentTarget
video.pause()
video.currentTime = 0
if (activeVideoRef.current === video) {
activeVideoRef.current = null
}
},
[],
)
return (
<div style={{ marginTop: 14 }}>
{materialsLoading ? (
<Text style={{ color: "var(--text-secondary)", padding: "16px 0" }}>加载素材中…</Text>
) : materials.items.length === 0 ? (
<Text style={{ color: "var(--text-secondary)", padding: "16px 0" }}>
加载素材中…
</Text>
) : materials.items.length === 0 ? (
<Text
style={{ color: "var(--text-secondary)", padding: "16px 0" }}
>
暂无素材,请先在视频库中上传
</Text>
) : (
@@ -43,7 +66,6 @@ const ManualMaterialList: React.FC<ManualMaterialListProps> = ({
{materials.items.map((m) => {
const checked = selectedMaterials.includes(m.id)
const isVideo = m.mime_type?.startsWith("video/") ?? false
// thumbnail_url 是静态截图,可直接用于 <img>
const thumbSrc = m.thumbnail_url || undefined
return (
<label
@@ -53,7 +75,9 @@ const ManualMaterialList: React.FC<ManualMaterialListProps> = ({
alignItems: "center",
gap: 10,
padding: "8px 12px",
background: checked ? "var(--primary-soft, #eef2ff)" : "#f8fafc",
background: checked
? "var(--primary-soft, #eef2ff)"
: "#f8fafc",
borderRadius: 10,
cursor: "pointer",
border: checked
@@ -66,7 +90,10 @@ const ManualMaterialList: React.FC<ManualMaterialListProps> = ({
type="checkbox"
checked={checked}
onChange={() => onToggle(m.id)}
style={{ accentColor: "var(--primary-color, #4f46e5)", flexShrink: 0 }}
style={{
accentColor: "var(--primary-color, #4f46e5)",
flexShrink: 0,
}}
/>
{/* 缩略图预览 48×48 */}
<div
@@ -84,13 +111,8 @@ const ManualMaterialList: React.FC<ManualMaterialListProps> = ({
>
{isVideo && m.file_url ? (
<video
ref={(el) => {
if (el && m.thumbnail_url) {
// 显示封面帧作为初始画面
el.poster = m.thumbnail_url
}
}}
src={m.file_url}
poster={m.thumbnail_url || undefined}
muted
loop
playsInline
@@ -116,20 +138,43 @@ const ManualMaterialList: React.FC<ManualMaterialListProps> = ({
onError={(e) => {
const target = e.target as HTMLImageElement
target.style.display = "none"
const fallback = target.nextElementSibling as HTMLElement | null
const fallback =
target.nextElementSibling as HTMLElement | null
if (fallback) fallback.style.display = "flex"
}}
/>
) : null}
{!thumbSrc && !isVideo && (
<span style={{ fontSize: 20, opacity: 0.5, display: "flex" }}>🎵</span>
<span
style={{
fontSize: 20,
opacity: 0.5,
display: "flex",
}}
>
🎵
</span>
)}
{!thumbSrc && isVideo && !m.file_url && (
<span style={{ fontSize: 20, opacity: 0.5, display: "flex" }}>🎬</span>
<span
style={{
fontSize: 20,
opacity: 0.5,
display: "flex",
}}
>
🎬
</span>
)}
{/* img onError 时显示的 fallback(初始隐藏) */}
{thumbSrc && !(isVideo && m.file_url) && (
<span style={{ fontSize: 20, opacity: 0.5, display: "none" }}>
<span
style={{
fontSize: 20,
opacity: 0.5,
display: "none",
}}
>
{isVideo ? "🎬" : "🎵"}
</span>
)}