完成sun分支功能
This commit is contained in:
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 272 KiB |
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 MiB |
+118
-118
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 459 KiB |
Vendored
+1
-1
@@ -27,7 +27,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<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" />
|
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
|
||||||
<title>民众智创</title>
|
<title>民众智创</title>
|
||||||
<script type="module" crossorigin src="/assets/index-BioNCQJL.js"></script>
|
<script type="module" crossorigin src="/assets/index-BuKny7Fe.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-IrbjYl3V.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-IrbjYl3V.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -1 +1,5 @@
|
|||||||
{}
|
{
|
||||||
|
"src/pages/InvoicePage.tsx": {
|
||||||
|
"description": "申请开票"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,7 +27,6 @@ import MaterialListPage from './pages/MaterialListPage';
|
|||||||
import RemoveInfo from './pages/RemoveInfo';
|
import RemoveInfo from './pages/RemoveInfo';
|
||||||
import RemoveRw from './pages/RemoveRw';
|
import RemoveRw from './pages/RemoveRw';
|
||||||
import HomePage from './pages/HomePage';
|
import HomePage from './pages/HomePage';
|
||||||
// import RemoveFenbu from './pages/RemoveFenbu';
|
|
||||||
import ConsumePage from './pages/ConsumePage';
|
import ConsumePage from './pages/ConsumePage';
|
||||||
import AuthorizationWaitingPage from './pages/AuthorizationWaitingPage';
|
import AuthorizationWaitingPage from './pages/AuthorizationWaitingPage';
|
||||||
import PopularPage from './pages/PopularPage';
|
import PopularPage from './pages/PopularPage';
|
||||||
@@ -35,6 +34,9 @@ import CreativePlazaPage from './pages/CreativePlazaPage';
|
|||||||
import TeamManagementPage from './pages/TeamManagementPage';
|
import TeamManagementPage from './pages/TeamManagementPage';
|
||||||
import JoinTeamPage from './pages/JoinTeamPage';
|
import JoinTeamPage from './pages/JoinTeamPage';
|
||||||
import PrivatePortraitAuthorizeResult from './pages/PrivatePortraitAuthorizeResult';
|
import PrivatePortraitAuthorizeResult from './pages/PrivatePortraitAuthorizeResult';
|
||||||
|
import InvoicePage from './pages/InvoicePage';
|
||||||
|
import VideovEditing from './pages/VideovEditing';
|
||||||
|
import VideoProjectList from './pages/VideoProjectList';
|
||||||
import { useAuthStore } from './store/useAuthStore';
|
import { useAuthStore } from './store/useAuthStore';
|
||||||
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
|
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
|
||||||
const { user, loading, checkAuth } = useAuthStore();
|
const { user, loading, checkAuth } = useAuthStore();
|
||||||
@@ -119,7 +121,6 @@ const App = () => {
|
|||||||
<Route path="removelens" element={<RemoveLens />} />
|
<Route path="removelens" element={<RemoveLens />} />
|
||||||
<Route path="removelens/:creatID/removeinfo" element={<RemoveInfo />} />
|
<Route path="removelens/:creatID/removeinfo" element={<RemoveInfo />} />
|
||||||
<Route path="removelens/:creatID/removefenbu" element={<RemoveRw />} />
|
<Route path="removelens/:creatID/removefenbu" element={<RemoveRw />} />
|
||||||
{/* <Route path="removelens/:creatID/removefenbu" element={<RemoveFenbu />} /> */}
|
|
||||||
<Route path="generated" element={<GeneratedRecord />} />
|
<Route path="generated" element={<GeneratedRecord />} />
|
||||||
<Route path="pretest" element={<PreTest />} />
|
<Route path="pretest" element={<PreTest />} />
|
||||||
<Route path="authorization" element={<AuthorizationPage />} />
|
<Route path="authorization" element={<AuthorizationPage />} />
|
||||||
@@ -131,7 +132,18 @@ const App = () => {
|
|||||||
<Route path="authacc" element={<AuthAccountPage />} />
|
<Route path="authacc" element={<AuthAccountPage />} />
|
||||||
<Route path="creativeplaza" element={<CreativePlazaPage />} />
|
<Route path="creativeplaza" element={<CreativePlazaPage />} />
|
||||||
<Route path="team-management" element={<TeamManagementPage />} />
|
<Route path="team-management" element={<TeamManagementPage />} />
|
||||||
|
<Route path="invoice" element={<InvoicePage />} />
|
||||||
|
<Route path="videoprojects" element={<VideoProjectList />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
{/* 视频剪辑页面:独立全屏路由,不使用 AppLayout */}
|
||||||
|
<Route
|
||||||
|
path="/videoediting"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute>
|
||||||
|
<VideovEditing />
|
||||||
|
</ProtectedRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route path="*" element={<Navigate to="/projects" replace />} />
|
<Route path="*" element={<Navigate to="/projects" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ import {
|
|||||||
MenuOutlined,
|
MenuOutlined,
|
||||||
ArrowLeftOutlined,
|
ArrowLeftOutlined,
|
||||||
RobotOutlined,
|
RobotOutlined,
|
||||||
|
ProfileOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { useAuthStore } from '../../store/useAuthStore';
|
import { useAuthStore } from '../../store/useAuthStore';
|
||||||
@@ -619,6 +620,7 @@ const AppLayout: React.FC = () => {
|
|||||||
...(user?.isTeamManager ? [{ key: 'teamManagement' as const, icon: <TeamOutlined style={{ color: '#6366f1' }} />, label: '团队管理' }] : []),
|
...(user?.isTeamManager ? [{ key: 'teamManagement' as const, icon: <TeamOutlined style={{ color: '#6366f1' }} />, label: '团队管理' }] : []),
|
||||||
{ key: 'myCredits', icon: <WalletOutlined />, label: '积分明细' },
|
{ key: 'myCredits', icon: <WalletOutlined />, label: '积分明细' },
|
||||||
{ key: 'orderRecords', icon: <FileTextOutlined />, label: '订单记录' },
|
{ key: 'orderRecords', icon: <FileTextOutlined />, label: '订单记录' },
|
||||||
|
{ key: 'invoice', icon: <ProfileOutlined />, label: '申请开票' },
|
||||||
{ key: 'messages', icon: <BellOutlined />, label: `消息中心${unreadCount > 0 ? `(${unreadCount})` : ''}` },
|
{ key: 'messages', icon: <BellOutlined />, label: `消息中心${unreadCount > 0 ? `(${unreadCount})` : ''}` },
|
||||||
...(operationManualUrl ? [{ key: 'manual' as const, icon: <FileTextOutlined />, label: '操作手册' }] : []),
|
...(operationManualUrl ? [{ key: 'manual' as const, icon: <FileTextOutlined />, label: '操作手册' }] : []),
|
||||||
{ type: 'divider' as const },
|
{ type: 'divider' as const },
|
||||||
@@ -635,6 +637,7 @@ const AppLayout: React.FC = () => {
|
|||||||
else if (key === 'teamManagement') { navigate('/team-management'); }
|
else if (key === 'teamManagement') { navigate('/team-management'); }
|
||||||
else if (key === 'myCredits') { navigate('/user-center?tab=credits'); }
|
else if (key === 'myCredits') { navigate('/user-center?tab=credits'); }
|
||||||
else if (key === 'orderRecords') { navigate('/user-center?tab=orders'); }
|
else if (key === 'orderRecords') { navigate('/user-center?tab=orders'); }
|
||||||
|
else if (key === 'invoice') { navigate('/invoice'); }
|
||||||
else if (key === 'manual') { window.open(operationManualUrl, '_blank'); }
|
else if (key === 'manual') { window.open(operationManualUrl, '_blank'); }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ body {
|
|||||||
|
|
||||||
.content_box {
|
.content_box {
|
||||||
margin: -24px -32px -32px;
|
margin: -24px -32px -32px;
|
||||||
border-radius: 0 0 16px 16px;
|
border-radius: 16px;
|
||||||
height: calc(100vh - 34px);
|
height: calc(100vh - 34px);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
|
|||||||
@@ -120,15 +120,64 @@ const GenerateConver: React.FC = () => {
|
|||||||
setVideoEngines(engines);
|
setVideoEngines(engines);
|
||||||
if (engines.length > 0) {
|
if (engines.length > 0) {
|
||||||
const first = engines[0];
|
const first = engines[0];
|
||||||
|
const firstMaxDur = (() => {
|
||||||
|
const raw = first.maxDuration ?? first.max_duration;
|
||||||
|
if (raw != null) return Number(raw);
|
||||||
|
const durs = first.supportedDurations;
|
||||||
|
if (Array.isArray(durs) && durs.length > 0) return Math.max(...durs);
|
||||||
|
return 15;
|
||||||
|
})();
|
||||||
|
const rawDurs = first.supportedDurations;
|
||||||
|
const generatedDurs = Array.from({ length: firstMaxDur - 4 }, (_, i) => i + 5);
|
||||||
|
const mergedDurs = Array.from(new Set([...(rawDurs || []), ...generatedDurs].sort((a, b) => a - b)));
|
||||||
setEngineId(first.id);
|
setEngineId(first.id);
|
||||||
setVideoAspectRatio(first.supportedRatios?.includes('9:16') ? '9:16' : first.supportedRatios?.[0] || '9:16');
|
setVideoAspectRatio(first.supportedRatios?.includes('9:16') ? '9:16' : first.supportedRatios?.[0] || '9:16');
|
||||||
setVideoResolution(first.supportedResolutions?.[0] || '480p');
|
setVideoResolution(first.supportedResolutions?.[0] || '480p');
|
||||||
setVideoDuration(first.supportedDurations?.[0] || 5);
|
const defaultDur = (rawDurs || []).find((d: number) => d <= firstMaxDur) ?? generatedDurs[0] ?? 5;
|
||||||
|
setVideoDuration(defaultDur);
|
||||||
|
setEngineOptions({
|
||||||
|
ratios: first.supportedRatios || ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'],
|
||||||
|
resolutions: first.supportedResolutions || ['480p', '720p', '1080p'],
|
||||||
|
durations: mergedDurs.length > 0 ? mergedDurs : generatedDurs,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}).catch(() => message.error('视频引擎加载失败'));
|
}).catch(() => message.error('视频引擎加载失败'));
|
||||||
calculateCredits().then((rules: any) => setCreditRules(Array.isArray(rules) ? rules : [])).catch(() => setCreditRules([]));
|
calculateCredits().then((rules: any) => setCreditRules(Array.isArray(rules) ? rules : [])).catch(() => setCreditRules([]));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 当引擎变化时,同步 engineOptions 的 durations 以匹配 maxDuration
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedEngine) return;
|
||||||
|
|
||||||
|
const engineMaxDur = (() => {
|
||||||
|
const raw = selectedEngine.maxDuration ?? selectedEngine.max_duration;
|
||||||
|
if (raw != null) return Number(raw);
|
||||||
|
const durs = selectedEngine.supportedDurations;
|
||||||
|
if (Array.isArray(durs) && durs.length > 0) return Math.max(...durs);
|
||||||
|
return 15;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const rawDurs = selectedEngine.supportedDurations;
|
||||||
|
const generatedDurs = Array.from({ length: Math.max(engineMaxDur - 4, 1) }, (_, i) => i + 5);
|
||||||
|
const mergedDurs = Array.from(new Set([...(rawDurs || []), ...generatedDurs].sort((a, b) => a - b)));
|
||||||
|
|
||||||
|
setEngineOptions(prev => {
|
||||||
|
const prevDurs = prev.durations;
|
||||||
|
const maxDur = prevDurs.length > 0 ? Math.max(...prevDurs) : 0;
|
||||||
|
if (maxDur === engineMaxDur) return prev;
|
||||||
|
return {
|
||||||
|
ratios: selectedEngine.supportedRatios || prev.ratios,
|
||||||
|
resolutions: selectedEngine.supportedResolutions || prev.resolutions,
|
||||||
|
durations: mergedDurs.length > 0 ? mergedDurs : generatedDurs,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
setVideoDuration(prev => {
|
||||||
|
if (prev <= engineMaxDur) return prev;
|
||||||
|
return (rawDurs || []).find((d: number) => d <= engineMaxDur) ?? generatedDurs[0] ?? 5;
|
||||||
|
});
|
||||||
|
}, [engineId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (e: MouseEvent) => {
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
const target = e.target as HTMLElement;
|
const target = e.target as HTMLElement;
|
||||||
@@ -142,6 +191,14 @@ const GenerateConver: React.FC = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const selectedEngine = videoEngines.find((item: any) => item.id === engineId);
|
const selectedEngine = videoEngines.find((item: any) => item.id === engineId);
|
||||||
|
const currentMaxDuration = (() => {
|
||||||
|
if (!selectedEngine) return 15;
|
||||||
|
const raw = selectedEngine.maxDuration ?? selectedEngine.max_duration;
|
||||||
|
if (raw != null) return Number(raw);
|
||||||
|
const durs = selectedEngine.supportedDurations;
|
||||||
|
if (Array.isArray(durs) && durs.length > 0) return Math.max(...durs);
|
||||||
|
return 15;
|
||||||
|
})();
|
||||||
const selectedEngineSupportsImage = !imageUrl || supportsReferenceImage(selectedEngine);
|
const selectedEngineSupportsImage = !imageUrl || supportsReferenceImage(selectedEngine);
|
||||||
const estimatedCredits = (() => {
|
const estimatedCredits = (() => {
|
||||||
const rule = creditRules.find((item: any) => item.modelConfigId === engineId && item.genType === 'video' && item.resolution === videoResolution);
|
const rule = creditRules.find((item: any) => item.modelConfigId === engineId && item.genType === 'video' && item.resolution === videoResolution);
|
||||||
@@ -301,8 +358,8 @@ const GenerateConver: React.FC = () => {
|
|||||||
resolve(false);
|
resolve(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (video.duration >= 16) {
|
if (video.duration > currentMaxDuration) {
|
||||||
message.error('视频时长不能超过15秒');
|
message.error(`视频时长不能超过${currentMaxDuration}秒`);
|
||||||
URL.revokeObjectURL(video.src);
|
URL.revokeObjectURL(video.src);
|
||||||
resolve(false);
|
resolve(false);
|
||||||
return;
|
return;
|
||||||
@@ -888,6 +945,7 @@ const GenerateConver: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div className="replication-form" style={{
|
<div className="replication-form" style={{
|
||||||
overflow: 'auto', width: '30%',
|
overflow: 'auto', width: '30%',
|
||||||
background: 'transparent',
|
background: 'transparent',
|
||||||
@@ -903,11 +961,15 @@ const GenerateConver: React.FC = () => {
|
|||||||
background: 'rgba(258, 250, 252, 0.2)',
|
background: 'rgba(258, 250, 252, 0.2)',
|
||||||
padding: 24, overflowY: 'auto'
|
padding: 24, overflowY: 'auto'
|
||||||
}}>
|
}}>
|
||||||
|
|
||||||
{/* 上传视频 */}
|
{/* 上传视频 */}
|
||||||
<div style={{ marginBottom: 16 }}>
|
<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 }}>
|
||||||
上传复刻视频
|
上传复刻视频
|
||||||
<span style={{ color: '#f94444' }}>(必选)</span>
|
<span style={{ color: '#f94444' }}>(必选)</span>
|
||||||
|
<p style={{ fontSize: 12, color: '#64748b', fontWeight: 400, }}>
|
||||||
|
当前引擎最多可上传 {currentMaxDuration} 秒
|
||||||
|
</p>
|
||||||
</p>
|
</p>
|
||||||
{videoUrl ? (
|
{videoUrl ? (
|
||||||
<div style={{ position: 'relative' }}>
|
<div style={{ position: 'relative' }}>
|
||||||
@@ -982,7 +1044,7 @@ const GenerateConver: React.FC = () => {
|
|||||||
上传中...
|
上传中...
|
||||||
</p>
|
</p>
|
||||||
<p style={{ margin: 0, fontSize: 11, color: '#94a3b8', marginTop: 4 }}>
|
<p style={{ margin: 0, fontSize: 11, color: '#94a3b8', marginTop: 4 }}>
|
||||||
支持的文件类型:MP4、MOV | 视频最大时长:15 秒 | 最大大小:50M
|
支持的文件类型:MP4、MOV | 视频最大时长:{currentMaxDuration} 秒 | 最大大小:50M
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -1003,7 +1065,7 @@ const GenerateConver: React.FC = () => {
|
|||||||
点击或拖拽上传视频
|
点击或拖拽上传视频
|
||||||
</p>
|
</p>
|
||||||
<p style={{ margin: 0, fontSize: 11, color: '#94a3b8', marginTop: 4 }}>
|
<p style={{ margin: 0, fontSize: 11, color: '#94a3b8', marginTop: 4 }}>
|
||||||
支持的文件类型:MP4、MOV | 视频最大时长:15 秒 | 最大大小:50M
|
支持的文件类型:MP4、MOV | 视频最大时长:{currentMaxDuration} 秒 | 最大大小:50M
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -1048,7 +1110,7 @@ const GenerateConver: React.FC = () => {
|
|||||||
>
|
>
|
||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<UploadSelector
|
<UploadSelector
|
||||||
@@ -1149,8 +1211,8 @@ const GenerateConver: React.FC = () => {
|
|||||||
border: '1px solid rgba(99, 102, 241, 0.15)',
|
border: '1px solid rgba(99, 102, 241, 0.15)',
|
||||||
background: 'rgba(255,255,255,0.8)',
|
background: 'rgba(255,255,255,0.8)',
|
||||||
}}
|
}}
|
||||||
maxLength={10}
|
maxLength={50}
|
||||||
suffix={<span style={{ color: '#94a3b8', fontSize: 12 }}>{originalProductName.length}/10</span>}
|
suffix={<span style={{ color: '#94a3b8', fontSize: 12 }}>{originalProductName.length}/50</span>}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1171,8 +1233,8 @@ const GenerateConver: React.FC = () => {
|
|||||||
border: '1px solid rgba(99, 102, 241, 0.15)',
|
border: '1px solid rgba(99, 102, 241, 0.15)',
|
||||||
background: 'rgba(255,255,255,0.8)',
|
background: 'rgba(255,255,255,0.8)',
|
||||||
}}
|
}}
|
||||||
maxLength={10}
|
maxLength={50}
|
||||||
suffix={<span style={{ color: '#94a3b8', fontSize: 12 }}>{ownProductName.length}/10</span>}
|
suffix={<span style={{ color: '#94a3b8', fontSize: 12 }}>{ownProductName.length}/50</span>}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1196,16 +1258,16 @@ const GenerateConver: React.FC = () => {
|
|||||||
background: 'rgba(255,255,255,0.8)',
|
background: 'rgba(255,255,255,0.8)',
|
||||||
}}
|
}}
|
||||||
rows={3}
|
rows={3}
|
||||||
maxLength={30}
|
maxLength={200}
|
||||||
/>
|
/>
|
||||||
<span style={{ position: 'absolute', right: 10, bottom: 8, color: '#94a3b8', fontSize: 12 }}>
|
<span style={{ position: 'absolute', right: 10, bottom: 8, color: '#94a3b8', fontSize: 12 }}>
|
||||||
{productSellingPoints.length}/30
|
{productSellingPoints.length}/200
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginBottom: 12, display: 'grid', gap: 10 }}>
|
<div style={{ marginBottom: 12, display: 'grid', gap: 10 }}>
|
||||||
<p style={{ margin: 0, fontSize: 13, fontWeight: 600, color: '#475569' }}>视频生成参数
|
<p style={{ margin: 0, fontSize: 14, fontWeight: 600, color: '#1e293b' }}>视频生成参数
|
||||||
<span style={{ color: '#f94444' }}>(必选)</span>
|
<span style={{ color: '#f94444' }}>(必选)</span>
|
||||||
</p>
|
</p>
|
||||||
<div style={{ width: '100%', display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
|
<div style={{ width: '100%', display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
|
||||||
@@ -1246,7 +1308,7 @@ const GenerateConver: React.FC = () => {
|
|||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: 'calc(100% + 8px)',
|
bottom: 'calc(100% + 8px)',
|
||||||
left: -10,
|
left: -19,
|
||||||
width: 300,
|
width: 300,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
@@ -1278,10 +1340,20 @@ const GenerateConver: React.FC = () => {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEngineId(engine.id);
|
setEngineId(engine.id);
|
||||||
|
const engineMaxDur = (() => {
|
||||||
|
const raw = engine.maxDuration ?? engine.max_duration;
|
||||||
|
if (raw != null) return Number(raw);
|
||||||
|
const durs = engine.supportedDurations;
|
||||||
|
if (Array.isArray(durs) && durs.length > 0) return Math.max(...durs);
|
||||||
|
return 15;
|
||||||
|
})();
|
||||||
|
const rawDurs = engine.supportedDurations;
|
||||||
|
const generatedDurs = Array.from({ length: engineMaxDur - 4 }, (_, i) => i + 5);
|
||||||
|
const newDurations = Array.from(new Set([...(rawDurs || []), ...generatedDurs].sort((a, b) => a - b)));
|
||||||
setEngineOptions({
|
setEngineOptions({
|
||||||
ratios: engine.supportedRatios || ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'],
|
ratios: engine.supportedRatios || ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'],
|
||||||
resolutions: engine.supportedResolutions || ['480p', '720p', '1080p'],
|
resolutions: engine.supportedResolutions || ['480p', '720p', '1080p'],
|
||||||
durations: engine.supportedDurations || [5, 8, 10, 12, 15],
|
durations: newDurations,
|
||||||
});
|
});
|
||||||
if (!engine.supportedRatios?.includes(videoAspectRatio)) {
|
if (!engine.supportedRatios?.includes(videoAspectRatio)) {
|
||||||
setVideoAspectRatio(engine.supportedRatios?.[0] || '16:9');
|
setVideoAspectRatio(engine.supportedRatios?.[0] || '16:9');
|
||||||
@@ -1289,8 +1361,9 @@ const GenerateConver: React.FC = () => {
|
|||||||
if (!engine.supportedResolutions?.includes(videoResolution)) {
|
if (!engine.supportedResolutions?.includes(videoResolution)) {
|
||||||
setVideoResolution(engine.supportedResolutions?.[0] || '720p');
|
setVideoResolution(engine.supportedResolutions?.[0] || '720p');
|
||||||
}
|
}
|
||||||
if (!engine.supportedDurations?.includes(videoDuration)) {
|
if (!newDurations.includes(videoDuration) || videoDuration > engineMaxDur) {
|
||||||
setVideoDuration(engine.supportedDurations?.[0] || 5);
|
const validDur = newDurations.find(d => d <= engineMaxDur) ?? newDurations[0] ?? 5;
|
||||||
|
setVideoDuration(validDur);
|
||||||
}
|
}
|
||||||
setShowEngineModal(false);
|
setShowEngineModal(false);
|
||||||
}}
|
}}
|
||||||
@@ -1322,11 +1395,6 @@ const GenerateConver: React.FC = () => {
|
|||||||
}}>
|
}}>
|
||||||
{engine.name}
|
{engine.name}
|
||||||
</span>
|
</span>
|
||||||
<span style={{
|
|
||||||
fontSize: 11,
|
|
||||||
color: '#9ca3af',
|
|
||||||
}}>
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -1436,13 +1504,13 @@ const GenerateConver: React.FC = () => {
|
|||||||
<span style={{
|
<span style={{
|
||||||
fontSize: 9,
|
fontSize: 9,
|
||||||
color: videoAspectRatio === ratio
|
color: videoAspectRatio === ratio
|
||||||
? '#6366f1'
|
? '#6366f1'
|
||||||
: '#6b7280',
|
: '#6b7280',
|
||||||
fontWeight: videoAspectRatio === ratio ? 600 : 400,
|
fontWeight: videoAspectRatio === ratio ? 600 : 400,
|
||||||
}}>
|
}}>
|
||||||
{ratio}
|
{ratio}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@@ -1477,15 +1545,15 @@ const GenerateConver: React.FC = () => {
|
|||||||
height: 6,
|
height: 6,
|
||||||
borderRadius: 3,
|
borderRadius: 3,
|
||||||
background: '#6366f1',
|
background: '#6366f1',
|
||||||
width: `${((videoDuration - Math.min(...engineOptions.durations)) / (Math.max(...engineOptions.durations) - Math.min(...engineOptions.durations))) * 100}%`,
|
width: `${((videoDuration - Math.min(...engineOptions.durations)) / (currentMaxDuration - Math.min(...engineOptions.durations))) * 100}%`,
|
||||||
transform: 'translateY(-50%)',
|
transform: 'translateY(-50%)',
|
||||||
}} />
|
}} />
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
min={Math.min(...engineOptions.durations)}
|
min={Math.min(...engineOptions.durations)}
|
||||||
max={Math.max(...engineOptions.durations)}
|
max={currentMaxDuration}
|
||||||
value={videoDuration}
|
value={videoDuration}
|
||||||
onChange={(e) => setVideoDuration(Number(e.target.value))}
|
onChange={(e) => setVideoDuration(Math.min(Number(e.target.value), currentMaxDuration))}
|
||||||
style={{
|
style={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -1567,6 +1635,8 @@ const GenerateConver: React.FC = () => {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* 立即生成按钮 */}
|
{/* 立即生成按钮 */}
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={((user?.credits || 0) < (estimatedCredits + optimizeHoldCredits)) ? '积分不足,请更换参数/充值积分' : ''}
|
title={((user?.credits || 0) < (estimatedCredits + optimizeHoldCredits)) ? '积分不足,请更换参数/充值积分' : ''}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -115,18 +115,76 @@ function RemoveInfo() {
|
|||||||
setVideoEngines(engines);
|
setVideoEngines(engines);
|
||||||
if (engines.length > 0) {
|
if (engines.length > 0) {
|
||||||
const first = engines[0];
|
const first = engines[0];
|
||||||
|
const firstMaxDur = (() => {
|
||||||
|
const raw = first.maxDuration ?? first.max_duration;
|
||||||
|
if (raw != null) return Number(raw);
|
||||||
|
const durs = first.supportedDurations;
|
||||||
|
if (Array.isArray(durs) && durs.length > 0) return Math.max(...durs);
|
||||||
|
return 15;
|
||||||
|
})();
|
||||||
|
const rawDurs = first.supportedDurations;
|
||||||
|
const generatedDurs = Array.from({ length: Math.max(firstMaxDur - 4, 1) }, (_, i) => i + 5);
|
||||||
|
const mergedDurs = Array.from(new Set([...(rawDurs || []), ...generatedDurs].sort((a, b) => a - b)));
|
||||||
setEngineId(first.id);
|
setEngineId(first.id);
|
||||||
setVideoAspectRatio(first.supportedRatios?.includes('9:16') ? '9:16' : first.supportedRatios?.[0] || '9:16');
|
setVideoAspectRatio(first.supportedRatios?.includes('9:16') ? '9:16' : first.supportedRatios?.[0] || '9:16');
|
||||||
setVideoResolution(first.supportedResolutions?.[0] || '480p');
|
setVideoResolution(first.supportedResolutions?.[0] || '480p');
|
||||||
setVideoDuration(first.supportedDurations?.[0] || 5);
|
const defaultDur = (rawDurs || []).find((d: number) => d <= firstMaxDur) ?? generatedDurs[0] ?? 5;
|
||||||
|
setVideoDuration(defaultDur);
|
||||||
|
setEngineOptions({
|
||||||
|
ratios: first.supportedRatios || ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'],
|
||||||
|
resolutions: first.supportedResolutions || ['480p', '720p', '1080p'],
|
||||||
|
durations: mergedDurs.length > 0 ? mergedDurs : generatedDurs,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}).catch(() => message.error('视频引擎加载失败'));
|
}).catch(() => message.error('视频引擎加载失败'));
|
||||||
calculateCredits().then((rules: any) => setCreditRules(Array.isArray(rules) ? rules : [])).catch(() => setCreditRules([]));
|
calculateCredits().then((rules: any) => setCreditRules(Array.isArray(rules) ? rules : [])).catch(() => setCreditRules([]));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 当引擎变化时,同步 engineOptions 的 durations 以匹配 maxDuration
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedEngine) return;
|
||||||
|
|
||||||
|
const engineMaxDur = (() => {
|
||||||
|
const raw = selectedEngine.maxDuration ?? selectedEngine.max_duration;
|
||||||
|
if (raw != null) return Number(raw);
|
||||||
|
const durs = selectedEngine.supportedDurations;
|
||||||
|
if (Array.isArray(durs) && durs.length > 0) return Math.max(...durs);
|
||||||
|
return 15;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const rawDurs = selectedEngine.supportedDurations;
|
||||||
|
const generatedDurs = Array.from({ length: Math.max(engineMaxDur - 4, 1) }, (_, i) => i + 5);
|
||||||
|
const mergedDurs = Array.from(new Set([...(rawDurs || []), ...generatedDurs].sort((a, b) => a - b)));
|
||||||
|
|
||||||
|
setEngineOptions(prev => {
|
||||||
|
const prevDurs = prev.durations;
|
||||||
|
const maxDur = prevDurs.length > 0 ? Math.max(...prevDurs) : 0;
|
||||||
|
if (maxDur === engineMaxDur) return prev;
|
||||||
|
return {
|
||||||
|
ratios: selectedEngine.supportedRatios || prev.ratios,
|
||||||
|
resolutions: selectedEngine.supportedResolutions || prev.resolutions,
|
||||||
|
durations: mergedDurs.length > 0 ? mergedDurs : generatedDurs,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
setVideoDuration(prev => {
|
||||||
|
if (prev <= engineMaxDur) return prev;
|
||||||
|
return (rawDurs || []).find((d: number) => d <= engineMaxDur) ?? generatedDurs[0] ?? 5;
|
||||||
|
});
|
||||||
|
}, [engineId]);
|
||||||
|
|
||||||
const createIdempotencyRef = useRef<{ segmentId: string; key: string } | null>(null);
|
const createIdempotencyRef = useRef<{ segmentId: string; key: string } | null>(null);
|
||||||
|
|
||||||
const selectedEngine = videoEngines.find((item: any) => item.id === engineId);
|
const selectedEngine = videoEngines.find((item: any) => item.id === engineId);
|
||||||
|
// 当前选中引擎支持的最大时长
|
||||||
|
const currentMaxDuration = (() => {
|
||||||
|
if (!selectedEngine) return 15;
|
||||||
|
const raw = selectedEngine.maxDuration ?? selectedEngine.max_duration;
|
||||||
|
if (raw != null) return Number(raw);
|
||||||
|
const durs = selectedEngine.supportedDurations;
|
||||||
|
if (Array.isArray(durs) && durs.length > 0) return Math.max(...durs);
|
||||||
|
return 15;
|
||||||
|
})();
|
||||||
const selectedEngineSupportsImage = !productImage || supportsReferenceImage(selectedEngine);
|
const selectedEngineSupportsImage = !productImage || supportsReferenceImage(selectedEngine);
|
||||||
const estimatedCredits = useMemo(() => {
|
const estimatedCredits = useMemo(() => {
|
||||||
const rule = creditRules.find((item: any) => item.modelConfigId === engineId && item.genType === 'video' && item.resolution === videoResolution);
|
const rule = creditRules.find((item: any) => item.modelConfigId === engineId && item.genType === 'video' && item.resolution === videoResolution);
|
||||||
@@ -145,7 +203,17 @@ function RemoveInfo() {
|
|||||||
if (!engine) return;
|
if (!engine) return;
|
||||||
setVideoAspectRatio(engine.supportedRatios?.[0] || '16:9');
|
setVideoAspectRatio(engine.supportedRatios?.[0] || '16:9');
|
||||||
setVideoResolution(engine.supportedResolutions?.[0] || '480p');
|
setVideoResolution(engine.supportedResolutions?.[0] || '480p');
|
||||||
setVideoDuration(engine.supportedDurations?.[0] || 5);
|
const engineMaxDur = (() => {
|
||||||
|
const raw = engine.maxDuration ?? engine.max_duration;
|
||||||
|
if (raw != null) return Number(raw);
|
||||||
|
const durs = engine.supportedDurations;
|
||||||
|
if (Array.isArray(durs) && durs.length > 0) return Math.max(...durs);
|
||||||
|
return 15;
|
||||||
|
})();
|
||||||
|
const rawDurs = engine.supportedDurations;
|
||||||
|
const generatedDurs = Array.from({ length: Math.max(engineMaxDur - 4, 1) }, (_, i) => i + 5);
|
||||||
|
const mergedDurs = Array.from(new Set([...(rawDurs || []), ...generatedDurs].sort((a, b) => a - b)));
|
||||||
|
setVideoDuration(mergedDurs.find((d: number) => d <= engineMaxDur) ?? mergedDurs[0] ?? 5);
|
||||||
};
|
};
|
||||||
|
|
||||||
const autoSplitButtonText = useMemo(() => {
|
const autoSplitButtonText = useMemo(() => {
|
||||||
@@ -744,6 +812,90 @@ function RemoveInfo() {
|
|||||||
},
|
},
|
||||||
], []);
|
], []);
|
||||||
|
|
||||||
|
// 将表格部分用 useMemo 缓存,避免输入字段变化时触发 VideoCell 重新挂载
|
||||||
|
const tableSection = useMemo(() => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
background: '#fff',
|
||||||
|
borderRadius: 16,
|
||||||
|
overflow: 'hidden',
|
||||||
|
border: '1px solid rgba(99, 102, 241, 0.1)',
|
||||||
|
boxShadow: '0 8px 32px rgba(99, 102, 241, 0.08)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ padding: '16px 24px', borderBottom: '1px solid rgba(99, 102, 241, 0.1)', background: 'linear-gradient(135deg, rgba(99,102,241,0.02) 0%, transparent 100%)' }}>
|
||||||
|
<h3 style={{ fontSize: 16, fontWeight: 600, color: '#1e293b', margin: 0 }}>拆镜片段列表</h3>
|
||||||
|
<span style={{ fontSize: 12, color: '#64748b', marginLeft: 12 }}>共 {tableData.length} 个片段</span>
|
||||||
|
</div>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={tableData}
|
||||||
|
pagination={{
|
||||||
|
current: currentPage,
|
||||||
|
pageSize: pageSize,
|
||||||
|
total: total,
|
||||||
|
onChange: (page, size) => {
|
||||||
|
setCurrentPage(page);
|
||||||
|
setPageSize(size);
|
||||||
|
},
|
||||||
|
showSizeChanger: true,
|
||||||
|
showTotal: (total) => `共 ${total} 条`,
|
||||||
|
}}
|
||||||
|
bordered={false}
|
||||||
|
rowKey="id"
|
||||||
|
scroll={{ y: 300 }}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
components={{
|
||||||
|
body: {
|
||||||
|
row: ({ className, style, ...rest }) => (
|
||||||
|
<tr
|
||||||
|
{...rest}
|
||||||
|
className={className}
|
||||||
|
style={{
|
||||||
|
...style,
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
borderBottom: '1px solid rgba(99, 102, 241, 0.05)',
|
||||||
|
height: 80,
|
||||||
|
overflow: 'auto',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
cell: ({ className, style, ...rest }) => (
|
||||||
|
<td
|
||||||
|
{...rest}
|
||||||
|
className={className}
|
||||||
|
style={{
|
||||||
|
...style,
|
||||||
|
padding: '16px 24px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
cell: ({ className, style, ...rest }) => (
|
||||||
|
<th
|
||||||
|
{...rest}
|
||||||
|
className={className}
|
||||||
|
style={{
|
||||||
|
...style,
|
||||||
|
background: 'rgba(255, 255, 255, 1)',
|
||||||
|
color: '#64748b',
|
||||||
|
fontWeight: 500,
|
||||||
|
fontSize: 13,
|
||||||
|
padding: '16px 24px',
|
||||||
|
borderBottom: 'none',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
), [tableData, columns, currentPage, pageSize, total]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
@@ -1028,87 +1180,7 @@ function RemoveInfo() {
|
|||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
{tableSection}
|
||||||
style={{
|
|
||||||
flex: 2,
|
|
||||||
background: 'linear-gradient(135deg, rgba(255,255,255,0.9) 0%, rgba(255,255,255,0.7) 100%)',
|
|
||||||
backdropFilter: 'blur(20px)',
|
|
||||||
borderRadius: 20,
|
|
||||||
overflow: 'hidden',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
border: '1px solid rgba(99, 102, 241, 0.1)',
|
|
||||||
boxShadow: '0 8px 32px rgba(99, 102, 241, 0.08)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ padding: '16px 24px', borderBottom: '1px solid rgba(99, 102, 241, 0.1)', background: 'linear-gradient(135deg, rgba(99,102,241,0.02) 0%, transparent 100%)' }}>
|
|
||||||
<h3 style={{ fontSize: 16, fontWeight: 600, color: '#1e293b', margin: 0 }}>拆镜片段列表</h3>
|
|
||||||
<span style={{ fontSize: 12, color: '#64748b', marginLeft: 12 }}>共 {tableData.length} 个片段</span>
|
|
||||||
</div>
|
|
||||||
<Table
|
|
||||||
columns={columns}
|
|
||||||
dataSource={tableData}
|
|
||||||
pagination={{
|
|
||||||
current: currentPage,
|
|
||||||
pageSize: pageSize,
|
|
||||||
total: total,
|
|
||||||
onChange: (page, size) => {
|
|
||||||
setCurrentPage(page);
|
|
||||||
setPageSize(size);
|
|
||||||
},
|
|
||||||
showSizeChanger: true,
|
|
||||||
showTotal: (total) => `共 ${total} 条`,
|
|
||||||
}}
|
|
||||||
bordered={false}
|
|
||||||
rowKey="id"
|
|
||||||
scroll={{ y: 300 }}
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
components={{
|
|
||||||
body: {
|
|
||||||
row: ({ className, style, ...rest }) => (
|
|
||||||
<tr
|
|
||||||
{...rest}
|
|
||||||
className={className}
|
|
||||||
style={{
|
|
||||||
...style,
|
|
||||||
transition: 'all 0.2s ease',
|
|
||||||
borderBottom: '1px solid rgba(99, 102, 241, 0.05)',
|
|
||||||
height: 80,
|
|
||||||
overflow: 'auto',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
cell: ({ className, style, ...rest }) => (
|
|
||||||
<td
|
|
||||||
{...rest}
|
|
||||||
className={className}
|
|
||||||
style={{
|
|
||||||
...style,
|
|
||||||
padding: '16px 24px',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
header: {
|
|
||||||
cell: ({ className, style, ...rest }) => (
|
|
||||||
<th
|
|
||||||
{...rest}
|
|
||||||
className={className}
|
|
||||||
style={{
|
|
||||||
...style,
|
|
||||||
background: 'rgba(255, 255, 255, 1)',
|
|
||||||
color: '#64748b',
|
|
||||||
fontWeight: 500,
|
|
||||||
fontSize: 13,
|
|
||||||
padding: '16px 24px',
|
|
||||||
borderBottom: 'none',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
@@ -1211,7 +1283,7 @@ function RemoveInfo() {
|
|||||||
onChange={(e) => setProductName(e.target.value)}
|
onChange={(e) => setProductName(e.target.value)}
|
||||||
placeholder="请输入项目名称"
|
placeholder="请输入项目名称"
|
||||||
style={{ height: 44, borderRadius: 8, border: '1px solid rgba(99, 102, 241, 0.2)' }}
|
style={{ height: 44, borderRadius: 8, border: '1px solid rgba(99, 102, 241, 0.2)' }}
|
||||||
maxLength={10}
|
maxLength={50}
|
||||||
showCount
|
showCount
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -1225,7 +1297,7 @@ function RemoveInfo() {
|
|||||||
onChange={(e) => setProductSellingPoint(e.target.value)}
|
onChange={(e) => setProductSellingPoint(e.target.value)}
|
||||||
placeholder="请输入项目描述"
|
placeholder="请输入项目描述"
|
||||||
style={{ borderRadius: 8, border: '1px solid rgba(99, 102, 241, 0.2)' }}
|
style={{ borderRadius: 8, border: '1px solid rgba(99, 102, 241, 0.2)' }}
|
||||||
maxLength={100}
|
maxLength={200}
|
||||||
showCount
|
showCount
|
||||||
rows={3}
|
rows={3}
|
||||||
/>
|
/>
|
||||||
@@ -1305,10 +1377,20 @@ function RemoveInfo() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEngineId(engine.id);
|
setEngineId(engine.id);
|
||||||
|
const engineMaxDur = (() => {
|
||||||
|
const raw = engine.maxDuration ?? engine.max_duration;
|
||||||
|
if (raw != null) return Number(raw);
|
||||||
|
const durs = engine.supportedDurations;
|
||||||
|
if (Array.isArray(durs) && durs.length > 0) return Math.max(...durs);
|
||||||
|
return 15;
|
||||||
|
})();
|
||||||
|
const rawDurs = engine.supportedDurations;
|
||||||
|
const generatedDurs = Array.from({ length: Math.max(engineMaxDur - 4, 1) }, (_, i) => i + 5);
|
||||||
|
const newDurations = Array.from(new Set([...(rawDurs || []), ...generatedDurs].sort((a, b) => a - b)));
|
||||||
setEngineOptions({
|
setEngineOptions({
|
||||||
ratios: engine.supportedRatios || ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'],
|
ratios: engine.supportedRatios || ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'],
|
||||||
resolutions: engine.supportedResolutions || ['480p', '720p', '1080p'],
|
resolutions: engine.supportedResolutions || ['480p', '720p', '1080p'],
|
||||||
durations: engine.supportedDurations || [5, 8, 10, 12, 15],
|
durations: newDurations,
|
||||||
});
|
});
|
||||||
if (!engine.supportedRatios?.includes(videoAspectRatio)) {
|
if (!engine.supportedRatios?.includes(videoAspectRatio)) {
|
||||||
setVideoAspectRatio(engine.supportedRatios?.[0] || '16:9');
|
setVideoAspectRatio(engine.supportedRatios?.[0] || '16:9');
|
||||||
@@ -1316,8 +1398,9 @@ function RemoveInfo() {
|
|||||||
if (!engine.supportedResolutions?.includes(videoResolution)) {
|
if (!engine.supportedResolutions?.includes(videoResolution)) {
|
||||||
setVideoResolution(engine.supportedResolutions?.[0] || '720p');
|
setVideoResolution(engine.supportedResolutions?.[0] || '720p');
|
||||||
}
|
}
|
||||||
if (!engine.supportedDurations?.includes(videoDuration)) {
|
if (!newDurations.includes(videoDuration) || videoDuration > engineMaxDur) {
|
||||||
setVideoDuration(engine.supportedDurations?.[0] || 5);
|
const validDur = newDurations.find(d => d <= engineMaxDur) ?? newDurations[0] ?? 5;
|
||||||
|
setVideoDuration(validDur);
|
||||||
}
|
}
|
||||||
setShowEngineModal(false);
|
setShowEngineModal(false);
|
||||||
}}
|
}}
|
||||||
@@ -1504,15 +1587,15 @@ function RemoveInfo() {
|
|||||||
height: 6,
|
height: 6,
|
||||||
borderRadius: 3,
|
borderRadius: 3,
|
||||||
background: '#6366f1',
|
background: '#6366f1',
|
||||||
width: `${((videoDuration - Math.min(...engineOptions.durations)) / (Math.max(...engineOptions.durations) - Math.min(...engineOptions.durations))) * 100}%`,
|
width: `${((videoDuration - Math.min(...engineOptions.durations)) / (currentMaxDuration - Math.min(...engineOptions.durations))) * 100}%`,
|
||||||
transform: 'translateY(-50%)',
|
transform: 'translateY(-50%)',
|
||||||
}} />
|
}} />
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
min={Math.min(...engineOptions.durations)}
|
min={Math.min(...engineOptions.durations)}
|
||||||
max={Math.max(...engineOptions.durations)}
|
max={currentMaxDuration}
|
||||||
value={videoDuration}
|
value={videoDuration}
|
||||||
onChange={(e) => setVideoDuration(Number(e.target.value))}
|
onChange={(e) => setVideoDuration(Math.min(Number(e.target.value), currentMaxDuration))}
|
||||||
style={{
|
style={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
|||||||
@@ -0,0 +1,333 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Button, Card, Empty, Modal, Input, message, Dropdown } from 'antd';
|
||||||
|
import {
|
||||||
|
PlusOutlined,
|
||||||
|
VideoCameraOutlined,
|
||||||
|
EllipsisOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
FolderOpenOutlined,
|
||||||
|
ClockCircleOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
const { TextArea } = Input;
|
||||||
|
|
||||||
|
interface VideoProject {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VideoProjectList: React.FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [projects, setProjects] = useState<VideoProject[]>([
|
||||||
|
{
|
||||||
|
id: 'demo-1',
|
||||||
|
name: '示例项目',
|
||||||
|
description: '这是一个视频剪辑示例项目',
|
||||||
|
createdAt: dayjs().subtract(2, 'day').format('YYYY-MM-DD HH:mm'),
|
||||||
|
updatedAt: dayjs().subtract(1, 'day').format('YYYY-MM-DD HH:mm'),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const [createModalOpen, setCreateModalOpen] = useState(false);
|
||||||
|
const [renameModalOpen, setRenameModalOpen] = useState(false);
|
||||||
|
const [editingProject, setEditingProject] = useState<VideoProject | null>(null);
|
||||||
|
const [formValues, setFormValues] = useState({ name: '', description: '' });
|
||||||
|
|
||||||
|
// 创建项目
|
||||||
|
const handleCreate = () => {
|
||||||
|
if (!formValues.name.trim()) {
|
||||||
|
message.warning('请输入项目名称');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newProject: VideoProject = {
|
||||||
|
id: `proj-${Date.now()}`,
|
||||||
|
name: formValues.name.trim(),
|
||||||
|
description: formValues.description.trim(),
|
||||||
|
createdAt: dayjs().format('YYYY-MM-DD HH:mm'),
|
||||||
|
updatedAt: dayjs().format('YYYY-MM-DD HH:mm'),
|
||||||
|
};
|
||||||
|
setProjects(prev => [newProject, ...prev]);
|
||||||
|
setCreateModalOpen(false);
|
||||||
|
setFormValues({ name: '', description: '' });
|
||||||
|
message.success('项目创建成功');
|
||||||
|
// 跳转到剪辑页面
|
||||||
|
navigate(`/videoediting?id=${newProject.id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 打开重命名弹窗
|
||||||
|
const handleRenameClick = (project: VideoProject) => {
|
||||||
|
setEditingProject(project);
|
||||||
|
setFormValues({ name: project.name, description: project.description });
|
||||||
|
setRenameModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 确认重命名
|
||||||
|
const handleRename = () => {
|
||||||
|
if (!editingProject) return;
|
||||||
|
if (!formValues.name.trim()) {
|
||||||
|
message.warning('请输入项目名称');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setProjects(prev => prev.map(p =>
|
||||||
|
p.id === editingProject.id
|
||||||
|
? { ...p, name: formValues.name.trim(), description: formValues.description.trim(), updatedAt: dayjs().format('YYYY-MM-DD HH:mm') }
|
||||||
|
: p
|
||||||
|
));
|
||||||
|
setRenameModalOpen(false);
|
||||||
|
setEditingProject(null);
|
||||||
|
setFormValues({ name: '', description: '' });
|
||||||
|
message.success('项目已更新');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除项目
|
||||||
|
const handleDelete = (id: string) => {
|
||||||
|
setProjects(prev => prev.filter(p => p.id !== id));
|
||||||
|
message.success('项目已删除');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 打开项目
|
||||||
|
const handleOpenProject = (project: VideoProject) => {
|
||||||
|
navigate(`/videoediting?id=${project.id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 右上角操作菜单
|
||||||
|
const getDropdownItems = (project: VideoProject) => ({
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
key: 'rename',
|
||||||
|
icon: <EditOutlined />,
|
||||||
|
label: '重命名',
|
||||||
|
onClick: () => handleRenameClick(project),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'delete',
|
||||||
|
icon: <DeleteOutlined />,
|
||||||
|
label: '删除',
|
||||||
|
danger: true,
|
||||||
|
onClick: () => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认删除',
|
||||||
|
content: `确定要删除项目「${project.name}」吗?`,
|
||||||
|
okText: '删除',
|
||||||
|
cancelText: '取消',
|
||||||
|
okButtonProps: { danger: true },
|
||||||
|
onOk: () => handleDelete(project.id),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="content_box" style={{ background: '#fff',}}>
|
||||||
|
{/* 顶部标题栏 */}
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
|
||||||
|
<div>
|
||||||
|
<h2 style={{ margin: 0, fontSize: 22, fontWeight: 600, color: '#1e293b' }}>
|
||||||
|
视频剪辑项目
|
||||||
|
</h2>
|
||||||
|
<p style={{ margin: '4px 0 0', fontSize: 13, color: '#64748b' }}>
|
||||||
|
创建和管理你的视频剪辑项目
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
size="large"
|
||||||
|
onClick={() => {
|
||||||
|
setFormValues({ name: '', description: '' });
|
||||||
|
setCreateModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
创建项目
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 项目列表 */}
|
||||||
|
{projects.length === 0 ? (
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: 400 }}>
|
||||||
|
<Empty
|
||||||
|
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||||
|
description={
|
||||||
|
<span style={{ color: '#64748b' }}>
|
||||||
|
暂无项目,点击「创建项目」开始剪辑
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={() => {
|
||||||
|
setFormValues({ name: '', description: '' });
|
||||||
|
setCreateModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
创建项目
|
||||||
|
</Button>
|
||||||
|
</Empty>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))',
|
||||||
|
gap: 16,
|
||||||
|
}}>
|
||||||
|
{projects.map(project => (
|
||||||
|
<Card
|
||||||
|
key={project.id}
|
||||||
|
hoverable
|
||||||
|
style={{
|
||||||
|
borderRadius: 12,
|
||||||
|
overflow: 'hidden',
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
|
border: '1px solid #e8ecf2',
|
||||||
|
}}
|
||||||
|
bodyStyle={{ padding: 0 }}
|
||||||
|
>
|
||||||
|
{/* 缩略图区域 */}
|
||||||
|
<div
|
||||||
|
onClick={() => handleOpenProject(project)}
|
||||||
|
style={{
|
||||||
|
height: 160,
|
||||||
|
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
cursor: 'pointer',
|
||||||
|
position: 'relative',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<VideoCameraOutlined style={{ fontSize: 48, color: 'rgba(255,255,255,0.8)' }} />
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 8,
|
||||||
|
right: 8,
|
||||||
|
}}>
|
||||||
|
<Dropdown
|
||||||
|
menu={getDropdownItems(project)}
|
||||||
|
trigger={['click']}
|
||||||
|
placement="bottomRight"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<EllipsisOutlined style={{ color: '#fff', fontSize: 18 }} />}
|
||||||
|
style={{ background: 'rgba(0,0,0,0.3)', borderRadius: 6 }}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* 信息区域 */}
|
||||||
|
<div
|
||||||
|
onClick={() => handleOpenProject(project)}
|
||||||
|
style={{ padding: '14px 16px', cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
<div style={{
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: 600,
|
||||||
|
color: '#1e293b',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
marginBottom: 4,
|
||||||
|
}}>
|
||||||
|
{project.name}
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
fontSize: 12,
|
||||||
|
color: '#94a3b8',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
marginBottom: 8,
|
||||||
|
}}>
|
||||||
|
{project.description || '暂无描述'}
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12, fontSize: 12, color: '#94a3b8' }}>
|
||||||
|
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||||
|
<ClockCircleOutlined />
|
||||||
|
{project.updatedAt}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 创建项目弹窗 */}
|
||||||
|
<Modal
|
||||||
|
title="创建视频剪辑项目"
|
||||||
|
open={createModalOpen}
|
||||||
|
onOk={handleCreate}
|
||||||
|
onCancel={() => setCreateModalOpen(false)}
|
||||||
|
okText="创建并开始剪辑"
|
||||||
|
cancelText="取消"
|
||||||
|
width={480}
|
||||||
|
>
|
||||||
|
<div style={{ paddingTop: 8 }}>
|
||||||
|
<div style={{ marginBottom: 16 }}>
|
||||||
|
<label style={{ display: 'block', marginBottom: 6, fontSize: 13, fontWeight: 500, color: '#475569' }}>项目名称</label>
|
||||||
|
<Input
|
||||||
|
placeholder="请输入项目名称"
|
||||||
|
value={formValues.name}
|
||||||
|
onChange={e => setFormValues(prev => ({ ...prev, name: e.target.value }))}
|
||||||
|
maxLength={30}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: 6, fontSize: 13, fontWeight: 500, color: '#475569' }}>项目描述(可选)</label>
|
||||||
|
<TextArea
|
||||||
|
placeholder="请输入项目描述"
|
||||||
|
value={formValues.description}
|
||||||
|
onChange={e => setFormValues(prev => ({ ...prev, description: e.target.value }))}
|
||||||
|
rows={3}
|
||||||
|
maxLength={100}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* 重命名弹窗 */}
|
||||||
|
<Modal
|
||||||
|
title="编辑项目"
|
||||||
|
open={renameModalOpen}
|
||||||
|
onOk={handleRename}
|
||||||
|
onCancel={() => { setRenameModalOpen(false); setEditingProject(null); }}
|
||||||
|
okText="保存"
|
||||||
|
cancelText="取消"
|
||||||
|
width={480}
|
||||||
|
>
|
||||||
|
<div style={{ paddingTop: 8 }}>
|
||||||
|
<div style={{ marginBottom: 16 }}>
|
||||||
|
<label style={{ display: 'block', marginBottom: 6, fontSize: 13, fontWeight: 500, color: '#475569' }}>项目名称</label>
|
||||||
|
<Input
|
||||||
|
placeholder="请输入项目名称"
|
||||||
|
value={formValues.name}
|
||||||
|
onChange={e => setFormValues(prev => ({ ...prev, name: e.target.value }))}
|
||||||
|
maxLength={30}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block', marginBottom: 6, fontSize: 13, fontWeight: 500, color: '#475569' }}>项目描述(可选)</label>
|
||||||
|
<TextArea
|
||||||
|
placeholder="请输入项目描述"
|
||||||
|
value={formValues.description}
|
||||||
|
onChange={e => setFormValues(prev => ({ ...prev, description: e.target.value }))}
|
||||||
|
rows={3}
|
||||||
|
maxLength={100}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VideoProjectList;
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user