feat :修改文件结构
This commit is contained in:
165
src/pages/home/components/OutputPreview.tsx
Normal file
165
src/pages/home/components/OutputPreview.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
// ============================================================
|
||||
// OutputPreview — 任务输出预览(图片 / 视频)
|
||||
// 使用 useTaskDetail() Hook 获取任务详情,组件仅负责渲染
|
||||
// ============================================================
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { Image, Empty, Spin, Typography, Button, Tag, theme as antTheme } from 'antd';
|
||||
import {
|
||||
EyeOutlined,
|
||||
DownloadOutlined,
|
||||
CloseCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
import { useHomeContext } from '@/contexts/home-context';
|
||||
import { useTaskDetail } from '@/hooks/use-api';
|
||||
import type { TaskStatusString } from '@/services/modules';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
// ---------- 状态标签配置 ----------
|
||||
|
||||
const STATUS_CONFIG: Record<TaskStatusString, { color: string; label: string }> = {
|
||||
pending: { color: 'blue', label: '排队中' },
|
||||
submitted: { color: 'cyan', label: '已提交' },
|
||||
processing: { color: 'orange', label: '处理中' },
|
||||
success: { color: 'green', label: '已完成' },
|
||||
failed: { color: 'red', label: '失败' },
|
||||
};
|
||||
|
||||
// ---------- 工具函数 ----------
|
||||
|
||||
function isVideoUrl(url: string): boolean {
|
||||
return /\.(mp4|webm|mov|avi|mkv)(\?|$)/i.test(url);
|
||||
}
|
||||
|
||||
function splitAssets(assets: Array<{ url: string }>): { images: string[]; videos: string[] } {
|
||||
const images: string[] = [];
|
||||
const videos: string[] = [];
|
||||
assets.forEach(({ url }) => {
|
||||
if (isVideoUrl(url)) videos.push(url);
|
||||
else images.push(url);
|
||||
});
|
||||
return { images, videos };
|
||||
}
|
||||
|
||||
// ---------- 组件 ----------
|
||||
|
||||
export function OutputPreview() {
|
||||
const { token } = antTheme.useToken();
|
||||
const { selectedTask } = useHomeContext();
|
||||
|
||||
// 数据获取(Hook 自动处理:taskId 为 null 时不请求、竞态、loading/error)
|
||||
const { data: detail, loading } = useTaskDetail(selectedTask?.id);
|
||||
|
||||
// 从 output_assets 提取图片/视频
|
||||
const { images, videos } = useMemo(() => {
|
||||
if (!detail?.output_assets || detail.output_assets.length === 0) {
|
||||
return { images: [], videos: [] };
|
||||
}
|
||||
return splitAssets(detail.output_assets);
|
||||
}, [detail]);
|
||||
|
||||
const status = selectedTask?.status as TaskStatusString | undefined;
|
||||
const statusCfg = status
|
||||
? (STATUS_CONFIG[status] || { color: 'default', label: status })
|
||||
: { color: 'default', label: '未知' };
|
||||
|
||||
// ---- 无选中任务 ----
|
||||
if (!selectedTask) {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
|
||||
<Empty description="选择任务记录查看输出" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---- 处理中 ----
|
||||
if (status === 'pending' || status === 'submitted' || status === 'processing') {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', gap: 16 }}>
|
||||
<Spin size="large" />
|
||||
<Tag color={statusCfg.color}>{statusCfg.label}</Tag>
|
||||
<Text type="secondary">任务正在处理中,请稍候...</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---- 失败 ----
|
||||
if (status === 'failed') {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', gap: 12 }}>
|
||||
<CloseCircleOutlined style={{ fontSize: 48, color: '#ff4d4f' }} />
|
||||
<Text type="danger" strong>任务执行失败</Text>
|
||||
{detail?.error_message && (
|
||||
<Text type="secondary" style={{ maxWidth: 300, textAlign: 'center', fontSize: 12 }}>
|
||||
{detail.error_message}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---- 加载中 ----
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---- 无结果 ----
|
||||
if (!detail || images.length + videos.length === 0) {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
|
||||
<Empty description="暂无输出结果" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---- 展示 ----
|
||||
return (
|
||||
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
|
||||
<div style={{
|
||||
padding: '12px 16px',
|
||||
borderBottom: `1px solid ${token.colorBorderSecondary}`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<EyeOutlined style={{ fontSize: 14 }} />
|
||||
<Text strong style={{ fontSize: 13 }}>输出预览</Text>
|
||||
<Tag color={statusCfg.color} style={{ fontSize: 11 }}>{statusCfg.label}</Tag>
|
||||
</div>
|
||||
{videos.length > 0 && (
|
||||
<Button size="small" type="link" icon={<DownloadOutlined />} href={videos[0]} target="_blank">
|
||||
下载视频
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, overflow: 'auto', padding: 12, display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
{videos.map((url, i) => (
|
||||
<div key={i} style={{ borderRadius: 6, overflow: 'hidden', background: '#000' }}>
|
||||
<video controls style={{ width: '100%', maxHeight: 360, display: 'block' }} src={url}>
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
</div>
|
||||
))}
|
||||
{images.length === 1 ? (
|
||||
<Image src={images[0]} alt="输出结果" style={{ borderRadius: 6, width: '100%', objectFit: 'contain' }} />
|
||||
) : (
|
||||
<Image.PreviewGroup>
|
||||
{images.map((url, i) => (
|
||||
<Image key={i} src={url} alt={`输出结果 ${i + 1}`}
|
||||
style={{ borderRadius: 6, width: '100%', objectFit: 'contain', marginBottom: 8 }} />
|
||||
))}
|
||||
</Image.PreviewGroup>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user