生成历史页面
This commit is contained in:
@@ -11,6 +11,7 @@ import CreditsPage from './pages/CreditsPage';
|
||||
import GenerateConver from './pages/GenerateConver';
|
||||
import InitialReplication from './pages/InitialReplication';
|
||||
import RemoveLens from './pages/RemoveLens';
|
||||
import GeneratedRecord from './pages/GeneratedRecord';
|
||||
|
||||
|
||||
|
||||
@@ -91,6 +92,8 @@ const App = () => {
|
||||
<Route path="conversation" element={<GenerateConver />} />
|
||||
<Route path="initial" element={<InitialReplication />} />
|
||||
<Route path="removelens" element={<RemoveLens />} />
|
||||
<Route path="generated" element={<GeneratedRecord />} />
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -280,6 +280,32 @@ export async function getRechargePackages(): Promise<any[]> {
|
||||
export async function getCreditRatios(): Promise<any[]> {
|
||||
return api.get('/credits/ratios');
|
||||
}
|
||||
// 引擎配置
|
||||
export async function getEngine(): Promise<any[]> {
|
||||
return api.get('/generation-ai/engines');
|
||||
}
|
||||
|
||||
// ── Generation AI Tasks ────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
export async function createGenerationTask(params: any): Promise<any> {
|
||||
return api.post('/generation-ai/tasks', params);
|
||||
}
|
||||
|
||||
export async function getgen_list(Pagebreak: any): Promise<any[]> {
|
||||
return api.get('/generation-ai/tasks?page='+Pagebreak.page+'&page_size='+Pagebreak.pageSize);
|
||||
}
|
||||
|
||||
export async function gethistory(Pagebreak: any): Promise<any[]> {
|
||||
return api.get('/generation-ai/history'+Pagebreak);
|
||||
}
|
||||
|
||||
export async function gethistoryItems(Pagebreak: any): Promise<any[]> {
|
||||
return api.get('/generation-ai/history/'+Pagebreak);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -364,3 +364,15 @@ export async function mockGetAdminNotifications(): Promise<AdminNotification[]>
|
||||
await delay(300);
|
||||
return [...MOCK_ADMIN_NOTIFICATIONS];
|
||||
}
|
||||
|
||||
// ── Generation AI Tasks Mock ───────────────────────────────
|
||||
|
||||
export async function mockCreateGenerationTask(params: any): Promise<any> {
|
||||
await delay(1000);
|
||||
return {
|
||||
task_id: `task-${Date.now()}`,
|
||||
status: 'pending',
|
||||
message: '任务已创建',
|
||||
...params,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
UserOutlined,
|
||||
ThunderboltOutlined,
|
||||
HomeOutlined,
|
||||
DashboardOutlined,
|
||||
CodeOutlined,
|
||||
LockOutlined,
|
||||
PlusCircleOutlined,
|
||||
LeftOutlined,
|
||||
@@ -43,6 +45,8 @@ interface MenuConfig {
|
||||
|
||||
const iconMap: Record<string, React.ReactNode> = {
|
||||
HomeOutlined: <HomeOutlined />,
|
||||
DashboardOutlined:<DashboardOutlined/>,
|
||||
CodeOutlined:<CodeOutlined/>,
|
||||
PlayCircleOutlined: <PlayCircleOutlined />,
|
||||
WalletOutlined: <WalletOutlined />,
|
||||
SettingOutlined: <LockOutlined />,
|
||||
@@ -86,6 +90,15 @@ const AppLayout: React.FC = () => {
|
||||
const [qrCodeModalOpen, setQrCodeModalOpen] = useState(false);
|
||||
const [currentPaymentInfo, setCurrentPaymentInfo] = useState<{ price: number; credits: number; qrCode: string } | null>(null);
|
||||
|
||||
// 监听预览弹窗状态,关闭浮动按钮
|
||||
useEffect(() => {
|
||||
const handleModalOpen = () => {
|
||||
setToggleHover(false);
|
||||
};
|
||||
window.addEventListener('previewOpen', handleModalOpen);
|
||||
return () => window.removeEventListener('previewOpen', handleModalOpen);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
getSiteInfo().then(info => {
|
||||
setSiteName(info.siteName || 'VideoGen.AI');
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,736 @@
|
||||
import React, { useEffect, useState, useLayoutEffect } from 'react';
|
||||
import { Button, Empty, Input, Select, Space, Typography, Tag } from 'antd';
|
||||
import {
|
||||
SearchOutlined,
|
||||
FilterOutlined,
|
||||
VideoCameraOutlined,
|
||||
PictureOutlined,
|
||||
FolderOpenOutlined,
|
||||
FileTextOutlined,
|
||||
DownloadOutlined,
|
||||
XOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { gethistory,gethistoryItems } from '../api';
|
||||
|
||||
const { Search } = Input;
|
||||
|
||||
const GeneratedRecord: React.FC = () => {
|
||||
const [filterType, setFilterType] = useState<'project' | 'creation'>('project');
|
||||
const [filterMedia, setFilterMedia] = useState<'video' | 'image'>('video');
|
||||
const [recordlist, setRecordList] = useState<any[]>([]);
|
||||
const [Pagebreak, setPagebreak] = useState<any>({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
});
|
||||
const [Totalnumber, setTotalnumber] = useState<number>(0);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [loadingGroups, setLoadingGroups] = useState<Set<string>>(new Set());
|
||||
const [previewVisible, setPreviewVisible] = useState(false);
|
||||
const [previewItem, setPreviewItem] = useState<any>(null);
|
||||
const videoRef = React.createRef<HTMLVideoElement>();
|
||||
|
||||
// 下载文件
|
||||
const handleDownload = (item: any) => {
|
||||
const url = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${item.videoUrl || item.imageUrl}`;
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = item.title || item.id || 'download';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
|
||||
// 预览文件
|
||||
const handlePreview = (item: any) => {
|
||||
|
||||
setPreviewItem(item);
|
||||
setPreviewVisible(true);
|
||||
// 触发事件通知布局组件关闭浮动按钮
|
||||
window.dispatchEvent(new Event('previewOpen'));
|
||||
};
|
||||
|
||||
// 关闭预览并暂停视频
|
||||
const handleClosePreview = () => {
|
||||
// 方法1: 使用 ref
|
||||
if (videoRef.current) {
|
||||
videoRef.current.pause();
|
||||
videoRef.current.currentTime = 0;
|
||||
}
|
||||
// 方法2: 直接通过 DOM 查询(备用)
|
||||
const videoElements = document.querySelectorAll('video');
|
||||
videoElements.forEach(video => {
|
||||
video.pause();
|
||||
video.currentTime = 0;
|
||||
});
|
||||
setPreviewVisible(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
let parameters = '';
|
||||
if (filterType === 'project') {
|
||||
parameters = `?gen_type=${filterMedia}&history_source=generation_record&page=${Pagebreak.page}&page_size=${Pagebreak.pageSize}`;
|
||||
} else {
|
||||
parameters = `?gen_type=${filterMedia}&page=${Pagebreak.page}&page_size=${Pagebreak.pageSize}`;
|
||||
}
|
||||
gethistory(parameters).then((res: any) => {
|
||||
const data = Array.isArray(res) ? res : (res?.groups || []);
|
||||
data.forEach(group => {
|
||||
group.page = 1;
|
||||
});
|
||||
// 如果是第一页,替换数据;否则追加数据
|
||||
if (Pagebreak.page === 1) {
|
||||
setRecordList(data);
|
||||
} else {
|
||||
setRecordList(prev => [...prev, ...data]);
|
||||
}
|
||||
|
||||
setTotalnumber(res?.totalDays || 0);
|
||||
}).catch((err) => {
|
||||
if (Pagebreak.page === 1) {
|
||||
setRecordList([]);
|
||||
}
|
||||
}).finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, [filterType, filterMedia, Pagebreak.page]);
|
||||
|
||||
// 加载更多
|
||||
const handleLoadMore = () => {
|
||||
if (loading) return;
|
||||
setPagebreak(prev => ({
|
||||
...prev,
|
||||
page: prev.page + 1
|
||||
}));
|
||||
|
||||
|
||||
};
|
||||
|
||||
// 分组加载更多
|
||||
const handleGroupLoadMore = async (time:string, date: string, page: number) => {
|
||||
if (loadingGroups.has(date)) return;
|
||||
setLoadingGroups(prev => new Set([...prev, date]));
|
||||
|
||||
const addpage = page + 1;
|
||||
|
||||
let parameters = ``;
|
||||
|
||||
if (filterType === 'project') {
|
||||
parameters = `${time}?gen_type=${filterMedia}&history_source=generation_record&page=${addpage}&page_size=${Pagebreak.pageSize}`;
|
||||
} else {
|
||||
parameters = `${time}?gen_type=${filterMedia}&page=${addpage}&page_size=${Pagebreak.pageSize}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const res: any = await gethistoryItems(parameters);
|
||||
|
||||
// gethistoryItems 返回数组,直接使用
|
||||
const newItems: any[] = res.items || [];
|
||||
|
||||
if (newItems && newItems.length > 0) {
|
||||
setRecordList(prev => prev.map(group => {
|
||||
if (group.generatedDate === time) {
|
||||
return {
|
||||
...group,
|
||||
items: [...group.items, ...newItems],
|
||||
page: addpage
|
||||
};
|
||||
}
|
||||
return group;
|
||||
}));
|
||||
}
|
||||
} catch (err) {
|
||||
} finally {
|
||||
setLoadingGroups(prev => {
|
||||
const next = new Set(prev);
|
||||
next.delete(date);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 当筛选条件改变时,重置页码
|
||||
useEffect(() => {
|
||||
setPagebreak(prev => ({
|
||||
...prev,
|
||||
page: 1
|
||||
}));
|
||||
}, [filterType, filterMedia]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Header */}
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<Typography.Title level={3} style={{ margin: '0 0 4px', color: '#1a1a2e', fontWeight: 700 }}>
|
||||
生成历史
|
||||
</Typography.Title>
|
||||
<Typography.Text style={{ color: '#94a3b8', fontSize: 14 }}>
|
||||
查看所有生成的视频和图片记录
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
{/* First row filter: 项目记录 / 创作记录 */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
marginBottom: 16,
|
||||
padding: '12px 20px',
|
||||
borderRadius: 12,
|
||||
background: '#fff',
|
||||
border: '1px solid #f0f0f5',
|
||||
}}>
|
||||
<FilterOutlined style={{ color: '#94a3b8', fontSize: 14 }} />
|
||||
<Space>
|
||||
<Button
|
||||
type={filterType === 'project' ? 'primary' : 'default'}
|
||||
onClick={() => setFilterType('project')}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
background: filterType === 'project'
|
||||
? 'linear-gradient(135deg, #6366f1, #8b5cf6)'
|
||||
: '#f8f9fc',
|
||||
border: filterType === 'project' ? 'none' : '1px solid #e2e8f0',
|
||||
color: filterType === 'project' ? '#fff' : '#64748b',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
icon={<FolderOpenOutlined />}
|
||||
>
|
||||
项目记录
|
||||
</Button>
|
||||
<Button
|
||||
type={filterType === 'creation' ? 'primary' : 'default'}
|
||||
onClick={() => setFilterType('creation')}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
background: filterType === 'creation'
|
||||
? 'linear-gradient(135deg, #6366f1, #8b5cf6)'
|
||||
: '#f8f9fc',
|
||||
border: filterType === 'creation' ? 'none' : '1px solid #e2e8f0',
|
||||
color: filterType === 'creation' ? '#fff' : '#64748b',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
icon={<FileTextOutlined />}
|
||||
>
|
||||
创作记录
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
{/* Second row filter: 视频 / 图片 */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
marginBottom: 24,
|
||||
padding: '12px 20px',
|
||||
borderRadius: 12,
|
||||
background: '#fff',
|
||||
border: '1px solid #f0f0f5',
|
||||
}}>
|
||||
<Typography.Text style={{ color: '#94a3b8', fontSize: 14 }}>媒体类型:</Typography.Text>
|
||||
<Space>
|
||||
<Button
|
||||
type={filterMedia === 'video' ? 'primary' : 'default'}
|
||||
onClick={() => setFilterMedia('video')}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
background: filterMedia === 'video'
|
||||
? 'linear-gradient(135deg, #6366f1, #8b5cf6)'
|
||||
: '#f8f9fc',
|
||||
border: filterMedia === 'video' ? 'none' : '1px solid #e2e8f0',
|
||||
color: filterMedia === 'video' ? '#fff' : '#64748b',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
icon={<VideoCameraOutlined />}
|
||||
>
|
||||
视频
|
||||
</Button>
|
||||
<Button
|
||||
type={filterMedia === 'image' ? 'primary' : 'default'}
|
||||
onClick={() => setFilterMedia('image')}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
background: filterMedia === 'image'
|
||||
? 'linear-gradient(135deg, #6366f1, #8b5cf6)'
|
||||
: '#f8f9fc',
|
||||
border: filterMedia === 'image' ? 'none' : '1px solid #e2e8f0',
|
||||
color: filterMedia === 'image' ? '#fff' : '#64748b',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
icon={<PictureOutlined />}
|
||||
>
|
||||
图片
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
{/* Content area */}
|
||||
{recordlist.length === 0 ? (
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description="暂无生成记录"
|
||||
style={{ padding: '60px 0' }}
|
||||
/>
|
||||
) : (
|
||||
<div style={{ padding: '0 4px' }}>
|
||||
{recordlist.map((group: any) => (
|
||||
<div key={group.date} style={{ marginBottom: 32 }}>
|
||||
{/* Date label */}
|
||||
<div style={{
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
color: '#64748b',
|
||||
marginBottom: 12,
|
||||
paddingLeft: 8,
|
||||
}}>
|
||||
{group.generatedDate}
|
||||
</div>
|
||||
{/* Media grid */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 8,
|
||||
}}>
|
||||
{group.items.map((item: any) => (
|
||||
<div
|
||||
key={item.id}
|
||||
style={{
|
||||
width: 160,
|
||||
height: 120,
|
||||
borderRadius: 4,
|
||||
overflow: 'hidden',
|
||||
cursor: 'pointer',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
||||
transition: 'transform 0.2s, box-shadow 0.2s',
|
||||
}}
|
||||
onClick={() => handlePreview(item)}
|
||||
onMouseEnter={(e) => {
|
||||
(e.currentTarget as HTMLElement).style.transform = 'scale(1.05)';
|
||||
(e.currentTarget as HTMLElement).style.boxShadow = '0 4px 16px rgba(0,0,0,0.2)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
(e.currentTarget as HTMLElement).style.transform = 'scale(1)';
|
||||
(e.currentTarget as HTMLElement).style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)';
|
||||
}}
|
||||
>
|
||||
{filterMedia === 'video' ? (
|
||||
<video
|
||||
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${item.videoUrl}`}
|
||||
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${item.imageUrl}`}
|
||||
alt="图片预览"
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
/>
|
||||
)}
|
||||
{/* 点击提示 */}
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
background: 'linear-gradient(transparent, rgba(0,0,0,0.5))',
|
||||
padding: '8px',
|
||||
color: '#fff',
|
||||
fontSize: 12,
|
||||
opacity: 0,
|
||||
transition: 'opacity 0.2s',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
(e.currentTarget as HTMLElement).style.opacity = '1';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
(e.currentTarget as HTMLElement).style.opacity = '0';
|
||||
}}
|
||||
>
|
||||
点击预览
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* 分组内加载更多 */}
|
||||
{group.total && group.total > group.items.length && (
|
||||
<div style={{ padding: '12px 0', textAlign: 'left' }}>
|
||||
<Button
|
||||
onClick={() => handleGroupLoadMore(group.generatedDate,group.items, group.page)}
|
||||
loading={loadingGroups.has(group.date)}
|
||||
disabled={loadingGroups.has(group.date)}
|
||||
size="small"
|
||||
style={{
|
||||
borderRadius: 6,
|
||||
background: 'transparent',
|
||||
border: '1px dashed #cbd5e1',
|
||||
color: '#64748b',
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
{loadingGroups.has(group.date) ? '加载中...' : `查看全部 (${group.total})`}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{/* 加载更多按钮 */}
|
||||
{recordlist.length > 0 && Totalnumber > recordlist.length && (
|
||||
<div style={{ textAlign: 'center', padding: '20px 0' }}>
|
||||
<Button
|
||||
onClick={handleLoadMore}
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
background: '#f8f9fc',
|
||||
border: '1px solid #e2e8f0',
|
||||
color: '#64748b',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
{loading ? '加载中...' : '加载更多'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 预览弹窗 */}
|
||||
{previewVisible && previewItem && (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'rgba(0,0,0,0.85)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 1000,
|
||||
padding: 16,
|
||||
boxSizing: 'border-box',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
onClick={handleClosePreview}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
background: '#fff',
|
||||
borderRadius: 16,
|
||||
padding: 0,
|
||||
width: '100%',
|
||||
maxWidth: '1200px',
|
||||
maxHeight: '95vh',
|
||||
minHeight: '300px',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
boxShadow: '0 20px 60px rgba(0,0,0,0.3)',
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* 头部 */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '12px 16px',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<Typography.Title level={5} style={{ margin: 0, color: '#1a1a2e', fontSize: 16 }}>
|
||||
{previewItem.title || '预览'}
|
||||
</Typography.Title>
|
||||
<Button
|
||||
icon={<XOutlined />}
|
||||
onClick={handleClosePreview}
|
||||
style={{
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
color: '#94a3b8',
|
||||
fontSize: 16,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 内容区域 - 响应式布局 */}
|
||||
<div style={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 20,
|
||||
padding: 20,
|
||||
overflow: 'auto',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'flex-start',
|
||||
}}>
|
||||
{/* 媒体预览 */}
|
||||
<div style={{
|
||||
flex: 1,
|
||||
minWidth: '280px',
|
||||
maxWidth: '800px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minHeight: '200px',
|
||||
}}>
|
||||
{filterMedia === 'video' ? (
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${previewItem.videoUrl}`}
|
||||
controls
|
||||
autoPlay
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
maxHeight: '55vh',
|
||||
borderRadius: 8,
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${previewItem.imageUrl}`}
|
||||
alt="预览"
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
maxHeight: '55vh',
|
||||
objectFit: 'contain',
|
||||
borderRadius: 8
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 参数信息 */}
|
||||
<div style={{
|
||||
width: '100%',
|
||||
minWidth: '280px',
|
||||
maxWidth: '320px',
|
||||
background: '#f8fafc',
|
||||
borderRadius: 12,
|
||||
padding: 20,
|
||||
maxHeight: '55vh',
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
}}>
|
||||
<Typography.Text strong style={{ fontSize: 14, color: '#475569', display: 'block', marginBottom: 16 }}>
|
||||
文件信息
|
||||
</Typography.Text>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
{/* ID */}
|
||||
{/* <div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span style={{ color: '#94a3b8', fontSize: 13 }}>ID</span>
|
||||
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
|
||||
{previewItem.id || '-'}
|
||||
</span>
|
||||
</div> */}
|
||||
|
||||
{/* 类型 */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span style={{ color: '#94a3b8', fontSize: 13 }}>类型</span>
|
||||
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
|
||||
{filterMedia === 'video' ? '视频' : '图片'}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 12 }}>
|
||||
<span style={{ color: '#94a3b8', fontSize: 13, flexShrink: 0, width: 40 }}>请求</span>
|
||||
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500, flex: 1, wordBreak: 'break-all' }}>
|
||||
{previewItem.originalPrompt}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 分辨率 */}
|
||||
{filterMedia === 'image' && (
|
||||
<>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span style={{ color: '#94a3b8', fontSize: 13 }}>比例</span>
|
||||
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
|
||||
{previewItem.imageProportion}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span style={{ color: '#94a3b8', fontSize: 13 }}>分辨率</span>
|
||||
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
|
||||
{previewItem.imageSize}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span style={{ color: '#94a3b8', fontSize: 13 }}>尺寸</span>
|
||||
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
|
||||
{previewItem.imagePx}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{filterMedia === 'video' && (
|
||||
<>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span style={{ color: '#94a3b8', fontSize: 13 }}>比例</span>
|
||||
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
|
||||
{previewItem.aspectRatio}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span style={{ color: '#94a3b8', fontSize: 13 }}>分辨率</span>
|
||||
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
|
||||
{previewItem.resolution}
|
||||
</span>
|
||||
</div>
|
||||
{/* <div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span style={{ color: '#94a3b8', fontSize: 13 }}>尺寸</span>
|
||||
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
|
||||
{previewItem.imagePx}
|
||||
</span>
|
||||
</div> */}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 时长(视频) */}
|
||||
{filterMedia === 'video' && (
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span style={{ color: '#94a3b8', fontSize: 13 }}>时长</span>
|
||||
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
|
||||
{`${previewItem.duration}秒` || '-'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 文件大小 */}
|
||||
{/* <div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span style={{ color: '#94a3b8', fontSize: 13 }}>文件大小</span>
|
||||
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
|
||||
{previewItem.size ? formatFileSize(previewItem.size) : '-'}
|
||||
</span>
|
||||
</div> */}
|
||||
|
||||
{/* 创建时间 */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span style={{ color: '#94a3b8', fontSize: 13 }}>创建时间</span>
|
||||
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
|
||||
{formatDateTime(previewItem.createdAt || previewItem.generatedDate)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 生成引擎 */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<span style={{ color: '#94a3b8', fontSize: 13 }}>生成引擎</span>
|
||||
<span style={{ color: '#334155', fontSize: 13, fontWeight: 500 }}>
|
||||
{previewItem.engine || previewItem.engineName || '-'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 分隔线 */}
|
||||
<div style={{ borderTop: '1px dashed #e2e8f0', margin: '16px 0' }} />
|
||||
|
||||
|
||||
{previewItem.mediaReferences && previewItem.mediaReferences.length > 0 && (
|
||||
<>
|
||||
<Typography.Text strong style={{ fontSize: 14, color: '#475569', display: 'block', marginBottom: 12 }}>
|
||||
依靠附件
|
||||
</Typography.Text>
|
||||
{previewItem.mediaReferences.map((item, index) => (
|
||||
<div
|
||||
key={item.url}
|
||||
onClick={() => {
|
||||
// 暂停视频
|
||||
if (videoRef.current) {
|
||||
videoRef.current.pause();
|
||||
}
|
||||
const url = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${item.url}`;
|
||||
window.open(url, '_blank');
|
||||
}}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
padding: 8,
|
||||
borderRadius: 6,
|
||||
cursor: 'pointer',
|
||||
backgroundColor: '#f1f5f9',
|
||||
marginBottom: 4,
|
||||
transition: 'background-color 0.2s',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
(e.currentTarget as HTMLElement).style.backgroundColor = '#e2e8f0';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
(e.currentTarget as HTMLElement).style.backgroundColor = '#f1f5f9';
|
||||
}}
|
||||
>
|
||||
{item.type === 'image' ? (
|
||||
<PictureOutlined style={{ color: '#3b82f6', fontSize: 14 }} />
|
||||
) : (
|
||||
<VideoCameraOutlined style={{ color: '#f59e0b', fontSize: 14 }} />
|
||||
)}
|
||||
<span style={{ color: '#334155', fontSize: 13 }}>
|
||||
{item.name || `媒体${index + 1}`}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{/* 操作按钮 */}
|
||||
<div style={{ display: 'flex', gap: 12 ,marginTop:20}}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={() => {
|
||||
// 暂停视频
|
||||
if (videoRef.current) {
|
||||
videoRef.current.pause();
|
||||
}
|
||||
const url = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${previewItem.videoUrl || previewItem.imageUrl}`;
|
||||
window.open(url, '_blank');
|
||||
}}
|
||||
style={{ flex: 1, borderRadius: 8 }}
|
||||
>
|
||||
下载
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleClosePreview}
|
||||
style={{ flex: 1, borderRadius: 8 }}
|
||||
>
|
||||
关闭
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 文件大小格式化
|
||||
function formatFileSize(bytes: number): string {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
// 日期格式化(年月日时分秒)
|
||||
function formatDateTime(dateString: string): string {
|
||||
if (!dateString) return '-';
|
||||
const date = new Date(dateString);
|
||||
if (isNaN(date.getTime())) return '-';
|
||||
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
}
|
||||
|
||||
export default GeneratedRecord;
|
||||
@@ -93,9 +93,12 @@ export interface OptimizeParams {
|
||||
aspectRatio?: string;
|
||||
references?: MediaReference[];
|
||||
idempotencyKey?: string;
|
||||
image_size:any;
|
||||
image_proportion:any;
|
||||
image_px:any
|
||||
image_size?: any;
|
||||
image_proportion?: any;
|
||||
image_px?: any;
|
||||
video_duration?: number;
|
||||
video_ratio?: string;
|
||||
video_resolution?: string;
|
||||
}
|
||||
|
||||
export interface GenerateParams {
|
||||
|
||||
Reference in New Issue
Block a user