Merge branch 'sun'
This commit is contained in:
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 1.0 MiB |
File diff suppressed because one or more lines are too long
-613
File diff suppressed because one or more lines are too long
Vendored
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 MiB |
Vendored
-14
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 897 KiB |
Vendored
-24
@@ -1,24 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<symbol id="bluesky-icon" viewBox="0 0 16 17">
|
||||
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
|
||||
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
|
||||
</symbol>
|
||||
<symbol id="discord-icon" viewBox="0 0 20 19">
|
||||
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
|
||||
</symbol>
|
||||
<symbol id="documentation-icon" viewBox="0 0 21 20">
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
|
||||
</symbol>
|
||||
<symbol id="github-icon" viewBox="0 0 19 19">
|
||||
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
<symbol id="social-icon" viewBox="0 0 20 20">
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
|
||||
</symbol>
|
||||
<symbol id="x-icon" viewBox="0 0 19 19">
|
||||
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.9 KiB |
Vendored
-36
@@ -1,36 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" id="favicon" />
|
||||
<script>
|
||||
// 立即从 localStorage 设置 favicon,避免闪烁
|
||||
(function() {
|
||||
try {
|
||||
var cached = localStorage.getItem('siteInfo');
|
||||
if (cached) {
|
||||
var info = JSON.parse(cached);
|
||||
if (info.siteLogo) {
|
||||
var link = document.getElementById('favicon');
|
||||
link.href = info.siteLogo;
|
||||
link.type = 'image/png';
|
||||
}
|
||||
if (info.siteName) {
|
||||
document.title = info.siteName;
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
<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 type="module" crossorigin src="/assets/index-MRCff8OF.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-KEztuQj6.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>␍
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,5 +1,10 @@
|
||||
{
|
||||
<<<<<<< HEAD
|
||||
"src/pages/CreativePlazaPage.tsx": {
|
||||
"description": "创意广场"
|
||||
=======
|
||||
"src/pages/InvoicePage.tsx": {
|
||||
"description": "申请开票"
|
||||
>>>>>>> sun
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,6 @@ import MaterialListPage from './pages/MaterialListPage';
|
||||
import RemoveInfo from './pages/RemoveInfo';
|
||||
import RemoveRw from './pages/RemoveRw';
|
||||
import HomePage from './pages/HomePage';
|
||||
// import RemoveFenbu from './pages/RemoveFenbu';
|
||||
import ConsumePage from './pages/ConsumePage';
|
||||
import AuthorizationWaitingPage from './pages/AuthorizationWaitingPage';
|
||||
import PopularPage from './pages/PopularPage';
|
||||
@@ -35,6 +34,9 @@ import CreativePlazaPage from './pages/CreativePlazaPage';
|
||||
import TeamManagementPage from './pages/TeamManagementPage';
|
||||
import JoinTeamPage from './pages/JoinTeamPage';
|
||||
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';
|
||||
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
|
||||
const { user, loading, checkAuth } = useAuthStore();
|
||||
@@ -119,7 +121,6 @@ const App = () => {
|
||||
<Route path="removelens" element={<RemoveLens />} />
|
||||
<Route path="removelens/:creatID/removeinfo" element={<RemoveInfo />} />
|
||||
<Route path="removelens/:creatID/removefenbu" element={<RemoveRw />} />
|
||||
{/* <Route path="removelens/:creatID/removefenbu" element={<RemoveFenbu />} /> */}
|
||||
<Route path="generated" element={<GeneratedRecord />} />
|
||||
<Route path="pretest" element={<PreTest />} />
|
||||
<Route path="authorization" element={<AuthorizationPage />} />
|
||||
@@ -131,7 +132,18 @@ const App = () => {
|
||||
<Route path="authacc" element={<AuthAccountPage />} />
|
||||
<Route path="creativeplaza" element={<CreativePlazaPage />} />
|
||||
<Route path="team-management" element={<TeamManagementPage />} />
|
||||
<Route path="invoice" element={<InvoicePage />} />
|
||||
<Route path="videoprojects" element={<VideoProjectList />} />
|
||||
</Route>
|
||||
{/* 视频剪辑页面:独立全屏路由,不使用 AppLayout */}
|
||||
<Route
|
||||
path="/videoediting"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<VideovEditing />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route path="*" element={<Navigate to="/projects" replace />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
|
||||
@@ -62,6 +62,7 @@ import {
|
||||
MenuOutlined,
|
||||
ArrowLeftOutlined,
|
||||
RobotOutlined,
|
||||
ProfileOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useAuthStore } from '../../store/useAuthStore';
|
||||
@@ -692,6 +693,7 @@ const AppLayout: React.FC = () => {
|
||||
...(user?.isTeamManager ? [{ key: 'teamManagement' as const, icon: <TeamOutlined style={{ color: '#6366f1' }} />, label: '团队管理' }] : []),
|
||||
{ key: 'myCredits', icon: <WalletOutlined />, label: '积分明细' },
|
||||
{ key: 'orderRecords', icon: <FileTextOutlined />, label: '订单记录' },
|
||||
{ key: 'invoice', icon: <ProfileOutlined />, label: '申请开票' },
|
||||
{ key: 'messages', icon: <BellOutlined />, label: `消息中心${unreadCount > 0 ? `(${unreadCount})` : ''}` },
|
||||
...(operationManualUrl ? [{ key: 'manual' as const, icon: <FileTextOutlined />, label: '操作手册' }] : []),
|
||||
{ type: 'divider' as const },
|
||||
@@ -708,6 +710,7 @@ const AppLayout: React.FC = () => {
|
||||
else if (key === 'teamManagement') { navigate('/team-management'); }
|
||||
else if (key === 'myCredits') { navigate('/user-center?tab=credits'); }
|
||||
else if (key === 'orderRecords') { navigate('/user-center?tab=orders'); }
|
||||
else if (key === 'invoice') { navigate('/invoice'); }
|
||||
else if (key === 'manual') { window.open(operationManualUrl, '_blank'); }
|
||||
};
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ body {
|
||||
|
||||
.content_box {
|
||||
margin: -24px -32px -32px;
|
||||
border-radius: 0 0 16px 16px;
|
||||
border-radius: 16px;
|
||||
height: calc(100vh - 34px);
|
||||
overflow: auto;
|
||||
scrollbar-width: none;
|
||||
|
||||
@@ -120,15 +120,64 @@ const GenerateConver: React.FC = () => {
|
||||
setVideoEngines(engines);
|
||||
if (engines.length > 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);
|
||||
setVideoAspectRatio(first.supportedRatios?.includes('9:16') ? '9:16' : first.supportedRatios?.[0] || '9:16');
|
||||
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('视频引擎加载失败'));
|
||||
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(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
@@ -142,6 +191,14 @@ const GenerateConver: React.FC = () => {
|
||||
}, []);
|
||||
|
||||
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 estimatedCredits = (() => {
|
||||
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);
|
||||
return;
|
||||
}
|
||||
if (video.duration >= 16) {
|
||||
message.error('视频时长不能超过15秒');
|
||||
if (video.duration > currentMaxDuration) {
|
||||
message.error(`视频时长不能超过${currentMaxDuration}秒`);
|
||||
URL.revokeObjectURL(video.src);
|
||||
resolve(false);
|
||||
return;
|
||||
@@ -888,6 +945,7 @@ const GenerateConver: React.FC = () => {
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div className="replication-form" style={{
|
||||
overflow: 'auto', width: '30%',
|
||||
background: 'transparent',
|
||||
@@ -903,11 +961,15 @@ const GenerateConver: React.FC = () => {
|
||||
background: 'rgba(258, 250, 252, 0.2)',
|
||||
padding: 24, overflowY: 'auto'
|
||||
}}>
|
||||
|
||||
{/* 上传视频 */}
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<p style={{ margin: 0, fontSize: 14, fontWeight: 600, color: '#1e293b', marginBottom: 10 }}>
|
||||
上传复刻视频
|
||||
<span style={{ color: '#f94444' }}>(必选)</span>
|
||||
<p style={{ fontSize: 12, color: '#64748b', fontWeight: 400, }}>
|
||||
当前引擎最多可上传 {currentMaxDuration} 秒
|
||||
</p>
|
||||
</p>
|
||||
{videoUrl ? (
|
||||
<div style={{ position: 'relative' }}>
|
||||
@@ -982,7 +1044,7 @@ const GenerateConver: React.FC = () => {
|
||||
上传中...
|
||||
</p>
|
||||
<p style={{ margin: 0, fontSize: 11, color: '#94a3b8', marginTop: 4 }}>
|
||||
支持的文件类型:MP4、MOV | 视频最大时长:15 秒 | 最大大小:50M
|
||||
支持的文件类型:MP4、MOV | 视频最大时长:{currentMaxDuration} 秒 | 最大大小:50M
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
@@ -1003,7 +1065,7 @@ const GenerateConver: React.FC = () => {
|
||||
点击或拖拽上传视频
|
||||
</p>
|
||||
<p style={{ margin: 0, fontSize: 11, color: '#94a3b8', marginTop: 4 }}>
|
||||
支持的文件类型:MP4、MOV | 视频最大时长:15 秒 | 最大大小:50M
|
||||
支持的文件类型:MP4、MOV | 视频最大时长:{currentMaxDuration} 秒 | 最大大小:50M
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
@@ -1149,8 +1211,8 @@ const GenerateConver: React.FC = () => {
|
||||
border: '1px solid rgba(99, 102, 241, 0.15)',
|
||||
background: 'rgba(255,255,255,0.8)',
|
||||
}}
|
||||
maxLength={10}
|
||||
suffix={<span style={{ color: '#94a3b8', fontSize: 12 }}>{originalProductName.length}/10</span>}
|
||||
maxLength={50}
|
||||
suffix={<span style={{ color: '#94a3b8', fontSize: 12 }}>{originalProductName.length}/50</span>}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1171,8 +1233,8 @@ const GenerateConver: React.FC = () => {
|
||||
border: '1px solid rgba(99, 102, 241, 0.15)',
|
||||
background: 'rgba(255,255,255,0.8)',
|
||||
}}
|
||||
maxLength={10}
|
||||
suffix={<span style={{ color: '#94a3b8', fontSize: 12 }}>{ownProductName.length}/10</span>}
|
||||
maxLength={50}
|
||||
suffix={<span style={{ color: '#94a3b8', fontSize: 12 }}>{ownProductName.length}/50</span>}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1196,16 +1258,16 @@ const GenerateConver: React.FC = () => {
|
||||
background: 'rgba(255,255,255,0.8)',
|
||||
}}
|
||||
rows={3}
|
||||
maxLength={30}
|
||||
maxLength={200}
|
||||
/>
|
||||
<span style={{ position: 'absolute', right: 10, bottom: 8, color: '#94a3b8', fontSize: 12 }}>
|
||||
{productSellingPoints.length}/30
|
||||
{productSellingPoints.length}/200
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</p>
|
||||
<div style={{ width: '100%', display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
|
||||
@@ -1246,7 +1308,7 @@ const GenerateConver: React.FC = () => {
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 'calc(100% + 8px)',
|
||||
left: -10,
|
||||
left: -19,
|
||||
width: 300,
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 16,
|
||||
@@ -1278,10 +1340,20 @@ const GenerateConver: React.FC = () => {
|
||||
type="button"
|
||||
onClick={() => {
|
||||
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({
|
||||
ratios: engine.supportedRatios || ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'],
|
||||
resolutions: engine.supportedResolutions || ['480p', '720p', '1080p'],
|
||||
durations: engine.supportedDurations || [5, 8, 10, 12, 15],
|
||||
durations: newDurations,
|
||||
});
|
||||
if (!engine.supportedRatios?.includes(videoAspectRatio)) {
|
||||
setVideoAspectRatio(engine.supportedRatios?.[0] || '16:9');
|
||||
@@ -1289,8 +1361,9 @@ const GenerateConver: React.FC = () => {
|
||||
if (!engine.supportedResolutions?.includes(videoResolution)) {
|
||||
setVideoResolution(engine.supportedResolutions?.[0] || '720p');
|
||||
}
|
||||
if (!engine.supportedDurations?.includes(videoDuration)) {
|
||||
setVideoDuration(engine.supportedDurations?.[0] || 5);
|
||||
if (!newDurations.includes(videoDuration) || videoDuration > engineMaxDur) {
|
||||
const validDur = newDurations.find(d => d <= engineMaxDur) ?? newDurations[0] ?? 5;
|
||||
setVideoDuration(validDur);
|
||||
}
|
||||
setShowEngineModal(false);
|
||||
}}
|
||||
@@ -1322,11 +1395,6 @@ const GenerateConver: React.FC = () => {
|
||||
}}>
|
||||
{engine.name}
|
||||
</span>
|
||||
<span style={{
|
||||
fontSize: 11,
|
||||
color: '#9ca3af',
|
||||
}}>
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -1477,15 +1545,15 @@ const GenerateConver: React.FC = () => {
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
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%)',
|
||||
}} />
|
||||
<input
|
||||
type="range"
|
||||
min={Math.min(...engineOptions.durations)}
|
||||
max={Math.max(...engineOptions.durations)}
|
||||
max={currentMaxDuration}
|
||||
value={videoDuration}
|
||||
onChange={(e) => setVideoDuration(Number(e.target.value))}
|
||||
onChange={(e) => setVideoDuration(Math.min(Number(e.target.value), currentMaxDuration))}
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
@@ -1567,6 +1635,8 @@ const GenerateConver: React.FC = () => {
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{/* 立即生成按钮 */}
|
||||
<Tooltip
|
||||
title={((user?.credits || 0) < (estimatedCredits + optimizeHoldCredits)) ? '积分不足,请更换参数/充值积分' : ''}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -115,18 +115,76 @@ function RemoveInfo() {
|
||||
setVideoEngines(engines);
|
||||
if (engines.length > 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);
|
||||
setVideoAspectRatio(first.supportedRatios?.includes('9:16') ? '9:16' : first.supportedRatios?.[0] || '9:16');
|
||||
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('视频引擎加载失败'));
|
||||
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 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 estimatedCredits = useMemo(() => {
|
||||
const rule = creditRules.find((item: any) => item.modelConfigId === engineId && item.genType === 'video' && item.resolution === videoResolution);
|
||||
@@ -145,7 +203,17 @@ function RemoveInfo() {
|
||||
if (!engine) return;
|
||||
setVideoAspectRatio(engine.supportedRatios?.[0] || '16:9');
|
||||
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(() => {
|
||||
@@ -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 (
|
||||
<>
|
||||
<div
|
||||
@@ -1028,87 +1180,7 @@ function RemoveInfo() {
|
||||
</Popconfirm>
|
||||
</div>
|
||||
|
||||
<div
|
||||
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>
|
||||
{tableSection}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
@@ -1211,7 +1283,7 @@ function RemoveInfo() {
|
||||
onChange={(e) => setProductName(e.target.value)}
|
||||
placeholder="请输入项目名称"
|
||||
style={{ height: 44, borderRadius: 8, border: '1px solid rgba(99, 102, 241, 0.2)' }}
|
||||
maxLength={10}
|
||||
maxLength={50}
|
||||
showCount
|
||||
/>
|
||||
</div>
|
||||
@@ -1225,7 +1297,7 @@ function RemoveInfo() {
|
||||
onChange={(e) => setProductSellingPoint(e.target.value)}
|
||||
placeholder="请输入项目描述"
|
||||
style={{ borderRadius: 8, border: '1px solid rgba(99, 102, 241, 0.2)' }}
|
||||
maxLength={100}
|
||||
maxLength={200}
|
||||
showCount
|
||||
rows={3}
|
||||
/>
|
||||
@@ -1305,10 +1377,20 @@ function RemoveInfo() {
|
||||
type="button"
|
||||
onClick={() => {
|
||||
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({
|
||||
ratios: engine.supportedRatios || ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'],
|
||||
resolutions: engine.supportedResolutions || ['480p', '720p', '1080p'],
|
||||
durations: engine.supportedDurations || [5, 8, 10, 12, 15],
|
||||
durations: newDurations,
|
||||
});
|
||||
if (!engine.supportedRatios?.includes(videoAspectRatio)) {
|
||||
setVideoAspectRatio(engine.supportedRatios?.[0] || '16:9');
|
||||
@@ -1316,8 +1398,9 @@ function RemoveInfo() {
|
||||
if (!engine.supportedResolutions?.includes(videoResolution)) {
|
||||
setVideoResolution(engine.supportedResolutions?.[0] || '720p');
|
||||
}
|
||||
if (!engine.supportedDurations?.includes(videoDuration)) {
|
||||
setVideoDuration(engine.supportedDurations?.[0] || 5);
|
||||
if (!newDurations.includes(videoDuration) || videoDuration > engineMaxDur) {
|
||||
const validDur = newDurations.find(d => d <= engineMaxDur) ?? newDurations[0] ?? 5;
|
||||
setVideoDuration(validDur);
|
||||
}
|
||||
setShowEngineModal(false);
|
||||
}}
|
||||
@@ -1504,15 +1587,15 @@ function RemoveInfo() {
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
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%)',
|
||||
}} />
|
||||
<input
|
||||
type="range"
|
||||
min={Math.min(...engineOptions.durations)}
|
||||
max={Math.max(...engineOptions.durations)}
|
||||
max={currentMaxDuration}
|
||||
value={videoDuration}
|
||||
onChange={(e) => setVideoDuration(Number(e.target.value))}
|
||||
onChange={(e) => setVideoDuration(Math.min(Number(e.target.value), currentMaxDuration))}
|
||||
style={{
|
||||
position: 'relative',
|
||||
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