import React, { useState, useRef } from 'react';
import { Play, RotateCcw, ArrowUpToLine, Trash2, MoreVertical, Clock } from 'lucide-react';
import { formatTimeAgo, thumbUrl, h3FileUrl, getH3Base, getJobKind } from '../utils';
export default function JobCard({ job, isSelected, onSelect, onRerun, onPrioritize, onDelete }) {
const [isHovered, setIsHovered] = useState(false);
const [showMenu, setShowMenu] = useState(false);
const videoRef = useRef(null);
const req = job.req || {};
const kind = getJobKind(req);
const isDone = job.status === 'done';
const isRunning = job.status === 'running';
const isQueued = job.status === 'queued';
const isFailed = job.status === 'failed';
const isCancelled = job.status === 'cancelled';
// 进度计算
const curStep = job.step || 0;
const totalSteps = job.total_steps || req.steps || 20;
const progressPct = totalSteps > 0 ? Math.min(100, Math.max(curStep > 0 ? 5 : 2, Math.round((curStep / totalSteps) * 100))) : 0;
// 悬停时自动微播放
const handleMouseEnter = () => {
setIsHovered(true);
if (isDone && videoRef.current) {
videoRef.current.currentTime = 0;
videoRef.current.play().catch(() => {});
}
};
const handleMouseLeave = () => {
setIsHovered(false);
setShowMenu(false);
if (isDone && videoRef.current) {
videoRef.current.pause();
}
};
// 缩略图地址
const videoFileUrl = isDone ? h3FileUrl(`${getH3Base()}/v1/videos/${job.id}/file`) : null;
const coverImg = req.first_frame_url || (req.reference_image_urls || [])[0];
return (
onSelect(job.id)}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
className={`group relative p-3 rounded-2xl border transition-all duration-200 cursor-pointer flex flex-col gap-2 ${
isSelected
? 'bg-emerald-50/80 border-emerald-500 ring-1 ring-emerald-500/40 shadow-xs'
: 'bg-white border-slate-200/90 hover:bg-slate-50/80 hover:border-slate-300 shadow-xs'
}`}
>
{/* 缩略图容器 (自适应比例) */}
{isDone && videoFileUrl ? (
) : coverImg ? (
})
) : (
{kind.label}
{job.partition || 'H3'}
)}
{/* 状态悬浮微标 */}
{isDone && (
)}
{isRunning && (
{progressPct}%
)}
{isQueued && (
{job.queue_position ? `#${job.queue_position}` : '排队中'}
)}
{/* 右侧核心信息 */}
{/* 顶栏:短 ID 与状态胶囊 */}
#{job.id.substring(0, 8)}
{/* 状态 Pill */}
{isDone && (
已完成
)}
{isRunning && (
采样 {curStep}/{totalSteps}步
)}
{isQueued && (
排队中
)}
{isFailed && (
失败
)}
{isCancelled && (
已取消
)}
{/* Prompt 摘要 */}
{req.prompt || '(无 prompt 描述)'}
{/* 底栏:规格、时间与快捷菜单 */}
{kind.label} · {req.width ? `${req.width}p` : ''}
{formatTimeAgo(job.created_at)}
{/* 三点操作菜单 */}
e.stopPropagation()}>
{showMenu && (
{isQueued && (
)}
)}
{/* 处于运行中时,卡片底部直接展现清晰的横向进度条 */}
{isRunning && (
)}
);
}