Files
video-gen/video-gen-app/src/pages/RemoveLens.tsx
T
2026-06-17 16:33:52 +08:00

466 lines
15 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';
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 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;
}
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()}`,
};
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 (
<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={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 24 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div>
<div style={{
width: 36,
height: 36,
background: 'linear-gradient(135deg, #6366f1 0%, #ec4899 100%)',
borderRadius: 10,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
fontSize: 18,
fontWeight: 600
}}>
</div>
</div>
<div>
<h1 style={{
fontSize: 22,
fontWeight: 600,
margin: 0,
color: '#1e293b'
}}>
拆镜复刻
</h1>
<p style={{ fontSize: 13, color: '#94a3b8', margin: 4 }}>
一键拆解画面分镜,助力仿拍或创作
</p>
</div>
</div>
<button
onClick={handleOpenModal}
style={{
padding: '10px 20px',
background: 'white',
border: '1px solid #e2e8f0',
borderRadius: 8,
cursor: 'pointer',
fontSize: 14,
color: '#6366f1',
display: 'flex',
alignItems: 'center',
gap: 8,
boxShadow: '0 2px 8px rgba(0,0,0,0.06)'
}}
>
<FileTextOutlined />
<span>创作记录</span>
</button>
</div>
<div style={{ display: 'flex', justifyContent: 'center', marginBottom: 32 }}>
<div style={{ textAlign: 'center' }}>
<div style={{
width: 56,
height: 56,
background: 'linear-gradient(135deg, #e0e7ff 0%, #fce7f3 100%)',
borderRadius: 16,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
margin: '0 auto 16px'
}}>
<span style={{ fontSize: 28 }}>🎬</span>
</div>
<h2 style={{
fontSize: 22,
fontWeight: 600,
margin: 0,
background: 'linear-gradient(135deg, #6366f1 0%, #ec4899 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text'
}}>
上传视频开始创作
</h2>
<p style={{ fontSize: 14, color: '#888', margin: '8px 0 0 0' }}>
支持 MP4MOV 格式视频,最大支持 100MB
</p>
</div>
</div>
<div style={{ width: '100%', maxWidth: 720, margin: '0 auto' }}>
{error && (
<div style={{
backgroundColor: '#fff5f5',
border: '1px solid #ffccc7',
borderRadius: 8,
padding: '12px 16px',
marginBottom: 20,
color: '#d93026',
fontSize: 14,
textAlign: 'center'
}}>
{error}
</div>
)}
{!videoUrl && (
<Upload.Dragger
accept="video/*"
beforeUpload={handleFileChange}
showUploadList={false}
style={{
background: '#faf5ff',
border: '2px dashed #c4b5fd',
borderRadius: 24,
padding: '60px 20px'
}}
>
<div style={{
width: 100,
height: 100,
margin: '0 auto 20px',
background: 'linear-gradient(135deg, #e0e7ff 0%, #fce7f3 100%)',
borderRadius: 24,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<CloudUploadOutlined style={{ fontSize: 40, color: '#818cf8' }} />
</div>
<p style={{ fontSize: 16, color: '#333', margin: '0 0 8px 0', fontWeight: 500 }}>
点击或拖拽到此上传视频文件
</p>
<p style={{ fontSize: 13, color: '#999', margin: 0 }}>
支持 MP4MOV 格式视频,最大支持 100MB 的文件,支持上传 3 分钟内的视频
</p>
</Upload.Dragger>
)}
{videoUrl && (
<div style={{
position: 'relative',
borderRadius: 24,
padding: 4,
background: 'linear-gradient(135deg, #6366f1 0%, #ec4899 100%)'
}}>
<div style={{
borderRadius: 22,
background: '#ffffff',
padding: '24px'
}}>
<Popconfirm
title="确定要删除吗?"
okText="确定"
cancelText="取消"
onConfirm={cleanupResources}
>
<button
style={{
position: 'absolute',
top: 20,
right: 20,
width: 36,
height: 36,
border: '1px solid #e5e7eb',
background: '#fff',
borderRadius: 10,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 16,
color: '#666',
zIndex: 10
}}
>
🗑️
</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'
}}
/>
</div>
{/* 产品名称输入框 */}
<div style={{ marginBottom: 24 }}>
<label style={{
display: 'block',
fontSize: 14,
fontWeight: 500,
color: '#374151',
marginBottom: 8
}}>
产品名称
</label>
<Input
placeholder="请输入产品名称"
value={productName}
onChange={(e) => setProductName(e.target.value)}
style={{
width: '100%',
height: 40,
borderRadius: 8,
borderColor: '#e5e7eb'
}}
/>
</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%, #ec4899 100%)'
: '#e2e8f0',
color: 'white',
border: 'none',
borderRadius: 14,
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.35)' : 'none',
display: 'inline-flex',
alignItems: 'center',
gap: 10
}}
>
<span>{loading ? '创建中...' : '创建'}</span>
<span style={{
fontSize: 12,
opacity: 0.85,
fontWeight: 400
}}>
预计消耗 10 个积分
</span>
</button>
</div>
</div>
</div>
)}
<canvas ref={canvasRef} style={{ display: 'none' }} />
<Modal
title="创作记录"
open={isModalOpen}
onCancel={() => setIsModalOpen(false)}
width={800}
footer={null}
>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 16 }}>
<Input
placeholder="搜索产品名称"
style={{ width: 200, borderRadius: 6, marginRight: 8 }}
/>
<Button type="primary" style={{ borderRadius: 6 }}>
搜索
</Button>
</div>
<Table
columns={[
// {
// title: '产品图片',
// dataIndex: 'image',
// key: 'image',
// width: 90,
// render: (image: string) => (
// <img
// src={image}
// alt="产品图片"
// style={{ width: 50, height: 50, objectFit: 'cover', borderRadius: 6 }}
// />
// ),
// },
{
title: '产品名称',
dataIndex: 'title',
key: 'title',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
},
{
title: '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
},
{
title: '操作',
key: 'action',
render: (record) => (
<button
onClick={() => navigate(`/removelens/${record.id}/removeinfo`)}
style={{ color: '#6366f1', textDecoration: 'none', fontSize: 13, border: 'none', background: 'none', cursor: 'pointer' }}
>
查看详情
</button>
),
},
]}
dataSource={tableData}
rowKey="id"
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>
</div>
</div>
</div>
);
}