diff --git a/video-gen-app/src/pages/RemoveInfo.tsx b/video-gen-app/src/pages/RemoveInfo.tsx
new file mode 100644
index 00000000..ec24e62f
--- /dev/null
+++ b/video-gen-app/src/pages/RemoveInfo.tsx
@@ -0,0 +1,419 @@
+import React, { useState } from 'react';
+import { useParams, useNavigate } from 'react-router-dom';
+import { Button, Table, Tag, Drawer, Input, Upload, message } from 'antd';
+import { ArrowLeftOutlined, PlayCircleOutlined, XOutlined, PlusOutlined, UploadOutlined } from '@ant-design/icons';
+import type { UploadFile } from 'antd';
+
+const { TextArea } = Input;
+
+function RemoveInfo() {
+ const { creatID } = useParams<{ creatID: string }>();
+ const navigate = useNavigate();
+ const [drawerVisible, setDrawerVisible] = useState(false);
+ const [currentSegment, setCurrentSegment] = useState
(null);
+ const [productName, setProductName] = useState('');
+ const [productSellingPoint, setProductSellingPoint] = useState('');
+ const [productImage, setProductImage] = useState('');
+ const [detailImage, setDetailImage] = useState('');
+
+ const handleGenerate = (segmentId: number) => {
+ setCurrentSegment(segmentId);
+ setDrawerVisible(true);
+ };
+
+ const handleCloseDrawer = () => {
+ setDrawerVisible(false);
+ setCurrentSegment(null);
+ setProductName('');
+ setProductSellingPoint('');
+ setProductImage('');
+ setDetailImage('');
+ };
+
+ const handleProductImageChange: any = (info: any) => {
+ 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('');
+ }
+ };
+
+ const handleDetailImageChange: any = (info: any) => {
+ if (info.fileList.length > 0) {
+ const file = info.fileList[0];
+ if (file.originFileObj) {
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ setDetailImage(e.target?.result as string);
+ };
+ reader.readAsDataURL(file.originFileObj);
+ }
+ } else {
+ setDetailImage('');
+ }
+ };
+
+ const handleManualGenerate = () => {
+ // 必填校验
+ if (!productImage) {
+ message.warning('请上传产品图');
+ return;
+ }
+ if (!productName.trim()) {
+ message.warning('请输入产品名称');
+ return;
+ }
+ if (!productSellingPoint.trim()) {
+ message.warning('请输入产品卖点');
+ return;
+ }
+
+ // 输出内容
+ console.log('手动生成 - 片段', currentSegment);
+ console.log('产品图:', productImage);
+ console.log('细节图:', detailImage);
+ console.log('产品名称:', productName);
+ console.log('产品卖点:', productSellingPoint);
+
+ message.success(`手动生成成功!片段: ${currentSegment}`);
+ };
+
+ const mockData = {
+ productName: '返回',
+ uploadTime: '2026-06-09 09:01:16',
+ sellingPoints: ['一键匹配', '连麦聊天'],
+ audience: '123123',
+ audienceAnalysis: '123123',
+ videoUrl: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=320&h=180&fit=crop',
+ segments: [
+ {
+ key: '1',
+ 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 = [
+ {
+ title: '片段',
+ width: 100,
+ render: (text: any, record: any) => (
+
+
片段{record.id}
+
{record.timeRange}
+
+ )
+ },
+ {
+ title: '片段视频',
+ width: 120,
+ render: (text: any, record: any) => (
+
+

+
+
+ )
+ },
+ {
+ title: '画面内容',
+ width: 250,
+
+ render: (text: any, record: any) => (
+
+ {record.content}
+
+ )
+ },
+ {
+ title: '台词',
+ width: 250,
+
+ render: (text: any, record: any) => (
+
+ {record.lines}
+
+ )
+ },
+ {
+ title: '内容策略',
+ width: 120,
+ align: 'left' as const,
+ render: (text: any, record: any) => (
+
+ {record.contentStrategy || '-'}
+
+ )
+ },
+ {
+ title: '素材',
+ width: 140,
+ align: 'center' as const,
+ render: (text: any, record: any) => (
+
+
+ 等待生成
+
+
+
+ )
+ }
+ ];
+
+ return (
+
+
+
+
+ }
+ onClick={() => navigate(-1)}
+ style={{ fontSize: 16, color: '#666' }}
+ />
+ {mockData.productName}
+
+
+
+
+
+
+
+

+
+
+
+
+
+
+
视频总结
+
+ 上传时间: {mockData.uploadTime}
+
+
+
+
+ 产品名称:
+ {mockData.productName}
+
+
+
卖点词:
+
+ {mockData.sellingPoints.map((point, index) => (
+
+ {point}
+
+ ))}
+
+
+
+ 受众群体:
+ {mockData.audience}
+
+
+ 受众分析:
+ {mockData.audienceAnalysis}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 智能视频复刻
+ -片段{currentSegment}
+
+ }
+ onClick={handleCloseDrawer}
+ style={{ padding: 0 }}
+ />
+
+ }
+ placement="right"
+ closable={false}
+ onClose={handleCloseDrawer}
+ open={drawerVisible}
+ width={480}
+ bodyStyle={{ padding: '24px' }}
+ >
+
+
+
+
+
+ {!productImage && (
+
+ )}
+
+
+ {!detailImage && (
+
+ )}
+
+
+
+
+
+
+ setProductName(e.target.value)}
+ placeholder="请输入产品名称"
+ style={{ height: 48, borderRadius: 8 }}
+ maxLength={10}
+ showCount
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default RemoveInfo;
diff --git a/video-gen-app/src/pages/RemoveLens.tsx b/video-gen-app/src/pages/RemoveLens.tsx
index 4e555bc1..df60f6ba 100644
--- a/video-gen-app/src/pages/RemoveLens.tsx
+++ b/video-gen-app/src/pages/RemoveLens.tsx
@@ -1,613 +1,718 @@
-import { useState, useRef, useCallback } from 'react';
-
-interface FrameData {
- url: string;
- time: number;
- selected: boolean;
-}
+import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
+import { Button, Modal, Input, Table, Upload, Popconfirm } from 'antd';
+import { FileTextOutlined, CloudUploadOutlined } from '@ant-design/icons';
+import { useNavigate } from 'react-router-dom';
export default function VideoFrameExtractor() {
- const [videoFile, setVideoFile] = useState
(null);
+ const navigate = useNavigate();
+
const [videoUrl, setVideoUrl] = useState('');
- const [frames, setFrames] = useState([]);
- const [isExtracting, setIsExtracting] = useState(false);
- const [extractProgress, setExtractProgress] = useState(0);
- const [selectedFrames, setSelectedFrames] = useState([]);
const [error, setError] = useState('');
+ const [isModalOpen, setIsModalOpen] = useState(false);
+
+ const [videoDuration, setVideoDuration] = useState(0);
+ const [segmentStart, setSegmentStart] = useState(0);
+ const [segmentEnd, setSegmentEnd] = useState(0);
+ const [isSegmentSelected, setIsSegmentSelected] = useState(false);
+
+ const [dragging, setDragging] = useState<'start' | 'end' | null>(null);
+ const [isPlaying, setIsPlaying] = useState(false);
+ const [currentTime, setCurrentTime] = useState(0);
+ const [framePreviews, setFramePreviews] = useState([]);
+
+ const [tableData] = useState([
+ {
+ id: 1,
+ image: 'https://neeko-copilot.bytedance.net/api/text_to_image?prompt=product%20image%20red%20gift%20box%20with%20hearts&image_size=square',
+ originalName: '进圈',
+ productName: '他趣',
+ status: '视频成功',
+ createTime: '2026-05-14 17:49:20',
+ },
+ {
+ id: 2,
+ image: 'https://neeko-copilot.bytedance.net/api/text_to_image?prompt=luxury%20perfume%20bottle%20golden%20elegant&image_size=square',
+ originalName: '香水',
+ productName: '面霜',
+ status: '视频提示词成功',
+ createTime: '2026-05-08 08:57:46',
+ },
+ ]);
const videoRef = useRef(null);
+ const timelineRef = useRef(null);
const canvasRef = useRef(null);
+ const hiddenVideoRef = useRef(null);
const cleanupResources = useCallback(() => {
if (videoUrl) {
URL.revokeObjectURL(videoUrl);
}
- frames.forEach(frame => {
- if (frame.url.startsWith('blob:')) {
- URL.revokeObjectURL(frame.url);
- }
- });
- setFrames([]);
setVideoUrl('');
- setVideoFile(null);
setError('');
- setSelectedFrames([]);
- }, [videoUrl, frames]);
-
- const handleFileChange = (e: React.ChangeEvent) => {
- const file = e.target.files?.[0];
- if (!file) return;
+ setVideoDuration(0);
+ setSegmentStart(0);
+ setSegmentEnd(0);
+ setIsSegmentSelected(false);
+ setIsPlaying(false);
+ setCurrentTime(0);
+ setFramePreviews([]);
+ }, [videoUrl]);
+ const handleFileChange = (file: File) => {
if (!file.type.startsWith('video/')) {
setError('请选择视频文件');
- return;
+ return false;
}
if (file.size > 100 * 1024 * 1024) {
setError('视频文件大小不能超过 100MB');
- return;
+ return false;
}
cleanupResources();
- setVideoFile(file);
const url = URL.createObjectURL(file);
setVideoUrl(url);
setError('');
+ return false;
};
- const handleDrop = (e: React.DragEvent) => {
- e.preventDefault();
- const file = e.dataTransfer.files?.[0];
- if (!file) return;
-
- if (!file.type.startsWith('video/')) {
- setError('请选择视频文件');
- return;
- }
-
- if (file.size > 100 * 1024 * 1024) {
- setError('视频文件大小不能超过 100MB');
- return;
- }
-
- cleanupResources();
-
- setVideoFile(file);
- const url = URL.createObjectURL(file);
- setVideoUrl(url);
- setError('');
- };
-
- const handleDragOver = (e: React.DragEvent) => {
- e.preventDefault();
- };
-
- /**
- * 视频帧提取核心函数 - 拆镜功能的主要实现
- *
- * 功能说明:
- * - 从上传的视频中按时间间隔提取帧画面
- * - 每秒提取一帧,确保覆盖整个视频时长
- * - 默认选中前4秒的帧(4帧),用于后续创作
- *
- * 技术实现:
- * - 使用 HTML5 Video API 进行视频帧捕获
- * - 通过 Canvas API 将视频帧转换为图片
- * - 使用 Promise + 事件监听确保帧数据就绪
- * - 添加超时机制防止死锁
- */
- const extractFrames = useCallback(async () => {
- // 前置检查:确保视频和画布元素已就绪
- if (!videoRef.current || !canvasRef.current) return;
-
- // 初始化提取状态
- setIsExtracting(true); // 标记正在提取中
- setExtractProgress(1); // 重置进度为0%
- setSelectedFrames([]); // 清空已选帧列表
-
- // 获取视频和画布引用
- const video = videoRef.current;
+ const generateFramePreviews = useCallback(async () => {
+ if (!canvasRef.current || videoDuration <= 0 || !videoUrl) return;
+
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
- if (!ctx) return; // Canvas 2D上下文获取失败则退出
+ if (!ctx) return;
- // 计算提取参数
- const duration = video.duration; // 视频总时长(秒)
- const frameList: FrameData[] = []; // 存储提取的帧数据
- const extractInterval = 1; // 提取间隔:每1秒抽一帧
- const totalFrames = Math.max(1, Math.floor(duration / extractInterval)); // 计算总帧数
-
- try {
- // 遍历视频时间轴,按间隔提取帧
- for (let time = 0; time < duration; time += extractInterval) {
- // 设置视频当前播放位置到目标时间点
- video.currentTime = time;
-
- // 等待帧数据就绪(异步等待)
- await new Promise((resolve) => {
- /**
- * 检查视频就绪状态
- * readyState >= 2 表示当前帧数据已加载完成
- * readyState 值说明:
- * - 0 = HAVE_NOTHING: 无数据
- * - 1 = HAVE_METADATA: 仅元数据
- * - 2 = HAVE_CURRENT_DATA: 当前帧数据可用
- * - 3 = HAVE_FUTURE_DATA: 当前帧和后续帧可用
- * - 4 = HAVE_ENOUGH_DATA: 所有数据可用
- */
- const checkReadyState = () => {
- if (video.readyState >= 2) {
- processFrame();
- return true;
- }
- return false;
- };
-
- /**
- * 处理单帧提取的核心函数
- */
- const processFrame = () => {
- // 清理事件监听器,防止内存泄漏
- video.removeEventListener('seeked', onSeeked);
- video.removeEventListener('loadeddata', onLoadedData);
- clearTimeout(timeoutId); // 清除超时计时器
-
- // 设置画布尺寸为视频帧尺寸
- canvas.width = video.videoWidth;
- canvas.height = video.videoHeight;
-
- // 将视频当前帧绘制到画布
- ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
-
- // 将画布转换为 Base64 格式的图片 URL(JPEG格式,质量0.9)
- const frameUrl = canvas.toDataURL('image/jpeg', 0.9);
-
- // 将帧数据添加到列表
- frameList.push({
- url: frameUrl, // 帧图片URL
- time: Math.round(time * 10) / 10, // 帧对应的时间点(保留一位小数)
- selected: false, // 默认未选中
- });
-
- // 更新提取进度
- setExtractProgress(Math.round((frameList.length / totalFrames) * 100));
-
- // 完成当前帧提取,继续下一轮
- resolve();
- };
-
- /**
- * seeked 事件处理:视频定位完成后触发
- */
- const onSeeked = () => {
- checkReadyState();
- };
-
- /**
- * loadeddata 事件处理:帧数据加载完成后触发
- * 作为 seeked 的备选方案,确保兼容性
- */
- const onLoadedData = () => {
- processFrame();
- };
-
- /**
- * 超时机制:3秒超时防止无限等待
- * 如果3秒内帧数据仍未就绪,跳过当前帧继续下一帧
- */
- const timeoutId = setTimeout(() => {
- video.removeEventListener('seeked', onSeeked);
- video.removeEventListener('loadeddata', onLoadedData);
- console.warn(`Timeout waiting for frame at ${time}s`);
- resolve(); // 跳过当前帧,继续处理下一帧
- }, 3000);
-
- // 先同步检查状态,如果未就绪则注册事件监听器等待
- if (!checkReadyState()) {
- video.addEventListener('seeked', onSeeked);
- video.addEventListener('loadeddata', onLoadedData);
- }
- });
- }
-
- // 提取完成后,默认选中前4秒的帧(约8帧)
- const defaultSelected = Math.min(8, frameList.length);
- const defaultIndices: number[] = [];
- const updatedFrames = frameList.map((frame, index) => {
- if (index < defaultSelected) {
- defaultIndices.push(index);
- return { ...frame, selected: true }; // 标记为已选中
- }
- return frame;
+ // 创建隐藏的视频元素用于生成帧预览
+ if (!hiddenVideoRef.current) {
+ hiddenVideoRef.current = document.createElement('video');
+ hiddenVideoRef.current.style.display = 'none';
+ hiddenVideoRef.current.crossOrigin = 'anonymous';
+ document.body.appendChild(hiddenVideoRef.current);
+ }
+
+ const hiddenVideo = hiddenVideoRef.current;
+ hiddenVideo.src = videoUrl;
+
+ await new Promise((resolve) => {
+ hiddenVideo.addEventListener('loadedmetadata', () => resolve(), { once: true });
+ });
+
+ // 按秒数生成帧,每秒 1 帧,最多 15 帧
+ const numFrames = Math.min(Math.ceil(videoDuration), 15);
+ const previews: string[] = [];
+
+ for (let i = 0; i < numFrames; i++) {
+ const time = ((i + 0.5) / numFrames) * videoDuration;
+
+ await new Promise((resolve) => {
+ const handleSeeked = () => {
+ hiddenVideo.removeEventListener('seeked', handleSeeked);
+ setTimeout(resolve, 80);
+ };
+ hiddenVideo.addEventListener('seeked', handleSeeked);
+ hiddenVideo.currentTime = time;
});
+
+ canvas.width = 120;
+ canvas.height = 80;
+ ctx.drawImage(hiddenVideo, 0, 0, canvas.width, canvas.height);
+ previews.push(canvas.toDataURL('image/jpeg', 0.8));
+ }
+
+ setFramePreviews(previews);
+ }, [videoDuration, videoUrl]);
- // 更新状态:保存提取的帧列表和默认选中的帧索引
- setFrames(updatedFrames);
- setSelectedFrames(defaultIndices);
- } catch (err) {
- // 提取过程中发生错误
- setError('拆镜过程中发生错误');
- console.error('Extract frames error:', err);
- } finally {
- // 无论成功或失败,都标记提取完成
- setIsExtracting(false);
- setExtractProgress(100);
+ const handleVideoLoaded = useCallback(() => {
+ if (videoRef.current) {
+ const duration = videoRef.current.duration;
+ setVideoDuration(duration);
+ setSegmentStart(0);
+ setSegmentEnd(Math.min(15, duration));
+ setIsSegmentSelected(true);
}
}, []);
- const toggleFrameSelection = (index: number) => {
- setSelectedFrames(prev => {
- if (prev.includes(index)) {
- return prev.filter(i => i !== index);
- }
- return [...prev, index];
- });
+ useEffect(() => {
+ if (videoDuration > 0) {
+ generateFramePreviews();
+ }
+ }, [videoDuration, generateFramePreviews]);
- setFrames(prev =>
- prev.map((frame, i) =>
- i === index ? { ...frame, selected: !frame.selected } : frame
- )
- );
- };
+ const handleVideoTimeUpdate = useCallback(() => {
+ if (videoRef.current) {
+ setCurrentTime(videoRef.current.currentTime);
+ }
+ }, []);
- const formatTime = (seconds: number) => {
+ const togglePlay = useCallback(() => {
+ if (!videoRef.current) return;
+ if (videoRef.current.paused) {
+ videoRef.current.play();
+ setIsPlaying(true);
+ } else {
+ videoRef.current.pause();
+ setIsPlaying(false);
+ }
+ }, []);
+
+ const formatTime = useCallback((seconds: number) => {
const mins = Math.floor(seconds / 60);
- const secs = Math.round(seconds % 60);
- return `${mins}:${String(secs).padStart(2, '0')}`;
- };
+ const secs = Math.floor(seconds % 60);
+ const cs = Math.floor((seconds % 1) * 100);
+ return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}:${String(cs).padStart(2, '0')}`;
+ }, []);
- const handleRemoveVideo = () => {
- cleanupResources();
- };
+ const formatTimeShort = useCallback((seconds: number) => {
+ const mins = Math.floor(seconds / 60);
+ const secs = Math.floor(seconds % 60);
+ return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
+ }, []);
+
+ const getTimeFromEvent = useCallback((clientX: number): number | null => {
+ if (!timelineRef.current || videoDuration === 0) return null;
+ const rect = timelineRef.current.getBoundingClientRect();
+ const x = Math.max(0, Math.min(rect.width, clientX - rect.left));
+ const percentage = x / rect.width;
+ return percentage * videoDuration;
+ }, [videoDuration]);
+
+ const handleTimelineMouseDown = useCallback((
+ e: React.MouseEvent,
+ handle: 'start' | 'end'
+ ) => {
+ e.preventDefault();
+ e.stopPropagation();
+ setDragging(handle);
+ }, []);
+
+ const handleTimelineClick = useCallback((e: React.MouseEvent) => {
+ e.stopPropagation();
+ const time = getTimeFromEvent(e.clientX);
+ if (time !== null && videoRef.current) {
+ videoRef.current.currentTime = time;
+ }
+ }, [getTimeFromEvent]);
+
+ useEffect(() => {
+ if (!dragging) return;
+
+ const handleMouseMove = (e: MouseEvent) => {
+ const time = getTimeFromEvent(e.clientX);
+ if (time === null) return;
+
+ if (dragging === 'start') {
+ setSegmentStart(Math.max(0, Math.min(time, segmentEnd - 0.3)));
+ } else {
+ setSegmentEnd(Math.max(segmentStart + 0.3, Math.min(time, videoDuration)));
+ }
+ };
+
+ const handleMouseUp = () => {
+ setDragging(null);
+ };
+
+ window.addEventListener('mousemove', handleMouseMove);
+ window.addEventListener('mouseup', handleMouseUp);
+
+ return () => {
+ window.removeEventListener('mousemove', handleMouseMove);
+ window.removeEventListener('mouseup', handleMouseUp);
+ };
+ }, [dragging, segmentStart, segmentEnd, videoDuration, getTimeFromEvent]);
+
+ const segmentDuration = useMemo(() => segmentEnd - segmentStart, [segmentEnd, segmentStart]);
+ const segmentValid = useMemo(() => segmentDuration >= 4 && segmentDuration <= 15, [segmentDuration]);
+
+ const startPercent = useMemo(() => videoDuration > 0 ? (segmentStart / videoDuration) * 100 : 0, [segmentStart, videoDuration]);
+ const endPercent = useMemo(() => videoDuration > 0 ? (segmentEnd / videoDuration) * 100 : 100, [segmentEnd, videoDuration]);
+ const rangeWidth = useMemo(() => endPercent - startPercent, [endPercent, startPercent]);
return (
-
-
- {/* 标题区域 */}
+
+ }
+ style={{
+ borderRadius: 6,
+ fontSize: 16,
+ fontWeight: 600,
+ color: '#656efa'
+ }}
+ onClick={() => setIsModalOpen(true)}
+ >
+ 创作记录
+
+
+
+
- {/* 错误提示 */}
- {error && (
-
- ⚠️ {error}
-
- )}
-
- {/* 上传/视频区域 */}
-
- {/* 删除按钮 */}
- {videoUrl && (
-
+
+ {error && (
+
+ {error}
+
)}
- {/* 未上传状态 */}
{!videoUrl && (
-
-
+
-
-
+
+
支持 MP4、MOV 格式视频,最大支持 100MB 的文件,支持上传 3 分钟内的视频,可处理 4-15 秒片段
-
+
)}
- {/* 已上传状态 - 视频预览 */}
- {videoUrl && !frames.length && (
-
-
-
- 时长: {formatTime(videoRef.current?.duration || 0)}
-
-
- )}
-
- {/* 拆镜结果 */}
- {frames.length > 0 && (
-
- {/* 视频预览 */}
-
-
-
-
- {/* 帧选择区域 */}
-
+
-
+
-
- 00:00:00 / 00:00:{String(Math.round(videoRef.current?.duration || 0)).padStart(2, '0')}
-
+
+
+
+
- {/* 帧缩略图 */}
-
- {frames.map((frame, index) => (
-
toggleFrameSelection(index)}
- style={{
- flexShrink: 0,
- width: 60,
- height: 45,
- border: frame.selected ? '2px solid #6366f1' : '1px solid #e5e7eb',
- borderRadius: 6,
- overflow: 'hidden',
- cursor: 'pointer',
- position: 'relative'
- }}
- >
-

- {frame.selected && (
-
+
+
+
+ {formatTime(currentTime)} / {formatTime(videoDuration)}
+
+
+
+
+
+
+
+ {framePreviews.map((frame, index) => (
+
+ {frame && (
+

+ )}
+
+ ))}
+
+
+
handleTimelineMouseDown(e, 'start')}
+ onClick={(e) => e.stopPropagation()}
+ style={{
+ position: 'absolute',
+ top: 0,
+ bottom: 0,
+ left: `calc(${startPercent}% - 10px)`,
+ width: 20,
+ background: 'linear-gradient(180deg, #6366f1 0%, #4f46e5 100%)',
+ borderRadius: 6,
+ cursor: 'ew-resize',
+ boxShadow: dragging === 'start'
+ ? '0 4px 16px rgba(99, 102, 241, 0.4)'
+ : '0 2px 8px rgba(99, 102, 241, 0.3)',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ zIndex: 10
+ }}
+ >
+
+
+
+
handleTimelineMouseDown(e, 'end')}
+ onClick={(e) => e.stopPropagation()}
+ style={{
+ position: 'absolute',
+ top: 0,
+ bottom: 0,
+ left: `calc(${endPercent}% - 10px)`,
+ width: 20,
+ background: 'linear-gradient(180deg, #ec4899 0%, #db2777 100%)',
+ borderRadius: 6,
+ cursor: 'ew-resize',
+ boxShadow: dragging === 'end'
+ ? '0 4px 16px rgba(236, 72, 153, 0.4)'
+ : '0 2px 8px rgba(236, 72, 153, 0.3)',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ zIndex: 10
+ }}
+ >
+
+
+
+
+
+ 已选取 {formatTimeShort(segmentDuration)}
+ {!segmentValid && (
+
+ (建议 4-15 秒)
+
)}
- ))}
-
-
-
-
- 已选取 {selectedFrames.length} 帧
-
-
+
+ )}
)}
- {/* 进度条 */}
- {isExtracting && (
-
-
- 正在拆解...
- {extractProgress}%
-
-
-
- )}
+ 预计消耗 10 个积分
+
+
+
-
- {/* 操作按钮 */}
-
-
-
-
- {/* 创作记录 */}
- {/*
*/}
- {/* 隐藏画布 */}
+
+
setIsModalOpen(false)}
+ width={800}
+ footer={null}
+ >
+
+
+
+
+
+ (
+
+ ),
+ },
+ {
+ title: '原产品名称',
+ dataIndex: 'originalName',
+ key: 'originalName',
+ },
+ {
+ title: '产品名称',
+ dataIndex: 'productName',
+ key: 'productName',
+ },
+ {
+ title: '状态',
+ dataIndex: 'status',
+ key: 'status',
+ },
+ {
+ title: '创建时间',
+ dataIndex: 'createTime',
+ key: 'createTime',
+ },
+ {
+ title: '操作',
+ key: 'action',
+ render: (_, record) => (
+
+ ),
+ },
+ ]}
+ dataSource={tableData}
+ rowKey="id"
+ pagination={false}
+ />
+
);
}
\ No newline at end of file