import React, { useCallback, useEffect, useState } from 'react';
import {
Alert,
Button,
Card,
Collapse,
Descriptions,
Drawer,
Empty,
Select,
Space,
Spin,
Table,
Tag,
Tooltip,
Typography,
message,
} from 'antd';
import { ArrowLeftOutlined, EyeOutlined, PlayCircleOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons';
import { useNavigate, useParams } from 'react-router-dom';
import { getAdminShotSegmentDetail, getAdminShotSegments, getAdminShotTaskSetDetail } from '../api';
import type { ShotAiSuggestionOut, ShotSegmentDetailOut, ShotSegmentOut, ShotTaskSetDetailOut } from '../types';
import { formatDate } from '../utils/formatDate';
import { getStepCodeLabel } from './adminReplication/components/StatusTag';
import { getShotAnalysisStatusMeta, getShotReplicateStatusMeta, getShotSplitStatusMeta, getShotTaskStatusMeta } from '../utils/shotReplicateStatus';
const RAW_API_BASE = import.meta.env.VITE_API_BASE || 'http://localhost:8000';
const RESOURCE_BASE = RAW_API_BASE.replace(/\/api\/?$/i, '').replace(/\/$/, '');
const PAGE_SIZE = 20;
const SOURCE_OPTIONS = [
{ value: 'ai_suggestion', label: 'AI 建议' },
{ value: 'custom', label: '自定义' },
];
const SPLIT_STATUS_OPTIONS = [
{ value: 'none', label: '未拆镜' },
{ value: 'pending', label: '待拆镜' },
{ value: 'processing', label: '拆镜中' },
{ value: 'completed', label: '拆镜完成' },
{ value: 'failed', label: '拆镜失败' },
{ value: 'retry_waiting', label: '等待重试' },
];
const ANALYSIS_STATUS_OPTIONS = [
{ value: 'not_required', label: '无需分析' },
{ value: 'pending', label: '待分析' },
{ value: 'processing', label: '分析中' },
{ value: 'completed', label: '分析完成' },
{ value: 'failed', label: '分析失败' },
];
const REPLICATE_STATUS_OPTIONS = [
{ value: 'not_started', label: '未复刻' },
{ value: 'project_created', label: '已创建项目' },
{ value: 'processing', label: '复刻中' },
{ value: 'completed', label: '复刻完成' },
{ value: 'failed', label: '复刻失败' },
];
const apiUrl = (url?: string | null): string => {
if (!url) return '';
const value = String(url).trim();
if (!value) return '';
if (/^(https?:)?\/\//i.test(value) || /^(blob|data):/i.test(value)) return value;
return `${RESOURCE_BASE}${value.startsWith('/') ? value : `/${value}`}`;
};
const safeDate = (value?: string | null): string => (value ? formatDate(value) : '-');
const shortId = (value?: string | null): string => (!value ? '-' : value.length > 16 ? `${value.slice(0, 10)}...` : value);
const StatusTag: React.FC<{ status?: string | null; kind?: 'task' | 'analysis' | 'split' | 'replicate' }> = ({ status, kind = 'task' }) => {
if (!status) return -;
const meta = kind === 'analysis'
? getShotAnalysisStatusMeta(status)
: (kind === 'split'
? getShotSplitStatusMeta(status)
: (kind === 'replicate' ? getShotReplicateStatusMeta(status) : getShotTaskStatusMeta(status)));
return {meta.text};
};
const JsonBlock: React.FC<{ value: unknown; maxHeight?: number }> = ({ value, maxHeight = 420 }) => (
{JSON.stringify(value ?? {}, null, 2)}
);
const VideoPreview: React.FC<{ url?: string | null; height?: number }> = ({ url, height = 280 }) => {
if (!url) return ;
return ;
};
const SuggestionTable: React.FC<{ items?: ShotAiSuggestionOut[] }> = ({ items = [] }) => (
`${Number(v || 0).toFixed(2)}s` },
{ title: '结束', dataIndex: 'endSecond', width: 80, render: (v: number) => `${Number(v || 0).toFixed(2)}s` },
{ title: '内容', dataIndex: 'content', ellipsis: true },
{ title: '分类', dataIndex: 'category', width: 140 },
{ title: '受众', dataIndex: 'audience', ellipsis: true },
]}
/>
);
const AdminShotTaskSetDetail: React.FC = () => {
const { taskSetId } = useParams<{ taskSetId: string }>();
const navigate = useNavigate();
const [detail, setDetail] = useState(null);
const [segments, setSegments] = useState([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const [segmentLoading, setSegmentLoading] = useState(false);
const [reloadKey, setReloadKey] = useState(0);
const [sourceMode, setSourceMode] = useState('');
const [splitStatus, setSplitStatus] = useState('');
const [analysisStatus, setAnalysisStatus] = useState('');
const [replicateStatus, setReplicateStatus] = useState('');
const [drawerOpen, setDrawerOpen] = useState(false);
const [segmentDetail, setSegmentDetail] = useState(null);
const loadDetail = useCallback(async () => {
if (!taskSetId) return;
setLoading(true);
try {
const res = await getAdminShotTaskSetDetail(taskSetId);
setDetail(res);
} catch (e: any) {
message.error(e?.message || '加载拆镜任务详情失败');
} finally {
setLoading(false);
}
}, [taskSetId]);
const loadSegments = useCallback(async () => {
if (!taskSetId) return;
setSegmentLoading(true);
try {
const res = await getAdminShotSegments(taskSetId, {
sourceMode: sourceMode || undefined,
splitStatus: splitStatus || undefined,
analysisStatus: analysisStatus || undefined,
replicateStatus: replicateStatus || undefined,
page,
pageSize: PAGE_SIZE,
});
setSegments(res.items || []);
setTotal(res.total || 0);
} catch (e: any) {
message.error(e?.message || '加载拆镜片段失败');
} finally {
setSegmentLoading(false);
}
}, [analysisStatus, page, replicateStatus, sourceMode, splitStatus, taskSetId]);
useEffect(() => { loadDetail(); }, [loadDetail, reloadKey]);
useEffect(() => { loadSegments(); }, [loadSegments, reloadKey]);
const hasActiveTasks = Boolean(detail && (
getShotTaskStatusMeta(detail.status).active
|| getShotAnalysisStatusMeta(detail.analysisStatus).active
|| getShotSplitStatusMeta(detail.splitStatus).active
|| segments.some((item) => (
getShotSplitStatusMeta(item.splitStatus).active
|| getShotAnalysisStatusMeta(item.analysisStatus).active
|| getShotReplicateStatusMeta(item.replicateStatus).active
))
));
useEffect(() => {
if (!hasActiveTasks) return undefined;
const refresh = () => {
if (document.visibilityState !== 'visible') return;
void loadDetail();
void loadSegments();
};
const timer = window.setInterval(refresh, 20000);
document.addEventListener('visibilitychange', refresh);
return () => {
window.clearInterval(timer);
document.removeEventListener('visibilitychange', refresh);
};
}, [hasActiveTasks, loadDetail, loadSegments]);
const openSegmentDetail = async (segmentId: string) => {
setDrawerOpen(true);
setSegmentDetail(null);
try {
const res = await getAdminShotSegmentDetail(segmentId);
setSegmentDetail(res);
} catch (e: any) {
message.error(e?.message || '加载片段详情失败');
}
};
const resetFilters = () => {
setSourceMode('');
setSplitStatus('');
setAnalysisStatus('');
setReplicateStatus('');
setPage(1);
setReloadKey(v => v + 1);
};
if (loading && !detail) {
return
;
}
if (!detail) {
return ;
}
return (
} onClick={() => navigate('/shot-replications')}>返回列表
拆镜任务详情
} onClick={() => setReloadKey(v => v + 1)}>刷新
{detail.id}
{detail.userId || '-'}
{detail.userName || '-'}
{detail.title || '-'}
{Number(detail.videoDurationSeconds || 0).toFixed(2)}s
{detail.completedSegmentCount}/{detail.segmentCount},失败 {detail.failedSegmentCount}
{detail.originalVideoCategory || '-'}
{safeDate(detail.createdAt)}
{safeDate(detail.updatedAt)}
{detail.originalVideoContent || '-'}
{detail.originalVideoAudience || '-'}
{detail.analysisErrorMessage ? : null}
{detail.splitErrorMessage ? : null}
原视频预览}>
{detail.aiSuggestions?.length ? : }
}]}
/>
`共 ${value} 条`, onChange: setPage }}
scroll={{ x: 1550 }}
columns={[
{ title: '片段ID', dataIndex: 'id', width: 150, render: (v: string) => {shortId(v)} },
{ title: '序号', dataIndex: 'segmentIndex', width: 70 },
{ title: '来源', dataIndex: 'sourceMode', width: 100, render: (v: string) => v === 'ai_suggestion' ? AI建议 : 自定义 },
{ title: '时间节点', dataIndex: 'timeNode', width: 130 },
{ title: '时长', dataIndex: 'durationSeconds', width: 90, render: (v: number) => `${Number(v || 0).toFixed(2)}s` },
{ title: '切割', dataIndex: 'splitStatus', width: 100, render: (v: string) => },
{ title: '分析', dataIndex: 'analysisStatus', width: 100, render: (v: string) => },
{ title: '复刻', dataIndex: 'replicateStatus', width: 110, render: (v: string) => },
{ title: '片段内容', dataIndex: 'segmentContent', width: 260, ellipsis: true, render: (v: string) => v || '-' },
{ title: '分类', dataIndex: 'segmentCategory', width: 120, render: (v: string) => v || '-' },
{
title: '关联项目',
width: 220,
render: (_, record) => record.moduleProjectId ? (
{record.moduleProjectTitle || getStepCodeLabel(record.moduleProjectCurrentStepCode)}
{String(record.moduleProjectFlowVersion || 'v1').toUpperCase()}
) : 未创建,
},
{ title: '错误', dataIndex: 'splitLastError', width: 220, ellipsis: true, render: (v: string, record) => v || record.analysisErrorMessage || '-' },
{ title: '创建时间', dataIndex: 'createdAt', width: 170, render: safeDate },
{
title: '操作',
fixed: 'right',
width: 100,
render: (_, record) => } onClick={() => openSegmentDetail(record.id)}>详情,
},
]}
/>
setDrawerOpen(false)} destroyOnClose>
{!segmentDetail ? : (
{segmentDetail.id}
{segmentDetail.timeNode}
{Number(segmentDetail.durationSeconds || 0).toFixed(2)}s
{segmentDetail.moduleProjectId ? : '-'}
{segmentDetail.moduleProjectId ? {String(segmentDetail.moduleProjectFlowVersion || 'v1').toUpperCase()} : '-'}
{segmentDetail.segmentContent || '-'}
{segmentDetail.segmentCategory || '-'}
{segmentDetail.segmentAudience || '-'}
{segmentDetail.splitLastError ? : null}
{segmentDetail.analysisErrorMessage ? : null}
},
{ key: 'suggestion', label: 'AI 建议原始 JSON', children: },
]}
/>
)}
);
};
export default AdminShotTaskSetDetail;