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": {
|
"src/pages/CreativePlazaPage.tsx": {
|
||||||
"description": "创意广场"
|
"description": "创意广场"
|
||||||
|
=======
|
||||||
|
"src/pages/InvoicePage.tsx": {
|
||||||
|
"description": "申请开票"
|
||||||
|
>>>>>>> sun
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -62,6 +62,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';
|
||||||
@@ -692,6 +693,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 },
|
||||||
@@ -708,6 +710,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>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -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