媒体生成百分比显示
This commit is contained in:
+39
-39
File diff suppressed because one or more lines are too long
Vendored
+1
-1
@@ -28,7 +28,7 @@
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<script type="module" crossorigin src="/assets/index-DUztRbKf.js"></script>
|
<script type="module" crossorigin src="/assets/index-DWsZ8Hba.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-DviWdElm.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-DviWdElm.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -551,8 +551,13 @@ const AppLayout: React.FC = () => {
|
|||||||
if (data.alipay) setPaymentMethod('alipay');
|
if (data.alipay) setPaymentMethod('alipay');
|
||||||
else if (data.wechat) setPaymentMethod('wechat');
|
else if (data.wechat) setPaymentMethod('wechat');
|
||||||
}).catch(() => { });
|
}).catch(() => { });
|
||||||
loadUnreadCount();
|
}, []);
|
||||||
}, [user]);
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user) {
|
||||||
|
loadUnreadCount();
|
||||||
|
}
|
||||||
|
}, [user?.id, user?.credits]);
|
||||||
|
|
||||||
const selectedKey = location.pathname.startsWith('/records') ? '/records' : location.pathname;
|
const selectedKey = location.pathname.startsWith('/records') ? '/records' : location.pathname;
|
||||||
const sidebarW = SIDEBAR_W;
|
const sidebarW = SIDEBAR_W;
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ interface UploadSelectorProps {
|
|||||||
maxAudioDuration?: number;
|
maxAudioDuration?: number;
|
||||||
usedAudioDuration?: number;
|
usedAudioDuration?: number;
|
||||||
hideLimitHint?: boolean;
|
hideLimitHint?: boolean;
|
||||||
|
/** 创作类型:'image' 时隐藏真人素材库/虚拟素材库选项 */
|
||||||
|
mediaType?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UploadSelector: React.FC<UploadSelectorProps> = ({
|
const UploadSelector: React.FC<UploadSelectorProps> = ({
|
||||||
@@ -54,6 +56,7 @@ const UploadSelector: React.FC<UploadSelectorProps> = ({
|
|||||||
maxAudioDuration,
|
maxAudioDuration,
|
||||||
usedAudioDuration,
|
usedAudioDuration,
|
||||||
hideLimitHint,
|
hideLimitHint,
|
||||||
|
mediaType,
|
||||||
}) => {
|
}) => {
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [portraitPickerOpen, setPortraitPickerOpen] = useState(false);
|
const [portraitPickerOpen, setPortraitPickerOpen] = useState(false);
|
||||||
@@ -146,46 +149,50 @@ const UploadSelector: React.FC<UploadSelectorProps> = ({
|
|||||||
<DatabaseOutlined style={{ fontSize: 14, color: '#64748b' }} />
|
<DatabaseOutlined style={{ fontSize: 14, color: '#64748b' }} />
|
||||||
<span style={{ fontSize: 14, color: '#334155' }}>从资产中选择</span>
|
<span style={{ fontSize: 14, color: '#334155' }}>从资产中选择</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
{mediaType !== 'image' && (
|
||||||
onClick={() => openPortraitPicker('real_person')}
|
<div
|
||||||
style={{
|
onClick={() => openPortraitPicker('real_person')}
|
||||||
display: 'flex',
|
style={{
|
||||||
alignItems: 'center',
|
display: 'flex',
|
||||||
gap: 12,
|
alignItems: 'center',
|
||||||
padding: '8px 16px',
|
gap: 12,
|
||||||
cursor: 'pointer',
|
padding: '8px 16px',
|
||||||
transition: 'background 0.15s ease',
|
cursor: 'pointer',
|
||||||
}}
|
transition: 'background 0.15s ease',
|
||||||
onMouseEnter={(e) => {
|
}}
|
||||||
e.currentTarget.style.background = '#f1f5f9';
|
onMouseEnter={(e) => {
|
||||||
}}
|
e.currentTarget.style.background = '#f1f5f9';
|
||||||
onMouseLeave={(e) => {
|
}}
|
||||||
e.currentTarget.style.background = 'transparent';
|
onMouseLeave={(e) => {
|
||||||
}}
|
e.currentTarget.style.background = 'transparent';
|
||||||
>
|
}}
|
||||||
<UserOutlined style={{ fontSize: 14, color: '#64748b' }} />
|
>
|
||||||
<span style={{ fontSize: 14, color: '#334155' }}>真人素材库</span>
|
<UserOutlined style={{ fontSize: 14, color: '#64748b' }} />
|
||||||
</div>
|
<span style={{ fontSize: 14, color: '#334155' }}>真人素材库</span>
|
||||||
<div
|
</div>
|
||||||
onClick={() => openPortraitPicker('aigc_virtual')}
|
)}
|
||||||
style={{
|
{mediaType !== 'image' && (
|
||||||
display: 'flex',
|
<div
|
||||||
alignItems: 'center',
|
onClick={() => openPortraitPicker('aigc_virtual')}
|
||||||
gap: 12,
|
style={{
|
||||||
padding: '8px 16px',
|
display: 'flex',
|
||||||
cursor: 'pointer',
|
alignItems: 'center',
|
||||||
transition: 'background 0.15s ease',
|
gap: 12,
|
||||||
}}
|
padding: '8px 16px',
|
||||||
onMouseEnter={(e) => {
|
cursor: 'pointer',
|
||||||
e.currentTarget.style.background = '#f1f5f9';
|
transition: 'background 0.15s ease',
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseEnter={(e) => {
|
||||||
e.currentTarget.style.background = 'transparent';
|
e.currentTarget.style.background = '#f1f5f9';
|
||||||
}}
|
}}
|
||||||
>
|
onMouseLeave={(e) => {
|
||||||
<TeamOutlined style={{ fontSize: 14, color: '#64748b' }} />
|
e.currentTarget.style.background = 'transparent';
|
||||||
<span style={{ fontSize: 14, color: '#334155' }}>虚拟素材库</span>
|
}}
|
||||||
</div>
|
>
|
||||||
|
<TeamOutlined style={{ fontSize: 14, color: '#64748b' }} />
|
||||||
|
<span style={{ fontSize: 14, color: '#334155' }}>虚拟素材库</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { LoadingOutlined, PlayCircleFilled, WarningOutlined } from '@ant-design/icons';
|
import { LoadingOutlined, PlayCircleFilled, WarningOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
export interface GenerationTaskResourceItem {
|
export interface GenerationTaskResourceItem {
|
||||||
@@ -12,6 +12,7 @@ export interface GenerationTaskResourceItem {
|
|||||||
videoCoverUrl?: string | null;
|
videoCoverUrl?: string | null;
|
||||||
errorMessage?: string | null;
|
errorMessage?: string | null;
|
||||||
generationIndex?: number | null;
|
generationIndex?: number | null;
|
||||||
|
createdAt?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GenerationTaskResourceGroup extends GenerationTaskResourceItem {
|
export interface GenerationTaskResourceGroup extends GenerationTaskResourceItem {
|
||||||
@@ -49,6 +50,125 @@ const isPending = (item: GenerationTaskResourceItem): boolean => {
|
|||||||
return !status || ['pending', 'queued', 'preparing', 'generating', 'creating_provider_task', 'waiting_remote', 'polling', 'result_ready', 'download_queued', 'downloading', 'retry_waiting'].includes(status);
|
return !status || ['pending', 'queued', 'preparing', 'generating', 'creating_provider_task', 'waiting_remote', 'polling', 'result_ready', 'download_queued', 'downloading', 'retry_waiting'].includes(status);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MAX_DURATION_SECONDS = 180;
|
||||||
|
|
||||||
|
interface ProgressItemProps {
|
||||||
|
item: GenerationTaskResourceItem;
|
||||||
|
isPending: boolean;
|
||||||
|
isCompleted: boolean;
|
||||||
|
onAnimationComplete?: () => void;
|
||||||
|
onProgressChange?: (progress: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PROGRESS_RATE = 0.6;
|
||||||
|
|
||||||
|
const calculateProgressValue = (createdAt?: string): number => {
|
||||||
|
if (!createdAt) return 0;
|
||||||
|
const createdTime = new Date(createdAt).getTime();
|
||||||
|
const now = Date.now();
|
||||||
|
const elapsedSeconds = (now - createdTime) / 1000;
|
||||||
|
if (elapsedSeconds >= MAX_DURATION_SECONDS) {
|
||||||
|
return 99;
|
||||||
|
}
|
||||||
|
return Math.min(99, elapsedSeconds * PROGRESS_RATE);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProgressItem: React.FC<ProgressItemProps> = ({ item, isPending: isPendingProp, isCompleted, onAnimationComplete, onProgressChange }) => {
|
||||||
|
const [progress, setProgress] = useState<number>(0);
|
||||||
|
const [displayProgress, setDisplayProgress] = useState<number>(0);
|
||||||
|
const [isFinishing, setIsFinishing] = useState(false);
|
||||||
|
const intervalRef = useRef<number | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (onProgressChange) {
|
||||||
|
onProgressChange(displayProgress);
|
||||||
|
}
|
||||||
|
}, [displayProgress, onProgressChange]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const newProgress = calculateProgressValue(item.createdAt);
|
||||||
|
setProgress(newProgress);
|
||||||
|
setDisplayProgress(newProgress);
|
||||||
|
}, [item]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isCompleted && !isFinishing) {
|
||||||
|
setIsFinishing(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isPendingProp && !isFinishing) {
|
||||||
|
if (intervalRef.current) {
|
||||||
|
clearInterval(intervalRef.current);
|
||||||
|
intervalRef.current = null;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateProgress = () => {
|
||||||
|
const newProgress = calculateProgressValue(item.createdAt);
|
||||||
|
setProgress(newProgress);
|
||||||
|
setDisplayProgress(newProgress);
|
||||||
|
};
|
||||||
|
|
||||||
|
intervalRef.current = window.setInterval(updateProgress, 1000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (intervalRef.current) {
|
||||||
|
clearInterval(intervalRef.current);
|
||||||
|
intervalRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [item.createdAt, isPendingProp, isCompleted, isFinishing, item.id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isFinishing) {
|
||||||
|
const targetProgress = 100;
|
||||||
|
const currentProgress = displayProgress;
|
||||||
|
const duration = 800;
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
const animate = () => {
|
||||||
|
const elapsed = Date.now() - startTime;
|
||||||
|
const progress = Math.min(elapsed / duration, 1);
|
||||||
|
const easeProgress = 1 - Math.pow(1 - progress, 3);
|
||||||
|
const newProgress = currentProgress + (targetProgress - currentProgress) * easeProgress;
|
||||||
|
|
||||||
|
setDisplayProgress(newProgress);
|
||||||
|
|
||||||
|
if (progress < 1) {
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
} else {
|
||||||
|
setTimeout(() => {
|
||||||
|
onAnimationComplete?.();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
}
|
||||||
|
}, [isFinishing, displayProgress, onAnimationComplete]);
|
||||||
|
|
||||||
|
if (!isPendingProp && !isFinishing) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
marginTop: 4,
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
textAlign: 'center',
|
||||||
|
fontSize: 13,
|
||||||
|
color: '#8b5cf6',
|
||||||
|
fontWeight: 700,
|
||||||
|
textShadow: '0 0 8px rgba(139, 92, 246, 0.3)',
|
||||||
|
transition: 'all 0.3s ease-out',
|
||||||
|
}}>
|
||||||
|
{Math.round(displayProgress)}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const GenerationTaskResourceGrid: React.FC<Props> = ({ task, onPreview, resolveUrl = (url) => url || '' }) => {
|
const GenerationTaskResourceGrid: React.FC<Props> = ({ task, onPreview, resolveUrl = (url) => url || '' }) => {
|
||||||
const count = Math.max(1, Math.min(5, Number(task.generationCount || task.childItems?.length || 1)));
|
const count = Math.max(1, Math.min(5, Number(task.generationCount || task.childItems?.length || 1)));
|
||||||
const children = [...(task.childItems || [])].sort((a, b) => Number(a.generationIndex || 0) - Number(b.generationIndex || 0));
|
const children = [...(task.childItems || [])].sort((a, b) => Number(a.generationIndex || 0) - Number(b.generationIndex || 0));
|
||||||
@@ -62,8 +182,49 @@ const GenerationTaskResourceGrid: React.FC<Props> = ({ task, onPreview, resolveU
|
|||||||
pipelineStage: task.pipelineStage,
|
pipelineStage: task.pipelineStage,
|
||||||
generationIndex: index + 1,
|
generationIndex: index + 1,
|
||||||
errorMessage: task.errorMessage,
|
errorMessage: task.errorMessage,
|
||||||
|
createdAt: task.createdAt,
|
||||||
})) : [task]);
|
})) : [task]);
|
||||||
|
|
||||||
|
const [finishingItems, setFinishingItems] = useState<Set<string>>(new Set());
|
||||||
|
const [fullProgressItems, setFullProgressItems] = useState<Set<string>>(new Set());
|
||||||
|
const processedItems = useRef<Set<string>>(new Set());
|
||||||
|
|
||||||
|
const handleProgressChange = (itemId: string, progress: number) => {
|
||||||
|
if (progress >= 100) {
|
||||||
|
setFullProgressItems(prev => {
|
||||||
|
if (prev.has(itemId)) return prev;
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.add(itemId);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
items.slice(0, 5).forEach((item, index) => {
|
||||||
|
const displayStatus = item.displayStatus || item.pipelineStage || item.status || 'generating';
|
||||||
|
const pending = isPending(item);
|
||||||
|
const completed = !pending && displayStatus !== 'failed' && displayStatus !== 'download_failed' && displayStatus !== 'deleted';
|
||||||
|
const itemId = item.id || `${index}`;
|
||||||
|
if (completed && !processedItems.current.has(itemId) && pending === false) {
|
||||||
|
processedItems.current.add(itemId);
|
||||||
|
setFinishingItems(prev => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.add(itemId);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [items]);
|
||||||
|
|
||||||
|
const handleFinishAnimation = (itemId: string) => {
|
||||||
|
setFinishingItems(prev => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.delete(itemId);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width: '100%', height: '100%', display: 'grid', gridTemplateColumns: 'repeat(6, minmax(0, 1fr))', gridAutoRows: 'minmax(0, 1fr)', gap: count > 1 ? 6 : 0 }}>
|
<div style={{ width: '100%', height: '100%', display: 'grid', gridTemplateColumns: 'repeat(6, minmax(0, 1fr))', gridAutoRows: 'minmax(0, 1fr)', gap: count > 1 ? 6 : 0 }}>
|
||||||
{items.slice(0, 5).map((item, index) => {
|
{items.slice(0, 5).map((item, index) => {
|
||||||
@@ -73,6 +234,14 @@ const GenerationTaskResourceGrid: React.FC<Props> = ({ task, onPreview, resolveU
|
|||||||
const coverUrl = resolveUrl(`/static${item.videoCoverUrl}&w=300&q=50`);
|
const coverUrl = resolveUrl(`/static${item.videoCoverUrl}&w=300&q=50`);
|
||||||
const isVideo = (item.genType || task.genType) === 'video';
|
const isVideo = (item.genType || task.genType) === 'video';
|
||||||
const hasResource = isVideo ? !!videoUrl : !!imageUrl;
|
const hasResource = isVideo ? !!videoUrl : !!imageUrl;
|
||||||
|
const pending = isPending(item);
|
||||||
|
const completed = !pending && displayStatus !== 'failed' && displayStatus !== 'download_failed' && displayStatus !== 'deleted';
|
||||||
|
const itemId = item.id || `${index}`;
|
||||||
|
const isFinishing = finishingItems.has(itemId);
|
||||||
|
const isFullProgress = fullProgressItems.has(itemId);
|
||||||
|
const shouldShowContent = completed && !isFinishing;
|
||||||
|
const statusLabel = isFinishing ? '加载中' : statusText(item);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={item.id || `${index}`}
|
key={item.id || `${index}`}
|
||||||
@@ -87,7 +256,7 @@ const GenerationTaskResourceGrid: React.FC<Props> = ({ task, onPreview, resolveU
|
|||||||
border: '1px solid #E7EAF0',
|
border: '1px solid #E7EAF0',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{hasResource && displayStatus !== 'deleted' && displayStatus !== 'failed' && displayStatus !== 'download_failed' && !isPending(item) ? (
|
{hasResource && displayStatus !== 'deleted' && displayStatus !== 'failed' && displayStatus !== 'download_failed' && shouldShowContent ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onPreview(isVideo ? videoUrl : imageUrl, isVideo ? 'video' : 'image')}
|
onClick={() => onPreview(isVideo ? videoUrl : imageUrl, isVideo ? 'video' : 'image')}
|
||||||
@@ -103,9 +272,10 @@ const GenerationTaskResourceGrid: React.FC<Props> = ({ task, onPreview, resolveU
|
|||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ width: '100%', height: '100%', minHeight: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 7, padding: 8, textAlign: 'center' }}>
|
<div style={{ width: '100%', height: '100%', minHeight: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 7, padding: 8, textAlign: 'center' }}>
|
||||||
{isPending(item) ? <LoadingOutlined spin style={{ color: '#8b5cf6', fontSize: items.length > 2 ? 20 : 34 }} /> : <WarningOutlined style={{ color: displayStatus === 'deleted' ? '#98A2B3' : '#A45B5B', fontSize: items.length > 2 ? 20 : 34 }} />}
|
{pending || isFinishing ? <LoadingOutlined spin style={{ color: '#8b5cf6', fontSize: items.length > 2 ? 20 : 34 }} /> : <WarningOutlined style={{ color: displayStatus === 'deleted' ? '#98A2B3' : '#A45B5B', fontSize: items.length > 2 ? 20 : 34 }} />}
|
||||||
<span style={{ fontSize: items.length > 2 ? 10 : 12, color: isPending(item) ? '#8b5cf6' : (displayStatus === 'deleted' ? '#98A2B3' : '#A45B5B'), fontWeight: 500 }}>{statusText(item)}</span>
|
<span style={{ fontSize: items.length > 2 ? 10 : 12, color: pending || isFinishing ? '#8b5cf6' : (displayStatus === 'deleted' ? '#98A2B3' : '#A45B5B'), fontWeight: 500 }}>{statusLabel}</span>
|
||||||
{!isPending(item) && item.errorMessage && items.length <= 2 ? <span style={{ fontSize: 10, color: '#A45B5B', lineHeight: 1.3, maxHeight: 28, overflow: 'hidden' }}>{item.errorMessage}</span> : null}
|
<ProgressItem item={item} isPending={pending || isFinishing} isCompleted={completed} onAnimationComplete={() => handleFinishAnimation(itemId)} onProgressChange={(progress) => handleProgressChange(itemId, progress)} />
|
||||||
|
{!pending && item.errorMessage && items.length <= 2 ? <span style={{ fontSize: 10, color: '#A45B5B', lineHeight: 1.3, maxHeight: 28, overflow: 'hidden' }}>{item.errorMessage}</span> : null}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{items.length > 1 ? <span style={{ position: 'absolute', top: 5, left: 5, zIndex: 2, padding: '1px 6px', borderRadius: 10, background: 'rgba(17,24,39,.58)', color: '#fff', fontSize: 10 }}>#{item.generationIndex || index + 1}</span> : null}
|
{items.length > 1 ? <span style={{ position: 'absolute', top: 5, left: 5, zIndex: 2, padding: '1px 6px', borderRadius: 10, background: 'rgba(17,24,39,.58)', color: '#fff', fontSize: 10 }}>#{item.generationIndex || index + 1}</span> : null}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import GenerationTaskResourceGrid from '../components/generation/GenerationTaskR
|
|||||||
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getParameters, createGenerationTask, getgen_list, getEngine, uploadImage,uploadAudio,
|
getParameters, createGenerationTask, getgen_list, getEngine, uploadImage, uploadAudio,
|
||||||
uploadVideo, getCreditRatios, deleteHistory, calculateCredits
|
uploadVideo, getCreditRatios, deleteHistory, calculateCredits
|
||||||
} from '../api';
|
} from '../api';
|
||||||
|
|
||||||
@@ -285,7 +285,7 @@ const AIChatPage: React.FC = () => {
|
|||||||
if (previewVisible && previewType === 'video') {
|
if (previewVisible && previewType === 'video') {
|
||||||
const playVideo = () => {
|
const playVideo = () => {
|
||||||
if (videoRef.current) {
|
if (videoRef.current) {
|
||||||
videoRef.current.play().catch(() => {});
|
videoRef.current.play().catch(() => { });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -376,7 +376,7 @@ const AIChatPage: React.FC = () => {
|
|||||||
playPromise.catch(() => {
|
playPromise.catch(() => {
|
||||||
// 自动播放被阻止时静音重试
|
// 自动播放被阻止时静音重试
|
||||||
v.muted = true;
|
v.muted = true;
|
||||||
v.play().catch(() => {});
|
v.play().catch(() => { });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (!attachmentPreviewVisible && attachmentPreviewVideoRef.current) {
|
} else if (!attachmentPreviewVisible && attachmentPreviewVideoRef.current) {
|
||||||
@@ -754,10 +754,8 @@ const AIChatPage: React.FC = () => {
|
|||||||
setCreditCalculationData(data);
|
setCreditCalculationData(data);
|
||||||
})
|
})
|
||||||
getgen_list(Pagebreak).then((data: any) => {
|
getgen_list(Pagebreak).then((data: any) => {
|
||||||
// API 按创建时间倒序返回;对话区按时间正序展示,最新消息保持在底部。
|
|
||||||
const mess_list = data.items
|
|
||||||
const total = data.total
|
const total = data.total
|
||||||
setGen_list(mess_list)
|
setGen_list(data.items)
|
||||||
setTotalnumber(total)
|
setTotalnumber(total)
|
||||||
})
|
})
|
||||||
// getParameters()
|
// getParameters()
|
||||||
@@ -809,16 +807,12 @@ const AIChatPage: React.FC = () => {
|
|||||||
// 设置5秒轮询
|
// 设置5秒轮询
|
||||||
pollingRef.current = window.setInterval(() => {
|
pollingRef.current = window.setInterval(() => {
|
||||||
getgen_list(Pagebreak).then((data: any) => {
|
getgen_list(Pagebreak).then((data: any) => {
|
||||||
// 只更新正在生成的任务,不影响其他已加载的数据
|
|
||||||
setGen_list((prevList: any[]) => {
|
setGen_list((prevList: any[]) => {
|
||||||
return prevList.map((prevItem: any) => {
|
return prevList.map((prevItem: any) => {
|
||||||
// 只更新正在生成的任务
|
|
||||||
if (prevItem.status === 'generating') {
|
if (prevItem.status === 'generating') {
|
||||||
const newItem = data.items.find((item: any) => item.id === prevItem.id);
|
const newItem = data.items.find((item: any) => item.id === prevItem.id);
|
||||||
// 如果找到了对应的新数据,使用新数据;否则保持旧数据
|
|
||||||
return newItem || prevItem;
|
return newItem || prevItem;
|
||||||
}
|
}
|
||||||
// 非生成中的任务保持不变
|
|
||||||
return prevItem;
|
return prevItem;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1020,14 +1014,23 @@ const AIChatPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toSnakeCase = (str: string) => str.replace(/([A-Z])/g, '_$1').toLowerCase();
|
||||||
|
const convertToSnake = (obj: any) => {
|
||||||
|
if (!obj || typeof obj !== 'object') return obj;
|
||||||
|
if (Array.isArray(obj)) return obj.map(convertToSnake);
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(obj).map(([k, v]) => [toSnakeCase(k), convertToSnake(v)])
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
let mediaReferences: MediaReference[] | undefined;
|
let mediaReferences: MediaReference[] | undefined;
|
||||||
if (isFirstLastFrameMode) {
|
if (isFirstLastFrameMode) {
|
||||||
mediaReferences = [];
|
mediaReferences = [];
|
||||||
if (firstFrame) {
|
if (firstFrame) {
|
||||||
mediaReferences.push({ ...firstFrame, role: 'first_frame' });
|
mediaReferences.push(convertToSnake({ ...firstFrame, role: 'first_frame' }));
|
||||||
}
|
}
|
||||||
if (lastFrame) {
|
if (lastFrame) {
|
||||||
mediaReferences.push({ ...lastFrame, role: 'last_frame' });
|
mediaReferences.push(convertToSnake({ ...lastFrame, role: 'last_frame' }));
|
||||||
}
|
}
|
||||||
if (mediaReferences.length === 0) {
|
if (mediaReferences.length === 0) {
|
||||||
mediaReferences = undefined;
|
mediaReferences = undefined;
|
||||||
@@ -1037,9 +1040,9 @@ const AIChatPage: React.FC = () => {
|
|||||||
mediaReferences = currentMedia.map((m) => {
|
mediaReferences = currentMedia.map((m) => {
|
||||||
if (m.type === 'image') {
|
if (m.type === 'image') {
|
||||||
const { duration, ...rest } = m;
|
const { duration, ...rest } = m;
|
||||||
return rest;
|
return convertToSnake(rest);
|
||||||
}
|
}
|
||||||
return m;
|
return convertToSnake(m);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
mediaReferences = undefined;
|
mediaReferences = undefined;
|
||||||
@@ -1075,6 +1078,11 @@ const AIChatPage: React.FC = () => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
console.log(newMessage);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1094,9 +1102,21 @@ const AIChatPage: React.FC = () => {
|
|||||||
setPagebreak(newPagebreak);
|
setPagebreak(newPagebreak);
|
||||||
|
|
||||||
getgen_list(newPagebreak).then((data: any) => {
|
getgen_list(newPagebreak).then((data: any) => {
|
||||||
// 将data.items的最后一个元素添加到gen_list末尾
|
const convertedItems = data.items.map((item: any) => {
|
||||||
const newestItem = Array.isArray(data.items) && data.items.length > 0
|
if (item.media_references && Array.isArray(item.media_references)) {
|
||||||
? data.items[data.items.length - 1]
|
item.mediaReferences = item.media_references.map((ref: any) => ({
|
||||||
|
...ref,
|
||||||
|
display_url: ref.display_url || ref.displayUrl,
|
||||||
|
preview_url: ref.preview_url || ref.previewUrl,
|
||||||
|
private_asset_id: ref.private_asset_id || ref.privateAssetId,
|
||||||
|
provider_url: ref.provider_url || ref.providerUrl,
|
||||||
|
remote_asset_id: ref.remote_asset_id || ref.remoteAssetId,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
const newestItem = Array.isArray(convertedItems) && convertedItems.length > 0
|
||||||
|
? convertedItems[convertedItems.length - 1]
|
||||||
: null;
|
: null;
|
||||||
if (newestItem) {
|
if (newestItem) {
|
||||||
setGen_list((prev: any[]) => [...prev, newestItem]);
|
setGen_list((prev: any[]) => [...prev, newestItem]);
|
||||||
@@ -1498,7 +1518,7 @@ const AIChatPage: React.FC = () => {
|
|||||||
setCurrentMedia(newList.map((m, i) => ({ ...m, label: labels[i] })));
|
setCurrentMedia(newList.map((m, i) => ({ ...m, label: labels[i] })));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = error?.response?.data?.message || error?.response?.data?.detail || error?.message || '上传失败';
|
const errorMsg = error?.response?.data?.message || error?.response?.data?.detail || error?.message || '上传失败';
|
||||||
antdMessage.error(errorMsg);
|
antdMessage.error(errorMsg);
|
||||||
} finally {
|
} finally {
|
||||||
setUploading(false);
|
setUploading(false);
|
||||||
}
|
}
|
||||||
@@ -1634,7 +1654,7 @@ const AIChatPage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = error?.response?.data?.message || error?.response?.data?.detail || error?.message || '上传失败';
|
const errorMsg = error?.response?.data?.message || error?.response?.data?.detail || error?.message || '上传失败';
|
||||||
antdMessage.error(errorMsg);
|
antdMessage.error(errorMsg);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2349,6 +2369,7 @@ const AIChatPage: React.FC = () => {
|
|||||||
setReferenceMode('first_last_frame');
|
setReferenceMode('first_last_frame');
|
||||||
} else {
|
} else {
|
||||||
setCurrentMedia(msg.mediaReferences.map((ref: any) => ({
|
setCurrentMedia(msg.mediaReferences.map((ref: any) => ({
|
||||||
|
...ref,
|
||||||
name: ref.name,
|
name: ref.name,
|
||||||
type: ref.type,
|
type: ref.type,
|
||||||
url: ref.url,
|
url: ref.url,
|
||||||
@@ -2966,22 +2987,22 @@ const AIChatPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onPortraitSelect={(assets) => {
|
onPortraitSelect={(assets) => {
|
||||||
if (assets.length > 0) {
|
if (assets.length > 0) {
|
||||||
const asset = assets[0];
|
const asset = assets[0];
|
||||||
const previewUrl = asset.previewUrl || asset.displayUrl || asset.videoCoverUrl || asset.providerUrl || '';
|
const previewUrl = asset.previewUrl || asset.displayUrl || asset.videoCoverUrl || asset.providerUrl || '';
|
||||||
const mediaRef: MediaReference = {
|
const mediaRef: MediaReference = {
|
||||||
name: asset.name || '尾帧图片',
|
name: asset.name || '尾帧图片',
|
||||||
type: 'image',
|
type: 'image',
|
||||||
url: previewUrl,
|
url: previewUrl,
|
||||||
role: 'last_frame',
|
role: 'last_frame',
|
||||||
label: '',
|
label: '',
|
||||||
source: 'private_portrait_asset',
|
source: 'private_portrait_asset',
|
||||||
private_asset_id: asset.id,
|
private_asset_id: asset.id,
|
||||||
};
|
};
|
||||||
setLastFrame(mediaRef);
|
setLastFrame(mediaRef);
|
||||||
antdMessage.success('成功添加尾帧');
|
antdMessage.success('成功添加尾帧');
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
uploading={uploading}
|
uploading={uploading}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -3045,6 +3066,7 @@ const AIChatPage: React.FC = () => {
|
|||||||
maxAudioDuration={15}
|
maxAudioDuration={15}
|
||||||
usedAudioDuration={currentMedia.filter(m => m.type === 'audio').reduce((sum, m) => sum + (m.duration || 0), 0)}
|
usedAudioDuration={currentMedia.filter(m => m.type === 'audio').reduce((sum, m) => sum + (m.duration || 0), 0)}
|
||||||
hideLimitHint={mediaType === 'image'}
|
hideLimitHint={mediaType === 'image'}
|
||||||
|
mediaType={mediaType}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -3227,6 +3249,7 @@ const AIChatPage: React.FC = () => {
|
|||||||
maxAudioDuration={15}
|
maxAudioDuration={15}
|
||||||
usedAudioDuration={currentMedia.filter(m => m.type === 'audio').reduce((sum, m) => sum + (m.duration || 0), 0)}
|
usedAudioDuration={currentMedia.filter(m => m.type === 'audio').reduce((sum, m) => sum + (m.duration || 0), 0)}
|
||||||
hideLimitHint={mediaType === 'image'}
|
hideLimitHint={mediaType === 'image'}
|
||||||
|
mediaType={mediaType}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -3939,7 +3962,7 @@ const AIChatPage: React.FC = () => {
|
|||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
color: '#2f3440',
|
color: '#2f3440',
|
||||||
}}>
|
}}>
|
||||||
{selectedRatio === 'auto' ? '智能' : selectedRatio} · {selectedResolution == '2K' ? '2K高清' : '4K超清'} · {width}×{height}
|
{selectedRatio === 'auto' ? '智能' : selectedRatio} · {selectedResolution === '1K' ? '1K标清' : selectedResolution === '2K' ? '2K高清' : '4K超清'} · {width}×{height}
|
||||||
</Text>
|
</Text>
|
||||||
<CaretDownOutlined style={{ fontSize: 10, color: '#8b5cf6', marginLeft: 'auto' }} />
|
<CaretDownOutlined style={{ fontSize: 10, color: '#8b5cf6', marginLeft: 'auto' }} />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState, useCallback } from "react";
|
||||||
import { copyToClipboard } from "../utils/clipboard";
|
import { copyToClipboard } from "../utils/clipboard";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import {
|
import {
|
||||||
@@ -199,6 +199,101 @@ const GeneratePage: React.FC = () => {
|
|||||||
const [recordStates, setRecordStates] = useState<
|
const [recordStates, setRecordStates] = useState<
|
||||||
Record<string, "idle" | "generating" | "done" | "failed">
|
Record<string, "idle" | "generating" | "done" | "failed">
|
||||||
>({});
|
>({});
|
||||||
|
const [generationProgress, setGenerationProgress] = useState<Record<string, number>>({});
|
||||||
|
const [generationDisplayProgress, setGenerationDisplayProgress] = useState<Record<string, number>>({});
|
||||||
|
const [generationFinishing, setGenerationFinishing] = useState<Record<string, boolean>>({});
|
||||||
|
const generationIntervalRef = useRef<number | null>(null);
|
||||||
|
|
||||||
|
const MAX_DURATION_SECONDS = 180;
|
||||||
|
const PROGRESS_RATE = 0.6;
|
||||||
|
|
||||||
|
const calculateProgressValue = useCallback((createdAt?: string): number => {
|
||||||
|
if (!createdAt) return 0;
|
||||||
|
const createdTime = new Date(createdAt).getTime();
|
||||||
|
const now = Date.now();
|
||||||
|
const elapsedSeconds = (now - createdTime) / 1000;
|
||||||
|
if (elapsedSeconds >= MAX_DURATION_SECONDS) {
|
||||||
|
return 99;
|
||||||
|
}
|
||||||
|
return Math.min(99, elapsedSeconds * PROGRESS_RATE);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const generatingIds = Object.entries(recordStates)
|
||||||
|
.filter(([, state]) => state === "generating")
|
||||||
|
.map(([id]) => id);
|
||||||
|
|
||||||
|
if (generatingIds.length === 0) {
|
||||||
|
if (generationIntervalRef.current) {
|
||||||
|
clearInterval(generationIntervalRef.current);
|
||||||
|
generationIntervalRef.current = null;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateProgress = () => {
|
||||||
|
generatingIds.forEach((id) => {
|
||||||
|
const record = useAppStore.getState().records.items.find((r: any) => r.id === id);
|
||||||
|
if (record?.createdAt) {
|
||||||
|
const newProgress = calculateProgressValue(record.createdAt);
|
||||||
|
setGenerationProgress((prev) => ({ ...prev, [id]: newProgress }));
|
||||||
|
setGenerationDisplayProgress((prev) => ({ ...prev, [id]: newProgress }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
updateProgress();
|
||||||
|
generationIntervalRef.current = window.setInterval(updateProgress, 1000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (generationIntervalRef.current) {
|
||||||
|
clearInterval(generationIntervalRef.current);
|
||||||
|
generationIntervalRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [recordStates, calculateProgressValue]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Object.entries(recordStates).forEach(([id, state]) => {
|
||||||
|
if (state === "done" && !generationFinishing[id]) {
|
||||||
|
setGenerationFinishing((prev) => ({ ...prev, [id]: true }));
|
||||||
|
|
||||||
|
const currentProgress = generationDisplayProgress[id] || 0;
|
||||||
|
const targetProgress = 100;
|
||||||
|
const duration = 800;
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
const animate = () => {
|
||||||
|
const elapsed = Date.now() - startTime;
|
||||||
|
const progress = Math.min(elapsed / duration, 1);
|
||||||
|
const easeProgress = 1 - Math.pow(1 - progress, 3);
|
||||||
|
const newProgress = currentProgress + (targetProgress - currentProgress) * easeProgress;
|
||||||
|
|
||||||
|
setGenerationDisplayProgress((prev) => ({ ...prev, [id]: newProgress }));
|
||||||
|
|
||||||
|
if (progress < 1) {
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [recordStates]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Object.entries(recordStates).forEach(([id, state]) => {
|
||||||
|
if (state === "generating") {
|
||||||
|
const record = useAppStore.getState().records.items.find((r: any) => r.id === id);
|
||||||
|
if (record?.createdAt) {
|
||||||
|
const newProgress = calculateProgressValue(record.createdAt);
|
||||||
|
setGenerationProgress((prev) => ({ ...prev, [id]: newProgress }));
|
||||||
|
setGenerationDisplayProgress((prev) => ({ ...prev, [id]: newProgress }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [currentRecord]);
|
||||||
|
|
||||||
const [editablePrompts, setEditablePrompts] = useState<
|
const [editablePrompts, setEditablePrompts] = useState<
|
||||||
Record<string, string>
|
Record<string, string>
|
||||||
>({});
|
>({});
|
||||||
@@ -452,30 +547,33 @@ const GeneratePage: React.FC = () => {
|
|||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 根据比例计算尺寸
|
|
||||||
const calculateSizeFromRatione = (ratio: string) => {
|
const calculateSizeFromRatione = (ratio: string) => {
|
||||||
setBlindex(ratio);
|
setBlindex(ratio);
|
||||||
|
const size = supportedSizes[selectedResolution]?.[ratio];
|
||||||
// const resolution = selectedResolution === '4k' ? 4096 : 2048;
|
if (size) {
|
||||||
// const ratioParts = ratio.split(':');
|
const [w, h] = size.split(/x/i).map(Number);
|
||||||
// if (ratio === 'auto') {
|
setWidth(w);
|
||||||
// // 智能模式保持当前尺寸
|
setHeight(h);
|
||||||
// return;
|
}
|
||||||
// }
|
|
||||||
// const w = parseInt(ratioParts[0]);
|
|
||||||
// const h = parseInt(ratioParts[1]);
|
|
||||||
// if (w >= h) {
|
|
||||||
// // 横向或正方形
|
|
||||||
// setWidth(resolution);
|
|
||||||
// setHeight(Math.round(resolution * h / w));
|
|
||||||
// } else {
|
|
||||||
// // 纵向
|
|
||||||
// setHeight(resolution);
|
|
||||||
// setWidth(Math.round(resolution * w / h));
|
|
||||||
// }
|
|
||||||
};
|
};
|
||||||
const calculateSizeFromRatiotwo = (ratio: string) => {
|
const calculateSizeFromRatiotwo = (resolution: string) => {
|
||||||
setFBlindex(parseInt(ratio));
|
setFBlindex(resolution);
|
||||||
|
const resolutionRatios = Object.keys(supportedSizes[resolution] || {});
|
||||||
|
const ratioOpts = resolutionRatios.map((key) => ({
|
||||||
|
value: key,
|
||||||
|
label: key,
|
||||||
|
}));
|
||||||
|
setRatioOptions(ratioOpts);
|
||||||
|
const newRatio = resolutionRatios.includes(selectedRatio) ? selectedRatio : resolutionRatios[0] || "1:1";
|
||||||
|
if (newRatio !== selectedRatio) {
|
||||||
|
setSelectedRatio(newRatio);
|
||||||
|
}
|
||||||
|
const size = supportedSizes[resolution]?.[newRatio];
|
||||||
|
if (size) {
|
||||||
|
const [w, h] = size.split(/x/i).map(Number);
|
||||||
|
setWidth(w);
|
||||||
|
setHeight(h);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 图片比例选项
|
// 图片比例选项
|
||||||
@@ -486,6 +584,7 @@ const GeneratePage: React.FC = () => {
|
|||||||
const [widthandheight, setWidthandHeight] = useState([]);
|
const [widthandheight, setWidthandHeight] = useState([]);
|
||||||
const [blindex, setBlindex] = useState<any>(0);
|
const [blindex, setBlindex] = useState<any>(0);
|
||||||
const [fblindex, setFBlindex] = useState<any>(0);
|
const [fblindex, setFBlindex] = useState<any>(0);
|
||||||
|
const [supportedSizes, setSupportedSizes] = useState<Record<string, Record<string, string>>>({});
|
||||||
|
|
||||||
// 监听 blindex 状态变化(解决异步问题)
|
// 监听 blindex 状态变化(解决异步问题)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -556,40 +655,34 @@ const GeneratePage: React.FC = () => {
|
|||||||
|
|
||||||
getParameters()
|
getParameters()
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
let supportedSizes = (data as any).items?.[0]?.supportedSizes || {};
|
const sizes = (data as any).items?.[0]?.supportedSizes || {};
|
||||||
let supportedResolutions = [];
|
setSupportedSizes(sizes);
|
||||||
let twokwidth = [];
|
|
||||||
let fourkwidth = [];
|
|
||||||
|
|
||||||
for (let key in supportedSizes["2K"]) {
|
const resolutionKeys = Object.keys(sizes);
|
||||||
supportedResolutions.push(key);
|
const resolutionOpts = resolutionKeys.map((key) => ({
|
||||||
twokwidth.push(supportedSizes["2K"][key]);
|
value: key,
|
||||||
|
label: key,
|
||||||
|
}));
|
||||||
|
setResolutionOptions(resolutionOpts);
|
||||||
|
|
||||||
|
const initResolution = resolutionKeys.includes(selectedResolution) ? selectedResolution : resolutionKeys[0] || "2K";
|
||||||
|
const ratioKeys = Object.keys(sizes[initResolution] || {});
|
||||||
|
const ratioOpts = ratioKeys.map((key) => ({
|
||||||
|
value: key,
|
||||||
|
label: key,
|
||||||
|
}));
|
||||||
|
setRatioOptions(ratioOpts);
|
||||||
|
|
||||||
|
const initRatio = ratioKeys.includes(selectedRatio) ? selectedRatio : ratioKeys[0] || "1:1";
|
||||||
|
if (initRatio !== selectedRatio) {
|
||||||
|
setSelectedRatio(initRatio);
|
||||||
}
|
}
|
||||||
|
const initSize = sizes[initResolution]?.[initRatio];
|
||||||
supportedResolutions.forEach((res, index) => {
|
if (initSize) {
|
||||||
setRatioOptions((prev) => [
|
const [w, h] = initSize.split(/x/i).map(Number);
|
||||||
...prev,
|
setWidth(w);
|
||||||
{ value: res, label: String(index) },
|
setHeight(h);
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
for (let key in supportedSizes["4K"]) {
|
|
||||||
fourkwidth.push(supportedSizes["4K"][key]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const newWidthandHeight = [twokwidth, fourkwidth];
|
|
||||||
setWidthandHeight(newWidthandHeight);
|
|
||||||
|
|
||||||
let supportedRatios = [];
|
|
||||||
for (let key in supportedSizes) {
|
|
||||||
supportedRatios.push(key);
|
|
||||||
}
|
|
||||||
supportedRatios.forEach((ratio, index) => {
|
|
||||||
setResolutionOptions((prev) => [
|
|
||||||
...prev,
|
|
||||||
{ value: ratio, label: String(index) },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.catch(() => { });
|
.catch(() => { });
|
||||||
getVideoEngines()
|
getVideoEngines()
|
||||||
@@ -980,9 +1073,14 @@ const GeneratePage: React.FC = () => {
|
|||||||
if (currentPollingIds.includes(item.id)) {
|
if (currentPollingIds.includes(item.id)) {
|
||||||
const latest = allRecords.items.find((r: any) => r.id === item.id);
|
const latest = allRecords.items.find((r: any) => r.id === item.id);
|
||||||
if (latest) {
|
if (latest) {
|
||||||
// 如果状态不再是generating,标记为完成
|
const frontendState = recordStates[item.id];
|
||||||
|
// 如果后端状态不是generating,说明任务已经完成(成功或失败),需要处理
|
||||||
|
// 不管前端当前是什么状态,都要处理完成的任务
|
||||||
if (latest.status !== "generating") {
|
if (latest.status !== "generating") {
|
||||||
completedIds.push(item.id);
|
completedIds.push(item.id);
|
||||||
|
} else if (frontendState !== "generating") {
|
||||||
|
// 如果后端状态是generating,但前端不是,更新为generating(处理页面刷新后状态丢失的情况)
|
||||||
|
setRecordStates((p) => ({ ...p, [item.id]: "generating" }));
|
||||||
}
|
}
|
||||||
return latest;
|
return latest;
|
||||||
}
|
}
|
||||||
@@ -1052,6 +1150,14 @@ const GeneratePage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const generatingRecords = recordItems.filter((r) => r.status === "generating");
|
||||||
|
generatingRecords.forEach((record) => {
|
||||||
|
setRecordStates((p) => ({ ...p, [record.id]: "generating" }));
|
||||||
|
startPolling(record.id);
|
||||||
|
});
|
||||||
|
}, [recordItems]);
|
||||||
|
|
||||||
const handleGenerate = async (recordId: string) => {
|
const handleGenerate = async (recordId: string) => {
|
||||||
if (!canAffordVideo) {
|
if (!canAffordVideo) {
|
||||||
message.error("积分不足,请先充值");
|
message.error("积分不足,请先充值");
|
||||||
@@ -1114,6 +1220,9 @@ const GeneratePage: React.FC = () => {
|
|||||||
if (!record) return;
|
if (!record) return;
|
||||||
setGenerating((p) => ({ ...p, [recordId]: true }));
|
setGenerating((p) => ({ ...p, [recordId]: true }));
|
||||||
setRecordStates((p) => ({ ...p, [recordId]: "generating" }));
|
setRecordStates((p) => ({ ...p, [recordId]: "generating" }));
|
||||||
|
setGenerationProgress((p) => ({ ...p, [recordId]: 0 }));
|
||||||
|
setGenerationDisplayProgress((p) => ({ ...p, [recordId]: 0 }));
|
||||||
|
setGenerationFinishing((p) => ({ ...p, [recordId]: false }));
|
||||||
message.loading({
|
message.loading({
|
||||||
content: `「${projectName}」正在重新提交...`,
|
content: `「${projectName}」正在重新提交...`,
|
||||||
duration: 0,
|
duration: 0,
|
||||||
@@ -2245,9 +2354,8 @@ const GeneratePage: React.FC = () => {
|
|||||||
<Typography.Text
|
<Typography.Text
|
||||||
style={{ fontSize: 12, color: "#9ca3af" }}
|
style={{ fontSize: 12, color: "#9ca3af" }}
|
||||||
>
|
>
|
||||||
{selectedResolution == "2K" ? "高清 2K" : "超清 4K"}
|
{selectedResolution === "1K" ? "标清 1K" : selectedResolution === "2K" ? "高清 2K" : `${selectedResolution}分辨率`}
|
||||||
| {width}×{height}
|
| {width}×{height}
|
||||||
{/* {selectedResolution === '2k' ? '高清 2K' : '超清 4K'} | {width}×{height} */}
|
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2676,8 +2784,22 @@ const GeneratePage: React.FC = () => {
|
|||||||
: "#6366f1",
|
: "#6366f1",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{recordStates[currentRecord.id] === "generating" &&
|
{recordStates[currentRecord.id] === "generating" && (
|
||||||
`${mediaType === "image" ? "图片" : "视频"}生成中,请稍候...`}
|
<span>
|
||||||
|
{mediaType === "image" ? "图片" : "视频"}生成中,请稍候...
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
marginLeft: 8,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 700,
|
||||||
|
color: "#6366f1",
|
||||||
|
textShadow: "0 0 8px rgba(99, 102, 241, 0.3)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Math.round(generationDisplayProgress[currentRecord.id] || 0)}%
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{recordStates[currentRecord.id] === "done" &&
|
{recordStates[currentRecord.id] === "done" &&
|
||||||
`${mediaType === "image" ? "图片" : "视频"}生成成功`}
|
`${mediaType === "image" ? "图片" : "视频"}生成成功`}
|
||||||
{recordStates[currentRecord.id] === "failed" &&
|
{recordStates[currentRecord.id] === "failed" &&
|
||||||
@@ -3585,7 +3707,7 @@ const GeneratePage: React.FC = () => {
|
|||||||
className="record-actions"
|
className="record-actions"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<Space>
|
{/* <Space>
|
||||||
{status === "failed" && (
|
{status === "failed" && (
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -3599,7 +3721,7 @@ const GeneratePage: React.FC = () => {
|
|||||||
重试
|
重试
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -4324,22 +4446,20 @@ const GeneratePage: React.FC = () => {
|
|||||||
aspectRatio: params?.aspectRatio,
|
aspectRatio: params?.aspectRatio,
|
||||||
resolution: params?.resolution,
|
resolution: params?.resolution,
|
||||||
});
|
});
|
||||||
setRecordStates((p) => ({
|
startPolling(record.id);
|
||||||
...p,
|
|
||||||
[record.id]: "done",
|
|
||||||
}));
|
|
||||||
message.success({
|
message.success({
|
||||||
content: `「${projectName}」${mediaText}生成成功!`,
|
content: `「${projectName}」${mediaText}已提交,正在生成中...`,
|
||||||
key: record.id,
|
key: record.id,
|
||||||
duration: 3,
|
duration: 3,
|
||||||
});
|
});
|
||||||
} catch {
|
} catch (error: any) {
|
||||||
setRecordStates((p) => ({
|
setRecordStates((p) => ({
|
||||||
...p,
|
...p,
|
||||||
[record.id]: "failed",
|
[record.id]: "failed",
|
||||||
}));
|
}));
|
||||||
|
const errorMsg = error?.response?.data?.message || error?.message || `${mediaText}生成失败`;
|
||||||
message.error({
|
message.error({
|
||||||
content: `「${projectName}」${mediaText}生成失败`,
|
content: `「${projectName}」${errorMsg}`,
|
||||||
key: record.id,
|
key: record.id,
|
||||||
duration: 3,
|
duration: 3,
|
||||||
});
|
});
|
||||||
@@ -4401,7 +4521,7 @@ const GeneratePage: React.FC = () => {
|
|||||||
right: 20,
|
right: 20,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
{/* <Button
|
||||||
type="primary"
|
type="primary"
|
||||||
danger
|
danger
|
||||||
size="large"
|
size="large"
|
||||||
@@ -4416,7 +4536,7 @@ const GeneratePage: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
重新生成{type === "video" ? "视频" : "图片"}
|
重新生成{type === "video" ? "视频" : "图片"}
|
||||||
</Button>
|
</Button> */}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -98,14 +98,7 @@ const ProjectsPage: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={
|
className="content_box" >
|
||||||
{
|
|
||||||
margin: '-24px -32px -32px',
|
|
||||||
borderRadius: 20,
|
|
||||||
height: 'calc(100vh - 34px)',
|
|
||||||
padding:'24px'
|
|
||||||
}
|
|
||||||
}>
|
|
||||||
{/* Header banner */}
|
{/* Header banner */}
|
||||||
<div className="animate-fadeInUp" style={{
|
<div className="animate-fadeInUp" style={{
|
||||||
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||||||
|
|||||||
Reference in New Issue
Block a user