“爆款复刻/拆镜复刻积分不足提示”

This commit is contained in:
sjy
2026-07-15 13:38:33 +08:00
parent 4ade6bb347
commit b1a8c3ed5a
5 changed files with 464 additions and 152 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -28,7 +28,7 @@
}
})();
</script>
<script type="module" crossorigin src="/assets/index-nrhXZrQV.js"></script>
<script type="module" crossorigin src="/assets/index-Dq-lhFsY.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DviWdElm.css">
</head>
<body>
+101 -11
View File
@@ -31,6 +31,7 @@ import {
HeartOutlined,
CameraOutlined,
CalculatorOutlined,
CaretDownOutlined,
DollarOutlined,
FireOutlined,
CloudOutlined,
@@ -126,6 +127,7 @@ const StorageCard: React.FC<{ data: ResourceCapacityData | null }> = ({ data })
<div
style={{
// marginTop: 12,
marginBottom: 12,
padding: '0 12px',
paddingTop: 6,
borderRadius: 10,
@@ -906,33 +908,59 @@ const AppLayout: React.FC = () => {
<div style={{ padding: '16px 16px', flexShrink: 0, paddingTop: 0 }}>
<Dropdown menu={{ items: userMenuItems, onClick: handleUserMenuClick }} placement="topRight" arrow>
<div style={{
<div className="user-card-container" style={{
padding: '10px 14px',
display: 'flex', alignItems: 'center',
justifyContent: 'flex-start',
gap: 12,
// padding: '10px 14px',
borderRadius: 14, cursor: 'pointer',
transition: 'all 0.25s ease',
background: 'linear-gradient(135deg, rgba(248, 250, 252, 0.9) 0%, rgba(241, 245, 249, 0.9) 100%)',
boxShadow: '0 2px 12px rgba(0, 0, 0, 0.04)',
transition: 'all 0.35s cubic-bezier(0.4,0,0.2,1)',
background: 'linear-gradient(135deg, rgba(248, 250, 252, 0.95) 0%, rgba(241, 245, 249, 0.95) 100%)',
boxShadow: '0 2px 12px rgba(0, 0, 0, 0.04), inset 0 1px 0 rgba(255,255,255,0.8)',
position: 'relative',
border: '1px solid rgba(99, 102, 241, 0.15)',
animation: 'cardBreath 3s ease-in-out infinite',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'linear-gradient(135deg, #ffffff 0%, #f8fafc 100%)';
e.currentTarget.style.boxShadow = '0 4px 16px rgba(0, 0, 0, 0.08)';
e.currentTarget.style.boxShadow = '0 8px 24px rgba(99, 102, 241, 0.25), inset 0 1px 0 rgba(255,255,255,0.9)';
e.currentTarget.style.transform = 'translateY(-3px) scale(1.01)';
e.currentTarget.style.borderColor = 'rgba(99, 102, 241, 0.4)';
e.currentTarget.style.animation = 'none';
e.currentTarget.querySelector('.user-card-arrow')?.classList.add('arrow-hover');
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'linear-gradient(135deg, rgba(248, 250, 252, 0.9) 0%, rgba(241, 245, 249, 0.9) 100%)';
e.currentTarget.style.boxShadow = '0 2px 12px rgba(0, 0, 0, 0.04)';
e.currentTarget.style.background = 'linear-gradient(135deg, rgba(248, 250, 252, 0.95) 0%, rgba(241, 245, 249, 0.95) 100%)';
e.currentTarget.style.boxShadow = '0 2px 12px rgba(0, 0, 0, 0.04), inset 0 1px 0 rgba(255,255,255,0.8)';
e.currentTarget.style.transform = 'translateY(0) scale(1)';
e.currentTarget.style.borderColor = 'rgba(99, 102, 241, 0.15)';
e.currentTarget.style.animation = 'cardBreath 3s ease-in-out infinite';
e.currentTarget.querySelector('.user-card-arrow')?.classList.remove('arrow-hover');
}}
>
<div className="user-card-glow" style={{
position: 'absolute',
inset: -4,
borderRadius: 18,
background: 'linear-gradient(90deg, transparent 0%, rgba(99,102,241,0.15) 25%, rgba(139,92,246,0.15) 50%, rgba(99,102,241,0.15) 75%, transparent 100%)',
backgroundSize: '200% 100%',
opacity: 0.8,
animation: 'borderShimmer 2.5s linear infinite',
pointerEvents: 'none',
}} />
<Avatar size={36} icon={<UserOutlined />}
style={{
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
flexShrink: 0,
boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)',
transition: 'all 0.35s cubic-bezier(0.4,0,0.2,1)',
position: 'relative',
zIndex: 1,
animation: 'avatarPulse 2s ease-in-out infinite',
}} />
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ flex: 1, minWidth: 0, position: 'relative', zIndex: 1 }}>
<div style={{ color: '#1e293b', fontSize: 16, fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', letterSpacing: -0.01 }}>
{user?.username}
</div>
@@ -941,12 +969,74 @@ const AppLayout: React.FC = () => {
fontSize: 16,
fontWeight: 500,
letterSpacing: 0,
// background: 'rgba(99, 102, 241, 0.08)',
// padding: '2px 8px',
borderRadius: 6,
display: 'inline-block',
animation: 'creditGlow 1.5s ease-in-out infinite',
}}>: {user?.credits || 0}</div>
</div>
<div className="user-card-arrow" style={{
fontSize: 12,
color: '#6366f1',
flexShrink: 0,
position: 'relative',
zIndex: 1,
}}>
<MenuOutlined />
</div>
<style>{`
@keyframes arrowFlash {
0%, 100% {
transform: translateY(0) scale(1);
opacity: 0.6;
filter: drop-shadow(0 0 4px rgba(99, 102, 241, 0.4));
}
50% {
transform: translateY(4px) scale(1.1);
opacity: 1;
filter: drop-shadow(0 0 12px rgba(99, 102, 241, 0.7));
}
}
@keyframes creditGlow {
0%, 100% {
text-shadow: 0 0 0 transparent;
}
50% {
text-shadow: 0 0 15px rgba(99, 102, 241, 0.6), 0 0 30px rgba(99, 102, 241, 0.3);
}
}
@keyframes borderShimmer {
0% { background-position: 200% center; }
100% { background-position: -200% center; }
}
@keyframes cardBreath {
0%, 100% {
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04), inset 0 1px 0 rgba(255,255,255,0.8);
border-color: rgba(99, 102, 241, 0.15);
}
50% {
box-shadow: 0 4px 20px rgba(99, 102, 241, 0.1), inset 0 1px 0 rgba(255,255,255,0.8);
border-color: rgba(99, 102, 241, 0.25);
}
}
@keyframes avatarPulse {
0%, 100% {
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
transform: scale(1);
}
50% {
box-shadow: 0 6px 20px rgba(99, 102, 241, 0.5);
transform: scale(1.02);
}
}
.user-card-arrow.arrow-hover {
color: #8b5cf6 !important;
transform: rotate(180deg) scale(1.15) !important;
animation: none !important;
filter: drop-shadow(0 0 15px rgba(139, 92, 246, 0.8)) !important;
}
`}</style>
</div>
</Dropdown>
+108 -19
View File
@@ -1,8 +1,9 @@
import React, { useState, useEffect } from 'react';
import { Button, Typography, Collapse, Space, Modal, Input, Table, message } from 'antd';
import { ArrowLeftOutlined, PlayCircleOutlined, CheckCircleOutlined, EditOutlined, DownloadOutlined, SettingOutlined, LayoutOutlined } from '@ant-design/icons';
import { Button, Typography, Collapse, Space, Modal, Input, Table, message, Tooltip } from 'antd';
import { ArrowLeftOutlined, PlayCircleOutlined, CheckCircleOutlined, EditOutlined, DownloadOutlined, SettingOutlined, LayoutOutlined, WarningOutlined } from '@ant-design/icons';
import { useNavigate, useParams } from 'react-router-dom';
import { getReplicationList, getReplicationDetail, gettwo, getthree, getfour, getEngine, updateHotOpeningVideoPromptSchema, updateImagePrompt } from '../api/index';
import { getReplicationList, getReplicationDetail, gettwo, getthree, getfour, getEngine, updateHotOpeningVideoPromptSchema, updateImagePrompt, calculateCredits } from '../api/index';
import { useAuthStore } from '../store/useAuthStore';
import VideoPromptSchemaEditor from '../components/VideoPromptSchemaEditor';
import { validateVideoPromptSchemaByConfig } from '../utils/videoPromptSchema';
import './css/InitialInfo.css';
@@ -24,6 +25,7 @@ const buildMediaUrl = (url: string): string => {
function InitialInfo() {
const navigate = useNavigate();
const { creatID } = useParams<{ creatID: string }>();
const { user } = useAuthStore();
const [modalVisible, setModalVisible] = useState(false);
const [promptText, setPromptText] = useState('');
@@ -55,6 +57,9 @@ function InitialInfo() {
durations: [5, 8, 10, 12, 15],
});
const [creditCalculationData, setCreditCalculationData] = useState<any[]>([]);
const [estimatedCredits, setEstimatedCredits] = useState<number>(0);
// 创作记录表格数据
const [tableData, setTableData] = useState<any[]>([]);
// 分页状态
@@ -222,27 +227,89 @@ function InitialInfo() {
useEffect(() => {
getEngine()
.then((data: any) => {
setEnginesele(data.engine || {});
// 默认选中第一个视频引擎
if (data.engine?.video && data.engine.video.length > 0) {
setCountType(data.engine.video[0].id);
// 设置默认引擎参数
const firstEngine = data.engine.video[0];
const filteredVideoEngines = (data.engine?.video || []).filter((engine: any) => engine.supportsUniversalReference !== false);
const filteredImageEngines = (data.engine?.image || []).filter((engine: any) => engine.supportsUniversalReference !== false);
setEnginesele({
video: filteredVideoEngines,
image: filteredImageEngines,
});
if (filteredVideoEngines.length > 0) {
setCountType(filteredVideoEngines[0].id);
const firstEngine = filteredVideoEngines[0];
setEngineOptions({
ratios: firstEngine.supportedRatios || ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'],
resolutions: firstEngine.supportedResolutions || ['480p', '720p', '1080p'],
durations: firstEngine.supportedDurations || [5, 8, 10, 12, 15],
});
}
// 默认选中第一个图片引擎
if (data.engine?.image && data.engine.image.length > 0) {
setImageEngineId(data.engine.image[0].id);
if (filteredImageEngines.length > 0) {
setImageEngineId(filteredImageEngines[0].id);
}
})
.catch((error: any) => {
});
}, []);
useEffect(() => {
calculateCredits().then((data: any) => {
setCreditCalculationData(data);
}).catch(() => {});
}, []);
const calculateEstimatedCredits = () => {
let config: any = {};
config = creditCalculationData.find((item: any) =>
item.modelConfigId === countType &&
item.genType === 'video' &&
item.resolution === videoResolution
);
if (!config) {
config = {
perSecondCredits: 2,
baseCredits: 60,
ratio: 1.3,
inputVideoRatio: 1.3,
inputVideoBaseCredits: 0,
inputVideoPerSecondCredits: 15,
inputImageRatio: 1,
inputImageBaseCredits: 0,
inputImagePerImageCredits: 0.0,
};
if (videoResolution === '1080p') {
config.ratio = 1.3;
} else if (videoResolution === '720p') {
config.ratio = 1.3;
} else if (videoResolution === '480p') {
config.ratio = 1.3;
}
}
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);
total += inputVideoCost;
}
const inputImageCount = taskDetail?.videoGeneration?.inputMedia?.image?.length || 0;
if (inputImageCount > 0) {
const inputImageCost = ((config.inputImageBaseCredits || 0) + (config.inputImagePerImageCredits || 0) * inputImageCount) * (config.inputImageRatio || 1);
total += inputImageCost;
}
setEstimatedCredits(Number(total.toFixed(2)));
};
useEffect(() => {
if (creditCalculationData.length > 0 && countType) {
calculateEstimatedCredits();
}
}, [creditCalculationData, countType, videoDuration, videoResolution, taskDetail]);
// 组件卸载时清理定时器
useEffect(() => {
return () => {
@@ -1190,14 +1257,36 @@ function InitialInfo() {
)}
</div>
</div>
<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)' }}
onClick={() => { handleNextStep(step.id); }}
disabled={step.status !== 'completed'}
<Tooltip
title={((user?.credits || 0) < estimatedCredits) ? '积分不足,请更换参数/充值积分' : ''}
placement="top"
>
</Button>
<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)'
}}
onClick={() => {
if ((user?.credits || 0) < estimatedCredits) {
message.warning('积分不足,请更换参数/充值积分');
return;
}
handleNextStep(step.id);
}}
disabled={step.status !== 'completed' || ((user?.credits || 0) < estimatedCredits)}
>
<span style={{ color: '#fff', marginLeft: 8 }}>
:{estimatedCredits}
</span>
</Button>
</Tooltip>
</>
)}
{/* 步骤4: 生成视频提示词 */}
+98 -16
View File
@@ -1,8 +1,9 @@
import React, { useState, useEffect } from 'react';
import { Button, Typography, Collapse, Space, Modal, Input, Table, message } from 'antd';
import { Button, Typography, Collapse, Space, Modal, Input, Table, message, Tooltip } from 'antd';
import { ArrowLeftOutlined, PlayCircleOutlined, CheckCircleOutlined, EditOutlined, DownloadOutlined, SettingOutlined, LayoutOutlined } from '@ant-design/icons';
import { useNavigate, useParams } from 'react-router-dom';
import { getShotReplicationList, removeDetail, removeone, removetwo, removethree, removefour, getEngine, updateShotImagePrompt, updateShotVideoPromptSchema } from '../api/index';
import { getShotReplicationList, removeDetail, removeone, removetwo, removethree, removefour, getEngine, updateShotImagePrompt, updateShotVideoPromptSchema, calculateCredits } from '../api/index';
import { useAuthStore } from '../store/useAuthStore';
import VideoPromptSchemaEditor from '../components/VideoPromptSchemaEditor';
import { validateVideoPromptSchemaByConfig } from '../utils/videoPromptSchema';
import './css/InitialInfo.css';
@@ -24,8 +25,11 @@ const buildMediaUrl = (url: string): string => {
function InitialInfo() {
const navigate = useNavigate();
const { creatID } = useParams<{ creatID: string }>();
const { user } = useAuthStore();
const [modalVisible, setModalVisible] = useState(false);
const [creditCalculationData, setCreditCalculationData] = useState<any[]>([]);
const [estimatedCredits, setEstimatedCredits] = useState<number>(0);
const [promptText, setPromptText] = useState('');
const [isModalOpen, setIsModalOpen] = useState(false);
const [currentType, setCurrentType] = useState<string>('image');
@@ -128,7 +132,7 @@ function InitialInfo() {
// 当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')) {
@@ -190,12 +194,17 @@ function InitialInfo() {
useEffect(() => {
getEngine()
.then((data: any) => {
setEnginesele(data.engine || {});
const filteredVideoEngines = (data.engine?.video || []).filter((engine: any) => engine.supportsUniversalReference !== false);
const filteredImageEngines = (data.engine?.image || []).filter((engine: any) => engine.supportsUniversalReference !== false);
setEnginesele({
video: filteredVideoEngines,
image: filteredImageEngines,
});
// 默认选中第一个视频引擎
if (data.engine?.video && data.engine.video.length > 0) {
setCountType(data.engine.video[0].id);
if (filteredVideoEngines.length > 0) {
setCountType(filteredVideoEngines[0].id);
// 设置默认引擎参数
const firstEngine = data.engine.video[0];
const firstEngine = filteredVideoEngines[0];
setEngineOptions({
ratios: firstEngine.supportedRatios || ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'],
resolutions: firstEngine.supportedResolutions || ['480p', '720p', '1080p'],
@@ -203,8 +212,8 @@ function InitialInfo() {
});
}
// 默认选中第一个图片引擎
if (data.engine?.image && data.engine.image.length > 0) {
setImageEngineId(data.engine.image[0].id);
if (filteredImageEngines.length > 0) {
setImageEngineId(filteredImageEngines[0].id);
}
})
.catch((error: any) => {
@@ -275,6 +284,57 @@ function InitialInfo() {
}
}, [steps]);
useEffect(() => {
calculateCredits().then((data: any) => {
setCreditCalculationData(data);
}).catch(() => {});
}, []);
const calculateEstimatedCredits = () => {
let config: any = {};
config = creditCalculationData.find((item: any) =>
item.modelConfigId === countType &&
item.genType === 'video' &&
item.resolution === videoResolution
);
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 = (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);
total += inputVideoCost;
}
const inputImageCount = taskDetail?.videoGeneration?.inputMedia?.image?.length || 0;
if (inputImageCount > 0) {
const inputImageCost = ((config.inputImageBaseCredits || 0) + (config.inputImagePerImageCredits || 0) * inputImageCount) * (config.inputImageRatio || 1);
total += inputImageCost;
}
setEstimatedCredits(Number(total.toFixed(2)));
};
useEffect(() => {
if (creditCalculationData.length > 0 && countType) {
calculateEstimatedCredits();
}
}, [creditCalculationData, countType, videoDuration, videoResolution, taskDetail]);
const handleOpenModal = (prompt?: any, type?: string, stepId?: string | number, schemaConfigSnapshot?: any) => {
setCurrentType(type || 'image');
setEditingPromptStepId(stepId ? String(stepId) : '');
@@ -1188,14 +1248,36 @@ function InitialInfo() {
)}
</div>
</div>
<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)' }}
onClick={() => { handleNextStep(step.id); }}
disabled={step.status !== 'completed'}
<Tooltip
title={((user?.credits || 0) < estimatedCredits) ? '积分不足,请更换参数/充值积分' : ''}
placement="top"
>
</Button>
<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)'
}}
onClick={() => {
if ((user?.credits || 0) < estimatedCredits) {
message.warning('积分不足,请更换参数/充值积分');
return;
}
handleNextStep(step.id);
}}
disabled={step.status !== 'completed' || ((user?.credits || 0) < estimatedCredits)}
>
<span style={{ color: '#fff', marginLeft: 8 }}>
:{estimatedCredits}
</span>
</Button>
</Tooltip>
</>
)}
{/* 步骤4: 生成视频提示词 */}