爆款开头复刻

This commit is contained in:
孙佳艺
2026-06-15 15:50:59 +08:00
parent 73cb64e1e8
commit 1de11d93d0
10 changed files with 2144 additions and 849 deletions
Binary file not shown.
+32
View File
@@ -342,3 +342,35 @@ export async function getAuthorizationList(params: OAuthAppParam): Promise<OAuth
return api.get<OAuthAppList>(`/admin/user-oauth-apps/list?${query.toString()}`);
}
// 爆款开头复刻
export async function generateReplication(params: any): Promise<any> {
return api.post('/hot-opening-replications/tasks', params);
}
// 获取爆款开头复刻任务列表
export async function getReplicationList(page: number,page_size: number): Promise<any[]> {
return api.get(`/hot-opening-replications/tasks?page=${page}&page_size=${page_size}`);
}
// 获取爆款开头复刻任务详情
export async function getReplicationDetail(id: string): Promise<any> {
return api.get(`/hot-opening-replications/tasks/${id}`);
}
// 第一步,生成提示词
export async function getone(projectId: string, stepId: string): Promise<any> {
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> {
return api.post(`/hot-opening-replications/tasks/${projectId}/steps/${stepId}/generate-image`, params);
}
// 第三步,生成视频提示词
export async function getthree(projectId: string, stepId: string ,params: any): Promise<any> {
return api.post(`/hot-opening-replications/tasks/${projectId}/steps/${stepId}/generate-video-prompt`, params);
}
// 第四步,生成视频
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);
}
+56 -28
View File
@@ -121,6 +121,9 @@ const AIChatPage: React.FC = () => {
const [previewType, setPreviewType] = useState<'image' | 'video'>('image');
const videoRef = useRef<HTMLVideoElement>(null);
// 提示词展开状态
const [expandedPrompts, setExpandedPrompts] = useState<Set<string>>(new Set());
// 从URL中提取exp时间戳(支持相对路径和完整URL)
const extractExpTimestamp = (url: string): number | null => {
if (!url) return null;
@@ -249,7 +252,6 @@ const AIChatPage: React.FC = () => {
}
console.log(config);
// 根据配置计算积分
@@ -456,7 +458,7 @@ const AIChatPage: React.FC = () => {
setTotalnumber(data.total);
}).catch((error) => {
});
}, 5000);
}, 10000);
// 清理定时器
return () => {
@@ -807,7 +809,7 @@ const AIChatPage: React.FC = () => {
const handleClosePreview = () => {
setPreviewVisible(false);
setPreviewUrl('');
setPreviewUrl('');
if (videoRef.current) {
videoRef.current.pause();
}
@@ -818,7 +820,7 @@ const AIChatPage: React.FC = () => {
e.stopPropagation();
if (!previewUrl) return;
const link = document.createElement('a');
link.href = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${previewUrl}`;
link.href = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${previewUrl}&download=1`;
link.download = previewType === 'image' ? 'image.png' : 'video.mp4';
document.body.appendChild(link);
link.click();
@@ -1088,7 +1090,7 @@ const AIChatPage: React.FC = () => {
}}
>
{/* 删除按钮 - 右上角 */}
<div style={{ position: 'absolute', top: 8, right: 8 }}>
<div style={{ position: 'absolute', top: 8, right: 8 ,zIndex: 100}}>
<Popconfirm
title="确定要删除吗?"
onConfirm={async () => {
@@ -1099,7 +1101,6 @@ const AIChatPage: React.FC = () => {
setGen_list(prev => prev.filter(item => item.id !== msg.id));
setTotalnumber(prev => prev - 1);
} catch (error: any) {
console.log(error);
const errorMsg = error?.response?.data?.message || error?.message || '删除失败';
msgApi.error(errorMsg);
@@ -1115,8 +1116,8 @@ const AIChatPage: React.FC = () => {
height: 28,
borderRadius: 8,
border: 'none',
background: 'rgba(0,0,0,0.05)',
color: '#999',
background: 'rgba(146, 144, 144, 1)',
color: '#ffffffff',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
@@ -1140,29 +1141,56 @@ const AIChatPage: React.FC = () => {
</div>
{/* 文本内容 */}
<Tooltip
title={msg.originalPrompt}
placement="top"
style={{ maxWidth: '400px' }}
>
<p style={{
margin: '8px 0',
fontSize: 14,
<div
style={{
position: 'relative',
margin: '8px 0',
padding: '12px 16px',
backgroundColor: '#f8fafc',
borderRadius: 8,
border: '1px solid #e2e8f0',
fontSize: 13,
color: '#475569',
lineHeight: 1.6,
cursor: 'pointer',
transition: 'all 0.2s ease',
boxShadow: '0 1px 3px rgba(0,0,0,0.05)',
}}
onMouseEnter={() => {
setExpandedPrompts(prev => {
const newSet = new Set(prev);
newSet.add(msg.id);
return newSet;
});
}}
onMouseLeave={() => {
setExpandedPrompts(prev => {
const newSet = new Set(prev);
newSet.delete(msg.id);
return newSet;
});
}}
>
{/* 默认显示:一行省略 */}
<div style={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
cursor: 'pointer',
padding: '4px 8px',
borderRadius: 4,
transition: 'background-color 0.2s',
}}
onMouseEnter={(e) => { e.currentTarget.style.backgroundColor = '#f1f5f9'; }}
onMouseLeave={(e) => { e.currentTarget.style.backgroundColor = 'transparent'; }}
>
{msg.originalPrompt}
</p>
</Tooltip>
display: expandedPrompts.has(msg.id) ? 'none' : 'block',
}}>
{msg.originalPrompt}
</div>
{/* 鼠标移入显示:完整内容 */}
<div style={{
maxHeight: 200,
overflowY: 'auto',
display: expandedPrompts.has(msg.id) ? 'block' : 'none',
wordBreak: 'break-word',
}}>
{msg.originalPrompt}
</div>
</div>
{/* 根据 status 显示不同内容 */}
{/* 生成中 - 显示加载动画 */}
@@ -2354,7 +2382,7 @@ const AIChatPage: React.FC = () => {
<Button key="download" type="primary" onClick={() => {
if (!attachmentPreviewUrl) return;
const link = document.createElement('a');
link.href = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${attachmentPreviewUrl}`;
link.href = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${attachmentPreviewUrl}&download=1`;
link.download = attachmentPreviewName || (attachmentPreviewType === 'image' ? 'image.png' : 'video.mp4');
document.body.appendChild(link);
link.click();
+1 -1
View File
@@ -4323,7 +4323,7 @@ const GeneratePage: React.FC = () => {
icon={<DownloadOutlined />}
onClick={() => {
const a = document.createElement("a");
a.href = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${type === "video" ? record.videoUrl : record.imageUrl}`;
a.href = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${type === "video" ? record.videoUrl : record.imageUrl}&download=1`;
a.download = `${record.projectName}${type === "video" ? ".mp4" : ".png"}`;
a.click();
}}
+1 -1
View File
@@ -1048,7 +1048,7 @@ const GeneratedRecord: React.FC = () => {
if (videoRef.current) {
videoRef.current.pause();
}
const url = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${previewItem.videoUrl || previewItem.imageUrl}`;
const url = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${previewItem.videoUrl || previewItem.imageUrl}&download=1`;
window.open(url, '_blank');
}}
style={{ flex: 1, borderRadius: 8 }}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -401,7 +401,7 @@ const RecordsPage: React.FC = () => {
<div style={{ position: 'absolute', top: 10, right: 10, zIndex: 10 }}>
<Tooltip title={`下载${type === 'video' ? '视频' : '图片'}`}>
<Button size="small" icon={<DownloadOutlined />}
onClick={() => { const a = document.createElement('a'); a.href = `${import.meta.env.VITE_API_BASE || 'http://localhost:8000'}${type === 'video' ? record.videoUrl : record.imageUrl}`; a.download = `${record.projectName}${type === 'video' ? '.mp4' : '.png'}`; a.click(); }}
onClick={() => { const a = document.createElement('a'); a.href = `${import.meta.env.VITE_API_BASE || 'http://localhost:8000'}${type === 'video' ? record.videoUrl : record.imageUrl}&download=1`; a.download = `${record.projectName}${type === 'video' ? '.mp4' : '.png'}`; a.click(); }}
style={{ background: 'rgba(0,0,0,0.5)', border: 'none', color: '#fff', backdropFilter: 'blur(4px)', borderRadius: 8 }}>
</Button>
+221 -536
View File
@@ -1,4 +1,4 @@
import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
import { useState, useRef, useCallback, useEffect } from 'react';
import { Button, Modal, Input, Table, Upload, Popconfirm } from 'antd';
import { FileTextOutlined, CloudUploadOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
@@ -9,40 +9,12 @@ export default function VideoFrameExtractor() {
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 [segmentStart, setSegmentStart] = useState<number>(0);
const [segmentEnd, setSegmentEnd] = useState<number>(0);
const [isSegmentSelected, setIsSegmentSelected] = useState<boolean>(false);
const [dragging, setDragging] = useState<'start' | 'end' | null>(null);
const [isPlaying, setIsPlaying] = useState<boolean>(false);
const [currentTime, setCurrentTime] = useState<number>(0);
const [framePreviews, setFramePreviews] = useState<string[]>([]);
const [tableData] = useState<any[]>([
{
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<HTMLVideoElement>(null);
const timelineRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const hiddenVideoRef = useRef<HTMLVideoElement>(null);
const cleanupResources = useCallback(() => {
if (videoUrl) {
@@ -51,12 +23,7 @@ export default function VideoFrameExtractor() {
setVideoUrl('');
setError('');
setVideoDuration(0);
setSegmentStart(0);
setSegmentEnd(0);
setIsSegmentSelected(false);
setIsPlaying(false);
setCurrentTime(0);
setFramePreviews([]);
setProductName('');
}, [videoUrl]);
const handleFileChange = (file: File) => {
@@ -78,213 +45,104 @@ export default function VideoFrameExtractor() {
return false;
};
const generateFramePreviews = useCallback(async () => {
if (!canvasRef.current || videoDuration <= 0 || !videoUrl) return;
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
if (!ctx) return;
// 创建隐藏的视频元素用于生成帧预览
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<void>((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<void>((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]);
const handleVideoLoaded = useCallback(() => {
if (videoRef.current) {
const duration = videoRef.current.duration;
setVideoDuration(duration);
setSegmentStart(0);
setSegmentEnd(Math.min(15, duration));
setIsSegmentSelected(true);
setVideoDuration(videoRef.current.duration);
}
}, []);
useEffect(() => {
if (videoDuration > 0) {
generateFramePreviews();
}
}, [videoDuration, generateFramePreviews]);
const handleVideoTimeUpdate = useCallback(() => {
if (videoRef.current) {
setCurrentTime(videoRef.current.currentTime);
}
}, []);
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.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 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<HTMLDivElement>,
handle: 'start' | 'end'
) => {
e.preventDefault();
e.stopPropagation();
setDragging(handle);
}, []);
const handleTimelineClick = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
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]);
const tableData = [
{
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',
},
];
return (
<div style={{
height: '94vh',
background: '#ffffff',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
padding: '20px',
boxSizing: 'border-box',
overflow: 'auto'
}}>
<div style={{ textAlign: 'right', marginBottom: 20 }}>
<Button
type="text"
icon={<FileTextOutlined />}
style={{
borderRadius: 6,
fontSize: 16,
fontWeight: 600,
color: '#656efa'
}}
onClick={() => setIsModalOpen(true)}
>
</Button>
</div>
<div style={{ minHeight: '100vh', 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>
<div style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: 'calc(100vh - 100px)'
}}>
<div style={{ textAlign: 'center', marginBottom: 40 }}>
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 12,
marginBottom: 8
}}>
<button
onClick={() => setIsModalOpen(true)}
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: 36,
height: 36,
background: 'linear-gradient(135deg, #6366f1 0%, #ec4899 100%)',
borderRadius: 10,
width: 56,
height: 56,
background: 'linear-gradient(135deg, #e0e7ff 0%, #fce7f3 100%)',
borderRadius: 16,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
fontSize: 20
margin: '0 auto 16px'
}}>
<span style={{ fontSize: 28 }}>🎬</span>
</div>
<h1 style={{
fontSize: 26,
<h2 style={{
fontSize: 22,
fontWeight: 600,
margin: 0,
background: 'linear-gradient(135deg, #6366f1 0%, #ec4899 100%)',
@@ -292,15 +150,15 @@ export default function VideoFrameExtractor() {
WebkitTextFillColor: 'transparent',
backgroundClip: 'text'
}}>
</h1>
</h2>
<p style={{ fontSize: 14, color: '#888', margin: '8px 0 0 0' }}>
MP4MOV 100MB
</p>
</div>
<p style={{ fontSize: 14, color: '#888', margin: 0 }}>
仿
</p>
</div>
<div style={{ width: '100%', maxWidth: 720 }}>
<div style={{ width: '100%', maxWidth: 720, margin: '0 auto' }}>
{error && (
<div style={{
backgroundColor: '#fff5f5',
@@ -325,7 +183,7 @@ export default function VideoFrameExtractor() {
background: '#faf5ff',
border: '2px dashed #c4b5fd',
borderRadius: 24,
padding: '40px 20px'
padding: '60px 20px'
}}
>
<div style={{
@@ -344,7 +202,7 @@ export default function VideoFrameExtractor() {
</p>
<p style={{ fontSize: 13, color: '#999', margin: 0 }}>
MP4MOV 100MB 3 4-15
MP4MOV 100MB 3
</p>
</Upload.Dragger>
)}
@@ -397,322 +255,149 @@ export default function VideoFrameExtractor() {
<video
ref={videoRef}
src={videoUrl}
controls
onLoadedMetadata={handleVideoLoaded}
onTimeUpdate={handleVideoTimeUpdate}
onPlay={() => setIsPlaying(true)}
onPause={() => setIsPlaying(false)}
onClick={togglePlay}
style={{
maxWidth: '100%',
maxHeight: 320,
borderRadius: 12,
background: '#000',
cursor: 'pointer'
background: '#000'
}}
/>
</div>
{isSegmentSelected && (
<div style={{
background: '#f8fafc',
borderRadius: 16,
padding: '20px'
{/* 产品名称输入框 */}
<div style={{ marginBottom: 24 }}>
<label style={{
display: 'block',
fontSize: 14,
fontWeight: 500,
color: '#374151',
marginBottom: 8
}}>
<div style={{
display: 'flex',
</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
disabled={!videoUrl || !productName.trim()}
style={{
padding: '14px 56px',
background: videoUrl && productName.trim()
? 'linear-gradient(135deg, #6366f1 0%, #ec4899 100%)'
: '#e2e8f0',
color: 'white',
border: 'none',
borderRadius: 14,
fontSize: 15,
fontWeight: 600,
cursor: videoUrl && productName.trim() ? 'pointer' : 'not-allowed',
boxShadow: videoUrl && productName.trim() ? '0 6px 20px rgba(99, 102, 241, 0.35)' : 'none',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
gap: 16,
marginBottom: 16
gap: 10
}}
>
<span></span>
<span style={{
fontSize: 12,
opacity: 0.85,
fontWeight: 400
}}>
<button
onClick={togglePlay}
style={{
width: 32,
height: 32,
border: 'none',
background: 'linear-gradient(135deg, #6366f1 0%, #ec4899 100%)',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: 16,
color: 'white',
borderRadius: 8
}}
>
{isPlaying ? '⏸' : '▶'}
</button>
<span style={{
fontSize: 14,
color: '#333',
fontFamily: 'monospace',
fontWeight: 500
}}>
{formatTime(currentTime)} / {formatTime(videoDuration)}
</span>
</div>
<div
ref={timelineRef}
style={{
position: 'relative',
height: 70,
background: '#fff',
borderRadius: 12,
cursor: dragging ? 'grabbing' : 'pointer',
// overflow: 'visible',
marginBottom: 12,
userSelect: 'none',
display: 'flex',
alignItems: 'center',
padding: '0 4px',
border: '1px solid #e2e8f0',
// overflow: 'auto',
}}
onClick={handleTimelineClick}
>
<div
style={{
position: 'absolute',
top: 4,
bottom: 4,
left: `calc(${startPercent}% + 0px)`,
width: `calc(${rangeWidth}% - 0px)`,
background: 'linear-gradient(90deg, rgba(99, 102, 241, 0.25) 0%, rgba(236, 72, 153, 0.25) 100%)',
borderRadius: 8,
pointerEvents: 'none'
}}
/>
<div style={{
position: 'absolute',
top: 8,
bottom: 8,
left: 0,
right: 0,
display: 'flex',
// justifyContent: 'space-around',
pointerEvents: 'none',
// overflow: 'auto',
}}>
{framePreviews.map((frame, index) => (
<div
key={index}
style={{
width: `calc((100% - 16px) / ${framePreviews.length})`,
// width: 200,
height: '100%',
objectFit: 'cover',
borderRadius: 4,
background: '#f1f5f9',
overflow: 'hidden',
marginRight: 8,
}}
>
{frame && (
<img
src={frame}
alt={`frame ${index}`}
style={{
width: '100%',
height: '100%',
objectFit: 'cover'
}}
/>
)}
</div>
))}
</div>
<div
onMouseDown={(e) => 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
}}
>
<div style={{
display: 'flex',
flexDirection: 'column',
gap: 2
}}>
<div style={{ width: 8, height: 1, background: 'white', borderRadius: 1 }} />
<div style={{ width: 8, height: 1, background: 'white', borderRadius: 1 }} />
<div style={{ width: 8, height: 1, background: 'white', borderRadius: 1 }} />
</div>
</div>
<div
onMouseDown={(e) => 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
}}
>
<div style={{
display: 'flex',
flexDirection: 'column',
gap: 2
}}>
<div style={{ width: 8, height: 1, background: 'white', borderRadius: 1 }} />
<div style={{ width: 8, height: 1, background: 'white', borderRadius: 1 }} />
<div style={{ width: 8, height: 1, background: 'white', borderRadius: 1 }} />
</div>
</div>
</div>
<div style={{
textAlign: 'center',
fontSize: 14,
color: '#666'
}}>
{formatTimeShort(segmentDuration)}
{!segmentValid && (
<span style={{ color: '#dc2626', marginLeft: 8 }}>
( 4-15 )
</span>
)}
</div>
</div>
)}
10
</span>
</button>
</div>
</div>
</div>
)}
<div style={{ textAlign: 'center', marginTop: 32 }}>
<button
disabled={!videoUrl || !segmentValid}
style={{
padding: '14px 56px',
background: videoUrl && segmentValid
? 'linear-gradient(135deg, #6366f1 0%, #ec4899 100%)'
: '#e2e8f0',
color: 'white',
border: 'none',
borderRadius: 14,
fontSize: 15,
fontWeight: 600,
cursor: videoUrl && segmentValid ? 'pointer' : 'not-allowed',
boxShadow: videoUrl && segmentValid ? '0 6px 20px rgba(99, 102, 241, 0.35)' : 'none',
display: 'inline-flex',
alignItems: 'center',
gap: 10
}}
>
<span></span>
<span style={{
fontSize: 12,
opacity: 0.85,
fontWeight: 400
}}>
10
</span>
</button>
</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: 'originalName',
key: 'originalName',
},
{
title: '产品名称',
dataIndex: 'productName',
key: 'productName',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
},
{
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={false}
/>
</Modal>
</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: 'originalName',
key: 'originalName',
},
{
title: '产品名称',
dataIndex: 'productName',
key: 'productName',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
},
{
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={false}
/>
</Modal>
</div>
);
}
}
+10 -2
View File
@@ -13,6 +13,14 @@
width: 100%;
height: 100%;
}
.product_img{
margin-left: 20px;
/* 加载旋转动画 */
@keyframes spinSlow {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}