This commit is contained in:
2026-07-23 18:37:05 +08:00
12 changed files with 2336 additions and 1843 deletions
File diff suppressed because one or more lines are too long
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-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>
+1 -1
View File
@@ -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>
)} )}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+51 -36
View File
@@ -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,41 +1552,56 @@ const GenerateConver: React.FC = () => {
{/* 立即生成按钮 */} {/* 立即生成按钮 */}
<Tooltip <Tooltip
title={((user?.credits || 0) < estimatedCredits) ? '积分不足,请更换参数/充值积分' : ''} title={((user?.credits || 0) < (estimatedCredits + optimizeHoldCredits)) ? '积分不足,请更换参数/充值积分' : ''}
placement="top" placement="top"
> >
<Button <Button
type="primary" type="primary"
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;
} }
handleGenerate(); handleGenerate();
}} }}
disabled={!selectedEngineSupportsImage} disabled={!selectedEngineSupportsImage}
style={{ style={{
borderRadius: 12, borderRadius: 12,
height: 44, height: 44,
fontWeight: 600, fontWeight: 600,
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a855f7 100%)', background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a855f7 100%)',
border: 'none', border: 'none',
fontSize: 15, fontSize: 15,
boxShadow: '0 4px 16px rgba(99, 102, 241, 0.3)', boxShadow: '0 4px 16px rgba(99, 102, 241, 0.3)',
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>
{/* 创作记录弹窗 */} {/* 创作记录弹窗 */}
+86 -70
View File
@@ -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);
@@ -316,19 +316,19 @@ function RemoveInfo() {
const params = { const params = {
target_project_name: productName.trim(), target_project_name: productName.trim(),
core_content_point: productSellingPoint.trim(), core_content_point: productSellingPoint.trim(),
project_description: productSellingPoint.trim() || undefined, project_description: productSellingPoint.trim() || undefined,
material_image_url: productImage || undefined, material_image_url: productImage || undefined,
material_image_resource_id: productImageResourceId || undefined, material_image_resource_id: productImageResourceId || undefined,
video_config: { video_config: {
engine_id: engineId, engine_id: engineId,
duration: videoDuration, duration: videoDuration,
aspect_ratio: videoAspectRatio, aspect_ratio: videoAspectRatio,
resolution: videoResolution, resolution: videoResolution,
}, },
idempotency_key: idempotencyKey, idempotency_key: idempotencyKey,
}; };
setLoading(true); setLoading(true);
@@ -640,7 +640,7 @@ function RemoveInfo() {
fixed: 'right', fixed: 'right',
align: 'center' as const, align: 'center' as const,
render: (_: any, record: any) => ( render: (_: any, record: any) => (
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
{record.moduleProjectId ? ( {record.moduleProjectId ? (
<Button <Button
type="text" type="text"
@@ -792,34 +792,34 @@ function RemoveInfo() {
</div> </div>
</div> </div>
</div> </div>
{taskDetail.analysisStatus === 'failed' && ( {taskDetail.analysisStatus === 'failed' && (
<div style={{ marginTop: 12, textAlign: 'center' }}> <div style={{ marginTop: 12, textAlign: 'center' }}>
<Button <Button
onClick={handleReanalyze} onClick={handleReanalyze}
style={{ style={{
width: "100%", width: "100%",
padding: '6px 16px', padding: '6px 16px',
borderRadius: 8, borderRadius: 8,
fontSize: 12, fontSize: 12,
border: '1px solid #ef4444', border: '1px solid #ef4444',
color: '#ef4444', color: '#ef4444',
background: 'rgba(239, 68, 68, 0.05)', background: 'rgba(239, 68, 68, 0.05)',
cursor: taskDetail.analysisStatus === 'processing' ? 'not-allowed' : 'pointer', cursor: taskDetail.analysisStatus === 'processing' ? 'not-allowed' : 'pointer',
transition: 'all 0.2s', transition: 'all 0.2s',
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
if (taskDetail.analysisStatus !== 'processing') { if (taskDetail.analysisStatus !== 'processing') {
e.currentTarget.style.background = 'rgba(239, 68, 68, 0.1)'; e.currentTarget.style.background = 'rgba(239, 68, 68, 0.1)';
} }
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
e.currentTarget.style.background = 'rgba(239, 68, 68, 0.05)'; e.currentTarget.style.background = 'rgba(239, 68, 68, 0.05)';
}} }}
> >
{taskDetail.analysisStatus === 'processing' ? '分析中...' : '重新分析'} {taskDetail.analysisStatus === 'processing' ? '分析中...' : '重新分析'}
</Button> </Button>
</div> </div>
)} )}
</div> </div>
@@ -1521,34 +1521,50 @@ function RemoveInfo() {
</div> </div>
<div style={{ display: 'flex', gap: 12, marginTop: 8 }}> <div style={{ display: 'flex', gap: 12, marginTop: 8 }}>
<Button <Tooltip
type="primary" title={((user?.credits || 0) < (estimatedCredits + optimizeHoldCredits)) ? '积分不足,请更换参数/充值积分' : ''}
onClick={() => { placement="top"
if ((user?.credits || 0) < estimatedCredits) {
message.warning('积分不足,请更换参数/充值积分');
return;
}
handleManualGenerate();
}}
loading={loading}
disabled={loading || !engineId || !selectedEngineSupportsImage || ((user?.credits || 0) < estimatedCredits)}
style={{
flex: 1,
height: 44,
borderRadius: 12,
fontWeight: 600,
fontSize: 15,
border: 'none',
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a855f7 100%)',
boxShadow: '0 4px 16px rgba(99, 102, 241, 0.3)',
transition: 'all 0.25s cubic-bezier(0.4, 0, 0.2, 1)',
}}
> >
{loading ? '生成中...' : '手动生成'} <Button
<span style={{ color: '#fff', marginLeft: 8, fontSize: 13 }}> type="primary"
:{estimatedCredits}
</span> onClick={() => {
</Button> if ((user?.credits || 0) < (estimatedCredits + optimizeHoldCredits)) {
message.warning('积分不足,请更换参数/充值积分');
return;
}
handleManualGenerate();
}}
loading={loading}
// disabled={loading || !engineId || !selectedEngineSupportsImage || ((user?.credits || 0) < (estimatedCredits + optimizeHoldCredits))}
style={{
flex: 1,
height: 44,
borderRadius: 12,
fontWeight: 600,
fontSize: 15,
border: 'none',
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a855f7 100%)',
boxShadow: '0 4px 16px rgba(99, 102, 241, 0.3)',
transition: 'all 0.25s cubic-bezier(0.4, 0, 0.2, 1)',
}}
>
{loading ? '生成中...' : '手动生成'}
</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>
+180 -25
View File
@@ -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();
@@ -513,12 +564,12 @@ function InitialInfo() {
} }
removetwo(taskDetail.id, steps[1].id.toString(), params).then((res: any) => { removetwo(taskDetail.id, steps[1].id.toString(), params).then((res: any) => {
message.info('正在生成图片,请稍候...'); message.info('正在生成图片,请稍候...');
refreshTaskDetail(); refreshTaskDetail();
}).catch((error: any) => { }).catch((error: any) => {
const errorMsg = error?.message?.split(': ')?.[1] || error?.message || '生成失败'; const errorMsg = error?.message?.split(': ')?.[1] || error?.message || '生成失败';
message.error(errorMsg); message.error(errorMsg);
}); });
} }
@@ -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'}>
</Button>
</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 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>
</Tooltip>
)}
</> </>
)} )}
@@ -930,7 +1017,7 @@ function InitialInfo() {
> >
</Button> </Button>
<Button onClick={() => { createimage(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'}> <Button onClick={() => { createimage(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'}>
</Button> </Button>
</Space> </Space>
@@ -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 && (
<Button <Tooltip
type="default" title={((user?.credits || 0) < optimizeHoldCredits) ? `积分不足${optimizeHoldCredits},请充值积分` : ''}
onClick={() => { placement="top"
setRetryPromptStepId(String(step.id));
setRetryPromptModalVisible(true);
}}
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'}
> >
<Button
</Button> type="default"
onClick={() => {
if ((user?.credits || 0) < optimizeHoldCredits) {
message.warning(`积分不足${optimizeHoldCredits},请充值积分`);
return;
}
setRetryPromptStepId(String(step.id));
setRetryPromptModalVisible(true);
}}
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' || ((user?.credits || 0) < optimizeHoldCredits)}
>
</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()},请充值积分` : ''}
</Button> 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>
</Tooltip>
<Button <Button
type="primary" type="primary"
icon={<DownloadOutlined />} icon={<DownloadOutlined />}
+14 -1
View File
@@ -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 });
},
})); }));