生成历史页面

This commit is contained in:
孙佳艺
2026-06-01 09:19:31 +08:00
parent 655af60d71
commit 20bd5e84c5
7 changed files with 2426 additions and 102 deletions
+3
View File
@@ -11,6 +11,7 @@ import CreditsPage from './pages/CreditsPage';
import GenerateConver from './pages/GenerateConver'; import GenerateConver from './pages/GenerateConver';
import InitialReplication from './pages/InitialReplication'; import InitialReplication from './pages/InitialReplication';
import RemoveLens from './pages/RemoveLens'; import RemoveLens from './pages/RemoveLens';
import GeneratedRecord from './pages/GeneratedRecord';
@@ -91,6 +92,8 @@ const App = () => {
<Route path="conversation" element={<GenerateConver />} /> <Route path="conversation" element={<GenerateConver />} />
<Route path="initial" element={<InitialReplication />} /> <Route path="initial" element={<InitialReplication />} />
<Route path="removelens" element={<RemoveLens />} /> <Route path="removelens" element={<RemoveLens />} />
<Route path="generated" element={<GeneratedRecord />} />
+26
View File
@@ -280,6 +280,32 @@ export async function getRechargePackages(): Promise<any[]> {
export async function getCreditRatios(): Promise<any[]> { export async function getCreditRatios(): Promise<any[]> {
return api.get('/credits/ratios'); 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);
}
+12
View File
@@ -364,3 +364,15 @@ export async function mockGetAdminNotifications(): Promise<AdminNotification[]>
await delay(300); await delay(300);
return [...MOCK_ADMIN_NOTIFICATIONS]; 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, UserOutlined,
ThunderboltOutlined, ThunderboltOutlined,
HomeOutlined, HomeOutlined,
DashboardOutlined,
CodeOutlined,
LockOutlined, LockOutlined,
PlusCircleOutlined, PlusCircleOutlined,
LeftOutlined, LeftOutlined,
@@ -43,6 +45,8 @@ interface MenuConfig {
const iconMap: Record<string, React.ReactNode> = { const iconMap: Record<string, React.ReactNode> = {
HomeOutlined: <HomeOutlined />, HomeOutlined: <HomeOutlined />,
DashboardOutlined:<DashboardOutlined/>,
CodeOutlined:<CodeOutlined/>,
PlayCircleOutlined: <PlayCircleOutlined />, PlayCircleOutlined: <PlayCircleOutlined />,
WalletOutlined: <WalletOutlined />, WalletOutlined: <WalletOutlined />,
SettingOutlined: <LockOutlined />, SettingOutlined: <LockOutlined />,
@@ -86,6 +90,15 @@ const AppLayout: React.FC = () => {
const [qrCodeModalOpen, setQrCodeModalOpen] = useState(false); const [qrCodeModalOpen, setQrCodeModalOpen] = useState(false);
const [currentPaymentInfo, setCurrentPaymentInfo] = useState<{ price: number; credits: number; qrCode: string } | null>(null); 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(() => { useEffect(() => {
getSiteInfo().then(info => { getSiteInfo().then(info => {
setSiteName(info.siteName || 'VideoGen.AI'); setSiteName(info.siteName || 'VideoGen.AI');
File diff suppressed because it is too large Load Diff
+736
View File
@@ -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;
+6 -3
View File
@@ -93,9 +93,12 @@ export interface OptimizeParams {
aspectRatio?: string; aspectRatio?: string;
references?: MediaReference[]; references?: MediaReference[];
idempotencyKey?: string; idempotencyKey?: string;
image_size:any; image_size?: any;
image_proportion:any; image_proportion?: any;
image_px:any image_px?: any;
video_duration?: number;
video_ratio?: string;
video_resolution?: string;
} }
export interface GenerateParams { export interface GenerateParams {