Merge branch 'main' into dev

This commit is contained in:
孙佳艺
2026-06-17 16:39:38 +08:00
9 changed files with 1847 additions and 419 deletions
+1
View File
@@ -17,6 +17,7 @@
} }
} }
.hero { .hero {
position: relative; position: relative;
+9
View File
@@ -19,12 +19,17 @@ import RemoveLens from './pages/RemoveLens';
import GeneratedRecord from './pages/GeneratedRecord'; import GeneratedRecord from './pages/GeneratedRecord';
import AuthorizationPage from './pages/AuthorizationPage'; import AuthorizationPage from './pages/AuthorizationPage';
import RemoveInfo from './pages/RemoveInfo'; import RemoveInfo from './pages/RemoveInfo';
import RemoveRw from './pages/RemoveRw';
// import RemoveFenbu from './pages/RemoveFenbu';
import ConsumePage from './pages/ConsumePage'; import ConsumePage from './pages/ConsumePage';
import { useAuthStore } from './store/useAuthStore'; import { useAuthStore } from './store/useAuthStore';
const ProtectedRoute = ({ children }: { children: React.ReactNode }) => { const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
@@ -107,6 +112,10 @@ const App = () => {
<Route path="initial/:creatID/initialinfo" element={<InitialInfo />} /> <Route path="initial/:creatID/initialinfo" element={<InitialInfo />} />
<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={<RemoveFenbu />} /> */}
<Route path="generated" element={<GeneratedRecord />} /> <Route path="generated" element={<GeneratedRecord />} />
<Route path="authorization" element={<AuthorizationPage />} /> <Route path="authorization" element={<AuthorizationPage />} />
<Route path="consume" element={<ConsumePage />} /> <Route path="consume" element={<ConsumePage />} />
+66 -1
View File
@@ -378,7 +378,7 @@ export async function getReplicationDetail(id: string): Promise<any> {
export async function getone(projectId: string, stepId: string): Promise<any> { export async function getone(projectId: string, stepId: string): Promise<any> {
return api.post(`/hot-opening-replications/tasks/${projectId}/steps/${stepId}/generate-image-prompt`); return api.post(`/hot-opening-replications/tasks/${projectId}/steps/${stepId}/generate-image-prompt`);
} }
// 第二步,生成图片 // 第二步,生成图片
export async function gettwo(projectId: string, stepId: string ,params: any): Promise<any> { export async function gettwo(projectId: string, stepId: string ,params: any): Promise<any> {
return api.post(`/hot-opening-replications/tasks/${projectId}/steps/${stepId}/generate-image`, params); return api.post(`/hot-opening-replications/tasks/${projectId}/steps/${stepId}/generate-image`, params);
@@ -392,3 +392,68 @@ export async function getthree(projectId: string, stepId: string ,params: any):
export async function getfour(projectId: string, stepId: string ,params: any): Promise<any> { export async function getfour(projectId: string, stepId: string ,params: any): Promise<any> {
return api.post(`/hot-opening-replications/tasks/${projectId}/steps/${stepId}/generate-video`, params); return api.post(`/hot-opening-replications/tasks/${projectId}/steps/${stepId}/generate-video`, params);
} }
// 镜头复刻
export async function createShotReplication(params: any): Promise<any> {
return api.post('/shot-replications/task-sets', params);
}
// 获取镜头复刻任务列表
export async function getShotReplicationList(page: number, page_size: number): Promise<any> {
return api.get(`/shot-replications/task-sets?page=${page}&page_size=${page_size}`);
}
// 获取镜头复刻任务详情
export async function getShotReplicationDetail(taskSetId: string): Promise<any> {
return api.get(`/shot-replications/task-sets/${taskSetId}`);
}
// ai拆镜
export async function createRemoveLens(taskSetId: string, params: any): Promise<any> {
return api.post(`/shot-replications/task-sets/${taskSetId}/split-by-ai`, params);
}
// 手动分割
export async function splitCustom(taskSetId: string, params: any): Promise<any> {
return api.post(`/shot-replications/task-sets/${taskSetId}/split-custom`, params);
}
// 拆镜列表
export async function Removelist(taskSetId: string): Promise<any> {
return api.get(`/shot-replications/task-sets/${taskSetId}/segments`);
}
// 生成视频
export async function removeCreate(recordId: string, params: any): Promise<any> {
return api.post(`/shot-replications/segments/${recordId}/replication-projects`, params);
}
// 获取爆款开头复刻任务详情
export async function removeDetail(id: string): Promise<any> {
return api.get(`/shot-replications/projects/${id}`);
}
// 第一步,生成提示词
export async function removeone(projectId: string, stepId: string): Promise<any> {
return api.post(`/shot-replications/projects/${projectId}/steps/${stepId}/generate-image-prompt`);
}
// 第二步,生成图片
export async function removetwo(projectId: string, stepId: string ,params: any): Promise<any> {
return api.post(`/shot-replications/projects/${projectId}/steps/${stepId}/generate-image`, params);
}
// 第三步,生成视频提示词
export async function removethree(projectId: string, stepId: string ,params: any): Promise<any> {
return api.post(`/shot-replications/projects/${projectId}/steps/${stepId}/generate-video-prompt`, params);
}
// 第四步,生成视频
export async function removefour(projectId: string, stepId: string ,params: any): Promise<any> {
return api.post(`/shot-replications/projects/${projectId}/steps/${stepId}/generate-video`, params);
}
//
@@ -167,7 +167,7 @@ const AppLayout: React.FC = () => {
const countdownTimerRef = useRef<ReturnType<typeof setInterval> | null>(null); const countdownTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const currentOrderNoRef = useRef<string | null>(null); const currentOrderNoRef = useRef<string | null>(null);
const [enabledMethods, setEnabledMethods] = useState<{ alipay: boolean; wechat: boolean }>({ alipay: false, wechat: false }); const [enabledMethods, setEnabledMethods] = useState<{ alipay: boolean; wechat: boolean }>({ alipay: false, wechat: false });
// LocalStorage keys // LocalStorage keys
const PENDING_ORDER_KEY = 'pending_payment_order'; const PENDING_ORDER_KEY = 'pending_payment_order';
@@ -177,12 +177,12 @@ const AppLayout: React.FC = () => {
getSiteInfo().then(info => { getSiteInfo().then(info => {
const name = info.siteName || '民众智创'; const name = info.siteName || '民众智创';
const logo = info.siteLogo || ''; const logo = info.siteLogo || '';
if (name !== siteName) { if (name !== siteName) {
setSiteName(name); setSiteName(name);
document.title = name; document.title = name;
} }
if (logo && logo !== siteLogo) { if (logo && logo !== siteLogo) {
setSiteLogo(logo); setSiteLogo(logo);
let faviconLink = document.querySelector('link[rel="icon"]') as HTMLLinkElement; let faviconLink = document.querySelector('link[rel="icon"]') as HTMLLinkElement;
@@ -194,15 +194,15 @@ const AppLayout: React.FC = () => {
faviconLink.href = logo; faviconLink.href = logo;
faviconLink.type = 'image/png'; faviconLink.type = 'image/png';
} }
localStorage.setItem('siteInfo', JSON.stringify({ siteName: name, siteLogo: logo })); localStorage.setItem('siteInfo', JSON.stringify({ siteName: name, siteLogo: logo }));
}).catch(() => {}); }).catch(() => { });
}, []); }, []);
const loadUnreadCount = () => { const loadUnreadCount = () => {
getUnreadCount().then(count => { getUnreadCount().then(count => {
setUnreadCount(count); setUnreadCount(count);
}).catch(() => {}); }).catch(() => { });
}; };
// 检查并恢复待处理的支付订单 // 检查并恢复待处理的支付订单
@@ -229,7 +229,7 @@ const AppLayout: React.FC = () => {
const timeoutSeconds = savedOrder.timeoutSeconds || 180; const timeoutSeconds = savedOrder.timeoutSeconds || 180;
const elapsedSeconds = Math.floor((now - createdAt) / 1000); const elapsedSeconds = Math.floor((now - createdAt) / 1000);
const remainingSeconds = Math.max(0, timeoutSeconds - elapsedSeconds); const remainingSeconds = Math.max(0, timeoutSeconds - elapsedSeconds);
if (remainingSeconds > 0) { if (remainingSeconds > 0) {
setQrCodeModalOpen(true); setQrCodeModalOpen(true);
startPolling(savedOrder.orderNo, remainingSeconds); startPolling(savedOrder.orderNo, remainingSeconds);
@@ -252,7 +252,7 @@ const AppLayout: React.FC = () => {
} }
} }
}; };
checkPendingOrder(); checkPendingOrder();
}, []); }, []);
@@ -274,16 +274,16 @@ const AppLayout: React.FC = () => {
}); });
} }
setMenuItems(items); setMenuItems(items);
}).catch(() => {}); }).catch(() => { });
getRechargePackages().then(data => { getRechargePackages().then(data => {
setRechargeOptions(data.filter((p: any) => p.is_active !== false && p.isActive !== false)); setRechargeOptions(data.filter((p: any) => p.is_active !== false && p.isActive !== false));
}).catch(() => {}); }).catch(() => { });
getPaymentMethods().then(data => { getPaymentMethods().then(data => {
setEnabledMethods(data); setEnabledMethods(data);
// Auto-select the first enabled method // Auto-select the first enabled method
if (data.alipay) setPaymentMethod('alipay'); if (data.alipay) setPaymentMethod('alipay');
else if (data.wechat) setPaymentMethod('wechat'); else if (data.wechat) setPaymentMethod('wechat');
}).catch(() => {}); }).catch(() => { });
loadUnreadCount(); loadUnreadCount();
}, [user]); }, [user]);
@@ -344,7 +344,7 @@ const AppLayout: React.FC = () => {
const startPolling = useCallback((orderNo: string, timeoutSeconds: number = 180) => { const startPolling = useCallback((orderNo: string, timeoutSeconds: number = 180) => {
stopPolling(); stopPolling();
setCountdown(timeoutSeconds); setCountdown(timeoutSeconds);
// 订单状态轮询(每2秒查询一次,只查询当前订单 // 订单状态轮询(每2秒查询一次,只查询当前订单
const pollingTimer = setInterval(async () => { const pollingTimer = setInterval(async () => {
try { try {
@@ -368,7 +368,7 @@ const AppLayout: React.FC = () => {
} }
}, 2000); }, 2000);
pollingTimerRef.current = pollingTimer; pollingTimerRef.current = pollingTimer;
// 倒计时 // 倒计时
const countdownTimer = setInterval(() => { const countdownTimer = setInterval(() => {
setCountdown(prev => { setCountdown(prev => {
@@ -376,7 +376,7 @@ const AppLayout: React.FC = () => {
// 超时自动取消 // 超时自动取消
stopPolling(); stopPolling();
if (currentOrderNoRef.current) { if (currentOrderNoRef.current) {
cancelPaymentOrder(currentOrderNoRef.current).catch(() => {}); cancelPaymentOrder(currentOrderNoRef.current).catch(() => { });
currentOrderNoRef.current = null; currentOrderNoRef.current = null;
} }
localStorage.removeItem(PENDING_ORDER_KEY); localStorage.removeItem(PENDING_ORDER_KEY);
@@ -474,8 +474,8 @@ const AppLayout: React.FC = () => {
background: isActive ? 'linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(139, 92, 246, 0.08) 100%)' : 'transparent', background: isActive ? 'linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(139, 92, 246, 0.08) 100%)' : 'transparent',
transition: 'all 0.2s ease', transition: 'all 0.2s ease',
}}> }}>
<span style={{ <span style={{
fontSize: depth > 0 ? 14 : 16, fontSize: depth > 0 ? 14 : 16,
flexShrink: 0, flexShrink: 0,
color: isActive ? '#6366f1' : '#64748b', color: isActive ? '#6366f1' : '#64748b',
}}>{menuIcon}</span> }}>{menuIcon}</span>
@@ -521,13 +521,13 @@ const AppLayout: React.FC = () => {
boxShadow: '0 4px 16px rgba(99, 102, 241, 0.4)', boxShadow: '0 4px 16px rgba(99, 102, 241, 0.4)',
transition: 'all 0.3s ease', transition: 'all 0.3s ease',
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
e.currentTarget.style.background = 'linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%)'; e.currentTarget.style.background = 'linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%)';
e.currentTarget.style.transform = 'translateY(-1px)'; e.currentTarget.style.transform = 'translateY(-1px)';
e.currentTarget.style.boxShadow = '0 6px 20px rgba(99, 102, 241, 0.5)'; e.currentTarget.style.boxShadow = '0 6px 20px rgba(99, 102, 241, 0.5)';
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
e.currentTarget.style.background = 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)'; e.currentTarget.style.background = 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)';
e.currentTarget.style.transform = 'translateY(0)'; e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = '0 4px 16px rgba(99, 102, 241, 0.4)'; e.currentTarget.style.boxShadow = '0 4px 16px rgba(99, 102, 241, 0.4)';
}} }}
@@ -544,7 +544,7 @@ const AppLayout: React.FC = () => {
justifyContent: 'flex-start', justifyContent: 'flex-start',
gap: 12, gap: 12,
padding: '10px 14px', padding: '10px 14px',
borderRadius: 14, cursor: 'pointer', borderRadius: 14, cursor: 'pointer',
transition: 'all 0.25s ease', transition: 'all 0.25s ease',
background: 'linear-gradient(135deg, rgba(248, 250, 252, 0.9) 0%, rgba(241, 245, 249, 0.9) 100%)', background: 'linear-gradient(135deg, rgba(248, 250, 252, 0.9) 0%, rgba(241, 245, 249, 0.9) 100%)',
boxShadow: '0 2px 12px rgba(0, 0, 0, 0.04)', boxShadow: '0 2px 12px rgba(0, 0, 0, 0.04)',
@@ -559,8 +559,8 @@ const AppLayout: React.FC = () => {
}} }}
> >
<Avatar size={36} icon={<UserOutlined />} <Avatar size={36} icon={<UserOutlined />}
style={{ style={{
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
flexShrink: 0, flexShrink: 0,
boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)', boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)',
}} /> }} />
@@ -568,9 +568,9 @@ const AppLayout: React.FC = () => {
<div style={{ color: '#1e293b', fontSize: 14, fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', letterSpacing: -0.01 }}> <div style={{ color: '#1e293b', fontSize: 14, fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', letterSpacing: -0.01 }}>
{user?.username} {user?.username}
</div> </div>
<div style={{ <div style={{
color: '#6366f1', color: '#6366f1',
fontSize: 12, fontSize: 12,
fontWeight: 500, fontWeight: 500,
letterSpacing: 0, letterSpacing: 0,
background: 'rgba(99, 102, 241, 0.08)', background: 'rgba(99, 102, 241, 0.08)',
@@ -586,18 +586,21 @@ const AppLayout: React.FC = () => {
{/* Main Content */} {/* Main Content */}
<div className="desktop-content" style={{ <div className="desktop-content" style={{
marginLeft: sidebarW + 48, marginLeft: sidebarW + 48,
marginRight: 32, marginRight: 32,
marginTop: 16, marginTop: 16,
marginBottom: 16, marginBottom: 16,
flex: 1, flex: 1,
minHeight: 'calc(100vh - 32px)', // height: '100%',
minHeight: 'calc(100vh - 32px)',
background: 'transparent', background: 'transparent',
padding: 0, padding: 0,
transition: 'margin-left 0.25s ease', transition: 'margin-left 0.25s ease',
}}> }}>
<div style={{ <div style={{
background: '#ffffff', boxSizing: 'border-box',
height: '100%',
background: '#ffffffff',
borderRadius: '20px', borderRadius: '20px',
boxShadow: '0 4px 32px rgba(0, 0, 0, 0.06), 0 1px 8px rgba(0, 0, 0, 0.04)', boxShadow: '0 4px 32px rgba(0, 0, 0, 0.06), 0 1px 8px rgba(0, 0, 0, 0.04)',
minHeight: '100%', minHeight: '100%',
@@ -672,33 +675,33 @@ const AppLayout: React.FC = () => {
const g = GRADIENTS[idx % GRADIENTS.length]; const g = GRADIENTS[idx % GRADIENTS.length];
const totalCredits = (opt.credits || 0) + (opt.bonus_credits || opt.bonusCredits || 0); const totalCredits = (opt.credits || 0) + (opt.bonus_credits || opt.bonusCredits || 0);
return ( return (
<div key={opt.id} onClick={() => setSelectedPlan(opt.id)} style={{ <div key={opt.id} onClick={() => setSelectedPlan(opt.id)} style={{
flex: '1 1 45%', minWidth: 200, borderRadius: 16, padding: '20px 16px', flex: '1 1 45%', minWidth: 200, borderRadius: 16, padding: '20px 16px',
background: selectedPlan === opt.id ? 'rgba(99,102,241,0.04)' : '#fafbff', background: selectedPlan === opt.id ? 'rgba(99,102,241,0.04)' : '#fafbff',
border: selectedPlan === opt.id ? '2px solid #6366f1' : '1px solid #f0f0f5', border: selectedPlan === opt.id ? '2px solid #6366f1' : '1px solid #f0f0f5',
cursor: 'pointer', position: 'relative', transition: 'all 0.2s', cursor: 'pointer', position: 'relative', transition: 'all 0.2s',
}}> }}>
{opt.description && ( {opt.description && (
<Tag color="purple" style={{ position: 'absolute', top: -10, left: '50%', transform: 'translateX(-50%)', borderRadius: 8, fontSize: 11 }}>{opt.description}</Tag> <Tag color="purple" style={{ position: 'absolute', top: -10, left: '50%', transform: 'translateX(-50%)', borderRadius: 8, fontSize: 11 }}>{opt.description}</Tag>
)} )}
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{
width: 44, height: 44, borderRadius: 12, flexShrink: 0,
background: g.gradient, display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 18, color: '#fff', boxShadow: `0 6px 16px ${g.shadow}`,
}}>{g.icon}</div>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 6 }}>
<Typography.Text strong style={{ fontSize: 15 }}>{opt.name}</Typography.Text>
<Typography.Text style={{ fontSize: 12, color: '#94a3b8' }}>{totalCredits.toLocaleString()} </Typography.Text>
</div>
<div style={{ <div style={{
fontSize: 22, fontWeight: 800, marginTop: 4, width: 44, height: 44, borderRadius: 12, flexShrink: 0,
background: g.gradient, WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', background: g.gradient, display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>¥{opt.price}</div> fontSize: 18, color: '#fff', boxShadow: `0 6px 16px ${g.shadow}`,
}}>{g.icon}</div>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 6 }}>
<Typography.Text strong style={{ fontSize: 15 }}>{opt.name}</Typography.Text>
<Typography.Text style={{ fontSize: 12, color: '#94a3b8' }}>{totalCredits.toLocaleString()} </Typography.Text>
</div>
<div style={{
fontSize: 22, fontWeight: 800, marginTop: 4,
background: g.gradient, WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent',
}}>¥{opt.price}</div>
</div>
</div> </div>
</div> </div>
</div>
); );
})} })}
</div> </div>
@@ -763,7 +766,7 @@ const AppLayout: React.FC = () => {
setRechargeModalOpen(false); setRechargeModalOpen(false);
setQrCodeModalOpen(true); setQrCodeModalOpen(true);
currentOrderNoRef.current = order.orderNo; currentOrderNoRef.current = order.orderNo;
// 保存到 localStorage // 保存到 localStorage
localStorage.setItem(PENDING_ORDER_KEY, JSON.stringify({ localStorage.setItem(PENDING_ORDER_KEY, JSON.stringify({
orderNo: order.orderNo, orderNo: order.orderNo,
@@ -774,7 +777,7 @@ const AppLayout: React.FC = () => {
createdAt: order.createdAt || new Date().toISOString(), createdAt: order.createdAt || new Date().toISOString(),
timeoutSeconds: 180, timeoutSeconds: 180,
})); }));
// Start polling for payment status // Start polling for payment status
startPolling(order.orderNo); startPolling(order.orderNo);
} else { } else {
@@ -808,7 +811,7 @@ const AppLayout: React.FC = () => {
stopPolling(); stopPolling();
// Mark order as cancelled if it's still pending // Mark order as cancelled if it's still pending
if (currentOrderNoRef.current) { if (currentOrderNoRef.current) {
try { await cancelPaymentOrder(currentOrderNoRef.current); } catch {} try { await cancelPaymentOrder(currentOrderNoRef.current); } catch { }
currentOrderNoRef.current = null; currentOrderNoRef.current = null;
} }
localStorage.removeItem(PENDING_ORDER_KEY); localStorage.removeItem(PENDING_ORDER_KEY);
@@ -934,7 +937,7 @@ const AppLayout: React.FC = () => {
onClick={async () => { onClick={async () => {
stopPolling(); stopPolling();
if (currentOrderNoRef.current) { if (currentOrderNoRef.current) {
try { await cancelPaymentOrder(currentOrderNoRef.current); } catch {} try { await cancelPaymentOrder(currentOrderNoRef.current); } catch { }
currentOrderNoRef.current = null; currentOrderNoRef.current = null;
} }
localStorage.removeItem(PENDING_ORDER_KEY); localStorage.removeItem(PENDING_ORDER_KEY);
+15 -11
View File
@@ -830,7 +830,7 @@ const AIChatPage: React.FC = () => {
// ==================== 渲染 ==================== // ==================== 渲染 ====================
return ( return (
<Layout style={{ height: '94vh', background: '#fafafa', overflow: 'hidden' }}> <Layout style={{ height: '90vh', background: '#fafafa', overflow: 'auto' }}>
{/* 左侧边栏 - 对话列表(已隐藏,保留代码) */} {/* 左侧边栏 - 对话列表(已隐藏,保留代码) */}
{false && ( {false && (
<Sider <Sider
@@ -2336,11 +2336,13 @@ const AIChatPage: React.FC = () => {
× ×
</button> </button>
} }
bodyStyle={{ styles={{
display: 'flex', body: {
alignItems: 'center', display: 'flex',
justifyContent: 'center', alignItems: 'center',
minHeight: '400px', justifyContent: 'center',
minHeight: '400px',
}
}} }}
> >
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%' }}> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%' }}>
@@ -2423,11 +2425,13 @@ const AIChatPage: React.FC = () => {
× ×
</button> </button>
} }
bodyStyle={{ styles={{
display: 'flex', body: {
alignItems: 'center', display: 'flex',
justifyContent: 'center', alignItems: 'center',
minHeight: '400px', justifyContent: 'center',
minHeight: '400px',
}
}} }}
> >
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%' }}> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%' }}>
+4 -1
View File
@@ -1055,7 +1055,10 @@ function InitialInfo() {
key: 'action', key: 'action',
render: (_, record) => ( render: (_, record) => (
<button <button
onClick={() => navigate(`/initial/${record.id}/initialinfo`)} onClick={() => {
setIsModalOpen(false);
navigate(`/initial/${record.id}/initialinfo`);
}}
style={{ color: '#6366f1', textDecoration: 'none', fontSize: 12, border: 'none', background: 'none', cursor: 'pointer' }} style={{ color: '#6366f1', textDecoration: 'none', fontSize: 12, border: 'none', background: 'none', cursor: 'pointer' }}
> >
+339 -290
View File
@@ -1,8 +1,9 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom'; import { useParams, useNavigate } from 'react-router-dom';
import { Button, Table, Tag, Drawer, Input, Upload, message } from 'antd'; import { Button, Table, Tag, Drawer, Input, Upload, message } from 'antd';
import { ArrowLeftOutlined, PlayCircleOutlined, XOutlined, PlusOutlined, UploadOutlined } from '@ant-design/icons'; import { ArrowLeftOutlined, PlayCircleOutlined, XOutlined, PlusOutlined, UploadOutlined } from '@ant-design/icons';
import type { UploadFile } from 'antd'; import type { UploadFile } from 'antd';
import { getShotReplicationDetail, createRemoveLens, Removelist, uploadImage, removeCreate, splitCustom } from '../api';
const { TextArea } = Input; const { TextArea } = Input;
@@ -15,6 +16,9 @@ function RemoveInfo() {
const [productSellingPoint, setProductSellingPoint] = useState(''); const [productSellingPoint, setProductSellingPoint] = useState('');
const [productImage, setProductImage] = useState(''); const [productImage, setProductImage] = useState('');
const [detailImage, setDetailImage] = useState(''); const [detailImage, setDetailImage] = useState('');
const [taskDetail, setTaskDetail] = useState<any>(null);
const [tableData, setTableData] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const handleGenerate = (segmentId: number) => { const handleGenerate = (segmentId: number) => {
setCurrentSegment(segmentId); setCurrentSegment(segmentId);
@@ -30,21 +34,42 @@ function RemoveInfo() {
setDetailImage(''); setDetailImage('');
}; };
useEffect(() => {
console.log('ididi', creatID);
if (creatID) {
getShotReplicationDetail(creatID).then((res: any) => {
setTaskDetail(res);
console.log('res', res);
}).catch((error: any) => {
message.error('获取任务详情失败');
});
Removelist(creatID).then((res: any) => {
console.log('Removelist res', res);
setTableData(res.items || []);
}).catch((error: any) => {
message.error('获取拆镜列表失败');
});
}
}, [creatID]);
const handleProductImageChange: any = (info: any) => { const handleProductImageChange: any = (info: any) => {
if (info.fileList.length > 0) { if (info.fileList.length === 0) {
const file = info.fileList[0];
if (file.originFileObj) {
const reader = new FileReader();
reader.onload = (e) => {
setProductImage(e.target?.result as string);
};
reader.readAsDataURL(file.originFileObj);
}
} else {
setProductImage(''); setProductImage('');
} }
}; };
const beforeUploadProductImage = async (file: File) => {
try {
const uploadResult = await uploadImage(file);
setProductImage(uploadResult.url);
message.success('图片上传成功');
} catch (err) {
message.error('图片上传失败,请重试');
}
return false;
};
const handleDetailImageChange: any = (info: any) => { const handleDetailImageChange: any = (info: any) => {
if (info.fileList.length > 0) { if (info.fileList.length > 0) {
const file = info.fileList[0]; const file = info.fileList[0];
@@ -60,8 +85,7 @@ function RemoveInfo() {
} }
}; };
const handleManualGenerate = () => { const handleManualGenerate = async () => {
// 必填校验
if (!productImage) { if (!productImage) {
message.warning('请上传产品图'); message.warning('请上传产品图');
return; return;
@@ -72,79 +96,74 @@ function RemoveInfo() {
} }
if (!productSellingPoint.trim()) { if (!productSellingPoint.trim()) {
message.warning('请输入产品卖点'); message.warning('请输入产品卖点');
return; return;
} }
// 输出内容 setLoading(true);
console.log('手动生成 - 片段', currentSegment); try {
console.log('产品图:', productImage); const params = {
console.log('细节图:', detailImage); target_project_name: productName.trim(),
console.log('产品名称:', productName); core_content_point: productSellingPoint.trim(),
console.log('产品卖点:', productSellingPoint); material_image_url: `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${productImage}`,
idempotency_key: `replication_${Date.now()}`,
};
console.log(params);
message.success(`手动生成成功!片段: ${currentSegment}`);
await removeCreate(String(currentSegment), params);
message.success('视频生成任务创建成功');
// handleCloseDrawer();
} catch (err) {
message.error('创建失败,请重试');
} finally {
setLoading(false);
}
}; };
const mockData = { const handleAutoGenerate = () => {
productName: '返回', // console.log('123123');
uploadTime: '2026-06-09 09:01:16', let params = {
sellingPoints: ['一键匹配', '连麦聊天'], selected_indices: [],
audience: '123123', replace_existing: false,
audienceAnalysis: '123123', }
videoUrl: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=320&h=180&fit=crop', createRemoveLens(creatID, params).then((res: any) => {
segments: [ console.log('res', res);
{ }).catch((error: any) => {
key: '1', message.error('拆镜失败');
id: 1, });
timeRange: '00:00 - 00:03',
thumbnail: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=120&h=80&fit=crop', }
content: '11111',
lines: 'qqqqqqqqqqq',
contentStrategy: '展示礼盒'
},
{
key: '2',
id: 2,
timeRange: '00:03 - 00:06',
thumbnail: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=120&h=80&fit=crop',
content: '1231231231',
lines: 'qqqqqqqqqqq',
contentStrategy: '开箱展示'
},
{
key: '3',
id: 3,
timeRange: '00:06 - 00:09',
thumbnail: 'https://images.unsplash.com/photo-1522202176988-66273c2fd55f?w=120&h=80&fit=crop',
content: '123123',
lines: 'qqqqqqqqqqq',
contentStrategy: '取出产品'
},
]
};
const columns = [ const columns = [
{ {
title: '片段', title: '片段',
width: 100, width: 100,
align: 'center' as const,
render: (text: any, record: any) => ( render: (text: any, record: any) => (
<div> <div>
<div style={{ fontSize: 16, fontWeight: 600, color: '#333' }}>{record.id}</div> <div style={{ fontSize: 16, fontWeight: 600, color: '#333' }}>{record.segmentName}</div>
<div style={{ fontSize: 12, color: '#999' }}>{record.timeRange}</div> <div style={{ fontSize: 12, color: '#999' }}>{record.timeNode}</div>
</div> </div>
) )
}, },
{ {
title: '片段视频', title: '片段视频',
width: 120, width: 150,
align: 'center' as const,
render: (text: any, record: any) => ( render: (text: any, record: any) => (
<div style={{ position: 'relative', width: 120, height: 80, borderRadius: 6, overflow: 'hidden' }}> <div style={{ position: 'relative', width: 120, height: 80, borderRadius: 6, overflow: 'hidden' }}>
<img <video
src={record.thumbnail} controls
alt={`片段${record.id}`} src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${record.segmentVideoUrl}`}
// alt={`片段${record.id}`}
style={{ width: '100%', height: '100%', objectFit: 'cover' }} style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/> />
<div style={{ {/* <div style={{
position: 'absolute', position: 'absolute',
top: '50%', top: '50%',
left: '50%', left: '50%',
@@ -158,261 +177,291 @@ function RemoveInfo() {
justifyContent: 'center' justifyContent: 'center'
}}> }}>
<PlayCircleOutlined style={{ fontSize: 16, color: '#fff' }} /> <PlayCircleOutlined style={{ fontSize: 16, color: '#fff' }} />
</div> </div> */}
</div> </div>
) )
}, },
{ {
title: '画面内容', title: '视频内容',
width: 250, width: 250,
render: (text: any, record: any) => (
<div style={{ fontSize: 14, color: '#333', lineHeight: 1.6 }}>
{record.content}
</div>
)
},
{
title: '台词',
width: 250,
render: (text: any, record: any) => (
<div style={{ fontSize: 14, color: '#333', lineHeight: 1.6 }}>
{record.lines}
</div>
)
},
{
title: '内容策略',
width: 120,
align: 'left' as const, align: 'left' as const,
render: (text: any, record: any) => (
<div style={{ fontSize: 14, color: '#333', lineHeight: 1.6 }}>
{record.segmentContent}
</div>
)
},
// {
// title: '台词',
// width: 250,
// render: (text: any, record: any) => (
// <div style={{ fontSize: 14, color: '#333', lineHeight: 1.6 }}>
// {record.lines}
// </div>
// )
// },
{
title: '视频类型',
width: 120,
align: 'center' as const,
render: (text: any, record: any) => ( render: (text: any, record: any) => (
<div style={{ fontSize: 14, color: '#333', fontWeight: 500 }}> <div style={{ fontSize: 14, color: '#333', fontWeight: 500 }}>
{record.contentStrategy || '-'} {record.segmentCategory || '-'}
</div> </div>
) )
}, },
{ {
title: '素材', title: '素材',
width: 140, width: 140,
align: 'center' as const, align: 'center' as const,
render: (text: any, record: any) => ( render: (text: any, record: any) => (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8 }}> <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8 }}>
<div style={{ fontSize: 12, color: '#999', padding: '12px 24px', border: '1px dashed #ddd', borderRadius: 4 }}>
</div> {record.moduleProjectId ? (
<Button <Button
type="text" type="text"
onClick={() => handleGenerate(record.id)} onClick={() => navigate(`/removelens/${record.moduleProjectId}/removefenbu`)}
style={{ color: '#656efa', fontSize: 12, padding: 0, display: 'flex', alignItems: 'center', gap: 4 }} style={{ color: '#656efa', fontSize: 12, padding: 0, display: 'flex', alignItems: 'center', gap: 4 }}
> >
</Button> </Button>
) : (
<Button
type="text"
onClick={() => handleGenerate(record.id)}
style={{ color: '#656efa', fontSize: 12, padding: 0, display: 'flex', alignItems: 'center', gap: 4 }}
>
</Button>
)}
</div> </div>
) )
} }
]; ];
return ( return (
<React.Fragment> <>
<div style={{ minHeight: '94vh', background: '#f5f5f5' }}> <div style={{
<div style={{ background: '#fff', padding: '16px 24px 0 24px', boxShadow: '0 2px 8px rgba(0,0,0,0.06)' }}> // height: '100%',
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}> minHeight: 'calc(100vh - 90px)',
<Button display: 'flex',
type="text" flexDirection: 'column',
icon={<ArrowLeftOutlined />} overflow: 'hidden',
onClick={() => navigate(-1)} }}>
style={{ fontSize: 16, color: '#666' }} <div style={{ background: '#fff',}}>
/> <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<span style={{ fontSize: 18, fontWeight: 600 }}>{mockData.productName}</span> <Button
type="text"
icon={<ArrowLeftOutlined />}
onClick={() => navigate(-1)}
style={{ fontSize: 16, color: '#666' }}
/>
</div>
</div> </div>
</div>
<div style={{ }}> {taskDetail ? (
<div style={{ background: '#fff', borderRadius: 12, padding: '10px 20px 20px 20px', marginBottom: 20 }}> <div style={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'space-between', overflow: 'hidden' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 30, marginBottom: 12 }}> <div style={{ flex: 0.4, background: '#fff', borderRadius: 12, }}>
<div style={{ position: 'relative', width: 280, height: 160, borderRadius: 8, overflow: 'hidden', flexShrink: 0 }}>
<img {/* 213123 */}
src={mockData.videoUrl} <div style={{ display: 'flex', alignItems: 'center', gap: 30, marginBottom: 12 }}>
alt="视频缩略图" <div style={{ position: 'relative', width: 280, height: 160, borderRadius: 8, overflow: 'hidden', flexShrink: 0 }}>
style={{ width: '100%', height: '100%', objectFit: 'cover' }} <video
controls
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${taskDetail.videoUrl}`}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
</div>
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'space-between' }}>
<div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<h2 style={{ fontSize: 20, fontWeight: 600, marginBottom: 16 }}></h2>
<div style={{ textAlign: 'right' }}>
<span style={{ color: '#999', fontSize: 12 }}>: {taskDetail.createdAt}</span>
</div>
</div>
<div style={{ display: 'flex', marginBottom: 12 }}>
<span style={{ color: '#999', fontSize: 14, marginRight: 12, width: 80, flexShrink: 0 }}>:</span>
<span style={{ color: '#333', fontWeight: 500 }}>{taskDetail.title}</span>
</div>
<div style={{ display: 'flex', marginBottom: 12 }}>
<span style={{ color: '#999', fontSize: 14, marginRight: 12, width: 80, flexShrink: 0 }}>:</span>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
{(taskDetail.originalVideoAudience?.split('、') || []).map((point, index) => (
<Tag key={index} color="purple" style={{ fontSize: 12 }}>
{point}
</Tag>
))}
</div>
</div>
<div style={{ display: 'flex' }}>
<span style={{ color: '#999', fontSize: 14, marginRight: 12, width: 80, flexShrink: 0 }}>:</span>
<span style={{ color: '#333' }}>{taskDetail.originalVideoContent}</span>
</div>
</div>
</div>
</div>
</div>
<div style={{ display: 'flex', gap: 16, marginBottom: 20, marginTop: 20 }}>
<Button
type="default"
onClick={() => handleAutoGenerate()}
style={{
flex: 1,
height: 48,
borderRadius: 8,
borderColor: '#6366f1',
color: '#6366f1',
fontWeight: 500
}}
>
</Button>
<Button
type="primary"
onClick={() => handleAutoGenerate()}
style={{
flex: 1,
height: 48,
borderRadius: 8,
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
fontWeight: 500,
border: 'none'
}}
>
</Button>
</div>
<div style={{ flex: 0.6, background: '#fff', borderRadius: 12, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
<Table
columns={columns}
dataSource={tableData}
pagination={false}
bordered={false}
rowKey="id"
scroll={{ y: '1005'}}
// style={{ flex: 1 }}
/> />
<div style={{ </div>
position: 'absolute',
top: '50%', </div>
left: '50%', ) : (
transform: 'translate(-50%, -50%)', <div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
width: 48, <span style={{ fontSize: 16, color: '#999' }}>...</span>
</div>
)}
</div>
<Drawer
title={
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
<span>
<span style={{ color: '#6366f1' }}></span>
<span style={{ color: '#6366f1' }}>-{currentSegment}</span>
</span>
<Button
type="text"
icon={<XOutlined />}
onClick={handleCloseDrawer}
style={{ padding: 0 }}
/>
</div>
}
placement="right"
closable={false}
onClose={handleCloseDrawer}
open={drawerVisible}
size={480}
styles={{
body: { padding: '24px' }
}}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
<div>
<label style={{ fontWeight: 400, color: '#333', marginBottom: 12, display: 'block' }}>
<span style={{ color: '#ff4d4f' }}>*</span>
</label>
<div style={{ display: 'flex', gap: 16 }}>
<Upload
listType="picture-card"
onChange={handleProductImageChange}
beforeUpload={beforeUploadProductImage}
maxCount={1}
accept="image/*"
style={{ width: 140, height: 140 }}
>
{!productImage && (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8 }}>
<PlusOutlined style={{ fontSize: 24, color: '#999' }} />
<span style={{ fontSize: 12, color: '#999' }}> *</span>
</div>
)}
</Upload>
</div>
</div>
<div>
<label style={{ fontWeight: 500, color: '#333', marginBottom: 12, display: 'block' }}>
</label>
<Input
value={productName}
onChange={(e) => setProductName(e.target.value)}
placeholder="请输入产品名称"
style={{ height: 48, borderRadius: 8 }}
maxLength={10}
showCount
/>
</div>
<div>
<label style={{ fontWeight: 500, color: '#333', marginBottom: 12, display: 'block' }}>
</label>
<TextArea
value={productSellingPoint}
onChange={(e) => setProductSellingPoint(e.target.value)}
placeholder="请输入产品卖点"
style={{ borderRadius: 8 }}
maxLength={100}
showCount
rows={3}
/>
</div>
<div style={{ display: 'flex', gap: 16, marginTop: 24 }}>
<Button
type="default"
onClick={handleManualGenerate}
loading={loading}
disabled={loading}
style={{
flex: 1,
height: 48, height: 48,
background: 'rgba(0,0,0,0.6)', borderRadius: 8,
borderRadius: '50%', borderColor: '#6366f1',
display: 'flex', color: '#6366f1',
alignItems: 'center', fontWeight: 500
justifyContent: 'center' }}
}}> >
<PlayCircleOutlined style={{ fontSize: 28, color: '#fff' }} /> {loading ? '生成中...' : '手动生成'}
</div> </Button>
</div>
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'space-between' }}>
<div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<h2 style={{ fontSize: 20, fontWeight: 600, marginBottom: 16 }}></h2>
<div style={{ textAlign: 'right' }}>
<span style={{ color: '#999', fontSize: 12 }}>: {mockData.uploadTime}</span>
</div>
</div>
<div style={{ display: 'flex', marginBottom: 12 }}>
<span style={{ color: '#999', fontSize: 14, marginRight: 12 }}>:</span>
<span style={{ color: '#333', fontWeight: 500 }}>{mockData.productName}</span>
</div>
<div style={{ display: 'flex', marginBottom: 12 }}>
<span style={{ color: '#999', fontSize: 14, marginRight: 12 }}>:</span>
<div style={{ display: 'flex', gap: 8 }}>
{mockData.sellingPoints.map((point, index) => (
<Tag key={index} color="purple" style={{ fontSize: 12 }}>
{point}
</Tag>
))}
</div>
</div>
<div style={{ display: 'flex', marginBottom: 12 }}>
<span style={{ color: '#999', fontSize: 14, marginRight: 12 }}>:</span>
<span style={{ color: '#333' }}>{mockData.audience}</span>
</div>
<div style={{ display: 'flex' }}>
<span style={{ color: '#999', fontSize: 14, marginRight: 12 }}>:</span>
<span style={{ color: '#333' }}>{mockData.audienceAnalysis}</span>
</div>
</div>
</div>
</div> </div>
</div> </div>
</Drawer>
<div style={{ background: '#fff', borderRadius: 12, overflow: 'hidden' }}> </>
<Table
columns={columns}
dataSource={mockData.segments}
pagination={false}
bordered={false}
rowKey="key"
scroll={{ y: 520 }}
/>
</div>
</div>
</div>
<Drawer
title={
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
<span>
<span style={{ color: '#6366f1' }}></span>
<span style={{ color: '#6366f1' }}>-{currentSegment}</span>
</span>
<Button
type="text"
icon={<XOutlined />}
onClick={handleCloseDrawer}
style={{ padding: 0 }}
/>
</div>
}
placement="right"
closable={false}
onClose={handleCloseDrawer}
open={drawerVisible}
width={480}
bodyStyle={{ padding: '24px' }}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
<div>
<label style={{ fontWeight: 400, color: '#333', marginBottom: 12, display: 'block' }}>
<span style={{ color: '#ff4d4f' }}>*</span>
</label>
<div style={{ display: 'flex', gap: 16 }}>
<Upload
listType="picture-card"
onChange={handleProductImageChange}
maxCount={1}
accept="image/*"
style={{ width: 140, height: 140 }}
>
{!productImage && (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8 }}>
<PlusOutlined style={{ fontSize: 24, color: '#999' }} />
<span style={{ fontSize: 12, color: '#999' }}> *</span>
</div>
)}
</Upload>
<Upload
listType="picture-card"
onChange={handleDetailImageChange}
maxCount={1}
accept="image/*"
style={{ width: 140, height: 140 }}
>
{!detailImage && (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8 }}>
<PlusOutlined style={{ fontSize: 24, color: '#999' }} />
<span style={{ fontSize: 12, color: '#999' }}></span>
</div>
)}
</Upload>
</div>
</div>
<div>
<label style={{ fontWeight: 500, color: '#333', marginBottom: 12, display: 'block' }}>
</label>
<Input
value={productName}
onChange={(e) => setProductName(e.target.value)}
placeholder="请输入产品名称"
style={{ height: 48, borderRadius: 8 }}
maxLength={10}
showCount
/>
</div>
<div>
<label style={{ fontWeight: 500, color: '#333', marginBottom: 12, display: 'block' }}>
</label>
<TextArea
value={productSellingPoint}
onChange={(e) => setProductSellingPoint(e.target.value)}
placeholder="请输入产品卖点"
style={{ borderRadius: 8 }}
maxLength={100}
showCount
rows={3}
/>
</div>
<div style={{ display: 'flex', gap: 16, marginTop: 24 }}>
<Button
type="default"
onClick={handleManualGenerate}
style={{
flex: 1,
height: 48,
borderRadius: 8,
borderColor: '#6366f1',
color: '#6366f1',
fontWeight: 500
}}
>
</Button>
</div>
</div>
</Drawer>
</React.Fragment>
); );
} }
+119 -57
View File
@@ -1,7 +1,8 @@
import { useState, useRef, useCallback, useEffect } from 'react'; import { useState, useRef, useCallback } from 'react';
import { Button, Modal, Input, Table, Upload, Popconfirm } from 'antd'; import { Button, Modal, Input, Table, Upload, Popconfirm, message } from 'antd';
import { FileTextOutlined, CloudUploadOutlined } from '@ant-design/icons'; import { FileTextOutlined, CloudUploadOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { uploadVideo, createShotReplication, getShotReplicationList } from '../api';
export default function VideoFrameExtractor() { export default function VideoFrameExtractor() {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -10,8 +11,12 @@ export default function VideoFrameExtractor() {
const [error, setError] = useState<string>(''); const [error, setError] = useState<string>('');
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [productName, setProductName] = useState<string>(''); const [productName, setProductName] = useState<string>('');
const [videoDuration, setVideoDuration] = useState<number>(0); const [videoDuration, setVideoDuration] = useState<number>(0);
const [loading, setLoading] = useState(false);
const [tableData, setTableData] = useState<any[]>([]);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const videoRef = useRef<HTMLVideoElement>(null); const videoRef = useRef<HTMLVideoElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
@@ -26,7 +31,7 @@ export default function VideoFrameExtractor() {
setProductName(''); setProductName('');
}, [videoUrl]); }, [videoUrl]);
const handleFileChange = (file: File) => { const handleFileChange = async (file: File) => {
if (!file.type.startsWith('video/')) { if (!file.type.startsWith('video/')) {
setError('请选择视频文件'); setError('请选择视频文件');
return false; return false;
@@ -38,10 +43,20 @@ export default function VideoFrameExtractor() {
} }
cleanupResources(); cleanupResources();
setLoading(true);
try {
const uploadResult = await uploadVideo(file);
setVideoUrl(uploadResult.url);
setError('');
message.success('视频上传成功');
} catch (err) {
setError('视频上传失败,请重试');
message.error('视频上传失败');
} finally {
setLoading(false);
}
const url = URL.createObjectURL(file);
setVideoUrl(url);
setError('');
return false; return false;
}; };
@@ -51,27 +66,57 @@ export default function VideoFrameExtractor() {
} }
}, []); }, []);
const tableData = [ const handleCreate = async () => {
{ if (!videoUrl || !productName.trim()) {
id: 1, message.warning('请先上传视频并输入产品名称');
image: 'https://neeko-copilot.bytedance.net/api/text_to_image?prompt=product%20image%20red%20gift%20box%20with%20hearts&image_size=square', return;
originalName: '进圈', }
productName: '他趣',
status: '视频成功', setLoading(true);
createTime: '2026-05-14 17:49:20', try {
}, const params = {
{ video_url: videoUrl,
id: 2, video_duration_seconds: videoDuration,
image: 'https://neeko-copilot.bytedance.net/api/text_to_image?prompt=luxury%20perfume%20bottle%20golden%20elegant&image_size=square', title: productName.trim(),
originalName: '香水', idempotency_key: `shot_${Date.now()}`,
productName: '面霜', };
status: '视频提示词成功',
createTime: '2026-05-08 08:57:46', await createShotReplication(params);
}, message.success('任务创建成功');
]; // 清空上传内容和输入框
cleanupResources();
navigate('/');
} catch (err) {
message.error('任务创建失败,请重试');
} finally {
setLoading(false);
}
};
const fetchList = async (page: number, size: number) => {
try {
const res = await getShotReplicationList(page, size);
setTableData(res.items || []);
setTotal(res.total || 0);
setCurrentPage(page);
setPageSize(size);
} catch (err) {
message.error('获取列表失败');
}
};
const handlePageChange = (page: number, size: number) => {
fetchList(page, size);
};
// 打开弹窗时获取列表数据
const handleOpenModal = () => {
setIsModalOpen(true);
fetchList(1, 10);
};
return ( return (
<div style={{ minHeight: '100vh', background: 'linear-gradient(135deg, #f5f3ff 0%, #fdf2f8 100%)', padding: '20px' }}> <div style={{ minHeight: 'calc(100vh - 90px)', overflow: 'auto', background: 'linear-gradient(135deg, #f5f3ff 0%, #fdf2f8 100%)', padding: '20px' }}>
<div style={{ maxWidth: 1200, margin: '0 auto' }}> <div style={{ maxWidth: 1200, margin: '0 auto' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 24 }}> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 24 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
@@ -107,7 +152,7 @@ export default function VideoFrameExtractor() {
</div> </div>
<button <button
onClick={() => setIsModalOpen(true)} onClick={handleOpenModal}
style={{ style={{
padding: '10px 20px', padding: '10px 20px',
background: 'white', background: 'white',
@@ -254,7 +299,7 @@ export default function VideoFrameExtractor() {
}}> }}>
<video <video
ref={videoRef} ref={videoRef}
src={videoUrl} src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${videoUrl}`}
controls controls
onLoadedMetadata={handleVideoLoaded} onLoadedMetadata={handleVideoLoaded}
style={{ style={{
@@ -292,10 +337,11 @@ export default function VideoFrameExtractor() {
<div style={{ textAlign: 'center', marginTop: 16 }}> <div style={{ textAlign: 'center', marginTop: 16 }}>
<button <button
disabled={!videoUrl || !productName.trim()} onClick={handleCreate}
disabled={!videoUrl || !productName.trim() || loading}
style={{ style={{
padding: '14px 56px', padding: '14px 56px',
background: videoUrl && productName.trim() background: videoUrl && productName.trim() && !loading
? 'linear-gradient(135deg, #6366f1 0%, #ec4899 100%)' ? 'linear-gradient(135deg, #6366f1 0%, #ec4899 100%)'
: '#e2e8f0', : '#e2e8f0',
color: 'white', color: 'white',
@@ -303,14 +349,14 @@ export default function VideoFrameExtractor() {
borderRadius: 14, borderRadius: 14,
fontSize: 15, fontSize: 15,
fontWeight: 600, fontWeight: 600,
cursor: videoUrl && productName.trim() ? 'pointer' : 'not-allowed', cursor: videoUrl && productName.trim() && !loading ? 'pointer' : 'not-allowed',
boxShadow: videoUrl && productName.trim() ? '0 6px 20px rgba(99, 102, 241, 0.35)' : 'none', boxShadow: videoUrl && productName.trim() && !loading ? '0 6px 20px rgba(99, 102, 241, 0.35)' : 'none',
display: 'inline-flex', display: 'inline-flex',
alignItems: 'center', alignItems: 'center',
gap: 10 gap: 10
}} }}
> >
<span></span> <span>{loading ? '创建中...' : '创建'}</span>
<span style={{ <span style={{
fontSize: 12, fontSize: 12,
opacity: 0.85, opacity: 0.85,
@@ -345,28 +391,23 @@ export default function VideoFrameExtractor() {
<Table <Table
columns={[ columns={[
{ // {
title: '产品图片', // title: '产品图片',
dataIndex: 'image', // dataIndex: 'image',
key: 'image', // key: 'image',
width: 90, // width: 90,
render: (image: string) => ( // render: (image: string) => (
<img // <img
src={image} // src={image}
alt="产品图片" // alt="产品图片"
style={{ width: 50, height: 50, objectFit: 'cover', borderRadius: 6 }} // style={{ width: 50, height: 50, objectFit: 'cover', borderRadius: 6 }}
/> // />
), // ),
}, // },
{
title: '原产品名称',
dataIndex: 'originalName',
key: 'originalName',
},
{ {
title: '产品名称', title: '产品名称',
dataIndex: 'productName', dataIndex: 'title',
key: 'productName', key: 'title',
}, },
{ {
title: '状态', title: '状态',
@@ -375,13 +416,13 @@ export default function VideoFrameExtractor() {
}, },
{ {
title: '创建时间', title: '创建时间',
dataIndex: 'createTime', dataIndex: 'createdAt',
key: 'createTime', key: 'createdAt',
}, },
{ {
title: '操作', title: '操作',
key: 'action', key: 'action',
render: (_, record) => ( render: (record) => (
<button <button
onClick={() => navigate(`/removelens/${record.id}/removeinfo`)} onClick={() => navigate(`/removelens/${record.id}/removeinfo`)}
style={{ color: '#6366f1', textDecoration: 'none', fontSize: 13, border: 'none', background: 'none', cursor: 'pointer' }} style={{ color: '#6366f1', textDecoration: 'none', fontSize: 13, border: 'none', background: 'none', cursor: 'pointer' }}
@@ -393,7 +434,28 @@ export default function VideoFrameExtractor() {
]} ]}
dataSource={tableData} dataSource={tableData}
rowKey="id" rowKey="id"
pagination={false} pagination={{
current: currentPage,
pageSize: pageSize,
total: total,
onChange: handlePageChange,
showSizeChanger: false,
showQuickJumper: false,
showTotal: (total) => `${total}`,
placement: ['bottomCenter'],
itemRender: (_, type, originalElement) => {
if (type === 'page') {
return <span style={{ borderRadius: 4, margin: '0 4px' }}>{originalElement}</span>;
}
if (type === 'prev') {
return <span style={{ borderRadius: 4, margin: '0 4px' }}></span>;
}
if (type === 'next') {
return <span style={{ borderRadius: 4, margin: '0 4px' }}></span>;
}
return originalElement;
},
}}
/> />
</Modal> </Modal>
</div> </div>
File diff suppressed because it is too large Load Diff