import React, { useEffect, useState } from 'react'; import { Button, Card, Input, message, Modal, Select, Space, Table, Tag, Typography, Tooltip, Image, } from 'antd'; import { PlayCircleOutlined, EyeOutlined, ClockCircleOutlined, CheckCircleOutlined, LoadingOutlined, CloseCircleOutlined, SearchOutlined, VideoCameraOutlined, ExclamationCircleOutlined, } from '@ant-design/icons'; import { getAdminGenerationRecords, adminUpdateGenerationStatus, adminGenerateVideo } from '../api'; import type { AdminGenerationRecord } from '../types'; import { formatDate } from '../utils/formatDate'; const API_BASE = import.meta.env.VITE_API_BASE || 'http://localhost:8000'; const apiUrl = (url?: string) => url ? (url.startsWith('http') ? url : `${API_BASE}${url}`) : ''; const STATUS_MAP: Record = { optimizing: { color: 'processing', text: '优化中', icon: }, prompt_optimized: { color: 'processing', text: '待生成', icon: }, generating: { color: 'warning', text: '生成中', icon: }, completed: { color: 'success', text: '已完成', icon: }, failed: { color: 'error', text: '失败', icon: }, }; const AdminGenerationRecords: React.FC = () => { const [records, setRecords] = useState([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(false); const [page, setPage] = useState(1); const [pageSize] = useState(20); const [filterStatus, setFilterStatus] = useState(''); const [filterUserId, setFilterUserId] = useState(''); const [preview, setPreview] = useState(null); const [updating, setUpdating] = useState(null); const [genModal, setGenModal] = useState<{ record: AdminGenerationRecord; ratio: string; resolution: string } | null>(null); const load = async () => { setLoading(true); try { const res = await getAdminGenerationRecords({ userId: filterUserId || undefined, status: filterStatus || undefined, page, pageSize, }); setRecords(res.items.map((item: any) => ({ id: item.id, userId: item.userId, username: item.username, projectId: item.projectId, projectName: item.projectName, originalPrompt: item.originalPrompt, optimizedPrompt: item.optimizedPrompt, duration: item.duration, aspectRatio: item.aspectRatio, resolution: item.resolution, status: item.status, videoUrl: item.videoUrl, references: item.references, creditsCost: item.creditsCost, textCreditsCost: item.textCreditsCost || 0, textTokensUsed: item.textTokensUsed || 0, videoTokensUsed: item.videoTokensUsed || 0, errorMessage: item.errorMessage, createdAt: item.createdAt, generatedAt: item.generatedAt, }))); setTotal(res.total); } catch { message.error('加载记录失败'); } finally { setLoading(false); } }; useEffect(() => { load(); }, [page, filterStatus]); const handleStatusUpdate = async (recordId: string, newStatus: string, videoUrl?: string) => { setUpdating(recordId); try { await adminUpdateGenerationStatus(recordId, newStatus, videoUrl); message.success('状态已更新'); load(); } catch (e: any) { message.error(e?.message || '更新失败'); } finally { setUpdating(null); } }; const handleGenerate = async () => { if (!genModal) return; setUpdating(genModal.record.id); try { await adminGenerateVideo(genModal.record.id, genModal.ratio, genModal.resolution); message.success('已提交视频生成'); setGenModal(null); load(); } catch (e: any) { message.error(e?.message || '生成失败'); } finally { setUpdating(null); } }; const columns = [ { title: '用户', key: 'user', width: 120, render: (_: any, r: AdminGenerationRecord) => (
{r.username}
{r.userId.slice(0, 8)}...
), }, { title: '项目', dataIndex: 'projectName', width: 120, ellipsis: true, render: (v: string) => {v}, }, { title: '提示词', key: 'prompt', ellipsis: true, render: (_: any, r: AdminGenerationRecord) => ( {r.originalPrompt} ), }, { title: '参数', key: 'params', width: 140, render: (_: any, r: AdminGenerationRecord) => ( r.duration ? ( {r.duration}s {r.aspectRatio} {r.resolution} ) : 待配置 ), }, { title: '积分', key: 'credits', width: 120, render: (_: any, r: AdminGenerationRecord) => (
{r.textCreditsCost > 0 && (
文字: {r.textCreditsCost}
)} {r.creditsCost > 0 && (
视频: {r.creditsCost}
)} {r.textCreditsCost === 0 && r.creditsCost === 0 && ( 0 )}
), }, { title: '状态', dataIndex: 'status', width: 90, render: (v: string) => { const cfg = STATUS_MAP[v] || { color: 'default', text: v, icon: null }; return {cfg.text}; }, }, { title: '时间', key: 'time', width: 140, render: (_: any, r: AdminGenerationRecord) => (
{formatDate(r.createdAt)}
{r.generatedAt &&
生成: {formatDate(r.generatedAt)}
}
), }, { title: '操作', key: 'action', width: 150, fixed: 'right' as const, render: (_: any, r: AdminGenerationRecord) => ( {r.status === 'generating' && ( )} {r.status === 'failed' && ( )} {r.status === 'prompt_optimized' && ( <> )} ), }, ]; return (
生成记录管理 {total} 条记录 } style={{ width: 200 }} value={filterUserId} onChange={(e) => setFilterUserId(e.target.value)} onPressEnter={() => { setPage(1); load(); }} allowClear />
`共 ${t} 条`, }} /> {/* Detail modal */} 生成记录详情} open={!!preview} onCancel={() => setPreview(null)} footer={null} width={720} > {preview && (
{/* User & Project info */}
用户 {preview.username}
项目 {preview.projectName}
状态 {STATUS_MAP[preview.status]?.text}
{/* Prompts */}
原始提示词
{preview.originalPrompt}
优化后提示词
{preview.optimizedPrompt}
{/* Credits */}
文字积分 {preview.textCreditsCost} ({preview.textTokensUsed} tokens)
视频积分 {preview.creditsCost} {preview.videoTokensUsed > 0 && ( ({preview.videoTokensUsed} tokens) )}
总积分 {preview.textCreditsCost + preview.creditsCost}
{/* Video Params */} {preview.duration ? (
{[ { label: '时长', value: `${preview.duration}秒` }, { label: '比例', value: preview.aspectRatio }, { label: '分辨率', value: preview.resolution }, ].map((item, i) => (
{item.label} {item.value}
))}
) : (
视频参数待用户配置
)} {/* Reference images */} {preview.references && preview.references.length > 0 && (
参考内容
{preview.references.map((ref, i) => (
{ref.type === 'image' ? ( ) : (
视频
)}
))}
)} {/* Video preview */} {preview.status === 'completed' && preview.videoUrl && (
生成视频
)} {/* Error message */} {preview.status === 'failed' && preview.errorMessage && (
错误信息: {preview.errorMessage}
)} {/* Timestamps */}
创建: {formatDate(preview.createdAt)} {preview.generatedAt && 生成: {formatDate(preview.generatedAt)}}
)}
{/* Generate modal */} 生成视频} open={!!genModal} onCancel={() => setGenModal(null)} onOk={handleGenerate} okText="提交生成" cancelText="取消" confirmLoading={genModal ? updating === genModal.record.id : false} width={420} > {genModal && (
时长 {genModal.record.duration || 5}s
画面比例 setGenModal(prev => prev ? { ...prev, resolution: v } : null)} style={{ width: '100%' }} options={['480p', '720p', '1080p'].map(r => ({ value: r, label: r }))} />
)}
); }; export default AdminGenerationRecords;