Merge branch 'main' of https://gitee.com/wg123/video-gen
This commit is contained in:
+613
File diff suppressed because one or more lines are too long
-613
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-BhfopyFy.js"></script>
|
<script type="module" crossorigin src="/assets/index-5mDsoYNB.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>
|
||||||
|
|||||||
@@ -301,7 +301,7 @@ export async function verifyCaptcha(captchaId: string, x: number): Promise<strin
|
|||||||
return res.token;
|
return res.token;
|
||||||
}
|
}
|
||||||
// ── Site Info ─────────────────────────────────────────────
|
// ── Site Info ─────────────────────────────────────────────
|
||||||
export async function getSiteInfo(): Promise<{ siteName: string; siteLogo: string; userAgreementPrivacyUrl: string; siteCopyright: string; operationManual: string; loginBgVideo: string }> {
|
export async function getSiteInfo(): Promise<{ siteName: string; siteLogo: string; userAgreementPrivacyUrl: string; siteCopyright: string; operationManual: string; loginBgVideo: string; optimizeHoldCredits?: number }> {
|
||||||
if (USE_MOCK) return { siteName: 'VideoGen.AI', siteLogo: '', userAgreementPrivacyUrl: '', siteCopyright: '© 2024 民众智创 版权所有', operationManual: '', loginBgVideo: '' };
|
if (USE_MOCK) return { siteName: 'VideoGen.AI', siteLogo: '', userAgreementPrivacyUrl: '', siteCopyright: '© 2024 民众智创 版权所有', operationManual: '', loginBgVideo: '' };
|
||||||
return api.get('/auth/site-info', false);
|
return api.get('/auth/site-info', false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -313,7 +313,7 @@ const GRADIENTS = [
|
|||||||
const AppLayout: React.FC = () => {
|
const AppLayout: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { user, logout, refreshUser } = useAuthStore();
|
const { user, logout, refreshUser, setOptimizeHoldCredits } = useAuthStore();
|
||||||
const [pwdModalOpen, setPwdModalOpen] = useState(false);
|
const [pwdModalOpen, setPwdModalOpen] = useState(false);
|
||||||
const [rechargeModalOpen, setRechargeModalOpen] = useState(false);
|
const [rechargeModalOpen, setRechargeModalOpen] = useState(false);
|
||||||
const [contactModalOpen, setContactModalOpen] = useState(false);
|
const [contactModalOpen, setContactModalOpen] = useState(false);
|
||||||
@@ -409,8 +409,14 @@ const AppLayout: React.FC = () => {
|
|||||||
setOperationManualUrl(info.operationManual);
|
setOperationManualUrl(info.operationManual);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (info.optimizeHoldCredits !== undefined) {
|
||||||
|
setOptimizeHoldCredits(info.optimizeHoldCredits);
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
|
||||||
localStorage.setItem('siteInfo', JSON.stringify({ siteName: name, siteLogo: logo }));
|
localStorage.setItem('siteInfo', JSON.stringify({ siteName: name, siteLogo: logo }));
|
||||||
}).catch(() => { });
|
}).catch((err) => {
|
||||||
|
});
|
||||||
|
|
||||||
getUser().then((res: any) => {
|
getUser().then((res: any) => {
|
||||||
// console.log('[Storage] getUser 返回:', res);
|
// console.log('[Storage] getUser 返回:', res);
|
||||||
|
|||||||
@@ -43,11 +43,17 @@ interface ProgressItemProps {
|
|||||||
isPending: boolean;
|
isPending: boolean;
|
||||||
isCompleted: boolean;
|
isCompleted: boolean;
|
||||||
onAnimationComplete?: () => void;
|
onAnimationComplete?: () => void;
|
||||||
|
index?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PROGRESS_RATE = 0.6;
|
const PROGRESS_RATE = 0.6;
|
||||||
|
|
||||||
const calculateProgressValue = (createdAt?: string): number => {
|
const seededRandom = (seed: number): number => {
|
||||||
|
const x = Math.sin(seed * 9999) * 10000;
|
||||||
|
return x - Math.floor(x);
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateProgressValue = (createdAt?: string, index: number = 0): number => {
|
||||||
if (!createdAt) return 0;
|
if (!createdAt) return 0;
|
||||||
const createdTime = new Date(createdAt).getTime();
|
const createdTime = new Date(createdAt).getTime();
|
||||||
if (isNaN(createdTime)) return 0;
|
if (isNaN(createdTime)) return 0;
|
||||||
@@ -56,18 +62,24 @@ const calculateProgressValue = (createdAt?: string): number => {
|
|||||||
if (elapsedSeconds >= MAX_DURATION_SECONDS) {
|
if (elapsedSeconds >= MAX_DURATION_SECONDS) {
|
||||||
return 99;
|
return 99;
|
||||||
}
|
}
|
||||||
return Math.min(99, elapsedSeconds * PROGRESS_RATE);
|
let progress = Math.min(99, elapsedSeconds * PROGRESS_RATE);
|
||||||
|
const baseOffset = index * 3;
|
||||||
|
const randomOffset = seededRandom(index + 1) * 8;
|
||||||
|
const offset = baseOffset + randomOffset;
|
||||||
|
progress = Math.max(0, progress - offset);
|
||||||
|
return progress;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ProgressItem: React.FC<ProgressItemProps> = ({ item, isPending: isPendingProp, isCompleted, onAnimationComplete }) => {
|
const ProgressItem: React.FC<ProgressItemProps> = ({ item, isPending: isPendingProp, isCompleted, onAnimationComplete, index = 0 }) => {
|
||||||
const [displayProgress, setDisplayProgress] = useState<number>(0);
|
const [displayProgress, setDisplayProgress] = useState<number>(0);
|
||||||
const [isFinishing, setIsFinishing] = useState(false);
|
const [isFinishing, setIsFinishing] = useState(false);
|
||||||
const intervalRef = useRef<number | null>(null);
|
const intervalRef = useRef<number | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newProgress = calculateProgressValue(item.createdAt);
|
if (isFinishing) return;
|
||||||
|
const newProgress = calculateProgressValue(item.createdAt, index);
|
||||||
setDisplayProgress(newProgress);
|
setDisplayProgress(newProgress);
|
||||||
}, [item]);
|
}, [item, index, isFinishing]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isCompleted && !isFinishing) {
|
if (isCompleted && !isFinishing) {
|
||||||
@@ -84,7 +96,8 @@ const ProgressItem: React.FC<ProgressItemProps> = ({ item, isPending: isPendingP
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updateProgress = () => {
|
const updateProgress = () => {
|
||||||
const newProgress = calculateProgressValue(item.createdAt);
|
if (isFinishing) return;
|
||||||
|
const newProgress = calculateProgressValue(item.createdAt, index);
|
||||||
setDisplayProgress(newProgress);
|
setDisplayProgress(newProgress);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -96,7 +109,7 @@ const ProgressItem: React.FC<ProgressItemProps> = ({ item, isPending: isPendingP
|
|||||||
intervalRef.current = null;
|
intervalRef.current = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [item.createdAt, isPendingProp, isCompleted, isFinishing, item.id]);
|
}, [item.createdAt, isPendingProp, isCompleted, isFinishing, item.id, index]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isFinishing) {
|
if (isFinishing) {
|
||||||
@@ -305,7 +318,7 @@ const GenerationTaskResourceGrid: React.FC<Props> = ({ task, onPreview, resolveU
|
|||||||
{pending || isFinishing ? <span className="gen-task-aurora-3" /> : null}
|
{pending || isFinishing ? <span className="gen-task-aurora-3" /> : null}
|
||||||
{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 }} />}
|
{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: pending || isFinishing ? '#8b5cf6' : (displayStatus === 'deleted' ? '#98A2B3' : '#A45B5B'), fontWeight: 500 }}>{statusLabel}</span>
|
<span style={{ fontSize: items.length > 2 ? 10 : 12, color: pending || isFinishing ? '#8b5cf6' : (displayStatus === 'deleted' ? '#98A2B3' : '#A45B5B'), fontWeight: 500 }}>{statusLabel}</span>
|
||||||
<ProgressItem item={item} isPending={pending || isFinishing} isCompleted={completed} onAnimationComplete={() => handleFinishAnimation(itemId)} />
|
<ProgressItem item={item} isPending={pending || isFinishing} isCompleted={completed} onAnimationComplete={() => handleFinishAnimation(itemId)} index={item.generationIndex || index} />
|
||||||
{!pending && item.errorMessage && items.length <= 2 ? <span style={{ fontSize: 10, color: '#A45B5B', lineHeight: 1.3, maxHeight: 28, overflow: 'hidden' }}>{item.errorMessage}</span> : null}
|
{!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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ import {
|
|||||||
updateRecordPrompt,
|
updateRecordPrompt,
|
||||||
getCreditRatios,
|
getCreditRatios,
|
||||||
getRecordsPage,
|
getRecordsPage,
|
||||||
|
calculateCredits,
|
||||||
} from "../api";
|
} from "../api";
|
||||||
import { formatDate } from "../utils/formatDate";
|
import { formatDate } from "../utils/formatDate";
|
||||||
import UploadSelector from "../components/UploadSelector";
|
import UploadSelector from "../components/UploadSelector";
|
||||||
@@ -135,8 +136,8 @@ const PortalDropdown: React.FC<PortalDropdownProps> = ({
|
|||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
userSelect: "none",
|
userSelect: "none",
|
||||||
transition: "background 0.2s, border-color 0.2s",
|
transition: "background 0.2s, border-color 0.2s",
|
||||||
background: expanded ? "rgba(99,102,241,0.08)" : "#f8f9fc",
|
background: "#fff",
|
||||||
border: expanded ? "1px solid #6366f1" : "1px solid #e2e8f0",
|
border: "1px solid #e2e8f0",
|
||||||
minWidth: 70,
|
minWidth: 70,
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
@@ -144,8 +145,8 @@ const PortalDropdown: React.FC<PortalDropdownProps> = ({
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
fontSize: 12,
|
fontSize: 13,
|
||||||
color: "#6366f1",
|
color: "#374151",
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
lineHeight: "18px",
|
lineHeight: "18px",
|
||||||
}}
|
}}
|
||||||
@@ -505,7 +506,7 @@ const GeneratePage: React.FC = () => {
|
|||||||
retryGeneration,
|
retryGeneration,
|
||||||
} = useAppStore();
|
} = useAppStore();
|
||||||
const recordItems = records.items;
|
const recordItems = records.items;
|
||||||
const { user } = useAuthStore();
|
const { user, optimizeHoldCredits } = useAuthStore();
|
||||||
|
|
||||||
const [optimizing, setOptimizing] = useState(false);
|
const [optimizing, setOptimizing] = useState(false);
|
||||||
const [showOptimized, setShowOptimized] = useState(false);
|
const [showOptimized, setShowOptimized] = useState(false);
|
||||||
@@ -707,6 +708,7 @@ const GeneratePage: React.FC = () => {
|
|||||||
const [showImageSettingsModal, setShowImageSettingsModal] = useState(false);
|
const [showImageSettingsModal, setShowImageSettingsModal] = useState(false);
|
||||||
|
|
||||||
const [creditRatios, setCreditRatios] = useState<any>([]);
|
const [creditRatios, setCreditRatios] = useState<any>([]);
|
||||||
|
const [creditCalculationData, setCreditCalculationData] = useState<any[]>([]);
|
||||||
const [cimage, setCimage] = useState<any>([]);
|
const [cimage, setCimage] = useState<any>([]);
|
||||||
|
|
||||||
const validateImageDimensions = (width: number, height: number): string | null => {
|
const validateImageDimensions = (width: number, height: number): string | null => {
|
||||||
@@ -851,6 +853,9 @@ const GeneratePage: React.FC = () => {
|
|||||||
// console.log('creditRatios (直接使用data.values):', data.values);
|
// console.log('creditRatios (直接使用data.values):', data.values);
|
||||||
|
|
||||||
})
|
})
|
||||||
|
calculateCredits().then((data: any) => {
|
||||||
|
setCreditCalculationData(data);
|
||||||
|
})
|
||||||
if (!showImageSettingsModal) return;
|
if (!showImageSettingsModal) return;
|
||||||
const handleClickOutside = (e: MouseEvent) => {
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
const target = e.target as HTMLElement;
|
const target = e.target as HTMLElement;
|
||||||
@@ -900,6 +905,134 @@ const GeneratePage: React.FC = () => {
|
|||||||
return Number(total.toFixed(2));
|
return Number(total.toFixed(2));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getEstimatedCredits = (): number => {
|
||||||
|
let config: any = {};
|
||||||
|
config = creditCalculationData.find((item: any) =>
|
||||||
|
item.modelConfigId === (currentRecord?.engineId || selectedEngineId) &&
|
||||||
|
item.genType === mediaType &&
|
||||||
|
item.resolution === (mediaType === 'video' ? (currentRecord?.resolution || videoResolution) : (currentRecord?.imageSize || selectedResolution))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
if (mediaType === 'video') {
|
||||||
|
config = {
|
||||||
|
perSecondCredits: 2,
|
||||||
|
baseCredits: 60,
|
||||||
|
ratio: 1.3,
|
||||||
|
inputVideoRatio: 1.3,
|
||||||
|
inputVideoBaseCredits: 0,
|
||||||
|
inputVideoPerSecondCredits: 15,
|
||||||
|
inputImageRatio: 1,
|
||||||
|
inputImageBaseCredits: 0,
|
||||||
|
inputImagePerImageCredits: 0.0,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
config = {
|
||||||
|
perSecondCredits: 0.1,
|
||||||
|
baseCredits: 2,
|
||||||
|
ratio: 3,
|
||||||
|
inputImageRatio: 1,
|
||||||
|
inputImageBaseCredits: 0,
|
||||||
|
inputImagePerImageCredits: 0.0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mediaType === 'video') {
|
||||||
|
let total = ((currentRecord?.duration || videoDuration) * config.perSecondCredits + config.baseCredits) * config.ratio;
|
||||||
|
if (currentRecord?.includeMediaReferences ?? includeMediaReferences) {
|
||||||
|
const usage = getReferenceUsage(currentRecord?.references || references);
|
||||||
|
if (usage.inputVideoDuration > 0) {
|
||||||
|
const inputVideoCost = ((config.inputVideoBaseCredits || 0) + (config.inputVideoPerSecondCredits || 0) * usage.inputVideoDuration) * (config.inputVideoRatio || 1);
|
||||||
|
total += inputVideoCost;
|
||||||
|
}
|
||||||
|
if (usage.inputImageCount > 0) {
|
||||||
|
const inputImageCost = ((config.inputImageBaseCredits || 0) + (config.inputImagePerImageCredits || 0) * usage.inputImageCount) * (config.inputImageRatio || 1);
|
||||||
|
total += inputImageCost;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Number(total.toFixed(2));
|
||||||
|
} else {
|
||||||
|
let total = config.baseCredits * config.ratio;
|
||||||
|
if (currentRecord?.includeMediaReferences ?? includeMediaReferences) {
|
||||||
|
const usage = getReferenceUsage(currentRecord?.references || references);
|
||||||
|
if (usage.inputImageCount > 0) {
|
||||||
|
const inputImageCost = ((config.inputImageBaseCredits || 0) + (config.inputImagePerImageCredits || 0) * usage.inputImageCount) * (config.inputImageRatio || 1);
|
||||||
|
total += inputImageCost;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Number(total.toFixed(2));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateRecordCredits = (record: any): number => {
|
||||||
|
const genType = record.genType || record.gen_type || 'video';
|
||||||
|
const engineId = record.engineId || record.engine_id;
|
||||||
|
const resolution = record.resolution;
|
||||||
|
const duration = Number(record.duration || 0);
|
||||||
|
const imageSize = record.imageSize || record.image_size;
|
||||||
|
const includeMediaRefs = record.includeMediaReferences ?? record.include_media_references ?? false;
|
||||||
|
const references = record.references || [];
|
||||||
|
|
||||||
|
let config: any = creditCalculationData.find((item: any) =>
|
||||||
|
item.modelConfigId === engineId &&
|
||||||
|
item.genType === genType &&
|
||||||
|
item.resolution === (genType === 'video' ? resolution : imageSize)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
if (genType === 'video') {
|
||||||
|
config = {
|
||||||
|
perSecondCredits: 2,
|
||||||
|
baseCredits: 60,
|
||||||
|
ratio: 1.3,
|
||||||
|
inputVideoRatio: 1.3,
|
||||||
|
inputVideoBaseCredits: 0,
|
||||||
|
inputVideoPerSecondCredits: 15,
|
||||||
|
inputImageRatio: 1,
|
||||||
|
inputImageBaseCredits: 0,
|
||||||
|
inputImagePerImageCredits: 0.0,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
config = {
|
||||||
|
perSecondCredits: 0.1,
|
||||||
|
baseCredits: 2,
|
||||||
|
ratio: 3,
|
||||||
|
inputImageRatio: 1,
|
||||||
|
inputImageBaseCredits: 0,
|
||||||
|
inputImagePerImageCredits: 0.0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let total = 0;
|
||||||
|
if (genType === 'video') {
|
||||||
|
total = (duration * config.perSecondCredits + config.baseCredits) * config.ratio;
|
||||||
|
if (includeMediaRefs) {
|
||||||
|
const usage = getReferenceUsage(references);
|
||||||
|
if (usage.inputVideoDuration > 0) {
|
||||||
|
const inputVideoCost = ((config.inputVideoBaseCredits || 0) + (config.inputVideoPerSecondCredits || 0) * usage.inputVideoDuration) * (config.inputVideoRatio || 1);
|
||||||
|
total += inputVideoCost;
|
||||||
|
}
|
||||||
|
if (usage.inputImageCount > 0) {
|
||||||
|
const inputImageCost = ((config.inputImageBaseCredits || 0) + (config.inputImagePerImageCredits || 0) * usage.inputImageCount) * (config.inputImageRatio || 1);
|
||||||
|
total += inputImageCost;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
total = config.baseCredits * config.ratio;
|
||||||
|
if (includeMediaRefs) {
|
||||||
|
const usage = getReferenceUsage(references);
|
||||||
|
if (usage.inputImageCount > 0) {
|
||||||
|
const inputImageCost = ((config.inputImageBaseCredits || 0) + (config.inputImagePerImageCredits || 0) * usage.inputImageCount) * (config.inputImageRatio || 1);
|
||||||
|
total += inputImageCost;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Number(total.toFixed(2));
|
||||||
|
};
|
||||||
|
|
||||||
// 根据图片分辨率获取积分
|
// 根据图片分辨率获取积分
|
||||||
const getImageCredits = (imageSize: string): any => {
|
const getImageCredits = (imageSize: string): any => {
|
||||||
|
|
||||||
@@ -1378,20 +1511,7 @@ const GeneratePage: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Media credits estimate for step 2 (video or image)
|
// Media credits estimate for step 2 (video or image)
|
||||||
const estimatedVideoCredits = mediaType === "image"
|
const estimatedVideoCredits = getEstimatedCredits();
|
||||||
? getImageCreditsFromCimage(
|
|
||||||
currentRecord?.imageSize || selectedResolution,
|
|
||||||
currentRecord?.engineId || selectedEngineId,
|
|
||||||
currentRecord?.includeMediaReferences ?? includeMediaReferences,
|
|
||||||
currentRecord?.references || references,
|
|
||||||
)
|
|
||||||
: calcVideoCredits(
|
|
||||||
currentRecord?.duration || videoDuration,
|
|
||||||
(currentRecord?.resolution || videoResolution) as Resolution,
|
|
||||||
currentRecord?.engineId || selectedEngineId,
|
|
||||||
currentRecord?.includeMediaReferences ?? includeMediaReferences,
|
|
||||||
currentRecord?.references || references,
|
|
||||||
);
|
|
||||||
const canAffordVideo = userCredits >= estimatedVideoCredits;
|
const canAffordVideo = userCredits >= estimatedVideoCredits;
|
||||||
|
|
||||||
// Step 1: Optimize prompt (text credits)
|
// Step 1: Optimize prompt (text credits)
|
||||||
@@ -2430,18 +2550,31 @@ const GeneratePage: React.FC = () => {
|
|||||||
minHeight: 48,
|
minHeight: 48,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 媒体类型为视频(mediaType=2)时显示时长选择器和行业选项 */}
|
|
||||||
{mediaType !== "image" && (
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: 6,
|
gap: 10,
|
||||||
flex: 1,
|
|
||||||
flexWrap: "wrap",
|
flexWrap: "wrap",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Duration selector */}
|
<PortalDropdown
|
||||||
|
label="引擎"
|
||||||
|
value={(mediaType === "image" ? imageEngines : videoEngines).find((item: any) => item.id === selectedEngineId)?.name || "选择引擎"}
|
||||||
|
options={(mediaType === "image" ? imageEngines : videoEngines).map((item: any) => item.name)}
|
||||||
|
expanded={expandedEngine === "engine"}
|
||||||
|
onToggle={() => setExpandedEngine(expandedEngine === "engine" ? null : "engine")}
|
||||||
|
onSelect={(value) => {
|
||||||
|
const engine = (mediaType === "image" ? imageEngines : videoEngines).find((item: any) => item.name === value);
|
||||||
|
if (engine) {
|
||||||
|
handleGenerationEngineChange(engine.id);
|
||||||
|
}
|
||||||
|
setExpandedEngine(null);
|
||||||
|
}}
|
||||||
|
onClose={() => setExpandedEngine(null)}
|
||||||
|
/>
|
||||||
|
{mediaType !== "image" && (
|
||||||
|
<>
|
||||||
<PortalDropdown
|
<PortalDropdown
|
||||||
label="时长"
|
label="时长"
|
||||||
value={`${videoDuration}`}
|
value={`${videoDuration}`}
|
||||||
@@ -2452,6 +2585,24 @@ const GeneratePage: React.FC = () => {
|
|||||||
onSelect={handleDurationSelect}
|
onSelect={handleDurationSelect}
|
||||||
onClose={handleDurationClose}
|
onClose={handleDurationClose}
|
||||||
/>
|
/>
|
||||||
|
<PortalDropdown
|
||||||
|
label="比例"
|
||||||
|
value={videoAspectRatio as string}
|
||||||
|
options={engineOptions.ratios}
|
||||||
|
expanded={expandedEngine === "aspectRatio"}
|
||||||
|
onToggle={() => setExpandedEngine(expandedEngine === "aspectRatio" ? null : "aspectRatio")}
|
||||||
|
onSelect={(value) => { setVideoAspectRatio(value as AspectRatio); setExpandedEngine(null); }}
|
||||||
|
onClose={() => setExpandedEngine(null)}
|
||||||
|
/>
|
||||||
|
<PortalDropdown
|
||||||
|
label="分辨率"
|
||||||
|
value={videoResolution as string}
|
||||||
|
options={engineOptions.resolutions}
|
||||||
|
expanded={expandedEngine === "resolution"}
|
||||||
|
onToggle={() => setExpandedEngine(expandedEngine === "resolution" ? null : "resolution")}
|
||||||
|
onSelect={(value) => { setVideoResolution(value as Resolution); setExpandedEngine(null); }}
|
||||||
|
onClose={() => setExpandedEngine(null)}
|
||||||
|
/>
|
||||||
{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];
|
||||||
@@ -2468,7 +2619,7 @@ const GeneratePage: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</>
|
||||||
)}
|
)}
|
||||||
{mediaType == "image" && (
|
{mediaType == "image" && (
|
||||||
<div
|
<div
|
||||||
@@ -2480,50 +2631,38 @@ const GeneratePage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
className="image-settings-trigger"
|
className="image-settings-trigger"
|
||||||
style={{
|
style={{
|
||||||
minWidth: 220,
|
minWidth: 200,
|
||||||
padding: "0px 16px",
|
padding: "0px 14px",
|
||||||
height: 30,
|
height: 32,
|
||||||
borderRadius: 10,
|
borderRadius: 8,
|
||||||
border: "1px solid #e2e8f0",
|
border: "1px solid #e2e8f0",
|
||||||
backgroundColor: "#f8f9fc",
|
backgroundColor: "#fff",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
gap: 12,
|
gap: 10,
|
||||||
transition: "all 0.2s",
|
transition: "all 0.2s",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 10,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* 比例图标 */}
|
|
||||||
|
|
||||||
<div style={{ textAlign: "left" }}>
|
<div style={{ textAlign: "left" }}>
|
||||||
<Typography.Text
|
<Typography.Text
|
||||||
style={{
|
style={{
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: 600,
|
fontWeight: 500,
|
||||||
color: "#374151",
|
color: "#000000ff",
|
||||||
marginRight: 10,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{selectedRatio === "auto" ? "智能" : selectedRatio}
|
{selectedRatio === "auto" ? "智能" : selectedRatio}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<Typography.Text
|
<Typography.Text
|
||||||
style={{ fontSize: 12, color: "#9ca3af" }}
|
style={{ fontSize: 11, color: "#000000ff" }}
|
||||||
>
|
>
|
||||||
{selectedResolution === "1K" ? "标清 1K" : selectedResolution === "2K" ? "高清 2K" : `${selectedResolution}分辨率`}
|
{selectedResolution === "1K" ? "标清" : selectedResolution === "2K" ? "高清" : `${selectedResolution}`} | {width}×{height}
|
||||||
| {width}×{height}
|
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<CaretDownOutlined
|
<CaretDownOutlined
|
||||||
style={{ fontSize: 14, color: "#9ca3af" }}
|
style={{ fontSize: 14, color: "#000000ff" }}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -2536,20 +2675,19 @@ const GeneratePage: React.FC = () => {
|
|||||||
left: 0,
|
left: 0,
|
||||||
width: 520,
|
width: 520,
|
||||||
backgroundColor: "#fff",
|
backgroundColor: "#fff",
|
||||||
borderRadius: 16,
|
borderRadius: 12,
|
||||||
boxShadow: "0 10px 40px rgba(0,0,0,0.15)",
|
boxShadow: "0 8px 32px rgba(0,0,0,0.12)",
|
||||||
padding: 20,
|
padding: 16,
|
||||||
border: "none",
|
border: "1px solid rgba(231, 234, 240, 0.8)",
|
||||||
zIndex: 9999,
|
zIndex: 9999,
|
||||||
}}
|
}}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{/* 选择比例 */}
|
<div style={{ marginBottom: 16 }}>
|
||||||
<div style={{ marginBottom: 20 }}>
|
|
||||||
<Typography.Text
|
<Typography.Text
|
||||||
style={{
|
style={{
|
||||||
display: "block",
|
display: "block",
|
||||||
marginBottom: 10,
|
marginBottom: 8,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
color: "#666666",
|
color: "#666666",
|
||||||
@@ -2574,8 +2712,8 @@ const GeneratePage: React.FC = () => {
|
|||||||
style={{
|
style={{
|
||||||
flex: "0 0 calc(11.11% - 5px)",
|
flex: "0 0 calc(11.11% - 5px)",
|
||||||
minWidth: 48,
|
minWidth: 48,
|
||||||
height: 56,
|
height: 52,
|
||||||
borderRadius: 8,
|
borderRadius: 6,
|
||||||
border:
|
border:
|
||||||
selectedRatio === item.value
|
selectedRatio === item.value
|
||||||
? "2px solid #6366f1"
|
? "2px solid #6366f1"
|
||||||
@@ -2614,7 +2752,6 @@ const GeneratePage: React.FC = () => {
|
|||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
||||||
{item.value === "auto" && (
|
{item.value === "auto" && (
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
@@ -2647,12 +2784,11 @@ const GeneratePage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 选择分辨率 */}
|
<div style={{ marginBottom: 16 }}>
|
||||||
<div style={{ marginBottom: 20 }}>
|
|
||||||
<Typography.Text
|
<Typography.Text
|
||||||
style={{
|
style={{
|
||||||
display: "block",
|
display: "block",
|
||||||
marginBottom: 10,
|
marginBottom: 8,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
color: "#666666",
|
color: "#666666",
|
||||||
@@ -2660,7 +2796,7 @@ const GeneratePage: React.FC = () => {
|
|||||||
>
|
>
|
||||||
选择分辨率
|
选择分辨率
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<div style={{ display: "flex", gap: 8 }}>
|
<div style={{ display: "flex", gap: 6 }}>
|
||||||
{resolutionOptions.map((item) => (
|
{resolutionOptions.map((item) => (
|
||||||
<button
|
<button
|
||||||
key={item.value}
|
key={item.value}
|
||||||
@@ -2670,8 +2806,8 @@ const GeneratePage: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
height: 48,
|
height: 44,
|
||||||
borderRadius: 8,
|
borderRadius: 6,
|
||||||
border:
|
border:
|
||||||
selectedResolution === item.value
|
selectedResolution === item.value
|
||||||
? "2px solid #6366f1"
|
? "2px solid #6366f1"
|
||||||
@@ -2689,7 +2825,7 @@ const GeneratePage: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
fontSize: 13,
|
fontSize: 12,
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
color:
|
color:
|
||||||
selectedResolution === item.value
|
selectedResolution === item.value
|
||||||
@@ -2707,12 +2843,11 @@ const GeneratePage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 尺寸 */}
|
|
||||||
<div>
|
<div>
|
||||||
<Typography.Text
|
<Typography.Text
|
||||||
style={{
|
style={{
|
||||||
display: "block",
|
display: "block",
|
||||||
marginBottom: 10,
|
marginBottom: 8,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
color: "#666666",
|
color: "#666666",
|
||||||
@@ -2724,7 +2859,7 @@ const GeneratePage: React.FC = () => {
|
|||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: 8,
|
gap: 6,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -2753,10 +2888,10 @@ const GeneratePage: React.FC = () => {
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
height: 40,
|
height: 36,
|
||||||
border: "1px solid #e5e7eb",
|
border: "1px solid #e5e7eb",
|
||||||
backgroundColor: "#f9fafb",
|
backgroundColor: "#f9fafb",
|
||||||
fontSize: 14,
|
fontSize: 13,
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
color: "#1f2937",
|
color: "#1f2937",
|
||||||
}}
|
}}
|
||||||
@@ -2766,8 +2901,8 @@ const GeneratePage: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
onClick={handleSwap}
|
onClick={handleSwap}
|
||||||
style={{
|
style={{
|
||||||
width: 28,
|
width: 26,
|
||||||
height: 28,
|
height: 26,
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
border: "1px solid #e5e7eb",
|
border: "1px solid #e5e7eb",
|
||||||
backgroundColor: "#fff",
|
backgroundColor: "#fff",
|
||||||
@@ -2807,10 +2942,10 @@ const GeneratePage: React.FC = () => {
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
height: 40,
|
height: 36,
|
||||||
border: "1px solid #e5e7eb",
|
border: "1px solid #e5e7eb",
|
||||||
backgroundColor: "#f9fafb",
|
backgroundColor: "#f9fafb",
|
||||||
fontSize: 14,
|
fontSize: 13,
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
color: "#1f2937",
|
color: "#1f2937",
|
||||||
}}
|
}}
|
||||||
@@ -2826,32 +2961,10 @@ const GeneratePage: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: 10,
|
|
||||||
flexWrap: "wrap",
|
|
||||||
marginRight: 16,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
value={selectedEngineId || undefined}
|
|
||||||
onChange={handleGenerationEngineChange}
|
|
||||||
placeholder="选择生成引擎"
|
|
||||||
style={{ minWidth: 180 }}
|
|
||||||
options={(mediaType === "image" ? imageEngines : videoEngines).map((item: any) => ({ value: item.id, label: item.name }))}
|
|
||||||
/>
|
|
||||||
{mediaType === "video" && (
|
|
||||||
<>
|
|
||||||
<Select value={videoAspectRatio} onChange={(value) => setVideoAspectRatio(value as AspectRatio)} style={{ width: 110 }} options={engineOptions.ratios.map((value) => ({ value, label: value }))} />
|
|
||||||
<Select value={videoResolution} onChange={(value) => setVideoResolution(value as Resolution)} style={{ width: 110 }} options={engineOptions.resolutions.map((value) => ({ value, label: value }))} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{references.length > 0 && (
|
{references.length > 0 && (
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
||||||
<Switch size="small" checked={includeMediaReferences} onChange={setIncludeMediaReferences} />
|
<Switch size="small" checked={includeMediaReferences} onChange={setIncludeMediaReferences} />
|
||||||
<Typography.Text style={{ fontSize: 12 }}>生成时携带附件</Typography.Text>
|
<Typography.Text style={{ fontSize: 12, color: "#64748b" }}>生成时携带附件</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -2869,11 +2982,22 @@ const GeneratePage: React.FC = () => {
|
|||||||
(请在左侧选择图片比例和尺寸)
|
(请在左侧选择图片比例和尺寸)
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
<Tooltip
|
||||||
|
title={((user?.credits || 0) < optimizeHoldCredits) ? `积分不足${optimizeHoldCredits},请充值积分` : ''}
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
size="large"
|
size="large"
|
||||||
onClick={handleOptimize}
|
onClick={() => {
|
||||||
|
if ((user?.credits || 0) < optimizeHoldCredits) {
|
||||||
|
message.warning(`积分不足${optimizeHoldCredits},请充值积分`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleOptimize();
|
||||||
|
}}
|
||||||
loading={optimizing}
|
loading={optimizing}
|
||||||
|
disabled={(user?.credits || 0) < optimizeHoldCredits}
|
||||||
style={{
|
style={{
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
@@ -2887,6 +3011,7 @@ const GeneratePage: React.FC = () => {
|
|||||||
>
|
>
|
||||||
AI优化提示词
|
AI优化提示词
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -3343,6 +3468,8 @@ const GeneratePage: React.FC = () => {
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
gap: 12,
|
||||||
marginTop: 20,
|
marginTop: 20,
|
||||||
padding: "14px 18px",
|
padding: "14px 18px",
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
@@ -3350,53 +3477,37 @@ const GeneratePage: React.FC = () => {
|
|||||||
border: "1px solid #f0f0f5",
|
border: "1px solid #f0f0f5",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ minWidth: 220, marginRight: 16 }}>
|
<div
|
||||||
<Typography.Text style={{ fontSize: 12, color: '#64748b', display: 'block', marginBottom: 6 }}>生成引擎</Typography.Text>
|
style={{
|
||||||
<Select
|
display: "flex",
|
||||||
value={currentRecord.engineId || undefined}
|
alignItems: "center",
|
||||||
disabled
|
gap: 6,
|
||||||
style={{ width: '100%' }}
|
padding: "5px 12px",
|
||||||
options={(mediaType === 'image' ? imageEngines : videoEngines).map((item: any) => ({ value: item.id, label: item.name }))}
|
borderRadius: 8,
|
||||||
/>
|
background: "#f8f9fc",
|
||||||
{currentRecord?.references?.length ? (
|
border: "1px solid #e2e8f0",
|
||||||
<div style={{ marginTop: 10, display: 'flex', alignItems: 'center', gap: 8 }}>
|
}}
|
||||||
<Switch size="small" checked={Boolean(currentRecord.includeMediaReferences)} disabled />
|
|
||||||
<Typography.Text style={{ fontSize: 12, color: '#64748b' }}>提词阶段已冻结附件配置</Typography.Text>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{currentRecord.configComplete === false && (
|
|
||||||
<Typography.Text
|
|
||||||
type={currentRecord.canGenerate === false ? "danger" : "warning"}
|
|
||||||
style={{ fontSize: 12, display: "block", marginTop: 8 }}
|
|
||||||
>
|
>
|
||||||
{currentRecord.configFallbackHint || (currentRecord.canGenerate === false
|
<Typography.Text style={{ fontSize: 12, color: "#94a3b8" }}>
|
||||||
? "旧版本配置不完整,请重新生成提词"
|
引擎
|
||||||
: "旧版本配置缺失,提交生成时将由后端自动补齐")}
|
</Typography.Text>
|
||||||
|
<Typography.Text
|
||||||
|
strong
|
||||||
|
style={{ fontSize: 13, color: "#6366f1" }}
|
||||||
|
>
|
||||||
|
{(mediaType === 'image' ? imageEngines : videoEngines).find((item: any) => item.id === currentRecord.engineId)?.name || '-'}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Video params selection */}
|
{/* Video params selection */}
|
||||||
{mediaType !== "image" && (
|
{mediaType !== "image" && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
marginTop: 24,
|
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
background: "#f8f9fc",
|
background: "#f8f9fc",
|
||||||
width: "25%",
|
padding: "8px 12px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography.Text
|
|
||||||
style={{
|
|
||||||
fontSize: 13,
|
|
||||||
color: "#1a1a2e",
|
|
||||||
fontWeight: 600,
|
|
||||||
display: "block",
|
|
||||||
marginBottom: 12,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
已冻结视频参数
|
|
||||||
</Typography.Text>
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -3521,54 +3632,102 @@ const GeneratePage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
|
<div
|
||||||
<div>
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
padding: "6px 12px",
|
||||||
|
borderRadius: 10,
|
||||||
|
background: "rgba(255, 255, 255, 0.95)",
|
||||||
|
border: "1px solid rgba(231, 234, 240, 0.95)",
|
||||||
|
boxShadow: "0 2px 8px rgba(47, 52, 64, 0.03)",
|
||||||
|
flexShrink: 1,
|
||||||
|
minWidth: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 4,
|
||||||
|
padding: "3px 8px",
|
||||||
|
borderRadius: 6,
|
||||||
|
background: "rgba(99, 102, 241, 0.08)",
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Typography.Text
|
<Typography.Text
|
||||||
style={{ fontSize: 11, color: "#94a3b8", display: "block" }}
|
style={{ fontSize: 12, color: "#6366f1", fontWeight: 500 }}
|
||||||
>
|
>
|
||||||
文字积分
|
文字积分
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<Typography.Text
|
<Typography.Text
|
||||||
strong
|
strong
|
||||||
style={{ fontSize: 14, color: "#6366f1" }}
|
style={{ fontSize: 13, color: "#6366f1", fontWeight: 700 }}
|
||||||
>
|
>
|
||||||
{lastTextCredits}
|
{lastTextCredits}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
<Typography.Text style={{ color: "#cbd5e1", fontSize: 18 }}>
|
<Typography.Text
|
||||||
|
style={{ color: "#94a3b8", fontSize: 12, fontWeight: 500, flexShrink: 0 }}
|
||||||
|
>
|
||||||
+
|
+
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<div>
|
<div
|
||||||
<Typography.Text
|
style={{
|
||||||
style={{ fontSize: 11, color: "#94a3b8", display: "block" }}
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 4,
|
||||||
|
padding: "3px 8px",
|
||||||
|
borderRadius: 6,
|
||||||
|
background: "rgba(16, 185, 129, 0.08)",
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
预估{mediaType === "image" ? "图片" : "视频"}积分
|
<Typography.Text
|
||||||
|
style={{ fontSize: 12, color: "#10b981", fontWeight: 500 }}
|
||||||
|
>
|
||||||
|
预估{mediaType === "image" ? "图" : "视"}积分
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<Typography.Text
|
<Typography.Text
|
||||||
strong
|
strong
|
||||||
style={{ fontSize: 14, color: "#10b981" }}
|
style={{ fontSize: 13, color: "#10b981", fontWeight: 700 }}
|
||||||
>
|
>
|
||||||
{estimatedVideoCredits}
|
{getEstimatedCredits()}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
<Typography.Text style={{ color: "#cbd5e1", fontSize: 18 }}>
|
<Typography.Text
|
||||||
|
style={{ color: "#94a3b8", fontSize: 12, fontWeight: 500, flexShrink: 0 }}
|
||||||
|
>
|
||||||
=
|
=
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<div>
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 4,
|
||||||
|
padding: "4px 10px",
|
||||||
|
borderRadius: 6,
|
||||||
|
background: "linear-gradient(135deg, rgba(99, 102, 241, 0.12) 0%, rgba(139, 92, 246, 0.12) 100%)",
|
||||||
|
border: "1px solid rgba(99, 102, 241, 0.2)",
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Typography.Text
|
<Typography.Text
|
||||||
style={{ fontSize: 11, color: "#94a3b8", display: "block" }}
|
style={{ fontSize: 12, color: "#6366f1", fontWeight: 600 }}
|
||||||
>
|
>
|
||||||
总计
|
总计
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<Typography.Text
|
<Typography.Text
|
||||||
strong
|
strong
|
||||||
style={{ fontSize: 16, color: "#1a1a2e" }}
|
style={{ fontSize: 14, color: "#6366f1", fontWeight: 800 }}
|
||||||
>
|
>
|
||||||
{(lastTextCredits + estimatedVideoCredits).toFixed(2)}
|
{(lastTextCredits + getEstimatedCredits()).toFixed(2)}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: "flex", gap: 12 }}>
|
<div style={{ display: "flex", gap: 12, marginLeft: "auto" }}>
|
||||||
<Button
|
<Button
|
||||||
size="large"
|
size="large"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -3625,20 +3784,7 @@ const GeneratePage: React.FC = () => {
|
|||||||
? "生成完成"
|
? "生成完成"
|
||||||
: recordStates[currentRecord.id] === "generating"
|
: recordStates[currentRecord.id] === "generating"
|
||||||
? "生成中..."
|
? "生成中..."
|
||||||
: `生成${mediaType === "image" ? "图片" : "视频"} (${(mediaType === "image"
|
: `生成${mediaType === "image" ? "图片" : "视频"} (${getEstimatedCredits()}积分)`}
|
||||||
? getImageCreditsFromCimage(
|
|
||||||
currentRecord.imageSize || selectedResolution,
|
|
||||||
currentRecord.engineId,
|
|
||||||
currentRecord.includeMediaReferences,
|
|
||||||
currentRecord.references,
|
|
||||||
)
|
|
||||||
: calcVideoCredits(
|
|
||||||
currentRecord.duration || videoDuration,
|
|
||||||
(currentRecord.resolution || videoResolution) as Resolution,
|
|
||||||
currentRecord.engineId,
|
|
||||||
currentRecord.includeMediaReferences,
|
|
||||||
currentRecord.references,
|
|
||||||
))}积分)`}
|
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -4088,38 +4234,30 @@ const GeneratePage: React.FC = () => {
|
|||||||
{type === "image" ? (
|
{type === "image" ? (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
|
||||||
gap: 14,
|
|
||||||
padding: "10px 14px",
|
padding: "10px 14px",
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
background: "#f8f9fc",
|
background: "#f8f9fc",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{[
|
{(() => {
|
||||||
{
|
const items = [
|
||||||
label: "文字积分",
|
{ label: "引擎", value: `${record.engineName}` },
|
||||||
value: `${record.textCreditsCost || 0}`,
|
{ label: "文字积分", value: `${record.textCreditsCost || 0}` },
|
||||||
},
|
{ label: "图片积分", value: `${record.creditsCost || 0}` },
|
||||||
{
|
{ label: "分辨率", value: record.imageSize || "-" },
|
||||||
label: "图片积分",
|
{ label: "比例", value: record.imageProportion || "-" },
|
||||||
value: `${record.creditsCost || 0}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "分辨率",
|
|
||||||
value: record.imageSize
|
|
||||||
? `${record.imageSize}`
|
|
||||||
: "-",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "比例",
|
|
||||||
value: record.imageProportion || "-",
|
|
||||||
},
|
|
||||||
{ label: "尺寸", value: record.imagePx || "-" },
|
{ label: "尺寸", value: record.imagePx || "-" },
|
||||||
].map((item, j) => (
|
{ label: "附件", value: record.includeMediaReferences ? "生成时携带" : "仅用于提词或不携带" },
|
||||||
<div key={j} style={{ flex: 1 }}>
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div style={{ width: "100%", display: "flex", justifyContent: "flex-start",flexWrap: "wrap" }}>
|
||||||
|
{items.map((item, j) => (
|
||||||
|
<div key={j} style={{width: "21%", textAlign: "left",marginBottom: 10,}}>
|
||||||
<Typography.Text
|
<Typography.Text
|
||||||
style={{
|
style={{
|
||||||
fontSize: 11,
|
fontSize: 12,
|
||||||
color: "#94a3b8",
|
color: "#94a3b8",
|
||||||
display: "block",
|
display: "block",
|
||||||
}}
|
}}
|
||||||
@@ -4128,51 +4266,44 @@ const GeneratePage: React.FC = () => {
|
|||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<Typography.Text
|
<Typography.Text
|
||||||
strong
|
strong
|
||||||
style={{ fontSize: 13, color: "#1a1a2e" }}
|
style={{ fontSize: 12, color: "#1a1a2e" }}
|
||||||
>
|
>
|
||||||
{item.value}
|
{item.value}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
|
||||||
gap: 14,
|
|
||||||
padding: "10px 14px",
|
padding: "10px 14px",
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
background: "#f8f9fc",
|
background: "#f8f9fc",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{[
|
{(() => {
|
||||||
{
|
const items = [
|
||||||
label: "文字积分",
|
{ label: "引擎", value: `${record.engineName}` },
|
||||||
value: `${record.textCreditsCost || 0}`,
|
{ label: "文字积分", value: `${record.textCreditsCost || 0}` },
|
||||||
},
|
{ label: "视频积分", value: `${record.creditsCost || 0}` },
|
||||||
{
|
{ label: "时长", value: record.duration ? `${record.duration}秒` : "-" },
|
||||||
label: "视频积分",
|
{ label: "比例", value: record.aspectRatio || "-" },
|
||||||
value: `${record.creditsCost || 0}`,
|
{ label: "分辨率", value: record.resolution || "-" },
|
||||||
},
|
{ label: "附件", value: record.includeMediaReferences ? "生成时携带" : "仅用于提词或不携带" },
|
||||||
{
|
];
|
||||||
label: "时长",
|
return (
|
||||||
value: record.duration
|
<div>
|
||||||
? `${record.duration}秒`
|
<div style={{ width: "100%", display: "flex", justifyContent: "flex-start",flexWrap: "wrap" }}>
|
||||||
: "-",
|
{items.map((item, j) => (
|
||||||
},
|
<div key={j} style={{width: "21%", textAlign: "left",marginBottom: 10,marginRight: 10}}>
|
||||||
{
|
|
||||||
label: "比例",
|
|
||||||
value: record.aspectRatio || "-",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "分辨率",
|
|
||||||
value: record.resolution || "-",
|
|
||||||
},
|
|
||||||
].map((item, j) => (
|
|
||||||
<div key={j} style={{ flex: 1 }}>
|
|
||||||
<Typography.Text
|
<Typography.Text
|
||||||
style={{
|
style={{
|
||||||
fontSize: 11,
|
fontSize: 12,
|
||||||
color: "#94a3b8",
|
color: "#94a3b8",
|
||||||
display: "block",
|
display: "block",
|
||||||
}}
|
}}
|
||||||
@@ -4181,13 +4312,17 @@ const GeneratePage: React.FC = () => {
|
|||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<Typography.Text
|
<Typography.Text
|
||||||
strong
|
strong
|
||||||
style={{ fontSize: 13, color: "#1a1a2e" }}
|
style={{ fontSize: 12, color: "#1a1a2e" }}
|
||||||
>
|
>
|
||||||
{item.value}
|
{item.value}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Error message for failed records */}
|
{/* Error message for failed records */}
|
||||||
@@ -4471,40 +4606,49 @@ const GeneratePage: React.FC = () => {
|
|||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div>
|
<div
|
||||||
<Typography.Text strong style={{ display: "block", marginBottom: 10 }}>冻结生成配置</Typography.Text>
|
style={{
|
||||||
<Typography.Text style={{ display: "block", fontSize: 12, color: "#64748b" }}>引擎:{record.engineName || record.engineId || "配置缺失"}</Typography.Text>
|
height: "100%",
|
||||||
{type === "video" ? (
|
minHeight: 180,
|
||||||
<>
|
borderRadius: 12,
|
||||||
<Typography.Text style={{ display: "block", fontSize: 12, color: "#64748b" }}>时长:{record.duration || "-"}秒</Typography.Text>
|
background:
|
||||||
<Typography.Text style={{ display: "block", fontSize: 12, color: "#64748b" }}>比例:{record.aspectRatio || "-"}</Typography.Text>
|
"linear-gradient(135deg, rgba(99,102,241,0.04), rgba(139,92,246,0.04))",
|
||||||
<Typography.Text style={{ display: "block", fontSize: 12, color: "#64748b" }}>分辨率:{record.resolution || "-"}</Typography.Text>
|
border: "1px dashed rgba(99,102,241,0.2)",
|
||||||
</>
|
display: "flex",
|
||||||
) : (
|
flexDirection: "column",
|
||||||
<>
|
alignItems: "center",
|
||||||
<Typography.Text style={{ display: "block", fontSize: 12, color: "#64748b" }}>分辨率:{record.imageSize || "-"}</Typography.Text>
|
justifyContent: "center",
|
||||||
<Typography.Text style={{ display: "block", fontSize: 12, color: "#64748b" }}>比例:{record.imageProportion || "-"}</Typography.Text>
|
gap: 10,
|
||||||
<Typography.Text style={{ display: "block", fontSize: 12, color: "#64748b" }}>尺寸:{record.imagePx || "-"}</Typography.Text>
|
}}
|
||||||
</>
|
>
|
||||||
)}
|
<Typography.Text
|
||||||
<Typography.Text style={{ display: "block", fontSize: 12, color: "#64748b" }}>附件:{record.includeMediaReferences ? "生成时携带" : "仅用于提词或不携带"}</Typography.Text>
|
style={{ color: "#94a3b8", fontSize: 13 }}
|
||||||
{record.configComplete === false && (
|
>
|
||||||
<Typography.Text type={record.canGenerate === false ? "danger" : "warning"} style={{ display: "block", marginTop: 8 }}>
|
{type === "video" ? "视频" : "图片"}等待生成
|
||||||
{record.configFallbackHint || (record.canGenerate === false ? "旧版本配置不完整,请重新生成提词" : "旧版本配置缺失,提交生成时将由后端自动补齐")}
|
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
<Tooltip
|
||||||
|
title={((user?.credits || 0) < calculateRecordCredits(record)) ? `积分不足${calculateRecordCredits(record)},请充值积分` : ''}
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
size="large"
|
size="large"
|
||||||
icon={<RocketOutlined />}
|
icon={<RocketOutlined />}
|
||||||
loading={generating[record.id]}
|
loading={generating[record.id]}
|
||||||
disabled={record.canGenerate === false}
|
disabled={record.canGenerate === false || ((user?.credits || 0) < calculateRecordCredits(record))}
|
||||||
onClick={() => handleGenerate(record.id)}
|
onClick={() => {
|
||||||
|
if ((user?.credits || 0) < calculateRecordCredits(record)) {
|
||||||
|
message.warning(`积分不足${calculateRecordCredits(record)},请充值积分`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleGenerate(record.id);
|
||||||
|
}}
|
||||||
style={{ borderRadius: 12, fontWeight: 600, height: 48 }}
|
style={{ borderRadius: 12, fontWeight: 600, height: 48 }}
|
||||||
>
|
>
|
||||||
生成{type === "video" ? "视频" : "图片"}
|
生成{type === "video" ? "视频" : "图片"} ({calculateRecordCredits(record)}积分)
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{statusState.isFailure && (
|
{statusState.isFailure && (
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ function InitialInfo() {
|
|||||||
const { creatID } = useParams<{ creatID: string }>();
|
const { creatID } = useParams<{ creatID: string }>();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const flowVersion: 'v1' | 'v2' = searchParams.get('flow_version') === 'v2' ? 'v2' : 'v1';
|
const flowVersion: 'v1' | 'v2' = searchParams.get('flow_version') === 'v2' ? 'v2' : 'v1';
|
||||||
const { user } = useAuthStore();
|
const { user, optimizeHoldCredits } = useAuthStore();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const [modalVisible, setModalVisible] = useState(false);
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
const [promptText, setPromptText] = useState('');
|
const [promptText, setPromptText] = useState('');
|
||||||
@@ -91,7 +93,7 @@ function InitialInfo() {
|
|||||||
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(() => { });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -168,6 +170,7 @@ function InitialInfo() {
|
|||||||
status: apiSteps[index]?.status || '',
|
status: apiSteps[index]?.status || '',
|
||||||
id: apiSteps[index]?.id || index,
|
id: apiSteps[index]?.id || index,
|
||||||
output: apiSteps[index]?.output || '',
|
output: apiSteps[index]?.output || '',
|
||||||
|
input: apiSteps[index]?.input || {},
|
||||||
engineId: apiSteps[index]?.input?.payload?.videoConfig?.engineId || '',
|
engineId: apiSteps[index]?.input?.payload?.videoConfig?.engineId || '',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -269,7 +272,7 @@ function InitialInfo() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
calculateCredits().then((data: any) => {
|
calculateCredits().then((data: any) => {
|
||||||
setCreditCalculationData(data);
|
setCreditCalculationData(data);
|
||||||
}).catch(() => {});
|
}).catch(() => { });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const calculateEstimatedCredits = () => {
|
const calculateEstimatedCredits = () => {
|
||||||
@@ -320,6 +323,56 @@ function InitialInfo() {
|
|||||||
setEstimatedCredits(Number(total.toFixed(2)));
|
setEstimatedCredits(Number(total.toFixed(2)));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const calculateCreditsFromVideoConfig = () => {
|
||||||
|
const videoConfig = steps[1]?.input?.payload?.videoConfig || steps[1]?.input?.payload?.video_config;
|
||||||
|
if (!videoConfig) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const engineId = videoConfig?.engineId || videoConfig?.engine_id;
|
||||||
|
const resolution = videoConfig?.resolution;
|
||||||
|
const duration = videoConfig?.duration || videoDuration;
|
||||||
|
|
||||||
|
let config: any = {};
|
||||||
|
config = creditCalculationData.find((item: any) =>
|
||||||
|
item.modelConfigId === engineId &&
|
||||||
|
item.genType === 'video' &&
|
||||||
|
item.resolution === resolution
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
config = {
|
||||||
|
perSecondCredits: 2,
|
||||||
|
baseCredits: 60,
|
||||||
|
ratio: 1.3,
|
||||||
|
inputVideoRatio: 1.3,
|
||||||
|
inputVideoBaseCredits: 0,
|
||||||
|
inputVideoPerSecondCredits: 15,
|
||||||
|
inputImageRatio: 1,
|
||||||
|
inputImageBaseCredits: 0,
|
||||||
|
inputImagePerImageCredits: 0.0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let total = (duration * config.perSecondCredits + config.baseCredits) * config.ratio;
|
||||||
|
|
||||||
|
const inputVideoDuration = taskDetail?.videoGeneration?.inputMedia?.video?.duration || 0;
|
||||||
|
if (inputVideoDuration > 0) {
|
||||||
|
const inputVideoCost = ((config.inputVideoBaseCredits || 0) + (config.inputVideoPerSecondCredits || 0) * inputVideoDuration) * (config.inputVideoRatio || 1);
|
||||||
|
total += inputVideoCost;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputImageCount = isV2
|
||||||
|
? (taskDetail?.material?.materialImageUrl ? 1 : 0)
|
||||||
|
: (taskDetail?.videoGeneration?.inputMedia?.image?.length || 0);
|
||||||
|
if (inputImageCount > 0) {
|
||||||
|
const inputImageCost = ((config.inputImageBaseCredits || 0) + (config.inputImagePerImageCredits || 0) * inputImageCount) * (config.inputImageRatio || 1);
|
||||||
|
total += inputImageCost;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Number(total.toFixed(2));
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (creditCalculationData.length > 0 && countType) {
|
if (creditCalculationData.length > 0 && countType) {
|
||||||
calculateEstimatedCredits();
|
calculateEstimatedCredits();
|
||||||
@@ -1015,6 +1068,21 @@ function InitialInfo() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ marginBottom: 8 }}>
|
||||||
|
<span style={{ fontSize: 12, fontWeight: 500, color: '#666666', marginRight: 8 }}>原产品名称:</span>
|
||||||
|
<span style={{ fontSize: 13, color: '#4b5563' }}>{steps[0]?.input?.payload?.sourceProjectName || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginBottom: 8 }}>
|
||||||
|
<span style={{ fontSize: 12, fontWeight: 500, color: '#666666', marginRight: 8 }}>自有产品名称:</span>
|
||||||
|
<span style={{ fontSize: 13, color: '#4b5563' }}>{steps[0]?.input?.payload?.targetProjectName || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span style={{ fontSize: 12, fontWeight: 500, color: '#666666', marginRight: 8 }}>产品卖点:</span>
|
||||||
|
<span style={{ fontSize: 13, color: '#4b5563' }}>{steps[0]?.input?.payload?.coreContentPoint || '-'}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</>
|
</>
|
||||||
@@ -1082,8 +1150,8 @@ function InitialInfo() {
|
|||||||
</div>
|
</div>
|
||||||
{!isV2 && (<>
|
{!isV2 && (<>
|
||||||
{/* 引擎选择器和视频参数设置 */}
|
{/* 引擎选择器和视频参数设置 */}
|
||||||
<p style={{marginBottom:6,fontSize: 14, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}>视频参数选择:</p>
|
<p style={{ marginBottom: 6, fontSize: 14, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}>视频参数选择:</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' }}>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -1484,6 +1552,36 @@ function InitialInfo() {
|
|||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style={{ padding: '12px 14px', background: 'rgba(255,255,255,0.6)', borderRadius: 10, border: '1px solid rgba(99, 102, 241, 0.08)', marginBottom: 4 }}>
|
||||||
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '12px' }}>
|
||||||
|
{(() => {
|
||||||
|
const videoConfig = steps[1]?.input?.payload?.videoConfig || steps[1]?.input?.payload?.video_config;
|
||||||
|
const engineId = videoConfig?.engineId || videoConfig?.engine_id;
|
||||||
|
const engineName = engineId ? (enginesele.video || []).find((e: any) => String(e.id) === String(engineId))?.name : '';
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<span style={{ fontSize: 12, fontWeight: 500, color: '#666666', marginRight: 4 }}>引擎:</span>
|
||||||
|
<span style={{ fontSize: 13, color: '#4b5563' }}>{engineName || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span style={{ fontSize: 12, fontWeight: 500, color: '#666666', marginRight: 4 }}>比例:</span>
|
||||||
|
<span style={{ fontSize: 13, color: '#4b5563' }}>{videoConfig?.aspectRatio || videoConfig?.aspect_ratio || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span style={{ fontSize: 12, fontWeight: 500, color: '#666666', marginRight: 4 }}>分辨率:</span>
|
||||||
|
<span style={{ fontSize: 13, color: '#4b5563' }}>{videoConfig?.resolution || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span style={{ fontSize: 12, fontWeight: 500, color: '#666666', marginRight: 4 }}>时长:</span>
|
||||||
|
<span style={{ fontSize: 13, color: '#4b5563' }}>{videoConfig?.duration || '-'}</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Space style={{ marginTop: 16, gap: 12, width: '100%', flexWrap: 'wrap' }}>
|
<Space style={{ marginTop: 16, gap: 12, width: '100%', flexWrap: 'wrap' }}>
|
||||||
<Button
|
<Button
|
||||||
type="default"
|
type="default"
|
||||||
@@ -1495,26 +1593,47 @@ function InitialInfo() {
|
|||||||
查看/修改视频提词
|
查看/修改视频提词
|
||||||
</Button>
|
</Button>
|
||||||
{isV2 && (
|
{isV2 && (
|
||||||
|
<Tooltip
|
||||||
|
title={((user?.credits || 0) < optimizeHoldCredits) ? `积分不足${optimizeHoldCredits},请充值积分` : ''}
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
type="default"
|
type="default"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
if ((user?.credits || 0) < optimizeHoldCredits) {
|
||||||
|
message.warning(`积分不足${optimizeHoldCredits},请充值积分`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setRetryPromptStepId(String(step.id));
|
setRetryPromptStepId(String(step.id));
|
||||||
setRetryPromptModalVisible(true);
|
setRetryPromptModalVisible(true);
|
||||||
}}
|
}}
|
||||||
style={{ flex: '0 0 auto', minWidth: 160, borderRadius: 10, borderColor: 'rgba(99, 102, 241, 0.3)', color: '#6366f1', height: 36, fontWeight: 500 }}
|
style={{ flex: '0 0 auto', minWidth: 160, borderRadius: 10, borderColor: 'rgba(99, 102, 241, 0.3)', color: '#6366f1', height: 36, fontWeight: 500 }}
|
||||||
disabled={step.status === 'processing' || steps[2]?.status === 'processing'}
|
disabled={step.status === 'processing' || steps[2]?.status === 'processing' || ((user?.credits || 0) < optimizeHoldCredits)}
|
||||||
>
|
>
|
||||||
重新生成视频提词
|
重新生成视频提词
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
<Tooltip
|
||||||
|
title={((user?.credits || 0) < calculateCreditsFromVideoConfig()) ? `积分不足${calculateCreditsFromVideoConfig()},请充值积分` : ''}
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => createvideo(step.id, step.engineId)}
|
onClick={() => {
|
||||||
|
const credits = calculateCreditsFromVideoConfig();
|
||||||
|
if ((user?.credits || 0) < credits) {
|
||||||
|
message.warning(`积分不足${credits},请充值积分`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
createvideo(step.id, step.engineId);
|
||||||
|
}}
|
||||||
type="primary"
|
type="primary"
|
||||||
style={{ flex: '1 0 auto', minWidth: 140, borderRadius: 10, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', border: 'none', height: 36, fontWeight: 500, boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)' }}
|
style={{ flex: '1 0 auto', minWidth: 140, borderRadius: 10, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', border: 'none', height: 36, fontWeight: 500, boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)' }}
|
||||||
disabled={step.status !== 'completed'}
|
disabled={step.status !== 'completed' || ((user?.credits || 0) < calculateCreditsFromVideoConfig())}
|
||||||
>
|
>
|
||||||
下一步:生成视频
|
下一步:生成视频(所需积分:{calculateCreditsFromVideoConfig()})
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
</Space>
|
</Space>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -1536,15 +1655,27 @@ function InitialInfo() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Space style={{ width: '100%', gap: 12 }}>
|
<Space style={{ width: '100%', gap: 12 }}>
|
||||||
|
<Tooltip
|
||||||
|
title={((user?.credits || 0) < calculateCreditsFromVideoConfig()) ? `积分不足${calculateCreditsFromVideoConfig()},请充值积分` : ''}
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => agincreatevideo()}
|
onClick={() => {
|
||||||
|
const credits = calculateCreditsFromVideoConfig();
|
||||||
|
if ((user?.credits || 0) < credits) {
|
||||||
|
message.warning(`积分不足${credits},请充值积分`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
agincreatevideo();
|
||||||
|
}}
|
||||||
type="default"
|
type="default"
|
||||||
icon={<EditOutlined />}
|
icon={<EditOutlined />}
|
||||||
style={{ flex: 1, borderRadius: 10, borderColor: 'rgba(99, 102, 241, 0.3)', color: '#6366f1', height: 36, fontWeight: 500, background: 'rgba(99, 102, 241, 0.04)' }}
|
style={{ flex: 1, borderRadius: 10, borderColor: 'rgba(99, 102, 241, 0.3)', color: '#6366f1', height: 36, fontWeight: 500, background: 'rgba(99, 102, 241, 0.04)' }}
|
||||||
disabled={isV2 ? !['completed', 'failed'].includes(step.status) : step.status !== 'completed'}
|
disabled={isV2 ? !['completed', 'failed'].includes(step.status) : step.status !== 'completed' || ((user?.credits || 0) < calculateCreditsFromVideoConfig())}
|
||||||
>
|
>
|
||||||
{step.status === 'failed' ? '重试生成' : '重新生成'}
|
{step.status === 'failed' ? '重试生成' : '重新生成'}(所需积分:{calculateCreditsFromVideoConfig()})
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<DownloadOutlined />}
|
icon={<DownloadOutlined />}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ const buildAssetUrl = (url?: string): string => {
|
|||||||
|
|
||||||
const GenerateConver: React.FC = () => {
|
const GenerateConver: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { user } = useAuthStore();
|
const { user, optimizeHoldCredits } = useAuthStore();
|
||||||
|
|
||||||
const [tableData, setTableData] = useState<any[]>(
|
const [tableData, setTableData] = useState<any[]>(
|
||||||
[]
|
[]
|
||||||
@@ -884,7 +884,7 @@ const GenerateConver: React.FC = () => {
|
|||||||
padding: 24, overflowY: 'auto'
|
padding: 24, overflowY: 'auto'
|
||||||
}}>
|
}}>
|
||||||
{/* 上传视频 */}
|
{/* 上传视频 */}
|
||||||
<div style={{ marginBottom: 20 }}>
|
<div style={{ marginBottom: 16 }}>
|
||||||
<p style={{ margin: 0, fontSize: 14, fontWeight: 600, color: '#1e293b', marginBottom: 10 }}>
|
<p style={{ margin: 0, fontSize: 14, fontWeight: 600, color: '#1e293b', marginBottom: 10 }}>
|
||||||
上传复刻视频
|
上传复刻视频
|
||||||
</p>
|
</p>
|
||||||
@@ -1000,7 +1000,7 @@ const GenerateConver: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 上传产品图片 */}
|
{/* 上传产品图片 */}
|
||||||
<div style={{ marginBottom: 20 }}>
|
<div style={{ marginBottom: 16 }}>
|
||||||
<p style={{ margin: 0, fontSize: 14, fontWeight: 600, color: '#1e293b', marginBottom: 10 }}>
|
<p style={{ margin: 0, fontSize: 14, fontWeight: 600, color: '#1e293b', marginBottom: 10 }}>
|
||||||
上传产品图片
|
上传产品图片
|
||||||
</p>
|
</p>
|
||||||
@@ -1169,7 +1169,7 @@ const GenerateConver: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 产品卖点 */}
|
{/* 产品卖点 */}
|
||||||
<div style={{ marginBottom: 24 }}>
|
<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>
|
||||||
@@ -1195,7 +1195,7 @@ const GenerateConver: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginBottom: 20, 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' }}>
|
||||||
@@ -1236,7 +1236,7 @@ const GenerateConver: React.FC = () => {
|
|||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: 'calc(100% + 8px)',
|
bottom: 'calc(100% + 8px)',
|
||||||
left: -10,
|
left: -10,
|
||||||
width: 350,
|
width: 300,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
boxShadow: '0 10px 40px rgba(0,0,0,0.15)',
|
boxShadow: '0 10px 40px rgba(0,0,0,0.15)',
|
||||||
@@ -1251,7 +1251,7 @@ const GenerateConver: React.FC = () => {
|
|||||||
display: 'block',
|
display: 'block',
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: 500,
|
fontWeight: 300,
|
||||||
color: '#666666',
|
color: '#666666',
|
||||||
}}>
|
}}>
|
||||||
选择引擎
|
选择引擎
|
||||||
@@ -1362,7 +1362,7 @@ const GenerateConver: React.FC = () => {
|
|||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: 'calc(100% + 8px)',
|
bottom: 'calc(100% + 8px)',
|
||||||
left: -160,
|
left: -160,
|
||||||
width: 350,
|
width: 320,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
boxShadow: '0 10px 40px rgba(0,0,0,0.15)',
|
boxShadow: '0 10px 40px rgba(0,0,0,0.15)',
|
||||||
@@ -1552,7 +1552,7 @@ const GenerateConver: React.FC = () => {
|
|||||||
|
|
||||||
{/* 立即生成按钮 */}
|
{/* 立即生成按钮 */}
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={((user?.credits || 0) < estimatedCredits) ? '积分不足,请更换参数/充值积分' : ''}
|
title={((user?.credits || 0) < (estimatedCredits + optimizeHoldCredits)) ? '积分不足,请更换参数/充值积分' : ''}
|
||||||
placement="top"
|
placement="top"
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
@@ -1560,7 +1560,7 @@ const GenerateConver: React.FC = () => {
|
|||||||
block
|
block
|
||||||
size="large"
|
size="large"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if ((user?.credits || 0) < estimatedCredits) {
|
if ((user?.credits || 0) < (estimatedCredits + optimizeHoldCredits)) {
|
||||||
message.warning('积分不足,请更换参数/充值积分');
|
message.warning('积分不足,请更换参数/充值积分');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1578,15 +1578,30 @@ const GenerateConver: React.FC = () => {
|
|||||||
transition: 'all 0.25s cubic-bezier(0.4, 0, 0.2, 1)',
|
transition: 'all 0.25s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
立即生成+
|
立即生成
|
||||||
<span style={{ color: '#fff', marginLeft: 8, fontSize: 13 }}>
|
|
||||||
预估积分:{estimatedCredits}
|
|
||||||
</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>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 创作记录弹窗 */}
|
{/* 创作记录弹窗 */}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ function buildAssetUrl(url?: string): string {
|
|||||||
function RemoveInfo() {
|
function RemoveInfo() {
|
||||||
const { creatID } = useParams<{ creatID: string }>();
|
const { creatID } = useParams<{ creatID: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { user } = useAuthStore();
|
const { user, optimizeHoldCredits } = useAuthStore();
|
||||||
const [drawerVisible, setDrawerVisible] = useState(false);
|
const [drawerVisible, setDrawerVisible] = useState(false);
|
||||||
const [trimModalVisible, setTrimModalVisible] = useState(false);
|
const [trimModalVisible, setTrimModalVisible] = useState(false);
|
||||||
const [currentSegment, setCurrentSegment] = useState<string | null>(null);
|
const [currentSegment, setCurrentSegment] = useState<string | null>(null);
|
||||||
@@ -1521,17 +1521,22 @@ function RemoveInfo() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', gap: 12, marginTop: 8 }}>
|
<div style={{ display: 'flex', gap: 12, marginTop: 8 }}>
|
||||||
|
<Tooltip
|
||||||
|
title={((user?.credits || 0) < (estimatedCredits + optimizeHoldCredits)) ? '积分不足,请更换参数/充值积分' : ''}
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if ((user?.credits || 0) < estimatedCredits) {
|
if ((user?.credits || 0) < (estimatedCredits + optimizeHoldCredits)) {
|
||||||
message.warning('积分不足,请更换参数/充值积分');
|
message.warning('积分不足,请更换参数/充值积分');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
handleManualGenerate();
|
handleManualGenerate();
|
||||||
}}
|
}}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
disabled={loading || !engineId || !selectedEngineSupportsImage || ((user?.credits || 0) < estimatedCredits)}
|
// disabled={loading || !engineId || !selectedEngineSupportsImage || ((user?.credits || 0) < (estimatedCredits + optimizeHoldCredits))}
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
height: 44,
|
height: 44,
|
||||||
@@ -1545,10 +1550,21 @@ function RemoveInfo() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{loading ? '生成中...' : '手动生成'}
|
{loading ? '生成中...' : '手动生成'}
|
||||||
<span style={{ color: '#fff', marginLeft: 8, fontSize: 13 }}>
|
|
||||||
预估积分:{estimatedCredits}
|
|
||||||
</span>
|
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</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>
|
</div>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ function InitialInfo() {
|
|||||||
const { creatID } = useParams<{ creatID: string }>();
|
const { creatID } = useParams<{ creatID: string }>();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const flowVersion: 'v1' | 'v2' = searchParams.get('flow_version') === 'v2' ? 'v2' : 'v1';
|
const flowVersion: 'v1' | 'v2' = searchParams.get('flow_version') === 'v2' ? 'v2' : 'v1';
|
||||||
const { user } = useAuthStore();
|
const { user, optimizeHoldCredits } = useAuthStore();
|
||||||
|
|
||||||
const [modalVisible, setModalVisible] = useState(false);
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
const [creditCalculationData, setCreditCalculationData] = useState<any[]>([]);
|
const [creditCalculationData, setCreditCalculationData] = useState<any[]>([]);
|
||||||
@@ -88,7 +88,7 @@ function InitialInfo() {
|
|||||||
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(() => { });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -138,6 +138,7 @@ function InitialInfo() {
|
|||||||
status: apiSteps[index]?.status || '',
|
status: apiSteps[index]?.status || '',
|
||||||
id: apiSteps[index]?.id || index,
|
id: apiSteps[index]?.id || index,
|
||||||
output: apiSteps[index]?.output || '',
|
output: apiSteps[index]?.output || '',
|
||||||
|
input: apiSteps[index]?.input || {},
|
||||||
engineId: apiSteps[index]?.input?.payload?.videoConfig?.engineId || '',
|
engineId: apiSteps[index]?.input?.payload?.videoConfig?.engineId || '',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -290,7 +291,7 @@ function InitialInfo() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
calculateCredits().then((data: any) => {
|
calculateCredits().then((data: any) => {
|
||||||
setCreditCalculationData(data);
|
setCreditCalculationData(data);
|
||||||
}).catch(() => {});
|
}).catch(() => { });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const calculateEstimatedCredits = () => {
|
const calculateEstimatedCredits = () => {
|
||||||
@@ -334,6 +335,56 @@ function InitialInfo() {
|
|||||||
setEstimatedCredits(Number(total.toFixed(2)));
|
setEstimatedCredits(Number(total.toFixed(2)));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const calculateCreditsFromVideoConfig = () => {
|
||||||
|
const videoConfig = steps[1]?.input?.payload?.videoConfig || steps[1]?.input?.payload?.video_config;
|
||||||
|
if (!videoConfig) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const engineId = videoConfig?.engineId || videoConfig?.engine_id;
|
||||||
|
const resolution = videoConfig?.resolution;
|
||||||
|
const duration = videoConfig?.duration || videoDuration;
|
||||||
|
|
||||||
|
let config: any = {};
|
||||||
|
config = creditCalculationData.find((item: any) =>
|
||||||
|
item.modelConfigId === engineId &&
|
||||||
|
item.genType === 'video' &&
|
||||||
|
item.resolution === resolution
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
config = {
|
||||||
|
perSecondCredits: 2,
|
||||||
|
baseCredits: 60,
|
||||||
|
ratio: 1.3,
|
||||||
|
inputVideoRatio: 1.3,
|
||||||
|
inputVideoBaseCredits: 0,
|
||||||
|
inputVideoPerSecondCredits: 15,
|
||||||
|
inputImageRatio: 1,
|
||||||
|
inputImageBaseCredits: 0,
|
||||||
|
inputImagePerImageCredits: 0.0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let total = (duration * config.perSecondCredits + config.baseCredits) * config.ratio;
|
||||||
|
|
||||||
|
const inputVideoDuration = taskDetail?.videoGeneration?.inputMedia?.video?.duration || 0;
|
||||||
|
if (inputVideoDuration > 0) {
|
||||||
|
const inputVideoCost = ((config.inputVideoBaseCredits || 0) + (config.inputVideoPerSecondCredits || 0) * inputVideoDuration) * (config.inputVideoRatio || 1);
|
||||||
|
total += inputVideoCost;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputImageCount = isV2
|
||||||
|
? (taskDetail?.material?.materialImageUrl ? 1 : 0)
|
||||||
|
: (taskDetail?.videoGeneration?.inputMedia?.image?.length || 0);
|
||||||
|
if (inputImageCount > 0) {
|
||||||
|
const inputImageCost = ((config.inputImageBaseCredits || 0) + (config.inputImagePerImageCredits || 0) * inputImageCount) * (config.inputImageRatio || 1);
|
||||||
|
total += inputImageCost;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Number(total.toFixed(2));
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (creditCalculationData.length > 0 && countType) {
|
if (creditCalculationData.length > 0 && countType) {
|
||||||
calculateEstimatedCredits();
|
calculateEstimatedCredits();
|
||||||
@@ -905,10 +956,46 @@ function InitialInfo() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={() => { createone(step.id); }} type="primary" style={{ flex: 1, borderRadius: 10, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', border: 'none', height: 36, fontWeight: 500, boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)' }} disabled={step.status !== 'completed'}>
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style={{ marginBottom: 8 }}>
|
||||||
|
<span style={{ fontSize: 12, fontWeight: 500, color: '#666666', marginRight: 8 }}>原产品名称:</span>
|
||||||
|
<span style={{ fontSize: 13, color: '#4b5563' }}>{steps[0]?.input?.payload?.sourceProjectName || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginBottom: 8 }}>
|
||||||
|
<span style={{ fontSize: 12, fontWeight: 500, color: '#666666', marginRight: 8 }}>自有产品名称:</span>
|
||||||
|
<span style={{ fontSize: 13, color: '#4b5563' }}>{steps[0]?.input?.payload?.targetProjectName || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginBottom: 8 }}>
|
||||||
|
<span style={{ fontSize: 12, fontWeight: 500, color: '#666666', marginRight: 8 }}>产品卖点:</span>
|
||||||
|
<span style={{ fontSize: 13, color: '#4b5563' }}>{steps[0]?.input?.payload?.coreContentPoint || '-'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!isV2 && (
|
||||||
|
<Tooltip
|
||||||
|
title={((user?.credits || 0) < optimizeHoldCredits) ? `积分不足${optimizeHoldCredits},请充值积分` : ''}
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if ((user?.credits || 0) < optimizeHoldCredits) {
|
||||||
|
message.warning(`积分不足${optimizeHoldCredits},请充值积分`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
createone(step.id);
|
||||||
|
}}
|
||||||
|
type="primary"
|
||||||
|
style={{ width: '100%', borderRadius: 10, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', border: 'none', height: 36, fontWeight: 500, boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)' }}
|
||||||
|
disabled={step.status !== 'completed' || ((user?.credits || 0) < optimizeHoldCredits)}
|
||||||
|
>
|
||||||
下一步:生成图片提示词
|
下一步:生成图片提示词
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -1370,6 +1457,35 @@ function InitialInfo() {
|
|||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style={{ padding: '12px 14px', background: 'rgba(255,255,255,0.6)', borderRadius: 10, border: '1px solid rgba(99, 102, 241, 0.08)', marginBottom: 4 }}>
|
||||||
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '12px' }}>
|
||||||
|
{(() => {
|
||||||
|
const videoConfig = steps[1]?.input?.payload?.videoConfig || steps[1]?.input?.payload?.video_config;
|
||||||
|
const engineId = videoConfig?.engineId || videoConfig?.engine_id;
|
||||||
|
const engineName = engineId ? (enginesele.video || []).find((e: any) => String(e.id) === String(engineId))?.name : '';
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<span style={{ fontSize: 12, fontWeight: 500, color: '#666666', marginRight: 4 }}>引擎:</span>
|
||||||
|
<span style={{ fontSize: 13, color: '#4b5563' }}>{engineName || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span style={{ fontSize: 12, fontWeight: 500, color: '#666666', marginRight: 4 }}>比例:</span>
|
||||||
|
<span style={{ fontSize: 13, color: '#4b5563' }}>{videoConfig?.aspectRatio || videoConfig?.aspect_ratio || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span style={{ fontSize: 12, fontWeight: 500, color: '#666666', marginRight: 4 }}>分辨率:</span>
|
||||||
|
<span style={{ fontSize: 13, color: '#4b5563' }}>{videoConfig?.resolution || '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span style={{ fontSize: 12, fontWeight: 500, color: '#666666', marginRight: 4 }}>时长:</span>
|
||||||
|
<span style={{ fontSize: 13, color: '#4b5563' }}>{videoConfig?.duration || '-'}</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<Space style={{ marginTop: 16, gap: 12, width: '100%', flexWrap: 'wrap' }}>
|
<Space style={{ marginTop: 16, gap: 12, width: '100%', flexWrap: 'wrap' }}>
|
||||||
@@ -1383,26 +1499,47 @@ function InitialInfo() {
|
|||||||
查看/修改视频提词
|
查看/修改视频提词
|
||||||
</Button>
|
</Button>
|
||||||
{isV2 && (
|
{isV2 && (
|
||||||
|
<Tooltip
|
||||||
|
title={((user?.credits || 0) < optimizeHoldCredits) ? `积分不足${optimizeHoldCredits},请充值积分` : ''}
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
type="default"
|
type="default"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
if ((user?.credits || 0) < optimizeHoldCredits) {
|
||||||
|
message.warning(`积分不足${optimizeHoldCredits},请充值积分`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setRetryPromptStepId(String(step.id));
|
setRetryPromptStepId(String(step.id));
|
||||||
setRetryPromptModalVisible(true);
|
setRetryPromptModalVisible(true);
|
||||||
}}
|
}}
|
||||||
style={{ flex: '0 0 auto', minWidth: 160, borderRadius: 10, borderColor: 'rgba(99, 102, 241, 0.3)', color: '#6366f1', height: 36, fontWeight: 500 }}
|
style={{ flex: '0 0 auto', minWidth: 160, borderRadius: 10, borderColor: 'rgba(99, 102, 241, 0.3)', color: '#6366f1', height: 36, fontWeight: 500 }}
|
||||||
disabled={step.status === 'processing' || steps[2]?.status === 'processing'}
|
disabled={step.status === 'processing' || steps[2]?.status === 'processing' || ((user?.credits || 0) < optimizeHoldCredits)}
|
||||||
>
|
>
|
||||||
重新生成视频提词
|
重新生成视频提词
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
<Tooltip
|
||||||
|
title={((user?.credits || 0) < calculateCreditsFromVideoConfig()) ? `积分不足${calculateCreditsFromVideoConfig()},请充值积分` : ''}
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => createvideo(step.id, step.engineId)}
|
onClick={() => {
|
||||||
|
const credits = calculateCreditsFromVideoConfig();
|
||||||
|
if ((user?.credits || 0) < credits) {
|
||||||
|
message.warning(`积分不足${credits},请充值积分`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
createvideo(step.id, step.engineId);
|
||||||
|
}}
|
||||||
type="primary"
|
type="primary"
|
||||||
style={{ flex: '1 0 auto', minWidth: 140, borderRadius: 10, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', border: 'none', height: 36, fontWeight: 500, boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)' }}
|
style={{ flex: '1 0 auto', minWidth: 140, borderRadius: 10, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', border: 'none', height: 36, fontWeight: 500, boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)' }}
|
||||||
disabled={step.status !== 'completed'}
|
disabled={step.status !== 'completed' || ((user?.credits || 0) < calculateCreditsFromVideoConfig())}
|
||||||
>
|
>
|
||||||
下一步:生成视频
|
下一步:生成视频(所需积分:{calculateCreditsFromVideoConfig()})
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
</Space>
|
</Space>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -1424,9 +1561,27 @@ function InitialInfo() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Space style={{ width: '100%', gap: 12 }}>
|
<Space style={{ width: '100%', gap: 12 }}>
|
||||||
<Button onClick={() => agincreatevideo()} type="default" icon={<EditOutlined />} style={{ flex: 1, borderRadius: 10, borderColor: 'rgba(99, 102, 241, 0.3)', color: '#6366f1', height: 36, fontWeight: 500, background: 'rgba(99, 102, 241, 0.04)' }} disabled={isV2 ? !['completed', 'failed'].includes(step.status) : step.status !== 'completed'}>
|
<Tooltip
|
||||||
{step.status === 'failed' ? '重试生成' : '重新生成'}
|
title={((user?.credits || 0) < calculateCreditsFromVideoConfig()) ? `积分不足${calculateCreditsFromVideoConfig()},请充值积分` : ''}
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
const credits = calculateCreditsFromVideoConfig();
|
||||||
|
if ((user?.credits || 0) < credits) {
|
||||||
|
message.warning(`积分不足${credits},请充值积分`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
agincreatevideo();
|
||||||
|
}}
|
||||||
|
type="default"
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
style={{ flex: 1, borderRadius: 10, borderColor: 'rgba(99, 102, 241, 0.3)', color: '#6366f1', height: 36, fontWeight: 500, background: 'rgba(99, 102, 241, 0.04)' }}
|
||||||
|
disabled={isV2 ? !['completed', 'failed'].includes(step.status) : step.status !== 'completed' || ((user?.credits || 0) < calculateCreditsFromVideoConfig())}
|
||||||
|
>
|
||||||
|
{step.status === 'failed' ? '重试生成' : '重新生成'}(所需积分:{calculateCreditsFromVideoConfig()})
|
||||||
</Button>
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<DownloadOutlined />}
|
icon={<DownloadOutlined />}
|
||||||
|
|||||||
@@ -5,17 +5,20 @@ import * as api from '../api';
|
|||||||
interface AuthState {
|
interface AuthState {
|
||||||
user: User | null;
|
user: User | null;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
optimizeHoldCredits: number;
|
||||||
login: (username: string, password: string, captchaToken?: string, rememberMe?: boolean) => Promise<void>;
|
login: (username: string, password: string, captchaToken?: string, rememberMe?: boolean) => Promise<void>;
|
||||||
logout: () => Promise<void>;
|
logout: () => Promise<void>;
|
||||||
checkAuth: () => Promise<void>;
|
checkAuth: () => Promise<void>;
|
||||||
changePassword: (oldPwd: string, newPwd: string) => Promise<void>;
|
changePassword: (oldPwd: string, newPwd: string) => Promise<void>;
|
||||||
refreshUser: () => Promise<void>;
|
refreshUser: () => Promise<void>;
|
||||||
setUserCredits: (credits: number) => void;
|
setUserCredits: (credits: number) => void;
|
||||||
|
setOptimizeHoldCredits: (credits: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAuthStore = create<AuthState>((set) => ({
|
export const useAuthStore = create<AuthState>((set) => ({
|
||||||
user: null,
|
user: null,
|
||||||
loading: true,
|
loading: true,
|
||||||
|
optimizeHoldCredits: 0,
|
||||||
|
|
||||||
login: async (username, password, captchaToken?, rememberMe?) => {
|
login: async (username, password, captchaToken?, rememberMe?) => {
|
||||||
const user = await api.login(username, password, captchaToken, rememberMe);
|
const user = await api.login(username, password, captchaToken, rememberMe);
|
||||||
@@ -31,8 +34,14 @@ export const useAuthStore = create<AuthState>((set) => ({
|
|||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('auth_token');
|
const token = localStorage.getItem('auth_token');
|
||||||
if (!token) { set({ user: null, loading: false }); return; }
|
if (!token) { set({ user: null, loading: false }); return; }
|
||||||
const user = await api.getUser();
|
const [user, siteInfo] = await Promise.all([
|
||||||
|
api.getUser(),
|
||||||
|
api.getSiteInfo()
|
||||||
|
]);
|
||||||
set({ user, loading: false });
|
set({ user, loading: false });
|
||||||
|
if (siteInfo.optimizeHoldCredits !== undefined) {
|
||||||
|
set({ optimizeHoldCredits: siteInfo.optimizeHoldCredits });
|
||||||
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error?.message?.includes('401') || error?.message?.includes('Unauthorized')) {
|
if (error?.message?.includes('401') || error?.message?.includes('Unauthorized')) {
|
||||||
localStorage.removeItem('auth_token');
|
localStorage.removeItem('auth_token');
|
||||||
@@ -59,4 +68,8 @@ export const useAuthStore = create<AuthState>((set) => ({
|
|||||||
setUserCredits: (credits: number) => {
|
setUserCredits: (credits: number) => {
|
||||||
set((state) => (state.user ? { user: { ...state.user, credits } } : {}));
|
set((state) => (state.user ? { user: { ...state.user, credits } } : {}));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setOptimizeHoldCredits: (credits: number) => {
|
||||||
|
set({ optimizeHoldCredits: credits });
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user