AI创作批量生成任务 main V1 init
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
import React from 'react';
|
||||
import { LoadingOutlined, PlayCircleFilled, WarningOutlined } from '@ant-design/icons';
|
||||
|
||||
export interface GenerationTaskResourceItem {
|
||||
id?: string;
|
||||
genType?: string;
|
||||
status?: string;
|
||||
displayStatus?: string | null;
|
||||
pipelineStage?: string | null;
|
||||
imageUrl?: string | null;
|
||||
videoUrl?: string | null;
|
||||
videoCoverUrl?: string | null;
|
||||
errorMessage?: string | null;
|
||||
generationIndex?: number | null;
|
||||
}
|
||||
|
||||
export interface GenerationTaskResourceGroup extends GenerationTaskResourceItem {
|
||||
generationCount?: number | null;
|
||||
childItems?: GenerationTaskResourceItem[] | null;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
task: GenerationTaskResourceGroup;
|
||||
onPreview: (url: string, type: 'image' | 'video') => void;
|
||||
resolveUrl?: (url?: string | null) => string;
|
||||
}
|
||||
|
||||
const spanByCount = (count: number, index: number): number => {
|
||||
if (count <= 1) return 6;
|
||||
if (count === 2 || count === 4) return 3;
|
||||
if (count === 3) return index < 2 ? 3 : 6;
|
||||
return index < 3 ? 2 : 3;
|
||||
};
|
||||
|
||||
const statusText = (item: GenerationTaskResourceItem): string => {
|
||||
const status = item.displayStatus || item.pipelineStage || item.status || 'generating';
|
||||
const labels: Record<string, string> = {
|
||||
pending: '待处理', queued: '排队中', preparing: '准备中', generating: '生成中',
|
||||
creating_provider_task: '创建任务中', waiting_remote: '等待生成', polling: '轮询中',
|
||||
result_ready: '结果就绪', download_queued: '等待下载', downloading: '下载中',
|
||||
retry_waiting: '等待重试', completed: '已完成', failed: '生成失败',
|
||||
download_failed: '下载失败', deleted: '已删除',
|
||||
};
|
||||
return labels[status] || status;
|
||||
};
|
||||
|
||||
const isPending = (item: GenerationTaskResourceItem): boolean => {
|
||||
const status = item.displayStatus || item.pipelineStage || item.status;
|
||||
return !status || ['pending', 'queued', 'preparing', 'generating', 'creating_provider_task', 'waiting_remote', 'polling', 'result_ready', 'download_queued', 'downloading', 'retry_waiting'].includes(status);
|
||||
};
|
||||
|
||||
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 children = [...(task.childItems || [])].sort((a, b) => Number(a.generationIndex || 0) - Number(b.generationIndex || 0));
|
||||
const items: GenerationTaskResourceItem[] = children.length > 0
|
||||
? children
|
||||
: (count > 1 ? Array.from({ length: count }, (_, index) => ({
|
||||
id: `${task.id || 'task'}-placeholder-${index + 1}`,
|
||||
genType: task.genType,
|
||||
status: task.status,
|
||||
displayStatus: task.displayStatus,
|
||||
pipelineStage: task.pipelineStage,
|
||||
generationIndex: index + 1,
|
||||
errorMessage: task.errorMessage,
|
||||
})) : [task]);
|
||||
|
||||
return (
|
||||
<div style={{ width: '100%', height: '100%', display: 'grid', gridTemplateColumns: 'repeat(6, minmax(0, 1fr))', gridAutoRows: 'minmax(0, 1fr)', gap: count > 1 ? 4 : 0 }}>
|
||||
{items.slice(0, 5).map((item, index) => {
|
||||
const displayStatus = item.displayStatus || item.pipelineStage || item.status || 'generating';
|
||||
const imageUrl = resolveUrl(item.imageUrl);
|
||||
const videoUrl = resolveUrl(item.videoUrl);
|
||||
const coverUrl = resolveUrl(item.videoCoverUrl);
|
||||
const isVideo = (item.genType || task.genType) === 'video';
|
||||
const hasResource = isVideo ? !!videoUrl : !!imageUrl;
|
||||
return (
|
||||
<div
|
||||
key={item.id || `${index}`}
|
||||
style={{
|
||||
gridColumn: `span ${spanByCount(items.length, index)}`,
|
||||
minWidth: 0,
|
||||
minHeight: 0,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
borderRadius: items.length === 1 ? 12 : 8,
|
||||
background: 'linear-gradient(135deg, #ffffff 0%, #FAFBFC 100%)',
|
||||
border: items.length === 1 ? 'none' : '1px solid #E7EAF0',
|
||||
}}
|
||||
>
|
||||
{hasResource && displayStatus !== 'deleted' ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onPreview(isVideo ? videoUrl : imageUrl, isVideo ? 'video' : 'image')}
|
||||
style={{ width: '100%', height: '100%', border: 0, padding: 0, background: 'transparent', cursor: 'pointer', position: 'relative' }}
|
||||
>
|
||||
{isVideo ? (
|
||||
coverUrl ? <img src={coverUrl} alt={`生成结果${item.generationIndex || index + 1}`} style={{ width: '100%', height: '100%', objectFit: 'contain' }} />
|
||||
: <video src={videoUrl} muted preload="metadata" style={{ width: '100%', height: '100%', objectFit: 'contain' }} />
|
||||
) : (
|
||||
<img src={imageUrl} alt={`生成结果${item.generationIndex || index + 1}`} style={{ width: '100%', height: '100%', objectFit: 'contain' }} />
|
||||
)}
|
||||
{isVideo ? <PlayCircleFilled style={{ position: 'absolute', left: '50%', top: '50%', transform: 'translate(-50%, -50%)', fontSize: items.length > 2 ? 28 : 46, color: 'rgba(255,255,255,.92)', filter: 'drop-shadow(0 4px 10px rgba(0,0,0,.28))' }} /> : null}
|
||||
</button>
|
||||
) : (
|
||||
<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 }} />}
|
||||
<span style={{ fontSize: items.length > 2 ? 10 : 12, color: isPending(item) ? '#8b5cf6' : (displayStatus === 'deleted' ? '#98A2B3' : '#A45B5B'), fontWeight: 500 }}>{statusText(item)}</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}
|
||||
</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}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GenerationTaskResourceGrid;
|
||||
@@ -24,6 +24,7 @@ import bg3 from '../assets/bg3.png';
|
||||
import text from '../assets/testb.png';
|
||||
|
||||
import UploadSelector from '../components/UploadSelector';
|
||||
import GenerationTaskResourceGrid from '../components/generation/GenerationTaskResourceGrid';
|
||||
|
||||
|
||||
|
||||
@@ -68,6 +69,17 @@ const { Option } = Select;
|
||||
// 解构Typography组件
|
||||
const { Text } = Typography;
|
||||
|
||||
const GENERATION_RESOURCE_BASE = (import.meta.env.VITE_API_BASE || 'http://localhost:8000')
|
||||
.replace(/\/api\/?$/i, '')
|
||||
.replace(/\/$/, '');
|
||||
const resolveGenerationResourceUrl = (url?: string | null): string => {
|
||||
if (!url) return '';
|
||||
const value = String(url).trim();
|
||||
if (!value) return '';
|
||||
if (/^(https?:)?\/\//i.test(value) || /^(blob|data):/i.test(value)) return value;
|
||||
return `${GENERATION_RESOURCE_BASE}${value.startsWith('/') ? value : `/${value}`}`;
|
||||
};
|
||||
|
||||
|
||||
interface MediaReference {
|
||||
name: string;
|
||||
@@ -97,6 +109,7 @@ interface Message {
|
||||
resolution?: string;
|
||||
timestamp?: string;
|
||||
engine_id: string;
|
||||
generation_count: number;
|
||||
}
|
||||
|
||||
|
||||
@@ -138,6 +151,7 @@ const AIChatPage: React.FC = () => {
|
||||
const {
|
||||
mediaType,
|
||||
countType,
|
||||
generationCount,
|
||||
selectedRatio,
|
||||
selectedResolution,
|
||||
width,
|
||||
@@ -150,6 +164,7 @@ const AIChatPage: React.FC = () => {
|
||||
inputValue,
|
||||
setMediaType,
|
||||
setCountType,
|
||||
setGenerationCount,
|
||||
setSelectedRatio,
|
||||
setSelectedResolution,
|
||||
setWidth,
|
||||
@@ -170,6 +185,24 @@ const AIChatPage: React.FC = () => {
|
||||
const currentEngine = currentEngineList?.find((e: any) => e.id === countType);
|
||||
const maxImageCount = currentEngine?.maxImageCount ?? 4;
|
||||
const maxVideoCount = currentEngine?.maxVideoCount ?? 1;
|
||||
const multiGenerationEnabled = Boolean(currentEngine?.multiGenerationEnabled);
|
||||
const configuredMaxGenerationCount = multiGenerationEnabled
|
||||
? Math.max(1, Math.min(5, Number(currentEngine?.maxGenerationCount || 1)))
|
||||
: 1;
|
||||
const referenceImageCount = mediaType === 'image'
|
||||
? currentMedia.filter((item) => item.type === 'image').length
|
||||
: 0;
|
||||
const imageProviderRemainingCount = mediaType === 'image'
|
||||
? Math.max(1, Number(currentEngine?.multiImageMaxImages || 15) - referenceImageCount)
|
||||
: 5;
|
||||
const effectiveMaxGenerationCount = Math.max(
|
||||
1,
|
||||
Math.min(
|
||||
5,
|
||||
configuredMaxGenerationCount,
|
||||
mediaType === 'image' ? imageProviderRemainingCount : 5,
|
||||
),
|
||||
);
|
||||
|
||||
const [uploading, setUploading] = useState<boolean>(false);
|
||||
|
||||
@@ -444,7 +477,7 @@ const AIChatPage: React.FC = () => {
|
||||
const inputImageCost = ((config.inputImageBaseCredits || 0) + (config.inputImagePerImageCredits || 0) * inputImageCount) * (config.inputImageRatio || 1);
|
||||
total += inputImageCost;
|
||||
}
|
||||
return Number(total.toFixed(2));
|
||||
return Number((total * generationCount).toFixed(2));
|
||||
} else {
|
||||
// 图片:baseCredits × ratio
|
||||
let total = config.baseCredits * config.ratio;
|
||||
@@ -456,7 +489,7 @@ const AIChatPage: React.FC = () => {
|
||||
const inputImageCost = ((config.inputImageBaseCredits || 0) + (config.inputImagePerImageCredits || 0) * inputImageCount) * (config.inputImageRatio || 1);
|
||||
total += inputImageCost;
|
||||
}
|
||||
return Number(total.toFixed(2));
|
||||
return Number((total * generationCount).toFixed(2));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -558,6 +591,22 @@ const AIChatPage: React.FC = () => {
|
||||
}
|
||||
}, [mediaType, enginesele, enginesLoaded]);
|
||||
|
||||
// 切换媒体类型或引擎时,默认回到最安全的单份生成。
|
||||
useEffect(() => {
|
||||
if (!enginesLoaded) return;
|
||||
setGenerationCount(1);
|
||||
}, [mediaType, countType, enginesLoaded, setGenerationCount]);
|
||||
|
||||
// 图片参考图数量变化后动态收敛本次可选数量;后端仍会再次校验。
|
||||
useEffect(() => {
|
||||
if (generationCount > effectiveMaxGenerationCount) {
|
||||
setGenerationCount(effectiveMaxGenerationCount);
|
||||
if (mediaType === 'image') {
|
||||
antdMessage.info(`受当前引擎或参考图数量限制,本次最多生成 ${effectiveMaxGenerationCount} 份`);
|
||||
}
|
||||
}
|
||||
}, [generationCount, effectiveMaxGenerationCount, mediaType, setGenerationCount, antdMessage]);
|
||||
|
||||
// 点击外部关闭弹窗
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
@@ -703,8 +752,9 @@ const AIChatPage: React.FC = () => {
|
||||
setCreditCalculationData(data);
|
||||
})
|
||||
getgen_list(Pagebreak).then((data: any) => {
|
||||
let mess_list = data.items
|
||||
let total = data.total
|
||||
// API 按创建时间倒序返回;对话区按时间正序展示,最新消息保持在底部。
|
||||
const mess_list = [...(data.items || [])].reverse()
|
||||
const total = data.total
|
||||
setGen_list(mess_list)
|
||||
setTotalnumber(total)
|
||||
})
|
||||
@@ -1005,6 +1055,7 @@ const AIChatPage: React.FC = () => {
|
||||
engine_id: countType,
|
||||
|
||||
idempotency_key: new Date().toLocaleString('zh-CN'),
|
||||
generation_count: generationCount,
|
||||
media_references: mediaReferences,
|
||||
// 图片参数(仅图片模式时添加)
|
||||
...(mediaType === 'image' && {
|
||||
@@ -1034,6 +1085,7 @@ const AIChatPage: React.FC = () => {
|
||||
setCurrentMedia([]);
|
||||
setFirstFrame(null);
|
||||
setLastFrame(null);
|
||||
setGenerationCount(1);
|
||||
|
||||
// 创建任务成功后,重置页数为1,获取最新列表
|
||||
const newPagebreak = { ...Pagebreak, page: 1 };
|
||||
@@ -1041,7 +1093,12 @@ const AIChatPage: React.FC = () => {
|
||||
|
||||
getgen_list(newPagebreak).then((data: any) => {
|
||||
// 将data.items的最后一个元素添加到gen_list末尾
|
||||
setGen_list((prev: any[]) => [...prev, data.items[data.items.length - 1]]);
|
||||
const newestItem = Array.isArray(data.items) && data.items.length > 0
|
||||
? data.items[data.items.length - 1]
|
||||
: null;
|
||||
if (newestItem) {
|
||||
setGen_list((prev: any[]) => [...prev, newestItem]);
|
||||
}
|
||||
setTotalnumber(data.total);
|
||||
// 发送消息后滚动到底部
|
||||
setTimeout(() => {
|
||||
@@ -1121,13 +1178,13 @@ const AIChatPage: React.FC = () => {
|
||||
setGen_list((prev: any[]) => {
|
||||
const existingIds = new Set(prev.map((item: any) => item.id));
|
||||
// 只添加不存在的新数据,保持新数据的原有顺序
|
||||
const newItems = data.items.filter((item: any) => {
|
||||
const newItems = (data.items || []).filter((item: any) => {
|
||||
if (!item.id) return false;
|
||||
if (existingIds.has(item.id)) return false;
|
||||
existingIds.add(item.id);
|
||||
return true;
|
||||
});
|
||||
// 新数据在前,旧数据在后
|
||||
}).reverse();
|
||||
// 加载的是更早一页,按时间正序放到现有消息前面。
|
||||
return [...newItems, ...prev];
|
||||
});
|
||||
|
||||
@@ -2453,50 +2510,16 @@ const AIChatPage: React.FC = () => {
|
||||
display: 'flex', gap: 16, marginBottom: 16, marginTop: 16, width: '100%', alignItems: 'flex-start',
|
||||
|
||||
}}>
|
||||
<div style={{ width: '50%', overflow: 'hidden', borderRadius: 12, position: 'relative', height: 220, border: '1px solid #E7EAF0', boxShadow: '0 4px 12px rgba(139, 92, 246, 0.08)', display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'linear-gradient(135deg, #ffffff 0%, #FAFBFC 100%)' }}>
|
||||
{msg.status === 'generating' ? (
|
||||
<>
|
||||
<div style={{ position: 'absolute', top: 12, left: 12, display: 'flex', alignItems: 'center', gap: 8, zIndex: 2 }}>
|
||||
{/* <div style={{ width: 20, height: 20, border: '2px solid #ddd6fe', borderTopColor: '#8b5cf6', borderRadius: '50%', animation: 'spin 1s linear infinite' }} /> */}
|
||||
</div>
|
||||
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.6) 50%, transparent 100%)', animation: 'shimmer 2s infinite' }} />
|
||||
<div style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 16 }}>
|
||||
<div style={{ width: 64, height: 64, borderRadius: '50%', background: 'rgba(139, 92, 246, 0.1)', display: 'flex', alignItems: 'center', justifyContent: 'center', boxShadow: '0 0 30px rgba(139, 92, 246, 0.2)' }}>
|
||||
<div style={{ width: 48, height: 48, border: '3px solid #ddd6fe', borderTopColor: '#8b5cf6', borderRadius: '50%', animation: 'spin 1s linear infinite' }} />
|
||||
</div>
|
||||
<span style={{ fontSize: 12, color: '#8b5cf6', fontWeight: 500 }}>生成中...</span>
|
||||
|
||||
{/* <div style={{ display: 'flex', gap: 4 }}>
|
||||
<div style={{ width: 6, height: 6, borderRadius: '50%', background: '#8b5cf6', animation: 'pulse 1.5s ease-in-out infinite' }} />
|
||||
<div style={{ width: 6, height: 6, borderRadius: '50%', background: '#a8a1b6', animation: 'pulse 1.5s ease-in-out 0.2s infinite' }} />
|
||||
<div style={{ width: 6, height: 6, borderRadius: '50%', background: '#ddd6fe', animation: 'pulse 1.5s ease-in-out 0.4s infinite' }} />
|
||||
</div> */}
|
||||
</div>
|
||||
</>
|
||||
) : msg.status === 'failed' ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 12 }}>
|
||||
<div style={{ width: 48, height: 48, borderRadius: '50%', background: 'rgba(168, 90, 106, 0.10)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<WarningOutlined style={{ color: '#A45B5B', fontSize: 22 }} />
|
||||
</div>
|
||||
<span style={{ fontSize: 14, color: '#A45B5B', fontWeight: 620 }}>生成失败(积分已退)</span>
|
||||
{msg.errorMessage && (
|
||||
<span style={{ fontSize: 13, color: '#A45B5B', textAlign: 'center', padding: '0 8px', lineHeight: 1.5 }}>{msg.errorMessage}</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{msg.genType === 'image' ? (
|
||||
<img src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}/static${msg.imageUrl}&w=300&p=50`} alt={msg.name} style={{ width: '100%', height: '100%', borderRadius: 12, objectFit: 'contain', cursor: 'pointer', transition: 'transform 0.3s ease' }} onClick={() => { setPreviewUrl(msg.imageUrl); setPreviewType('image'); setPreviewVisible(true); }} onMouseEnter={(e) => { e.currentTarget.style.transform = 'scale(1.05)'; }} onMouseLeave={(e) => { e.currentTarget.style.transform = 'scale(1)'; }} />
|
||||
) : (
|
||||
<>
|
||||
<img src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}/static${msg.videoCoverUrl}&w=300&p=50`} alt={msg.name} style={{ width: '100%', height: '100%', borderRadius: 12, objectFit: 'contain', cursor: 'pointer', transition: 'transform 0.3s ease' }} onClick={() => { setPreviewUrl(msg.videoUrl); setPreviewType('video'); setPreviewVisible(true); }} onMouseEnter={(e) => { e.currentTarget.style.transform = 'scale(1.05)'; }} onMouseLeave={(e) => { e.currentTarget.style.transform = 'scale(1)'; }} />
|
||||
<div style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', width: 56, height: 56, background: 'rgba(47, 52, 64, 0.72)', borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', pointerEvents: 'none', boxShadow: '0 10px 24px rgba(47, 52, 64, 0.22)' }}>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="#fff"><path d="M8 5v14l11-7z" /></svg>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div style={{ width: '50%', overflow: 'hidden', borderRadius: 12, position: 'relative', height: 220, border: '1px solid #E7EAF0', boxShadow: '0 4px 12px rgba(139, 92, 246, 0.08)', background: 'linear-gradient(135deg, #ffffff 0%, #FAFBFC 100%)' }}>
|
||||
<GenerationTaskResourceGrid
|
||||
task={msg}
|
||||
resolveUrl={resolveGenerationResourceUrl}
|
||||
onPreview={(url, type) => {
|
||||
setPreviewUrl(url);
|
||||
setPreviewType(type);
|
||||
setPreviewVisible(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ width: '50%', display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
<div style={{ background: '#FFFFFF', borderRadius: 12, padding: 12, border: '1px solid #E7EAF0', flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
@@ -2506,7 +2529,7 @@ const AIChatPage: React.FC = () => {
|
||||
<span style={{
|
||||
// background: 'rgba(139, 92, 246, 0.08)',
|
||||
borderRadius: 16, color: '#8b5cf6', fontWeight: 500
|
||||
}}>{msg.engineSnapshot.name}</span>
|
||||
}}>{msg.engineSnapshot?.name || '未知引擎'}</span>
|
||||
<span style={{
|
||||
// background: 'rgba(139, 92, 246, 0.08)',
|
||||
borderRadius: 16, color: '#667085'
|
||||
@@ -4418,6 +4441,21 @@ const AIChatPage: React.FC = () => {
|
||||
</Space>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexShrink: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, whiteSpace: 'nowrap', height: 34, padding: '0 8px 0 12px', borderRadius: 11, background: 'rgba(255, 255, 255, 0.92)', border: '1px solid rgba(231, 234, 240, 0.92)', boxShadow: '0 4px 12px rgba(47, 52, 64, 0.04)' }}>
|
||||
<Text style={{ fontSize: 13, color: '#667085', fontWeight: 600 }}>生成数量:</Text>
|
||||
<Select
|
||||
value={generationCount}
|
||||
onChange={(value) => setGenerationCount(Number(value || 1))}
|
||||
disabled={effectiveMaxGenerationCount <= 1}
|
||||
size="small"
|
||||
variant="borderless"
|
||||
style={{ width: 68 }}
|
||||
options={Array.from({ length: effectiveMaxGenerationCount }, (_, index) => ({
|
||||
value: index + 1,
|
||||
label: `${index + 1}份`,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, color: '#667085', whiteSpace: 'nowrap', height: 34, padding: '0 12px', borderRadius: 11, background: 'rgba(255, 255, 255, 0.92)', border: '1px solid rgba(231, 234, 240, 0.92)', boxShadow: '0 4px 12px rgba(47, 52, 64, 0.04)' }}>
|
||||
<Text style={{ fontSize: 13, color: '#667085', fontWeight: 600 }}>预估积分:</Text>
|
||||
<Text style={{ fontSize: 13, color: '#2f3440', fontWeight: 800 }}>{getEstimatedCredits()}</Text>
|
||||
|
||||
@@ -18,6 +18,7 @@ interface AppState {
|
||||
// 生成配置状态 - 页面跳转时保留,刷新时重置
|
||||
mediaType: string;
|
||||
countType: string;
|
||||
generationCount: number;
|
||||
selectedRatio: string;
|
||||
selectedResolution: string;
|
||||
width: number;
|
||||
@@ -46,6 +47,7 @@ interface AppState {
|
||||
// 生成配置状态更新方法
|
||||
setMediaType: (mediaType: string) => void;
|
||||
setCountType: (countType: string) => void;
|
||||
setGenerationCount: (generationCount: number) => void;
|
||||
setImageSettings: (ratio: string, resolution: string, width: number, height: number) => void;
|
||||
setVideoSettings: (duration: number, aspectRatio: string, resolution: string) => void;
|
||||
setEngineOptions: (options: { ratios: string[]; resolutions: string[]; durations: number[] }) => void;
|
||||
@@ -71,6 +73,7 @@ export const useAppStore = create<AppState>((set, get) => ({
|
||||
// 生成配置状态初始值
|
||||
mediaType: 'video',
|
||||
countType: '请选择',
|
||||
generationCount: 1,
|
||||
selectedRatio: '1:1',
|
||||
selectedResolution: '2K',
|
||||
width: 2048,
|
||||
@@ -160,6 +163,7 @@ export const useAppStore = create<AppState>((set, get) => ({
|
||||
// 生成配置状态更新方法
|
||||
setMediaType: (mediaType) => set({ mediaType }),
|
||||
setCountType: (countType) => set({ countType }),
|
||||
setGenerationCount: (generationCount) => set({ generationCount }),
|
||||
setImageSettings: (ratio, resolution, width, height) =>
|
||||
set({ selectedRatio: ratio, selectedResolution: resolution, width, height }),
|
||||
setVideoSettings: (duration, aspectRatio, resolution) =>
|
||||
@@ -179,6 +183,7 @@ export const useAppStore = create<AppState>((set, get) => ({
|
||||
resetGenerationConfig: () => set({
|
||||
mediaType: 'image',
|
||||
countType: '请选择',
|
||||
generationCount: 1,
|
||||
selectedRatio: '1:1',
|
||||
selectedResolution: '2K',
|
||||
width: 2048,
|
||||
|
||||
Reference in New Issue
Block a user