feat(results): add generated video management
This commit is contained in:
@@ -26,6 +26,9 @@ export interface GeneratedVideoItem {
|
||||
width: number;
|
||||
height: number;
|
||||
fps: number;
|
||||
status?: string;
|
||||
review_status?: 'pending_review' | 'approved' | 'rejected';
|
||||
generation_params?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export const createGenerationTask = async (data: {
|
||||
@@ -55,6 +58,11 @@ export const getGeneratedVideos = async (projectId: string): Promise<GeneratedVi
|
||||
return response.data.items;
|
||||
};
|
||||
|
||||
export const updateGeneratedVideoReviewStatus = async (videoId: string, reviewStatus: 'pending_review' | 'approved' | 'rejected'): Promise<GeneratedVideoItem> => {
|
||||
const response = await apiClient.patch(`/generated-videos/${videoId}/review`, { review_status: reviewStatus });
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getGeneratedVideoDownloadUrl = async (videoId: string): Promise<string> => {
|
||||
const response = await apiClient.get(`/generated-videos/${videoId}/download-url`);
|
||||
return response.data.download_url;
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import { Alert, Button, Card, Empty, List, Space, Spin, Tag, Typography, message } from 'antd';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getGeneratedVideoDownloadUrl, getGeneratedVideos } from '@/api/generation';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { getGeneratedVideoDownloadUrl, getGeneratedVideos, updateGeneratedVideoReviewStatus } from '@/api/generation';
|
||||
|
||||
const reviewLabels: Record<string, { label: string; color: string }> = {
|
||||
pending_review: { label: '待复核', color: 'gold' },
|
||||
approved: { label: '可发布', color: 'green' },
|
||||
rejected: { label: '需返工', color: 'red' },
|
||||
};
|
||||
|
||||
const ProjectResults: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const location = useLocation();
|
||||
const projectId = id || '';
|
||||
const routedWorkspaceId = (location.state as { workspaceId?: string } | null)?.workspaceId || '';
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
useEffect(() => {
|
||||
if (projectId && routedWorkspaceId) {
|
||||
@@ -23,6 +30,16 @@ const ProjectResults: React.FC = () => {
|
||||
refetchInterval: 5000,
|
||||
});
|
||||
|
||||
const reviewMutation = useMutation({
|
||||
mutationFn: ({ videoId, reviewStatus }: { videoId: string; reviewStatus: 'approved' | 'rejected' }) =>
|
||||
updateGeneratedVideoReviewStatus(videoId, reviewStatus),
|
||||
onSuccess: () => {
|
||||
message.success('成片复核状态已更新');
|
||||
queryClient.invalidateQueries({ queryKey: ['generated-videos', projectId] });
|
||||
},
|
||||
onError: () => message.error('成片复核状态更新失败'),
|
||||
});
|
||||
|
||||
const handleDownload = async (videoId: string) => {
|
||||
try {
|
||||
const url = await getGeneratedVideoDownloadUrl(videoId);
|
||||
@@ -34,7 +51,7 @@ const ProjectResults: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24 }}>
|
||||
<Card title="项目成片结果">
|
||||
<Card title="项目成片中心">
|
||||
{videosQuery.isError && (
|
||||
<Alert style={{ marginBottom: 16 }} type="error" showIcon message="生成结果加载失败" />
|
||||
)}
|
||||
@@ -44,31 +61,47 @@ const ProjectResults: React.FC = () => {
|
||||
) : videosQuery.data?.length ? (
|
||||
<List
|
||||
dataSource={videosQuery.data}
|
||||
renderItem={(item) => (
|
||||
<List.Item
|
||||
actions={[
|
||||
<Button key="download" type="link" onClick={() => handleDownload(item.id)}>
|
||||
获取下载地址
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<List.Item.Meta
|
||||
title={
|
||||
<Space wrap>
|
||||
<Typography.Text>{item.name}</Typography.Text>
|
||||
<Tag>{item.width}x{item.height}</Tag>
|
||||
<Tag>{item.duration}s</Tag>
|
||||
<Tag>{Math.round(item.file_size / 1024)} KB</Tag>
|
||||
</Space>
|
||||
}
|
||||
description={
|
||||
<Typography.Text copyable ellipsis style={{ maxWidth: 720 }}>
|
||||
{item.name}
|
||||
</Typography.Text>
|
||||
}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
renderItem={(item) => {
|
||||
const review = reviewLabels[item.review_status || 'pending_review'] || reviewLabels.pending_review;
|
||||
return (
|
||||
<List.Item
|
||||
actions={[
|
||||
<Button key="download" type="link" onClick={() => handleDownload(item.id)}>
|
||||
获取下载地址
|
||||
</Button>,
|
||||
<Button key="approve" type="link" onClick={() => reviewMutation.mutate({ videoId: item.id, reviewStatus: 'approved' })}>
|
||||
标记可发布
|
||||
</Button>,
|
||||
<Button key="reject" danger type="link" onClick={() => reviewMutation.mutate({ videoId: item.id, reviewStatus: 'rejected' })}>
|
||||
标记需返工
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<List.Item.Meta
|
||||
title={
|
||||
<Space wrap>
|
||||
<Typography.Text>{item.name}</Typography.Text>
|
||||
<Tag color="blue">{item.status || 'completed'}</Tag>
|
||||
<Tag color={review.color}>{review.label}</Tag>
|
||||
<Tag>{item.width}x{item.height}</Tag>
|
||||
<Tag>{item.duration}s</Tag>
|
||||
<Tag>{Math.round(item.file_size / 1024)} KB</Tag>
|
||||
</Space>
|
||||
}
|
||||
description={
|
||||
<Space direction="vertical" size={4}>
|
||||
<Typography.Text copyable ellipsis style={{ maxWidth: 720 }}>
|
||||
{item.name}
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary">
|
||||
生成参数:素材库 {String(item.generation_params?.asset_library_id || '未记录')},标题 {String(item.generation_params?.title_id || '自动/未选择')}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
</List.Item>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Empty description="暂无生成结果,先去发起生成任务" />
|
||||
|
||||
Reference in New Issue
Block a user