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
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:
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user