fix(ui): 模板选择预览条缩短+素材列表改为竖屏9:16卡片网格 #1521

Closed
xiaoxia wants to merge 1 commits from fix/ui-template-thumb-and-material-grid into develop
2 changed files with 165 additions and 114 deletions
@@ -1,5 +1,5 @@
/**
* 手动选择素材列表
* 手动选择素材列表 — 竖屏 9:16 卡片网格
*/
import React, { useRef, useCallback } from "react"
import { Typography } from "antd"
@@ -14,18 +14,24 @@ interface ManualMaterialListProps {
onToggle: (materialId: string) => void
}
/** 秒数格式化为 mm:ss */
const fmtDuration = (seconds?: number): string => {
if (!seconds && seconds !== 0) return "--:--"
const m = Math.floor(seconds / 60)
const s = Math.floor(seconds % 60)
return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`
}
const ManualMaterialList: React.FC<ManualMaterialListProps> = ({
materials,
materialsLoading,
selectedMaterials,
onToggle,
}) => {
// 追踪当前正在播放的视频元素,确保同时只有一个视频播放
const activeVideoRef = useRef<HTMLVideoElement | null>(null)
const handleVideoMouseEnter = useCallback((e: React.MouseEvent<HTMLVideoElement>) => {
const video = e.currentTarget
// 暂停之前正在播放的视频(检查是否仍在 DOM 中)
if (
activeVideoRef.current &&
activeVideoRef.current !== video &&
@@ -56,143 +62,188 @@ const ManualMaterialList: React.FC<ManualMaterialListProps> = ({
</Text>
) : (
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(110px, 1fr))",
gap: 10,
}}
>
{materials.items.map((m) => {
const checked = selectedMaterials.includes(m.id)
const isVideo = m.mime_type?.startsWith("video/") ?? false
const thumbSrc = m.thumbnail_url || undefined
return (
<label
<div
key={m.id}
onClick={() => onToggle(m.id)}
style={{
display: "flex",
alignItems: "center",
gap: 10,
padding: "8px 12px",
background: checked ? "var(--primary-soft, #eef2ff)" : "#f8fafc",
position: "relative",
aspectRatio: "9 / 16",
borderRadius: 10,
overflow: "hidden",
cursor: "pointer",
border: checked
? "1px solid var(--primary-color, #4f46e5)"
: "1px solid transparent",
? "2px solid var(--primary-color, #4f46e5)"
: "2px solid transparent",
boxShadow: checked
? "0 0 0 2px rgba(79, 70, 229, 0.2)"
: "0 1px 3px rgba(0, 0, 0, 0.1)",
background: "#1e293b",
transition: "all 0.15s ease",
}}
>
<input
type="checkbox"
checked={checked}
onChange={() => onToggle(m.id)}
style={{
accentColor: "var(--primary-color, #4f46e5)",
flexShrink: 0,
}}
/>
{/* 缩略图预览 48×48 */}
{/* 视频/图片封面 */}
{isVideo && m.file_url ? (
<video
src={m.file_url}
poster={m.thumbnail_url || undefined}
muted
loop
playsInline
preload="none"
onMouseEnter={handleVideoMouseEnter}
onMouseLeave={handleVideoMouseLeave}
style={{
width: "100%",
height: "100%",
objectFit: "cover",
display: "block",
}}
/>
) : thumbSrc ? (
<img
src={thumbSrc}
alt={m.name}
style={{
width: "100%",
height: "100%",
objectFit: "cover",
display: "block",
}}
onError={(e) => {
const target = e.target as HTMLImageElement
target.style.display = "none"
}}
/>
) : (
<div
style={{
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "linear-gradient(135deg, #334155, #1e293b)",
color: "rgba(255,255,255,0.5)",
fontSize: 28,
}}
>
{isVideo ? "🎬" : "🎵"}
</div>
)}
{/* 底部渐变遮罩 */}
<div
style={{
width: 48,
height: 48,
borderRadius: 6,
overflow: "hidden",
background: "#e2e8f0",
flexShrink: 0,
position: "absolute",
bottom: 0,
left: 0,
right: 0,
height: "50%",
background: "linear-gradient(0deg, rgba(0,0,0,0.6) 0%, transparent 100%)",
pointerEvents: "none",
}}
/>
{/* 播放按钮(中央) */}
<div
style={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 32,
height: 32,
borderRadius: "50%",
background: "rgba(99, 102, 241, 0.85)",
display: "flex",
alignItems: "center",
justifyContent: "center",
pointerEvents: "none",
}}
>
{isVideo && m.file_url ? (
<video
src={m.file_url}
poster={m.thumbnail_url || undefined}
muted
loop
playsInline
preload="none"
onMouseEnter={handleVideoMouseEnter}
onMouseLeave={handleVideoMouseLeave}
style={{
width: "100%",
height: "100%",
objectFit: "cover",
cursor: "pointer",
}}
/>
) : thumbSrc ? (
<img
src={thumbSrc}
alt={m.name}
style={{
width: "100%",
height: "100%",
objectFit: "cover",
}}
onError={(e) => {
const target = e.target as HTMLImageElement
target.style.display = "none"
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>
)}
{!thumbSrc && isVideo && !m.file_url && (
<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",
}}
>
{isVideo ? "🎬" : "🎵"}
</span>
)}
<svg width="14" height="14" viewBox="0 0 24 24" fill="white">
<path d="M8 5v14l11-7z" />
</svg>
</div>
<span
{/* 文件名(左下角) */}
<div
style={{
fontSize: 13,
color: "var(--text-primary)",
flex: 1,
minWidth: 0,
position: "absolute",
bottom: 6,
left: 6,
right: 50,
color: "white",
fontSize: 11,
fontWeight: 500,
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
textShadow: "0 1px 2px rgba(0,0,0,0.5)",
pointerEvents: "none",
zIndex: 1,
}}
>
{m.name}
</span>
<span
</div>
{/* 时长(右下角) */}
<div
style={{
fontSize: 11,
color: "var(--text-tertiary, #94a3b8)",
flexShrink: 0,
position: "absolute",
bottom: 6,
right: 6,
background: "rgba(0, 0, 0, 0.7)",
color: "white",
padding: "1px 5px",
borderRadius: 3,
fontSize: 10,
fontWeight: 600,
fontVariantNumeric: "tabular-nums",
pointerEvents: "none",
zIndex: 1,
}}
>
{m.mime_type?.split("/")?.[1]?.toUpperCase() ?? "FILE"}
</span>
</label>
{fmtDuration(m.duration)}
</div>
{/* 选中勾选标记(左上角) */}
{checked && (
<div
style={{
position: "absolute",
top: 6,
left: 6,
width: 20,
height: 20,
borderRadius: "50%",
background: "var(--primary-color, #4f46e5)",
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "white",
fontSize: 12,
fontWeight: 700,
zIndex: 2,
pointerEvents: "none",
}}
>
</div>
)}
</div>
)
})}
</div>
+4 -4
View File
@@ -203,7 +203,7 @@
background: var(--bg-primary);
border: 2px solid var(--border-color);
border-radius: var(--radius-md);
padding: 14px;
padding: 10px;
cursor: pointer;
transition: 0.18s ease;
text-align: center;
@@ -219,14 +219,14 @@
}
.xx-choice-thumb {
height: 60px;
height: 36px;
border-radius: var(--radius-sm);
display: grid;
place-items: center;
color: var(--text-inverse);
font-size: 24px;
font-size: 16px;
font-weight: 700;
margin-bottom: 10px;
margin-bottom: 6px;
}
.xx-choice-item h4 {