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
+36
-36
@@ -1,37 +1,37 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<title>民众智创</title>
|
||||
<script>
|
||||
(function() {
|
||||
var cached = localStorage.getItem('siteInfo');
|
||||
if (cached) {
|
||||
try {
|
||||
var info = JSON.parse(cached);
|
||||
if (info.siteName) {
|
||||
document.title = info.siteName;
|
||||
}
|
||||
if (info.siteLogo) {
|
||||
var link = document.querySelector('link[rel="icon"]');
|
||||
if (link) {
|
||||
link.href = info.siteLogo;
|
||||
link.type = 'image/png';
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-BhfopyFy.js"></script>
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<title>民众智创</title>
|
||||
<script>
|
||||
(function() {
|
||||
var cached = localStorage.getItem('siteInfo');
|
||||
if (cached) {
|
||||
try {
|
||||
var info = JSON.parse(cached);
|
||||
if (info.siteName) {
|
||||
document.title = info.siteName;
|
||||
}
|
||||
if (info.siteLogo) {
|
||||
var link = document.querySelector('link[rel="icon"]');
|
||||
if (link) {
|
||||
link.href = info.siteLogo;
|
||||
link.type = 'image/png';
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-5mDsoYNB.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Bsz_Xon-.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>␍
|
||||
</body>
|
||||
</html>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -301,7 +301,7 @@ export async function verifyCaptcha(captchaId: string, x: number): Promise<strin
|
||||
return res.token;
|
||||
}
|
||||
// ── 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: '' };
|
||||
return api.get('/auth/site-info', false);
|
||||
}
|
||||
|
||||
@@ -313,7 +313,7 @@ const GRADIENTS = [
|
||||
const AppLayout: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { user, logout, refreshUser } = useAuthStore();
|
||||
const { user, logout, refreshUser, setOptimizeHoldCredits } = useAuthStore();
|
||||
const [pwdModalOpen, setPwdModalOpen] = useState(false);
|
||||
const [rechargeModalOpen, setRechargeModalOpen] = useState(false);
|
||||
const [contactModalOpen, setContactModalOpen] = useState(false);
|
||||
@@ -409,8 +409,14 @@ const AppLayout: React.FC = () => {
|
||||
setOperationManualUrl(info.operationManual);
|
||||
}
|
||||
|
||||
if (info.optimizeHoldCredits !== undefined) {
|
||||
setOptimizeHoldCredits(info.optimizeHoldCredits);
|
||||
} else {
|
||||
}
|
||||
|
||||
localStorage.setItem('siteInfo', JSON.stringify({ siteName: name, siteLogo: logo }));
|
||||
}).catch(() => { });
|
||||
}).catch((err) => {
|
||||
});
|
||||
|
||||
getUser().then((res: any) => {
|
||||
// console.log('[Storage] getUser 返回:', res);
|
||||
|
||||
@@ -43,11 +43,17 @@ interface ProgressItemProps {
|
||||
isPending: boolean;
|
||||
isCompleted: boolean;
|
||||
onAnimationComplete?: () => void;
|
||||
index?: number;
|
||||
}
|
||||
|
||||
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;
|
||||
const createdTime = new Date(createdAt).getTime();
|
||||
if (isNaN(createdTime)) return 0;
|
||||
@@ -56,18 +62,24 @@ const calculateProgressValue = (createdAt?: string): number => {
|
||||
if (elapsedSeconds >= MAX_DURATION_SECONDS) {
|
||||
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 [isFinishing, setIsFinishing] = useState(false);
|
||||
const intervalRef = useRef<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const newProgress = calculateProgressValue(item.createdAt);
|
||||
if (isFinishing) return;
|
||||
const newProgress = calculateProgressValue(item.createdAt, index);
|
||||
setDisplayProgress(newProgress);
|
||||
}, [item]);
|
||||
}, [item, index, isFinishing]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isCompleted && !isFinishing) {
|
||||
@@ -84,7 +96,8 @@ const ProgressItem: React.FC<ProgressItemProps> = ({ item, isPending: isPendingP
|
||||
}
|
||||
|
||||
const updateProgress = () => {
|
||||
const newProgress = calculateProgressValue(item.createdAt);
|
||||
if (isFinishing) return;
|
||||
const newProgress = calculateProgressValue(item.createdAt, index);
|
||||
setDisplayProgress(newProgress);
|
||||
};
|
||||
|
||||
@@ -96,7 +109,7 @@ const ProgressItem: React.FC<ProgressItemProps> = ({ item, isPending: isPendingP
|
||||
intervalRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [item.createdAt, isPendingProp, isCompleted, isFinishing, item.id]);
|
||||
}, [item.createdAt, isPendingProp, isCompleted, isFinishing, item.id, index]);
|
||||
|
||||
useEffect(() => {
|
||||
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 ? <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>
|
||||
<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}
|
||||
</div>
|
||||
)}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -51,7 +51,7 @@ const buildAssetUrl = (url?: string): string => {
|
||||
|
||||
const GenerateConver: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuthStore();
|
||||
const { user, optimizeHoldCredits } = useAuthStore();
|
||||
|
||||
const [tableData, setTableData] = useState<any[]>(
|
||||
[]
|
||||
@@ -884,7 +884,7 @@ const GenerateConver: React.FC = () => {
|
||||
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>
|
||||
@@ -1000,7 +1000,7 @@ const GenerateConver: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* 上传产品图片 */}
|
||||
<div style={{ marginBottom: 20 }}>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<p style={{ margin: 0, fontSize: 14, fontWeight: 600, color: '#1e293b', marginBottom: 10 }}>
|
||||
上传产品图片
|
||||
</p>
|
||||
@@ -1169,7 +1169,7 @@ const GenerateConver: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* 产品卖点 */}
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<p style={{ margin: 0, fontSize: 13, fontWeight: 500, color: '#475569', marginBottom: 8 }}>
|
||||
产品卖点
|
||||
</p>
|
||||
@@ -1195,7 +1195,7 @@ const GenerateConver: React.FC = () => {
|
||||
</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>
|
||||
<div style={{ width: '100%', display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
|
||||
<div style={{ flex: 1, position: 'relative', display: 'inline-block' }}>
|
||||
@@ -1236,7 +1236,7 @@ const GenerateConver: React.FC = () => {
|
||||
position: 'absolute',
|
||||
bottom: 'calc(100% + 8px)',
|
||||
left: -10,
|
||||
width: 350,
|
||||
width: 300,
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 16,
|
||||
boxShadow: '0 10px 40px rgba(0,0,0,0.15)',
|
||||
@@ -1251,7 +1251,7 @@ const GenerateConver: React.FC = () => {
|
||||
display: 'block',
|
||||
marginBottom: 8,
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
fontWeight: 300,
|
||||
color: '#666666',
|
||||
}}>
|
||||
选择引擎
|
||||
@@ -1362,7 +1362,7 @@ const GenerateConver: React.FC = () => {
|
||||
position: 'absolute',
|
||||
bottom: 'calc(100% + 8px)',
|
||||
left: -160,
|
||||
width: 350,
|
||||
width: 320,
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 16,
|
||||
boxShadow: '0 10px 40px rgba(0,0,0,0.15)',
|
||||
@@ -1552,41 +1552,56 @@ const GenerateConver: React.FC = () => {
|
||||
|
||||
{/* 立即生成按钮 */}
|
||||
<Tooltip
|
||||
title={((user?.credits || 0) < estimatedCredits) ? '积分不足,请更换参数/充值积分' : ''}
|
||||
title={((user?.credits || 0) < (estimatedCredits + optimizeHoldCredits)) ? '积分不足,请更换参数/充值积分' : ''}
|
||||
placement="top"
|
||||
>
|
||||
<Button
|
||||
type="primary"
|
||||
block
|
||||
size="large"
|
||||
onClick={() => {
|
||||
if ((user?.credits || 0) < estimatedCredits) {
|
||||
message.warning('积分不足,请更换参数/充值积分');
|
||||
return;
|
||||
}
|
||||
handleGenerate();
|
||||
}}
|
||||
disabled={!selectedEngineSupportsImage}
|
||||
style={{
|
||||
borderRadius: 12,
|
||||
height: 44,
|
||||
fontWeight: 600,
|
||||
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a855f7 100%)',
|
||||
border: 'none',
|
||||
fontSize: 15,
|
||||
boxShadow: '0 4px 16px rgba(99, 102, 241, 0.3)',
|
||||
transition: 'all 0.25s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
}}
|
||||
>
|
||||
立即生成+
|
||||
<span style={{ color: '#fff', marginLeft: 8, fontSize: 13 }}>
|
||||
预估积分:{estimatedCredits}
|
||||
</span>
|
||||
</Button>
|
||||
type="primary"
|
||||
block
|
||||
size="large"
|
||||
onClick={() => {
|
||||
if ((user?.credits || 0) < (estimatedCredits + optimizeHoldCredits)) {
|
||||
message.warning('积分不足,请更换参数/充值积分');
|
||||
return;
|
||||
}
|
||||
handleGenerate();
|
||||
}}
|
||||
disabled={!selectedEngineSupportsImage}
|
||||
style={{
|
||||
borderRadius: 12,
|
||||
height: 44,
|
||||
fontWeight: 600,
|
||||
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a855f7 100%)',
|
||||
border: 'none',
|
||||
fontSize: 15,
|
||||
boxShadow: '0 4px 16px rgba(99, 102, 241, 0.3)',
|
||||
transition: 'all 0.25s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
}}
|
||||
>
|
||||
立即生成
|
||||
|
||||
|
||||
|
||||
</Button>
|
||||
|
||||
|
||||
</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>
|
||||
|
||||
{/* 创作记录弹窗 */}
|
||||
|
||||
@@ -32,7 +32,7 @@ function buildAssetUrl(url?: string): string {
|
||||
function RemoveInfo() {
|
||||
const { creatID } = useParams<{ creatID: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuthStore();
|
||||
const { user, optimizeHoldCredits } = useAuthStore();
|
||||
const [drawerVisible, setDrawerVisible] = useState(false);
|
||||
const [trimModalVisible, setTrimModalVisible] = useState(false);
|
||||
const [currentSegment, setCurrentSegment] = useState<string | null>(null);
|
||||
@@ -316,19 +316,19 @@ function RemoveInfo() {
|
||||
|
||||
const params = {
|
||||
|
||||
target_project_name: productName.trim(),
|
||||
core_content_point: productSellingPoint.trim(),
|
||||
project_description: productSellingPoint.trim() || undefined,
|
||||
material_image_url: productImage || undefined,
|
||||
material_image_resource_id: productImageResourceId || undefined,
|
||||
video_config: {
|
||||
engine_id: engineId,
|
||||
duration: videoDuration,
|
||||
aspect_ratio: videoAspectRatio,
|
||||
resolution: videoResolution,
|
||||
},
|
||||
idempotency_key: idempotencyKey,
|
||||
};
|
||||
target_project_name: productName.trim(),
|
||||
core_content_point: productSellingPoint.trim(),
|
||||
project_description: productSellingPoint.trim() || undefined,
|
||||
material_image_url: productImage || undefined,
|
||||
material_image_resource_id: productImageResourceId || undefined,
|
||||
video_config: {
|
||||
engine_id: engineId,
|
||||
duration: videoDuration,
|
||||
aspect_ratio: videoAspectRatio,
|
||||
resolution: videoResolution,
|
||||
},
|
||||
idempotency_key: idempotencyKey,
|
||||
};
|
||||
|
||||
|
||||
setLoading(true);
|
||||
@@ -640,7 +640,7 @@ function RemoveInfo() {
|
||||
fixed: 'right',
|
||||
align: 'center' as const,
|
||||
render: (_: any, record: any) => (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
{record.moduleProjectId ? (
|
||||
<Button
|
||||
type="text"
|
||||
@@ -792,34 +792,34 @@ function RemoveInfo() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{taskDetail.analysisStatus === 'failed' && (
|
||||
<div style={{ marginTop: 12, textAlign: 'center' }}>
|
||||
<Button
|
||||
onClick={handleReanalyze}
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: '6px 16px',
|
||||
borderRadius: 8,
|
||||
fontSize: 12,
|
||||
border: '1px solid #ef4444',
|
||||
color: '#ef4444',
|
||||
background: 'rgba(239, 68, 68, 0.05)',
|
||||
cursor: taskDetail.analysisStatus === 'processing' ? 'not-allowed' : 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (taskDetail.analysisStatus !== 'processing') {
|
||||
e.currentTarget.style.background = 'rgba(239, 68, 68, 0.1)';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'rgba(239, 68, 68, 0.05)';
|
||||
}}
|
||||
>
|
||||
{taskDetail.analysisStatus === 'processing' ? '分析中...' : '重新分析'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{taskDetail.analysisStatus === 'failed' && (
|
||||
<div style={{ marginTop: 12, textAlign: 'center' }}>
|
||||
<Button
|
||||
onClick={handleReanalyze}
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: '6px 16px',
|
||||
borderRadius: 8,
|
||||
fontSize: 12,
|
||||
border: '1px solid #ef4444',
|
||||
color: '#ef4444',
|
||||
background: 'rgba(239, 68, 68, 0.05)',
|
||||
cursor: taskDetail.analysisStatus === 'processing' ? 'not-allowed' : 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (taskDetail.analysisStatus !== 'processing') {
|
||||
e.currentTarget.style.background = 'rgba(239, 68, 68, 0.1)';
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'rgba(239, 68, 68, 0.05)';
|
||||
}}
|
||||
>
|
||||
{taskDetail.analysisStatus === 'processing' ? '分析中...' : '重新分析'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1521,34 +1521,50 @@ function RemoveInfo() {
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 12, marginTop: 8 }}>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
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)',
|
||||
}}
|
||||
<Tooltip
|
||||
title={((user?.credits || 0) < (estimatedCredits + optimizeHoldCredits)) ? '积分不足,请更换参数/充值积分' : ''}
|
||||
placement="top"
|
||||
>
|
||||
{loading ? '生成中...' : '手动生成'}
|
||||
<span style={{ color: '#fff', marginLeft: 8, fontSize: 13 }}>
|
||||
预估积分:{estimatedCredits}
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
|
||||
onClick={() => {
|
||||
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>
|
||||
</Drawer>
|
||||
|
||||
@@ -27,7 +27,7 @@ function InitialInfo() {
|
||||
const { creatID } = useParams<{ creatID: string }>();
|
||||
const [searchParams] = useSearchParams();
|
||||
const flowVersion: 'v1' | 'v2' = searchParams.get('flow_version') === 'v2' ? 'v2' : 'v1';
|
||||
const { user } = useAuthStore();
|
||||
const { user, optimizeHoldCredits } = useAuthStore();
|
||||
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [creditCalculationData, setCreditCalculationData] = useState<any[]>([]);
|
||||
@@ -88,10 +88,10 @@ function InitialInfo() {
|
||||
if (previewVisible && previewType === 'video') {
|
||||
const playVideo = () => {
|
||||
if (videoRef.current) {
|
||||
videoRef.current.play().catch(() => {});
|
||||
videoRef.current.play().catch(() => { });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
if (videoRef.current) {
|
||||
if (videoRef.current.readyState >= 2) {
|
||||
playVideo();
|
||||
@@ -99,9 +99,9 @@ function InitialInfo() {
|
||||
videoRef.current.addEventListener('loadedmetadata', playVideo);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const timer = setTimeout(playVideo, 300);
|
||||
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
if (videoRef.current) {
|
||||
@@ -138,13 +138,14 @@ function InitialInfo() {
|
||||
status: apiSteps[index]?.status || '',
|
||||
id: apiSteps[index]?.id || index,
|
||||
output: apiSteps[index]?.output || '',
|
||||
input: apiSteps[index]?.input || {},
|
||||
engineId: apiSteps[index]?.input?.payload?.videoConfig?.engineId || '',
|
||||
}));
|
||||
|
||||
// 当apiSteps更新时,逆向遍历找到第一个已完成或失败的步骤并展开
|
||||
useEffect(() => {
|
||||
const completedFailedKeys = new Set<string>();
|
||||
|
||||
|
||||
activeKey.forEach(key => {
|
||||
const step = steps.find(s => String(s.childId) === key);
|
||||
if (step && (step.status === 'completed' || step.status === 'failed')) {
|
||||
@@ -290,7 +291,7 @@ function InitialInfo() {
|
||||
useEffect(() => {
|
||||
calculateCredits().then((data: any) => {
|
||||
setCreditCalculationData(data);
|
||||
}).catch(() => {});
|
||||
}).catch(() => { });
|
||||
}, []);
|
||||
|
||||
const calculateEstimatedCredits = () => {
|
||||
@@ -316,7 +317,7 @@ function InitialInfo() {
|
||||
}
|
||||
|
||||
let total = (videoDuration * 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);
|
||||
@@ -334,6 +335,56 @@ function InitialInfo() {
|
||||
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(() => {
|
||||
if (creditCalculationData.length > 0 && countType) {
|
||||
calculateEstimatedCredits();
|
||||
@@ -513,12 +564,12 @@ function InitialInfo() {
|
||||
}
|
||||
|
||||
removetwo(taskDetail.id, steps[1].id.toString(), params).then((res: any) => {
|
||||
message.info('正在生成图片,请稍候...');
|
||||
|
||||
message.info('正在生成图片,请稍候...');
|
||||
|
||||
refreshTaskDetail();
|
||||
|
||||
}).catch((error: any) => {
|
||||
const errorMsg = error?.message?.split(': ')?.[1] || error?.message || '生成失败';
|
||||
const errorMsg = error?.message?.split(': ')?.[1] || error?.message || '生成失败';
|
||||
message.error(errorMsg);
|
||||
});
|
||||
}
|
||||
@@ -737,7 +788,7 @@ function InitialInfo() {
|
||||
<div className="medio_box" style={{ aspectRatio: '16/9', background: 'linear-gradient(135deg, rgba(248,250,252,0.8) 0%, rgba(238,242,255,0.6) 100%)', borderRadius: 14, overflow: 'hidden', boxShadow: '0 4px 12px rgba(99, 102, 241, 0.06)', border: '1px solid rgba(99, 102, 241, 0.08)', cursor: taskDetail?.material?.materialVideoUrl ? 'pointer' : 'default' }} onClick={() => taskDetail?.material?.materialVideoUrl && openPreview(taskDetail.material.materialVideoUrl, 'video')}>
|
||||
{taskDetail?.material?.materialVideoUrl ? (
|
||||
<video
|
||||
|
||||
|
||||
src={buildMediaUrl(taskDetail.material.materialVideoUrl)}
|
||||
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
|
||||
/>
|
||||
@@ -786,7 +837,7 @@ function InitialInfo() {
|
||||
</span>
|
||||
<div className="medio_box" style={{ aspectRatio: '16/9', background: 'linear-gradient(135deg, rgba(248,250,252,0.8) 0%, rgba(238,242,255,0.6) 100%)', borderRadius: 14, overflow: 'hidden', boxShadow: '0 4px 12px rgba(99, 102, 241, 0.06)', border: '1px solid rgba(99, 102, 241, 0.08)', cursor: 'pointer' }} onClick={() => openPreview(taskDetail.finalVideoUrl, 'video')}>
|
||||
<video
|
||||
|
||||
|
||||
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${taskDetail.finalVideoUrl}`}
|
||||
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
|
||||
/>
|
||||
@@ -905,10 +956,46 @@ function InitialInfo() {
|
||||
)}
|
||||
</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 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 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>
|
||||
</Space>
|
||||
@@ -1335,21 +1422,21 @@ function InitialInfo() {
|
||||
>
|
||||
<Button
|
||||
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)'
|
||||
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)'
|
||||
}}
|
||||
onClick={() => {
|
||||
onClick={() => {
|
||||
if ((user?.credits || 0) < estimatedCredits) {
|
||||
message.warning('积分不足,请更换参数/充值积分');
|
||||
return;
|
||||
}
|
||||
handleNextStep(step.id);
|
||||
handleNextStep(step.id);
|
||||
}}
|
||||
disabled={step.status !== 'completed' || ((user?.credits || 0) < estimatedCredits)}
|
||||
>
|
||||
@@ -1370,8 +1457,37 @@ function InitialInfo() {
|
||||
</Text>
|
||||
</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' }}>
|
||||
<Button
|
||||
type="default"
|
||||
@@ -1383,26 +1499,47 @@ function InitialInfo() {
|
||||
查看/修改视频提词
|
||||
</Button>
|
||||
{isV2 && (
|
||||
<Button
|
||||
type="default"
|
||||
onClick={() => {
|
||||
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'}
|
||||
<Tooltip
|
||||
title={((user?.credits || 0) < optimizeHoldCredits) ? `积分不足${optimizeHoldCredits},请充值积分` : ''}
|
||||
placement="top"
|
||||
>
|
||||
重新生成视频提词
|
||||
</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
|
||||
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"
|
||||
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>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
</>
|
||||
)}
|
||||
@@ -1424,9 +1561,27 @@ function InitialInfo() {
|
||||
)}
|
||||
</div>
|
||||
<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'}>
|
||||
{step.status === 'failed' ? '重试生成' : '重新生成'}
|
||||
</Button>
|
||||
<Tooltip
|
||||
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>
|
||||
</Tooltip>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<DownloadOutlined />}
|
||||
|
||||
@@ -5,17 +5,20 @@ import * as api from '../api';
|
||||
interface AuthState {
|
||||
user: User | null;
|
||||
loading: boolean;
|
||||
optimizeHoldCredits: number;
|
||||
login: (username: string, password: string, captchaToken?: string, rememberMe?: boolean) => Promise<void>;
|
||||
logout: () => Promise<void>;
|
||||
checkAuth: () => Promise<void>;
|
||||
changePassword: (oldPwd: string, newPwd: string) => Promise<void>;
|
||||
refreshUser: () => Promise<void>;
|
||||
setUserCredits: (credits: number) => void;
|
||||
setOptimizeHoldCredits: (credits: number) => void;
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>((set) => ({
|
||||
user: null,
|
||||
loading: true,
|
||||
optimizeHoldCredits: 0,
|
||||
|
||||
login: async (username, password, captchaToken?, rememberMe?) => {
|
||||
const user = await api.login(username, password, captchaToken, rememberMe);
|
||||
@@ -31,8 +34,14 @@ export const useAuthStore = create<AuthState>((set) => ({
|
||||
try {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
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 });
|
||||
if (siteInfo.optimizeHoldCredits !== undefined) {
|
||||
set({ optimizeHoldCredits: siteInfo.optimizeHoldCredits });
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error?.message?.includes('401') || error?.message?.includes('Unauthorized')) {
|
||||
localStorage.removeItem('auth_token');
|
||||
@@ -59,4 +68,8 @@ export const useAuthStore = create<AuthState>((set) => ({
|
||||
setUserCredits: (credits: number) => {
|
||||
set((state) => (state.user ? { user: { ...state.user, credits } } : {}));
|
||||
},
|
||||
|
||||
setOptimizeHoldCredits: (credits: number) => {
|
||||
set({ optimizeHoldCredits: credits });
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user