Files
video-gen/video-gen-app/src/pages/RemoveLens.tsx
T
2026-06-30 10:16:03 +08:00

655 lines
24 KiB
TypeScript

import { useState, useRef, useCallback } from 'react';
import { Button, Modal, Input, Table, Upload, Popconfirm, message } from 'antd';
import { FileTextOutlined, CloudUploadOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { uploadVideo, createShotReplication, getShotReplicationList } from '../api';
import bg1 from '../assets/bg1.png';
export default function VideoFrameExtractor() {
const navigate = useNavigate();
const [videoUrl, setVideoUrl] = useState<string>('');
const [error, setError] = useState<string>('');
const [isModalOpen, setIsModalOpen] = useState(false);
const [productName, setProductName] = useState<string>('');
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 [searchKeyword, setSearchKeyword] = useState('');
const videoRef = useRef<HTMLVideoElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const cleanupResources = useCallback(() => {
if (videoUrl) {
URL.revokeObjectURL(videoUrl);
}
setVideoUrl('');
setError('');
setVideoDuration(0);
setProductName('');
}, [videoUrl]);
const handleFileChange = async (file: File) => {
if (!file.type.startsWith('video/')) {
setError('请选择视频文件');
return false;
}
if (file.size > 100 * 1024 * 1024) {
setError('视频文件大小不能超过 100MB');
return false;
}
const duration = await new Promise<number>((resolve) => {
const video = document.createElement('video');
video.preload = 'metadata';
video.onloadedmetadata = () => {
resolve(video.duration);
URL.revokeObjectURL(video.src);
};
video.onerror = () => {
resolve(0);
};
video.src = URL.createObjectURL(file);
});
if (duration > 60) {
setError('视频时长不能超过 1分钟');
return false;
}
cleanupResources();
setLoading(true);
try {
const uploadResult = await uploadVideo(file);
setVideoUrl(uploadResult.url);
setError('');
message.success('视频上传成功');
} catch (err) {
setError('视频上传失败,请重试');
message.error('视频上传失败');
} finally {
setLoading(false);
}
return false;
};
const handleVideoLoaded = useCallback(() => {
if (videoRef.current) {
setVideoDuration(videoRef.current.duration);
}
}, []);
const handleCreate = async () => {
if (!videoUrl || !productName.trim()) {
message.warning('请先上传视频并输入产品名称');
return;
}
setLoading(true);
try {
const params = {
video_url: videoUrl,
video_duration_seconds: videoDuration,
title: productName.trim(),
idempotency_key: `shot_${Date.now()}`,
};
createShotReplication(params).then((res) => {
// message.success('任务创建成功');
getShotReplicationList(1, 20, searchKeyword).then((res) => {
// console.log('获取列表成功', res.items[0].id);
message.loading('创建中...', 3);
setTimeout(() => {
navigate(`/removelens/${res.items[0].id}/removeinfo`);
}, 3000);
// setTableData(res.items || []);
})
})
cleanupResources();
} catch (err) {
message.error('任务创建失败,请重试');
} finally {
setLoading(false);
}
};
const fetchList = async (page: number, size: number, keyword?: string) => {
try {
const res = await getShotReplicationList(page, size, keyword);
setTableData(res.items || []);
setTotal(res.total || 0);
setCurrentPage(page);
setPageSize(size);
} catch (err) {
message.error('获取列表失败');
}
};
const handlePageChange = (page: number, size: number) => {
fetchList(page, size, searchKeyword);
};
const handleSearch = () => {
fetchList(1, 10, searchKeyword);
};
const handleOpenModal = () => {
setIsModalOpen(true);
fetchList(1, 10, searchKeyword);
};
return (
<div
style={{
margin: '-24px -32px -32px',
borderRadius: 20,
minHeight: 'calc(100vh - 34px)',
overflow: 'auto',
background: 'linear-gradient(135deg, #f8fafc 0%, #eef2ff 50%, #f0f9ff 100%)',
position: 'relative',
padding: '20px 32px',
backgroundImage: `url(${bg1})`,
backgroundRepeat: 'no-repeat',
backgroundSize: '100% 100%',
backgroundPosition: 'center',
}}
>
<div style={{ maxWidth: 1200, margin: '0 auto', position: 'relative', zIndex: 10 }}>
<div className="animate-fadeInUp" style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
marginBottom: 24, padding: '16px 24px', borderRadius: 16,
background: 'rgba(255,255,255,0.6)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(99, 102, 241, 0.08)',
position: 'relative', overflow: 'hidden', flexWrap: 'wrap', gap: 12,
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<div style={{ width: 32, height: 2, background: 'linear-gradient(90deg, transparent, #6366f1, #8b5cf6, transparent)', borderRadius: 1 }} />
<div>
<h2 style={{
margin: 0,
fontSize: 18,
fontWeight: 700,
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text'
}}>
AI视频拆镜工作台
</h2>
<p style={{ fontSize: 13, color: '#64748b', margin: '4px 0 0 0' }}>
一键拆解画面分镜,助力仿拍或创作
</p>
</div>
<div style={{ width: 32, height: 2, background: 'linear-gradient(90deg, transparent, #8b5cf6, #6366f1, transparent)', borderRadius: 1 }} />
</div>
<Button
type="primary"
ghost
icon={<FileTextOutlined />}
onClick={handleOpenModal}
style={{
borderRadius: 10,
padding: '6px 16px',
fontSize: 13,
background: 'rgba(99, 102, 241, 0.1)',
borderColor: 'rgba(99, 102, 241, 0.2)',
color: '#6366f1',
}}
>
创作记录
</Button>
</div>
<div style={{ width: '100%', maxWidth: 720, margin: '0 auto' }}>
{error && (
<div style={{
background: 'linear-gradient(135deg, rgba(239, 68, 68, 0.1) 0%, rgba(239, 68, 68, 0.05) 100%)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(239, 68, 68, 0.2)',
borderRadius: 12,
padding: '14px 20px',
marginBottom: 24,
color: '#ef4444',
fontSize: 14,
textAlign: 'center',
fontWeight: 500,
}}>
{error}
</div>
)}
{!videoUrl && (
<div
style={{
background: 'linear-gradient(135deg, rgba(255,255,255,0.95) 0%, rgba(255,255,255,0.85) 100%)',
backdropFilter: 'blur(20px)',
borderRadius: 24,
border: '1px solid rgba(99, 102, 241, 0.1)',
boxShadow: '0 8px 32px rgba(99, 102, 241, 0.08)',
position: 'relative',
overflow: 'hidden',
}}
>
<div style={{ position: 'absolute', top: -50, right: -50, width: 200, height: 200, background: 'radial-gradient(circle, rgba(99,102,241,0.05) 0%, transparent 70%)', borderRadius: '50%' }} />
<Upload.Dragger
accept="video/*"
beforeUpload={handleFileChange}
showUploadList={false}
disabled={loading}
style={{
background: loading ? 'rgba(243, 244, 246, 0.3)' : 'transparent',
border: `2px dashed ${loading ? 'rgba(209, 213, 219, 0.5)' : 'rgba(99, 102, 241, 0.3)'}`,
borderRadius: 24,
padding: '56px 20px',
cursor: loading ? 'not-allowed' : 'pointer',
margin: 0,
}}
>
{loading ? (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<div style={{
width: 100,
height: 100,
margin: '0 auto 20px',
background: 'linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%)',
borderRadius: 24,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<div style={{
width: 40,
height: 40,
border: '4px solid #6366f1',
borderTopColor: 'transparent',
borderRadius: '50%',
animation: 'spin 1s linear infinite'
}} />
</div>
<p style={{ fontSize: 16, color: '#6366f1', margin: '0 0 8px 0', fontWeight: 600 }}>
上传中...
</p>
<p style={{ fontSize: 13, color: '#64748b', margin: 0 }}>
请稍候,视频正在上传
</p>
</div>
) : (
<>
<div style={{
width: 100,
height: 100,
margin: '0 auto 20px',
background: 'linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%)',
borderRadius: 24,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<CloudUploadOutlined style={{ fontSize: 40, color: '#6366f1' }} />
</div>
<p style={{ fontSize: 16, color: '#1e293b', margin: '0 0 8px 0', fontWeight: 600 }}>
点击或拖拽到此上传视频文件
</p>
<p style={{ fontSize: 13, color: '#64748b', margin: 0 }}>
支持 MP4MOV 格式视频,最大支持 100MB 的文件,支持上传 1 分钟内的视频
</p>
</>
)}
</Upload.Dragger>
</div>
)}
{videoUrl && (
<div
style={{
background: 'linear-gradient(135deg, rgba(255,255,255,0.95) 0%, rgba(255,255,255,0.85) 100%)',
backdropFilter: 'blur(20px)',
borderRadius: 24,
border: '1px solid rgba(99, 102, 241, 0.1)',
boxShadow: '0 8px 32px rgba(99, 102, 241, 0.08)',
overflow: 'hidden',
position: 'relative',
}}
>
<div style={{ position: 'absolute', top: -50, right: -50, width: 200, height: 200, background: 'radial-gradient(circle, rgba(99,102,241,0.05) 0%, transparent 70%)', borderRadius: '50%' }} />
<div style={{
borderRadius: 22,
background: '#ffffff',
padding: '24px',
position: 'relative',
zIndex: 1,
}}>
<Popconfirm
title="确定要删除吗?"
okText="确定"
cancelText="取消"
onConfirm={cleanupResources}
>
<button
style={{
position: 'absolute',
top: 20,
right: 20,
width: 36,
height: 36,
border: '1px solid rgba(99, 102, 241, 0.2)',
background: 'rgba(99, 102, 241, 0.05)',
borderRadius: 10,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 16,
color: '#6366f1',
zIndex: 10,
transition: 'all 0.2s ease',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(239, 68, 68, 0.1)';
e.currentTarget.style.borderColor = 'rgba(239, 68, 68, 0.3)';
e.currentTarget.style.color = '#ef4444';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'rgba(99, 102, 241, 0.05)';
e.currentTarget.style.borderColor = 'rgba(99, 102, 241, 0.2)';
e.currentTarget.style.color = '#6366f1';
}}
>
🗑️
</button>
</Popconfirm>
<div style={{
textAlign: 'center',
marginBottom: 20
}}>
<video
ref={videoRef}
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${videoUrl}`}
controls
onLoadedMetadata={handleVideoLoaded}
style={{
maxWidth: '100%',
maxHeight: 320,
borderRadius: 12,
background: '#000',
boxShadow: '0 4px 16px rgba(0,0,0,0.1)'
}}
/>
</div>
<div style={{ marginBottom: 24 }}>
<label style={{
display: 'block',
fontSize: 14,
fontWeight: 500,
color: '#334155',
marginBottom: 8
}}>
产品名称 <span style={{ color: '#ef4444' }}>*</span>
</label>
<Input
placeholder="请输入产品名称"
value={productName}
onChange={(e) => setProductName(e.target.value)}
style={{
width: '100%',
height: 44,
borderRadius: 10,
border: '1px solid rgba(99, 102, 241, 0.2)',
transition: 'all 0.2s ease',
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = 'rgba(99, 102, 241, 0.4)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = 'rgba(99, 102, 241, 0.2)';
}}
/>
</div>
<div style={{ textAlign: 'center', marginTop: 16 }}>
<button
onClick={handleCreate}
disabled={!videoUrl || !productName.trim() || loading}
style={{
padding: '14px 56px',
background: videoUrl && productName.trim() && !loading
? 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a855f7 100%)'
: '#e2e8f0',
color: 'white',
border: 'none',
borderRadius: 12,
fontSize: 15,
fontWeight: 600,
cursor: videoUrl && productName.trim() && !loading ? 'pointer' : 'not-allowed',
boxShadow: videoUrl && productName.trim() && !loading ? '0 6px 20px rgba(99, 102, 241, 0.3)' : 'none',
display: 'inline-flex',
alignItems: 'center',
gap: 10,
transition: 'all 0.3s ease',
}}
onMouseEnter={(e) => {
if (videoUrl && productName.trim() && !loading) {
e.currentTarget.style.transform = 'translateY(-2px)';
e.currentTarget.style.boxShadow = '0 8px 25px rgba(99, 102, 241, 0.4)';
}
}}
onMouseLeave={(e) => {
if (videoUrl && productName.trim() && !loading) {
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = '0 6px 20px rgba(99, 102, 241, 0.3)';
}
}}
>
<span>{loading ? '创建中...' : '创建'}</span>
</button>
</div>
</div>
</div>
)}
<canvas ref={canvasRef} style={{ display: 'none' }} />
<Modal
title={
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{ width: 4, height: 20, background: 'linear-gradient(180deg, #6366f1 0%, #8b5cf6 100%)', borderRadius: 2 }} />
<span style={{
fontSize: 16,
fontWeight: 700,
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text'
}}>
创作记录
</span>
</div>
}
open={isModalOpen}
onCancel={() => setIsModalOpen(false)}
width={850}
footer={null}
style={{ borderRadius: 20 }}
styles={{
body: { height: 580, display: 'flex', flexDirection: 'column', padding: 0 },
header: { background: 'rgba(255,255,255,0.6)', backdropFilter: 'blur(10px)', borderBottom: '1px solid rgba(99, 102, 241, 0.08)', padding: '16px 24px' },
}}
>
{/* 搜索区域 */}
<div style={{ display: 'flex', justifyContent: 'flex-end', margin: '16px 24px', flexShrink: 0 }}>
<Input
placeholder="搜索产品名称"
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
onPressEnter={handleSearch}
style={{
width: 220,
borderRadius: 10,
marginRight: 10,
border: '1px solid rgba(99, 102, 241, 0.15)',
background: 'rgba(255,255,255,0.8)',
}}
/>
<Button
type="primary"
onClick={handleSearch}
style={{
borderRadius: 10,
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
border: 'none',
}}
>
搜索
</Button>
</div>
<div style={{ flex: 1, overflow: 'auto', padding: '0 24px 24px' }}>
<div style={{
background: 'rgba(255,255,255,0.85)',
backdropFilter: 'blur(10px)',
borderRadius: 16,
overflow: 'hidden',
border: '1px solid rgba(99, 102, 241, 0.08)',
boxShadow: '0 4px 16px rgba(99, 102, 241, 0.06)',
}}>
<Table
columns={[
{
title: '产品名称',
dataIndex: 'title',
key: 'title',
render: (text: string) => (
<span style={{ fontSize: 14, color: '#1e293b', fontWeight: 500 }}>{text}</span>
),
},
{
title: '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
render: (text: string) => {
if (!text) return '-';
const date = new Date(text);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return <span style={{ fontSize: 13, color: '#64748b' }}>{`${year}-${month}-${day} ${hours}:${minutes}:${seconds}`}</span>;
},
},
{
title: '操作',
key: 'action',
align: 'center',
render: (record) => (
<Button
type="text"
onClick={() => navigate(`/removelens/${record.id}/removeinfo`)}
style={{
color: '#6366f1',
fontSize: 13,
padding: '4px 12px',
borderRadius: 6,
background: 'rgba(99, 102, 241, 0.1)',
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'rgba(99, 102, 241, 0.15)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'rgba(99, 102, 241, 0.1)';
}}
>
查看详情
</Button>
),
},
]}
dataSource={tableData}
rowKey="id"
pagination={{
current: currentPage,
pageSize: pageSize,
total: total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `共 ${total} 条`,
onChange: handlePageChange,
style: {
padding: '16px 24px',
borderTop: '1px solid rgba(99, 102, 241, 0.1)',
},
}}
style={{ fontSize: 13 }}
scroll={{ y: 350 }}
components={{
body: {
row: ({ className, style, ...rest }) => (
<tr
{...rest}
className={className}
style={{
...style,
transition: 'all 0.2s ease',
borderBottom: '1px solid rgba(99, 102, 241, 0.05)',
}}
/>
),
cell: ({ className, style, ...rest }) => (
<td
{...rest}
className={className}
style={{
...style,
padding: '16px 24px',
}}
/>
),
},
header: {
cell: ({ className, style, ...rest }) => (
<th
{...rest}
className={className}
style={{
...style,
background: 'rgba(99, 102, 241, 0.03)',
color: '#64748b',
fontWeight: 500,
fontSize: 13,
padding: '16px 24px',
borderBottom: 'none',
}}
/>
),
},
}}
/>
</div>
</div>
</Modal>
</div>
</div>
</div>
);
}