Merge branch 'main' of https://gitee.com/wg123/video-gen
This commit is contained in:
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
# App
|
# App
|
||||||
APP_NAME=VideoGen API
|
APP_NAME=VideoGen API
|
||||||
APP_VERSION=1.0.0
|
APP_VERSION=1.0.0
|
||||||
DEBUG=true
|
DEBUG=false
|
||||||
SECRET_KEY=local-dev-secret-key-not-for-production
|
SECRET_KEY=local-dev-secret-key-not-for-production
|
||||||
|
|
||||||
# Database (PostgreSQL)
|
# Database (PostgreSQL)
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select
|
||||||
|
|
||||||
from app.dependencies import get_db, get_current_user
|
from app.dependencies import get_db, get_current_user
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.models.credit_ratio import CreditRatio
|
from app.models.credit_ratio import CreditRatio
|
||||||
|
from app.models.video_engine import VideoEngine
|
||||||
|
from app.models.image_engine import ImageEngine
|
||||||
from app.schemas.credit import CreditBalanceOut, CreditRecordOut
|
from app.schemas.credit import CreditBalanceOut, CreditRecordOut
|
||||||
from app.schemas.credit_ratio import CreditRatioOut
|
from app.schemas.credit_ratio import CreditRatioOut
|
||||||
from app.services.credits import get_records
|
from app.services.credits import get_records
|
||||||
from sqlalchemy import select
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/credits", tags=["credits"])
|
router = APIRouter(prefix="/credits", tags=["credits"])
|
||||||
|
|
||||||
@@ -29,13 +31,40 @@ async def get_credit_ratios(
|
|||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
result = await db.execute(select(CreditRatio))
|
async def get_ratios_for_engine_type(gen_type: str, engine_ids: list):
|
||||||
ratios = result.scalars().all()
|
for engine_id in engine_ids:
|
||||||
|
result = await db.execute(
|
||||||
|
select(CreditRatio)
|
||||||
|
.where(CreditRatio.gen_type == gen_type)
|
||||||
|
.where(CreditRatio.model_config_id == engine_id)
|
||||||
|
)
|
||||||
|
ratios = result.scalars().all()
|
||||||
|
if ratios:
|
||||||
|
return [CreditRatioOut.model_validate(r) for r in ratios]
|
||||||
|
return []
|
||||||
|
|
||||||
|
video_engines_result = await db.execute(
|
||||||
|
select(VideoEngine.id)
|
||||||
|
.where(VideoEngine.is_active == True)
|
||||||
|
.order_by(VideoEngine.priority.desc())
|
||||||
|
)
|
||||||
|
video_engine_ids = video_engines_result.scalars().all()
|
||||||
|
|
||||||
|
image_engines_result = await db.execute(
|
||||||
|
select(ImageEngine.id)
|
||||||
|
.where(ImageEngine.is_active == True)
|
||||||
|
.order_by(ImageEngine.priority.desc())
|
||||||
|
)
|
||||||
|
image_engine_ids = image_engines_result.scalars().all()
|
||||||
|
|
||||||
grouped = {}
|
grouped = {}
|
||||||
for ratio in ratios:
|
|
||||||
if ratio.gen_type not in grouped:
|
video_ratios = await get_ratios_for_engine_type("video", video_engine_ids)
|
||||||
grouped[ratio.gen_type] = []
|
if video_ratios:
|
||||||
grouped[ratio.gen_type].append(CreditRatioOut.model_validate(ratio))
|
grouped["video"] = video_ratios
|
||||||
|
|
||||||
|
image_ratios = await get_ratios_for_engine_type("image", image_engine_ids)
|
||||||
|
if image_ratios:
|
||||||
|
grouped["image"] = image_ratios
|
||||||
|
|
||||||
return grouped
|
return grouped
|
||||||
|
|||||||
@@ -30,7 +30,10 @@ async def list_active_industries(
|
|||||||
try:
|
try:
|
||||||
raw = json.loads(ind.skills)
|
raw = json.loads(ind.skills)
|
||||||
if isinstance(raw, list):
|
if isinstance(raw, list):
|
||||||
skills = raw if raw and isinstance(raw[0], dict) else [{"key": s, "label": s} for s in raw]
|
if raw and isinstance(raw[0], dict):
|
||||||
|
skills = [s for s in raw if s.get("type") != "skill"]
|
||||||
|
else:
|
||||||
|
skills = [{"key": s, "label": s} for s in raw]
|
||||||
except (json.JSONDecodeError, TypeError):
|
except (json.JSONDecodeError, TypeError):
|
||||||
skills = []
|
skills = []
|
||||||
items.append({
|
items.append({
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.models.credit_record import CreditRecord
|
from app.models.credit_record import CreditRecord
|
||||||
from app.models.system_config import SystemConfig
|
from app.models.system_config import SystemConfig
|
||||||
|
from app.models.video_engine import VideoEngine
|
||||||
|
from app.models.image_engine import ImageEngine
|
||||||
from app.models.credit_ratio import CreditRatio
|
from app.models.credit_ratio import CreditRatio
|
||||||
from app.utils.id_gen import generate_id
|
from app.utils.id_gen import generate_id
|
||||||
from app.utils.exceptions import InsufficientCreditsError
|
from app.utils.exceptions import InsufficientCreditsError
|
||||||
@@ -70,6 +72,14 @@ async def calc_video_credits(
|
|||||||
2. gen_type=video + resolution 下 base_credits/per_second_credits 最高规则;
|
2. gen_type=video + resolution 下 base_credits/per_second_credits 最高规则;
|
||||||
3. 原硬编码默认算法。
|
3. 原硬编码默认算法。
|
||||||
"""
|
"""
|
||||||
|
# 如果engine_id为空,默认查询权重最高的视频引擎积分规则
|
||||||
|
if not engine_id:
|
||||||
|
video_engines_result = await db.execute(
|
||||||
|
select(VideoEngine.id)
|
||||||
|
.where(VideoEngine.is_active == True)
|
||||||
|
.order_by(VideoEngine.priority.desc())
|
||||||
|
)
|
||||||
|
engine_id = video_engines_result.scalar_one_or_none()
|
||||||
ratio = await _get_credit_ratio(
|
ratio = await _get_credit_ratio(
|
||||||
db,
|
db,
|
||||||
gen_type="video",
|
gen_type="video",
|
||||||
@@ -82,7 +92,7 @@ async def calc_video_credits(
|
|||||||
# Fallback
|
# Fallback
|
||||||
base = 60.0
|
base = 60.0
|
||||||
duration_cost = duration * 2.0
|
duration_cost = duration * 2.0
|
||||||
multiplier = {"4K": 2.5, "1080p": 1.5, "720p": 1.0}.get(resolution, 1.0)
|
multiplier = {"480p": 1, "1080p": 2, "720p": 1.5}.get(resolution, 1.0)
|
||||||
return round((base + duration_cost) * multiplier, 2)
|
return round((base + duration_cost) * multiplier, 2)
|
||||||
|
|
||||||
|
|
||||||
@@ -90,7 +100,7 @@ def calc_credits(duration: int, resolution: str) -> float:
|
|||||||
"""Legacy: hardcoded credit calculation. Prefer calc_video_credits for new code."""
|
"""Legacy: hardcoded credit calculation. Prefer calc_video_credits for new code."""
|
||||||
base = 60.0
|
base = 60.0
|
||||||
duration_cost = duration * 2.0
|
duration_cost = duration * 2.0
|
||||||
multiplier = {"4K": 2.5, "1080p": 1.5, "720p": 1.0}.get(resolution, 1.0)
|
multiplier = {"480p": 2.5, "1080p": 1.5, "720p": 1.0}.get(resolution, 1.0)
|
||||||
return round((base + duration_cost) * multiplier, 2)
|
return round((base + duration_cost) * multiplier, 2)
|
||||||
|
|
||||||
|
|
||||||
@@ -106,6 +116,14 @@ async def calc_image_credits(
|
|||||||
2. gen_type=image + image_size 下 base_credits/per_second_credits 最高规则;
|
2. gen_type=image + image_size 下 base_credits/per_second_credits 最高规则;
|
||||||
3. 原硬编码默认算法。
|
3. 原硬编码默认算法。
|
||||||
"""
|
"""
|
||||||
|
# 如果engine_id为空,默认查询权重最高的图片引擎积分规则
|
||||||
|
if not engine_id:
|
||||||
|
image_engines_result = await db.execute(
|
||||||
|
select(ImageEngine.id)
|
||||||
|
.where(ImageEngine.is_active == True)
|
||||||
|
.order_by(ImageEngine.priority.desc())
|
||||||
|
)
|
||||||
|
engine_id = image_engines_result.scalar_one_or_none()
|
||||||
ratio = await _get_credit_ratio(
|
ratio = await _get_credit_ratio(
|
||||||
db,
|
db,
|
||||||
gen_type="image",
|
gen_type="image",
|
||||||
|
|||||||
@@ -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 />} />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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;
|
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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user