Merge branch 'main' into dev
This commit is contained in:
@@ -17,6 +17,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.hero {
|
.hero {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
|||||||
@@ -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 />} />
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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%' }}>
|
||||||
|
|||||||
@@ -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' }}
|
||||||
>
|
>
|
||||||
查看详情
|
查看详情
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user