This commit is contained in:
2026-07-24 12:06:22 +08:00
7 changed files with 393 additions and 164 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -28,7 +28,7 @@
} }
})(); })();
</script> </script>
<script type="module" crossorigin src="/assets/index-5mDsoYNB.js"></script> <script type="module" crossorigin src="/assets/index-CVSmzRXM.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Bsz_Xon-.css"> <link rel="stylesheet" crossorigin href="/assets/index-Bsz_Xon-.css">
</head> </head>
<body> <body>
@@ -119,16 +119,7 @@ const NotificationPopup: React.FC = () => {
position: 'relative', position: 'relative',
overflow: 'hidden', overflow: 'hidden',
}}> }}>
<div style={{
position: 'absolute',
right: -50,
top: -50,
width: 200,
height: 200,
borderRadius: '50%',
background: 'radial-gradient(circle, rgba(255,255,255,0.15) 0%, transparent 60%)',
filter: 'blur(30px)',
}} />
{/* Top row: type badge + close */} {/* Top row: type badge + close */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
@@ -136,7 +127,7 @@ const NotificationPopup: React.FC = () => {
{tc.icon} {tc.label} {tc.icon} {tc.label}
</Tag> </Tag>
<button <button
onClick={handleClose} onClick={(e) => { e.stopPropagation(); handleClose(); }}
style={{ style={{
width: 28, height: 28, borderRadius: '50%', width: 28, height: 28, borderRadius: '50%',
background: 'rgba(255,255,255,0.1)', background: 'rgba(255,255,255,0.1)',
+298 -88
View File
@@ -29,6 +29,7 @@ import {
CloseCircleOutlined, CloseCircleOutlined,
EditOutlined, EditOutlined,
RocketOutlined, RocketOutlined,
LayoutOutlined,
CaretRightOutlined, CaretRightOutlined,
CaretDownOutlined, CaretDownOutlined,
VideoCameraOutlined, VideoCameraOutlined,
@@ -288,16 +289,20 @@ const PortalOptionGroup: React.FC<PortalOptionGroupProps> = ({ group, groupName,
borderRadius: 8, borderRadius: 8,
cursor: "pointer", cursor: "pointer",
userSelect: "none", userSelect: "none",
transition: "all 0.2s", transition: "background 0.2s, border-color 0.2s",
background: selected ? "rgba(99,102,241,0.08)" : "#f8f9fc", background: "#ffffffff",
border: expanded ? "1px solid #6366f1" : "1px solid #e2e8f0", border: "1px solid #e2e8f0",
minWidth: 70,
whiteSpace: "nowrap",
flexShrink: 0,
}} }}
> >
<Typography.Text <Typography.Text
style={{ style={{
fontSize: 12, fontSize: 13,
color: selected ? "#6366f1" : "#64748b", color: "#374151",
fontWeight: selected ? 500 : 400, fontWeight: 500,
lineHeight: "18px",
}} }}
> >
{selected || group.name} {selected || group.name}
@@ -642,13 +647,14 @@ const GeneratePage: React.FC = () => {
// Video params for step 2 // Video params for step 2
const [videoDuration, setVideoDuration] = useState(5); const [videoDuration, setVideoDuration] = useState(5);
const [videoAspectRatio, setVideoAspectRatio] = useState<AspectRatio>("16:9"); const [videoAspectRatio, setVideoAspectRatio] = useState<AspectRatio>("9:16");
const [videoResolution, setVideoResolution] = useState<Resolution>("720p"); const [videoResolution, setVideoResolution] = useState<Resolution>("720p");
const [videoEngines, setVideoEngines] = useState<any[]>([]); const [videoEngines, setVideoEngines] = useState<any[]>([]);
const [imageEngines, setImageEngines] = useState<any[]>([]); const [imageEngines, setImageEngines] = useState<any[]>([]);
const [selectedEngineId, setSelectedEngineId] = useState(""); const [selectedEngineId, setSelectedEngineId] = useState("");
const [includeMediaReferences, setIncludeMediaReferences] = useState(false); const [includeMediaReferences, setIncludeMediaReferences] = useState(false);
const [expandedEngine, setExpandedEngine] = useState<string | null>(null); const [expandedEngine, setExpandedEngine] = useState<string | null>(null);
const [showVideoSettingsModal, setShowVideoSettingsModal] = useState(false);
const [engineOptions, setEngineOptions] = useState<{ const [engineOptions, setEngineOptions] = useState<{
ratios: string[]; ratios: string[];
resolutions: string[]; resolutions: string[];
@@ -856,7 +862,7 @@ const GeneratePage: React.FC = () => {
calculateCredits().then((data: any) => { calculateCredits().then((data: any) => {
setCreditCalculationData(data); setCreditCalculationData(data);
}) })
if (!showImageSettingsModal) return; if (!showImageSettingsModal && !showVideoSettingsModal) return;
const handleClickOutside = (e: MouseEvent) => { const handleClickOutside = (e: MouseEvent) => {
const target = e.target as HTMLElement; const target = e.target as HTMLElement;
if ( if (
@@ -864,11 +870,12 @@ const GeneratePage: React.FC = () => {
!target.closest(".image-settings-trigger") !target.closest(".image-settings-trigger")
) { ) {
setShowImageSettingsModal(false); setShowImageSettingsModal(false);
setShowVideoSettingsModal(false);
} }
}; };
document.addEventListener("click", handleClickOutside); document.addEventListener("click", handleClickOutside);
return () => document.removeEventListener("click", handleClickOutside); return () => document.removeEventListener("click", handleClickOutside);
}, [showImageSettingsModal]); }, [showImageSettingsModal, showVideoSettingsModal]);
const getReferenceUsage = (items?: MediaReference[]) => { const getReferenceUsage = (items?: MediaReference[]) => {
@@ -1141,6 +1148,13 @@ const GeneratePage: React.FC = () => {
// console.log(numericValue); // console.log(numericValue);
// 更新状态(使用转换后的数字值) // 更新状态(使用转换后的数字值)
setMediaType(numericValue); setMediaType(numericValue);
// 切换媒体类型后,默认选择第一个可用引擎
const engines = numericValue === "image" ? imageEngines : videoEngines;
const filteredEngines = engines.filter((item: any) => item.supportsUniversalReference !== false);
if (filteredEngines.length > 0) {
handleGenerationEngineChange(filteredEngines[0].id);
}
}; };
useEffect(() => { useEffect(() => {
@@ -1271,8 +1285,23 @@ const GeneratePage: React.FC = () => {
if (mediaType === 'image') { if (mediaType === 'image') {
const sizes = engine.supportedSizes || {}; const sizes = engine.supportedSizes || {};
setSupportedSizes(sizes); setSupportedSizes(sizes);
const resolution = Object.keys(sizes)[0] || '2K';
const ratio = Object.keys(sizes[resolution] || {})[0] || '1:1'; const resolutionKeys = Object.keys(sizes);
const resolutionOpts = resolutionKeys.map((key) => ({
value: key,
label: key,
}));
setResolutionOptions(resolutionOpts);
const resolution = resolutionKeys.includes(selectedResolution) ? selectedResolution : resolutionKeys[0] || '2K';
const ratioKeys = Object.keys(sizes[resolution] || {});
const ratioOpts = ratioKeys.map((key) => ({
value: key,
label: key,
}));
setRatioOptions(ratioOpts);
const ratio = ratioKeys.includes(selectedRatio) ? selectedRatio : ratioKeys[0] || '1:1';
setSelectedResolution(resolution); setSelectedResolution(resolution);
setSelectedRatio(ratio); setSelectedRatio(ratio);
const pixelSize = sizes[resolution]?.[ratio] || ''; const pixelSize = sizes[resolution]?.[ratio] || '';
@@ -2417,7 +2446,7 @@ const GeneratePage: React.FC = () => {
} }
}} }}
rows={3} rows={3}
placeholder="上传参考素材只用做模型理解,不参与生成)、输入文字,自由组合图、文多元素。输入 @ 可引用参考内容..." placeholder="上传参考素材后(手动开启是否参与最终生成,如不开启则只用做模型理解)、输入文字,自由组合图、文多元素。输入 @ 可引用参考内容..."
maxLength={500} maxLength={500}
variant="borderless" variant="borderless"
autoSize={{ minRows: 2, maxRows: 6 }} autoSize={{ minRows: 2, maxRows: 6 }}
@@ -2560,12 +2589,12 @@ const GeneratePage: React.FC = () => {
> >
<PortalDropdown <PortalDropdown
label="引擎" label="引擎"
value={(mediaType === "image" ? imageEngines : videoEngines).find((item: any) => item.id === selectedEngineId)?.name || "选择引擎"} value={(mediaType === "image" ? imageEngines : videoEngines).filter((item: any) => item.supportsUniversalReference !== false).find((item: any) => item.id === selectedEngineId)?.name || "选择引擎"}
options={(mediaType === "image" ? imageEngines : videoEngines).map((item: any) => item.name)} options={(mediaType === "image" ? imageEngines : videoEngines).filter((item: any) => item.supportsUniversalReference !== false).map((item: any) => item.name)}
expanded={expandedEngine === "engine"} expanded={expandedEngine === "engine"}
onToggle={() => setExpandedEngine(expandedEngine === "engine" ? null : "engine")} onToggle={() => setExpandedEngine(expandedEngine === "engine" ? null : "engine")}
onSelect={(value) => { onSelect={(value) => {
const engine = (mediaType === "image" ? imageEngines : videoEngines).find((item: any) => item.name === value); const engine = (mediaType === "image" ? imageEngines : videoEngines).filter((item: any) => item.supportsUniversalReference !== false).find((item: any) => item.name === value);
if (engine) { if (engine) {
handleGenerationEngineChange(engine.id); handleGenerationEngineChange(engine.id);
} }
@@ -2575,34 +2604,228 @@ const GeneratePage: React.FC = () => {
/> />
{mediaType !== "image" && ( {mediaType !== "image" && (
<> <>
<PortalDropdown <div style={{ flex: 1, position: 'relative', display: 'inline-block' }}>
label="时长" <button
value={`${videoDuration}`} onClick={() => {
options={durationOptions} setShowVideoSettingsModal(!showVideoSettingsModal);
suffix="秒" setExpandedEngine(null);
expanded={expandedEngine === "duration"} }}
onToggle={handleDurationToggle} className="image-settings-trigger"
onSelect={handleDurationSelect} style={{
onClose={handleDurationClose} width: '100%',
/> padding: '4px 12px',
<PortalDropdown height: 34,
label="比例" borderRadius: 10,
value={videoAspectRatio as string} border: '1px solid rgba(99, 102, 241, 0.15)',
options={engineOptions.ratios} backgroundColor: 'rgba(255,255,255,0.7)',
expanded={expandedEngine === "aspectRatio"} cursor: 'pointer',
onToggle={() => setExpandedEngine(expandedEngine === "aspectRatio" ? null : "aspectRatio")} display: 'inline-flex',
onSelect={(value) => { setVideoAspectRatio(value as AspectRatio); setExpandedEngine(null); }} alignItems: 'center',
onClose={() => setExpandedEngine(null)} gap: 6,
/> transition: 'all 0.2s',
<PortalDropdown }}
label="分辨率" >
value={videoResolution as string} <LayoutOutlined style={{ fontSize: 14, color: '#6366f1' }} />
options={engineOptions.resolutions} <span style={{
expanded={expandedEngine === "resolution"} fontSize: 14,
onToggle={() => setExpandedEngine(expandedEngine === "resolution" ? null : "resolution")} fontWeight: 500,
onSelect={(value) => { setVideoResolution(value as Resolution); setExpandedEngine(null); }} color: '#64748b',
onClose={() => setExpandedEngine(null)} }}>
/> {videoAspectRatio} · {videoDuration}s · {videoResolution}
</span>
</button>
{showVideoSettingsModal && (
<div
className="image-settings-popover"
style={{
position: 'absolute',
bottom: 'calc(100% + 8px)',
left: -160,
width: 350,
backgroundColor: '#fff',
borderRadius: 16,
boxShadow: '0 10px 40px rgba(0,0,0,0.15)',
padding: 16,
border: 'none',
zIndex: 9999,
}}
onClick={(e) => e.stopPropagation()}
>
<div style={{ marginBottom: 16 }}>
<span style={{
display: 'block',
marginBottom: 8,
fontSize: 12,
fontWeight: 500,
color: '#666666',
}}>
</span>
<div style={{
display: 'flex',
flexWrap: 'wrap',
gap: 4,
}}>
{engineOptions.ratios.map((ratio) => (
<button
key={ratio}
onClick={() => setVideoAspectRatio(ratio as AspectRatio)}
style={{
flex: '0 0 calc(14.28% - 4px)',
minWidth: 44,
height: 52,
borderRadius: 6,
border: videoAspectRatio === ratio
? '2px solid #6366f1'
: '1px solid #e5e7eb',
backgroundColor: videoAspectRatio === ratio
? '#fff'
: '#f9fafb',
cursor: 'pointer',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
transition: 'all 0.2s',
}}
>
<div style={{
width: 18,
height: 18,
border: `2px solid ${videoAspectRatio === ratio ? '#6366f1' : '#9ca3af'}`,
borderRadius: 2,
marginBottom: 2,
}} />
<span style={{
fontSize: 9,
color: videoAspectRatio === ratio
? '#6366f1'
: '#6b7280',
fontWeight: videoAspectRatio === ratio ? 600 : 400,
}}>
{ratio}
</span>
</button>
))}
</div>
</div>
<div style={{ marginBottom: 16 }}>
<span style={{
display: 'block',
marginBottom: 8,
fontSize: 12,
fontWeight: 500,
color: '#666666',
}}>
</span>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{ flex: 1, position: 'relative', height: 24, display: 'flex', alignItems: 'center' }}>
<div style={{
position: 'absolute',
top: '50%',
left: 0,
right: 0,
height: 6,
borderRadius: 3,
background: '#e5e7eb',
transform: 'translateY(-50%)',
}} />
<div style={{
position: 'absolute',
top: '50%',
left: 0,
height: 6,
borderRadius: 3,
background: '#6366f1',
width: `${((videoDuration - Math.min(...engineOptions.durations)) / (Math.max(...engineOptions.durations) - Math.min(...engineOptions.durations))) * 100}%`,
transform: 'translateY(-50%)',
}} />
<input
type="range"
min={Math.min(...engineOptions.durations)}
max={Math.max(...engineOptions.durations)}
value={videoDuration}
onChange={(e) => setVideoDuration(Number(e.target.value))}
style={{
position: 'relative',
width: '100%',
height: 24,
borderRadius: 3,
background: 'transparent',
outline: 'none',
appearance: 'none',
cursor: 'pointer',
zIndex: 1,
}}
/>
</div>
<div style={{
display: 'flex',
alignItems: 'center',
gap: 4,
padding: '4px 12px',
backgroundColor: '#f1f5f9',
borderRadius: 6,
}}>
<span style={{ fontSize: 14, fontWeight: 600, color: '#64748b' }}>
{videoDuration}
</span>
<span style={{ fontSize: 12, color: '#9ca3af' }}></span>
</div>
</div>
</div>
<div>
<span style={{
display: 'block',
marginBottom: 8,
fontSize: 12,
fontWeight: 500,
color: '#666666',
}}>
</span>
<div style={{ display: 'flex', gap: 6 }}>
{engineOptions.resolutions.map((resolution) => (
<button
key={resolution}
onClick={() => setVideoResolution(resolution as Resolution)}
style={{
flex: 1,
height: 42,
borderRadius: 8,
border: videoResolution === resolution
? '2px solid #6366f1'
: '1px solid #e5e7eb',
backgroundColor: videoResolution === resolution
? '#6366f1'
: '#f9fafb',
cursor: 'pointer',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
transition: 'all 0.2s',
}}
>
<span style={{
fontSize: 12,
fontWeight: 600,
color: videoResolution === resolution
? '#fff'
: '#4b5563',
}}>
{resolution}
</span>
</button>
))}
</div>
</div>
</div>
)}
</div>
{currentOptionGroups.map((group, gi) => { {currentOptionGroups.map((group, gi) => {
const isExpanded = expandedGroup === group.name; const isExpanded = expandedGroup === group.name;
const selected = selectedOptions[group.name]; const selected = selectedOptions[group.name];
@@ -2622,63 +2845,50 @@ const GeneratePage: React.FC = () => {
</> </>
)} )}
{mediaType == "image" && ( {mediaType == "image" && (
<div <div style={{ flex: 1, position: 'relative', display: 'inline-block' }}>
style={{ position: "relative", display: "inline-block" }}
>
<button <button
onClick={() => onClick={() => {
setShowImageSettingsModal(!showImageSettingsModal) setShowImageSettingsModal(!showImageSettingsModal);
} setExpandedEngine(null);
}}
className="image-settings-trigger" className="image-settings-trigger"
style={{ style={{
minWidth: 200, width: '100%',
padding: "0px 14px", padding: '4px 12px',
height: 32, height: 34,
borderRadius: 8, borderRadius: 10,
border: "1px solid #e2e8f0", border: '1px solid rgba(99, 102, 241, 0.15)',
backgroundColor: "#fff", backgroundColor: 'rgba(255,255,255,0.7)',
cursor: "pointer", cursor: 'pointer',
display: "flex", display: 'inline-flex',
alignItems: "center", alignItems: 'center',
justifyContent: "space-between", gap: 6,
gap: 10, transition: 'all 0.2s',
transition: "all 0.2s",
}} }}
> >
<div style={{ textAlign: "left" }}> <LayoutOutlined style={{ fontSize: 14, color: '#6366f1' }} />
<Typography.Text <span style={{
style={{ fontSize: 14,
fontSize: 13, fontWeight: 500,
fontWeight: 500, color: '#64748b',
color: "#000000ff", }}>
}} {selectedRatio === "auto" ? "智能" : selectedRatio} · {selectedResolution} · {width}×{height}
> </span>
{selectedRatio === "auto" ? "智能" : selectedRatio}
</Typography.Text>
<Typography.Text
style={{ fontSize: 11, color: "#000000ff" }}
>
{selectedResolution === "1K" ? "标清" : selectedResolution === "2K" ? "高清" : `${selectedResolution}`} | {width}×{height}
</Typography.Text>
</div>
<CaretDownOutlined
style={{ fontSize: 14, color: "#000000ff" }}
/>
</button> </button>
{showImageSettingsModal && ( {showImageSettingsModal && (
<div <div
className="image-settings-popover" className="image-settings-popover"
style={{ style={{
position: "absolute", position: 'absolute',
bottom: "calc(100% + 8px)", bottom: 'calc(100% + 8px)',
left: 0, left: -160,
width: 520, width: 350,
backgroundColor: "#fff", backgroundColor: '#fff',
borderRadius: 12, borderRadius: 16,
boxShadow: "0 8px 32px rgba(0,0,0,0.12)", boxShadow: '0 10px 40px rgba(0,0,0,0.15)',
padding: 16, padding: 16,
border: "1px solid rgba(231, 234, 240, 0.8)", border: 'none',
zIndex: 9999, zIndex: 9999,
}} }}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
+20 -18
View File
@@ -87,7 +87,7 @@ const GenerateConver: React.FC = () => {
const [videoEngines, setVideoEngines] = useState<any[]>([]); const [videoEngines, setVideoEngines] = useState<any[]>([]);
const [engineId, setEngineId] = useState(''); const [engineId, setEngineId] = useState('');
const [videoDuration, setVideoDuration] = useState(5); const [videoDuration, setVideoDuration] = useState(5);
const [videoAspectRatio, setVideoAspectRatio] = useState('16:9'); const [videoAspectRatio, setVideoAspectRatio] = useState('9:16');
const [videoResolution, setVideoResolution] = useState('480p'); const [videoResolution, setVideoResolution] = useState('480p');
const [showEngineModal, setShowEngineModal] = useState(false); const [showEngineModal, setShowEngineModal] = useState(false);
const [showVideoSettingsModal, setShowVideoSettingsModal] = useState(false); const [showVideoSettingsModal, setShowVideoSettingsModal] = useState(false);
@@ -120,7 +120,7 @@ const GenerateConver: React.FC = () => {
if (engines.length > 0) { if (engines.length > 0) {
const first = engines[0]; const first = engines[0];
setEngineId(first.id); setEngineId(first.id);
setVideoAspectRatio(first.supportedRatios?.[0] || '16:9'); setVideoAspectRatio(first.supportedRatios?.includes('9:16') ? '9:16' : first.supportedRatios?.[0] || '9:16');
setVideoResolution(first.supportedResolutions?.[0] || '480p'); setVideoResolution(first.supportedResolutions?.[0] || '480p');
setVideoDuration(first.supportedDurations?.[0] || 5); setVideoDuration(first.supportedDurations?.[0] || 5);
} }
@@ -128,6 +128,18 @@ const GenerateConver: React.FC = () => {
calculateCredits().then((rules: any) => setCreditRules(Array.isArray(rules) ? rules : [])).catch(() => setCreditRules([])); calculateCredits().then((rules: any) => setCreditRules(Array.isArray(rules) ? rules : [])).catch(() => setCreditRules([]));
}, []); }, []);
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
const target = e.target as HTMLElement;
if (!target.closest('.image-settings-popover') && !target.closest('.image-settings-trigger')) {
setShowEngineModal(false);
setShowVideoSettingsModal(false);
}
};
document.addEventListener('click', handleClickOutside);
return () => document.removeEventListener('click', handleClickOutside);
}, []);
const selectedEngine = videoEngines.find((item: any) => item.id === engineId); const selectedEngine = videoEngines.find((item: any) => item.id === engineId);
const selectedEngineSupportsImage = !imageUrl || supportsReferenceImage(selectedEngine); const selectedEngineSupportsImage = !imageUrl || supportsReferenceImage(selectedEngine);
const estimatedCredits = (() => { const estimatedCredits = (() => {
@@ -1129,12 +1141,12 @@ const GenerateConver: React.FC = () => {
{/* 原视频产品名称 */} {/* 原视频产品名称 */}
<div style={{ marginBottom: 16 }}> <div style={{ marginBottom: 16 }}>
<p style={{ margin: 0, fontSize: 13, fontWeight: 500, color: '#475569', marginBottom: 8 }}> <p style={{ margin: 0, fontSize: 13, fontWeight: 500, color: '#475569', marginBottom: 8 }}>
</p> </p>
<Input <Input
value={originalProductName} value={originalProductName}
onChange={(e) => setOriginalProductName(e.target.value)} onChange={(e) => setOriginalProductName(e.target.value)}
placeholder="请输入原视频产品名称" placeholder="请输入原产品名称"
style={{ style={{
borderRadius: 10, borderRadius: 10,
height: 40, height: 40,
@@ -1195,7 +1207,7 @@ const GenerateConver: React.FC = () => {
</div> </div>
</div> </div>
<div style={{ marginBottom:16, display: 'grid', gap: 10 }}> <div style={{ marginBottom: 16, display: 'grid', gap: 10 }}>
<p style={{ margin: 0, fontSize: 13, fontWeight: 600, color: '#475569' }}></p> <p style={{ margin: 0, fontSize: 13, fontWeight: 600, color: '#475569' }}></p>
<div style={{ width: '100%', display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}> <div style={{ width: '100%', display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
<div style={{ flex: 1, position: 'relative', display: 'inline-block' }}> <div style={{ flex: 1, position: 'relative', display: 'inline-block' }}>
@@ -1580,24 +1592,14 @@ const GenerateConver: React.FC = () => {
> >
<span style={{ color: '#ffffffff', marginLeft: 8, fontSize: 13 }}>
:{(estimatedCredits + optimizeHoldCredits)}
</span>
</Button> </Button>
</Tooltip> </Tooltip>
<div style={{textAlign:'center'}}>
<span style={{ color: '#000000ff', marginLeft: 8, fontSize: 13 }}>
:{estimatedCredits}
</span>
<span style={{ color: '#000000ff', marginLeft: 8, fontSize: 13 }}>
+
</span>
<span style={{ color: '#000000ff', marginLeft: 8, fontSize: 13 }}>
:{optimizeHoldCredits}
</span>
</div>
</div> </div>
+46 -20
View File
@@ -47,7 +47,7 @@ function RemoveInfo() {
const [videoEngines, setVideoEngines] = useState<any[]>([]); const [videoEngines, setVideoEngines] = useState<any[]>([]);
const [engineId, setEngineId] = useState(''); const [engineId, setEngineId] = useState('');
const [videoDuration, setVideoDuration] = useState(5); const [videoDuration, setVideoDuration] = useState(5);
const [videoAspectRatio, setVideoAspectRatio] = useState('16:9'); const [videoAspectRatio, setVideoAspectRatio] = useState('9:16');
const [videoResolution, setVideoResolution] = useState('480p'); const [videoResolution, setVideoResolution] = useState('480p');
const [showEngineModal, setShowEngineModal] = useState(false); const [showEngineModal, setShowEngineModal] = useState(false);
const [showVideoSettingsModal, setShowVideoSettingsModal] = useState(false); const [showVideoSettingsModal, setShowVideoSettingsModal] = useState(false);
@@ -79,7 +79,7 @@ function RemoveInfo() {
if (engines.length > 0) { if (engines.length > 0) {
const first = engines[0]; const first = engines[0];
setEngineId(first.id); setEngineId(first.id);
setVideoAspectRatio(first.supportedRatios?.[0] || '16:9'); setVideoAspectRatio(first.supportedRatios?.includes('9:16') ? '9:16' : first.supportedRatios?.[0] || '9:16');
setVideoResolution(first.supportedResolutions?.[0] || '480p'); setVideoResolution(first.supportedResolutions?.[0] || '480p');
setVideoDuration(first.supportedDurations?.[0] || 5); setVideoDuration(first.supportedDurations?.[0] || 5);
} }
@@ -195,6 +195,18 @@ function RemoveInfo() {
}; };
}, [refreshPageData]); }, [refreshPageData]);
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
const target = e.target as HTMLElement;
if (!target.closest('.image-settings-popover') && !target.closest('.image-settings-trigger')) {
setShowEngineModal(false);
setShowVideoSettingsModal(false);
}
};
document.addEventListener('click', handleClickOutside);
return () => document.removeEventListener('click', handleClickOutside);
}, []);
useEffect(() => { useEffect(() => {
const processing = taskDetail?.analysisStatus === 'processing'; const processing = taskDetail?.analysisStatus === 'processing';
if (!processing || !creatID) { if (!processing || !creatID) {
@@ -207,8 +219,6 @@ function RemoveInfo() {
} }
if (!analysisStartedAtRef.current) analysisStartedAtRef.current = Date.now(); if (!analysisStartedAtRef.current) analysisStartedAtRef.current = Date.now();
const elapsed = Date.now() - analysisStartedAtRef.current;
const delay = elapsed < 60_000 ? 3_000 : (elapsed < 600_000 ? 10_000 : 30_000);
const poll = async () => { const poll = async () => {
if (document.visibilityState !== 'visible') return; if (document.visibilityState !== 'visible') return;
@@ -220,11 +230,26 @@ function RemoveInfo() {
} }
}; };
analysisPollingRef.current = window.setTimeout(() => void poll(), delay); const scheduleNextPoll = () => {
if (!analysisPollingRef.current) return;
const elapsed = Date.now() - (analysisStartedAtRef.current || Date.now());
const delay = elapsed < 60_000 ? 3_000 : (elapsed < 600_000 ? 10_000 : 30_000);
analysisPollingRef.current = window.setTimeout(async () => {
await poll();
scheduleNextPoll();
}, delay);
};
analysisPollingRef.current = window.setTimeout(async () => {
await poll();
scheduleNextPoll();
}, 3_000);
const handleVisibility = () => { const handleVisibility = () => {
if (document.visibilityState === 'visible') void poll(); if (document.visibilityState === 'visible') void poll();
}; };
document.addEventListener('visibilitychange', handleVisibility); document.addEventListener('visibilitychange', handleVisibility);
return () => { return () => {
if (analysisPollingRef.current) { if (analysisPollingRef.current) {
window.clearTimeout(analysisPollingRef.current); window.clearTimeout(analysisPollingRef.current);
@@ -356,7 +381,16 @@ function RemoveInfo() {
// //
// } // }
} catch (err: any) { } catch (err: any) {
message.error(err?.message || '创建失败,请重试'); let errorMsg = '';
if (err instanceof Error) {
try {
const errorData = JSON.parse(err.message);
errorMsg = errorData.detail || err.message;
} catch {
errorMsg = err.message;
}
}
message.error(errorMsg || '创建失败,请重试');
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -878,7 +912,7 @@ function RemoveInfo() {
<Button <Button
type="default" type="default"
onClick={handleOpenTrimModal} onClick={handleOpenTrimModal}
disabled={!videoUrl || splitLoading} disabled={!videoUrl || splitLoading || taskDetail?.analysisStatus === 'processing'}
style={{ style={{
flex: 1, flex: 1,
height: 52, height: 52,
@@ -911,7 +945,7 @@ function RemoveInfo() {
<Button <Button
type="primary" type="primary"
loading={autoSplitLoading} loading={autoSplitLoading}
disabled={autoSplitLoading} disabled={autoSplitLoading || taskDetail?.analysisStatus === 'processing'}
style={{ style={{
flex: 1, flex: 1,
height: 52, height: 52,
@@ -1551,21 +1585,13 @@ function RemoveInfo() {
> >
{loading ? '生成中...' : '手动生成'} {loading ? '生成中...' : '手动生成'}
<span style={{ color: '#ffffffff', marginLeft: 8, fontSize: 13 }}>
:{(estimatedCredits + optimizeHoldCredits)}
</span>
</Button> </Button>
</Tooltip> </Tooltip>
</div> </div>
<div style={{ textAlign: 'center' }}>
<span style={{ color: '#000000ff', marginLeft: 8, fontSize: 13 }}>
:{estimatedCredits}
</span>
<span style={{ color: '#000000ff', marginLeft: 8, fontSize: 13 }}>
+
</span>
<span style={{ color: '#000000ff', marginLeft: 8, fontSize: 13 }}>
:{optimizeHoldCredits}
</span>
</div>
</div> </div>
</Drawer> </Drawer>
+17 -17
View File
@@ -1,5 +1,5 @@
import { useEffect, useState, useRef, useCallback } from 'react'; import { useEffect, useState, useRef, useCallback } from 'react';
import { Button, Modal, Input, Table, Upload, Popconfirm, Tag, Space, message } from 'antd'; import { Button, Modal, Input, Table, Upload, Popconfirm, Tag, Space, App } from 'antd';
import { FileTextOutlined, CloudUploadOutlined } from '@ant-design/icons'; import { FileTextOutlined, CloudUploadOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
@@ -13,6 +13,7 @@ const renderStatus = (status: string) => {
}; };
export default function VideoFrameExtractor() { export default function VideoFrameExtractor() {
const { message } = App.useApp();
const navigate = useNavigate(); const navigate = useNavigate();
const [videoUrl, setVideoUrl] = useState<string>(''); const [videoUrl, setVideoUrl] = useState<string>('');
@@ -32,7 +33,7 @@ export default function VideoFrameExtractor() {
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
const cleanupResources = useCallback(() => { const cleanupResources = useCallback(() => {
if (videoUrl) { if (videoUrl && videoUrl.startsWith('blob:')) {
URL.revokeObjectURL(videoUrl); URL.revokeObjectURL(videoUrl);
} }
setVideoUrl(''); setVideoUrl('');
@@ -112,24 +113,23 @@ export default function VideoFrameExtractor() {
idempotency_key: `shot_${Date.now()}`, idempotency_key: `shot_${Date.now()}`,
}; };
createShotReplication(params).then((res) => { const createRes = await createShotReplication(params);
// message.success('任务创建成功'); const listRes = await getShotReplicationList(1, 20, searchKeyword);
getShotReplicationList(1, 20, searchKeyword).then((res) => {
// console.log('获取列表成功', res.items[0].id);
message.loading('创建中...', 3);
setTimeout(() => { message.loading('创建中...', 3);
navigate(`/removelens/${res.items[0].id}/removeinfo`); setTimeout(() => {
}, 3000); navigate(`/removelens/${listRes.items[0].id}/removeinfo`);
}, 3000);
// setTableData(res.items || []);
})
})
cleanupResources(); cleanupResources();
} catch (err) { } catch (err: any) {
message.error('任务创建失败,请重试'); let errorMsg = '';
if (err instanceof Error) {
errorMsg = err.message;
} else if (typeof err === 'string') {
errorMsg = err;
}
message.error(errorMsg || '任务创建失败,请重试');
} finally { } finally {
setLoading(false); setLoading(false);
} }