拆镜复刻跳转修改

This commit is contained in:
孙佳艺
2026-06-24 09:10:45 +08:00
parent da056ea64b
commit 4a1118a3ec
8 changed files with 205 additions and 124 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -28,7 +28,7 @@
}
})();
</script>
<script type="module" crossorigin src="/assets/index-ZwwuCCw2.js"></script>
<script type="module" crossorigin src="/assets/index-DTCz7Wbv.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-eLRg4pQk.css">
</head>
<body>
+5 -2
View File
@@ -501,8 +501,11 @@ 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 getShotReplicationList(page: number, page_size: number, keyword?: string): Promise<any> {
const url = keyword
? `/shot-replications/task-sets?page=${page}&page_size=${page_size}&keyword=${encodeURIComponent(keyword)}`
: `/shot-replications/task-sets?page=${page}&page_size=${page_size}`;
return api.get(url);
}
// 获取镜头复刻任务详情
export async function getShotReplicationDetail(taskSetId: string): Promise<any> {
@@ -552,23 +552,27 @@ const VideoTrimPicker: React.FC<VideoTrimPickerProps> = ({
<Text style={{ marginLeft: 8, color: '#64748b' }}>...</Text>
</div>
) : frames.length > 0 ? (
frames.map((frame) => (
<button
key={frame.second}
type="button"
onClick={() => handleFrameClick(frame.second)}
title={`${frame.second}s${frame.fallback ? `,实际取帧 ${frame.captureSecond?.toFixed(1)}s` : ''}`}
style={{
minWidth: 0,
height: 86,
border: 'none',
padding: 0,
background: 'transparent',
overflow: 'hidden',
cursor: disabledByDuration ? 'not-allowed' : 'pointer',
position: 'relative',
}}
>
frames.map((frame) => {
const isSelected = frame.second >= range[0] && frame.second < range[1];
return (
<button
key={frame.second}
type="button"
onClick={() => handleFrameClick(frame.second)}
title={`${frame.second}s${frame.fallback ? `,实际取帧 ${frame.captureSecond?.toFixed(1)}s` : ''}`}
style={{
minWidth: 0,
height: 86,
borderTop: isSelected ? '5px solid #6366f1' : 'none',
borderBottom: isSelected ? '5px solid #6366f1' : 'none',
padding: 0,
background: 'transparent',
overflow: 'hidden',
cursor: disabledByDuration ? 'not-allowed' : 'pointer',
position: 'relative',
// borderRadius: 4,
}}
>
{frame.status === 'success' && frame.image ? (
<img src={frame.image} alt={`${frame.second}s`} style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
) : frame.status === 'loading' ? (
@@ -594,7 +598,8 @@ const VideoTrimPicker: React.FC<VideoTrimPickerProps> = ({
{frame.second}s
</span>
</button>
))
);
})
) : (
<div style={{ gridColumn: '1 / -1', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#64748b' }}>
@@ -603,7 +608,7 @@ const VideoTrimPicker: React.FC<VideoTrimPickerProps> = ({
</div>
<div style={{ position: 'absolute', top: -6, left: 10, right: 0, padding: '0 8px 0' }}>
<div style={{ position: 'absolute', top: -6, left: 6, right: 0, }}>
<Slider
range
min={0}
+51 -42
View File
@@ -1070,42 +1070,7 @@ const AIChatPage: React.FC = () => {
<div style={{ flex: 1 }}>
{/* 时间戳和参数信息 */}
<div style={{ margin: 4, fontSize: 11, color: '#999', textAlign: 'left', display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center' }}>
<span>{msg.createdAt?.replace('T', ' ').split('.')[0]}</span>
{/* 引擎标签 */}
<span >
{/* <SettingsOutlined style={{ fontSize: 12 }} /> */}
{msg.engineSnapshot.name}
</span>
{/* 参数标签 */}
<span >
{/* <LayoutGridOutlined style={{ fontSize: 12 }} /> */}
{msg.genType === 'image'
? `${msg.imageProportion || ''} · ${msg.imagePx || ''} · ${msg.imageSize || ''}`
: `${msg.duration || ''}s · ${msg.aspectRatio || ''} · ${msg.resolution || ''}`
}
</span>
<span style={{ marginLeft: 8 }}>{msg.creditsCost}</span>
{msg.mediaReferences && msg.mediaReferences.length > 0 && (
<span
style={{ marginLeft: 20, color: '#6366f1', cursor: 'pointer' }}
onClick={(e) => {
e.stopPropagation();
const target = e.currentTarget as HTMLElement;
const rect = target.getBoundingClientRect();
setAttachmentPopupPosition({
x: rect.left,
y: rect.top - 10
});
setAttachmentPopupMessageId(msg.id);
setAttachmentPopupVisible(true);
}}
>
</span>
)}
</div>
{/* 消息气泡 */}
<div
style={{
@@ -1114,13 +1079,17 @@ const AIChatPage: React.FC = () => {
borderRadius: '16px 16px 16px 4px',
padding: '12px 16px',
width: '70%',
maxWidth: 800,
minWidth: 500,
boxSizing: 'border-box',
boxShadow: '0 4px 20px rgba(99, 102, 241, 0.08), 0 1px 3px rgba(0,0,0,0.04)',
position: 'relative',
border: '1px solid rgba(99, 102, 241, 0.06)',
}}
>
<div style={{ margin: 4, fontSize: 11, color: '#999', textAlign: 'left', display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center' }}>
<span>{msg.createdAt?.replace('T', ' ').split('.')[0]}</span>
</div>
{/* 删除按钮 - 右上角 */}
<div style={{ position: 'absolute', top: 8, right: 8, zIndex: 100 }}>
<Popconfirm
@@ -1173,7 +1142,7 @@ const AIChatPage: React.FC = () => {
</div>
{/* 文本内容 */}
<div
{/* <div
style={{
width: '100%',
position: 'relative',
@@ -1204,7 +1173,7 @@ const AIChatPage: React.FC = () => {
});
}}
>
{/* 默认显示:一行省略 */}
默认显示:一行省略
<div style={{
width: '100%',
overflow: 'hidden',
@@ -1215,7 +1184,7 @@ const AIChatPage: React.FC = () => {
{msg.originalPrompt}
</div>
{/* 鼠标移入显示:完整内容 */}
鼠标移入显示:完整内容
<div style={{
width: '100%',
maxHeight: 200,
@@ -1225,7 +1194,7 @@ const AIChatPage: React.FC = () => {
}}>
{msg.originalPrompt}
</div>
</div>
</div> */}
{/* 根据 status 显示不同内容 */}
{/* 生成中 - 显示加载动画 */}
@@ -1304,14 +1273,14 @@ const AIChatPage: React.FC = () => {
)}
{/* 已完成 - 显示媒体内容 */}
{msg.status === 'completed' && (
<div style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))', gap: 8, marginBottom: 10, marginTop: 12, width: '100%', }}>
<div style={{ display: 'flex', gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))', gap: 8, marginBottom: 10, marginTop: 12, width: '100%', }}>
<div
onClick={() => {
setPreviewUrl(msg.genType === 'image' ? msg.imageUrl : msg.videoUrl);
setPreviewType(msg.genType === 'image' ? 'image' : 'video');
setPreviewVisible(true);
}}
style={{ cursor: 'pointer', overflow: 'hidden', borderRadius: 8, position: 'relative', height: 200 }}
style={{ width: '50%', cursor: 'pointer', overflow: 'hidden', borderRadius: 8, position: 'relative', height: 200 }}
>
{msg.genType === 'image' ? (
<img
@@ -1353,6 +1322,46 @@ const AIChatPage: React.FC = () => {
</>
)}
</div>
<div style={{ width: '50%', }}>
<p style={{ marginBottom: 20 ,height: 100, overflow: 'auto', padding: 0, margin: 0 }}>
{msg.originalPrompt}
</p>
<div style={{fontSize: 11, color: '#999', textAlign: 'left', display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center' }}>
{/* 引擎标签 */}
<span >
{/* <SettingsOutlined style={{ fontSize: 12 }} /> */}
{msg.engineSnapshot.name}
</span>
{/* 参数标签 */}
<span >
{/* <LayoutGridOutlined style={{ fontSize: 12 }} /> */}
{msg.genType === 'image'
? `${msg.imageProportion || ''} · ${msg.imagePx || ''} · ${msg.imageSize || ''}`
: `${msg.duration || ''}s · ${msg.aspectRatio || ''} · ${msg.resolution || ''}`
}
</span>
<span style={{ marginLeft: 8 }}>{msg.creditsCost}</span>
{msg.mediaReferences && msg.mediaReferences.length > 0 && (
<span
style={{ marginLeft: 20, color: '#6366f1', cursor: 'pointer' }}
onClick={(e) => {
e.stopPropagation();
const target = e.currentTarget as HTMLElement;
const rect = target.getBoundingClientRect();
setAttachmentPopupPosition({
x: rect.left,
y: rect.top - 10
});
setAttachmentPopupMessageId(msg.id);
setAttachmentPopupVisible(true);
}}
>
</span>
)}
</div>
</div>
</div>
)}
+21 -3
View File
@@ -57,6 +57,8 @@ function InitialInfo() {
const [taskDetail, setTaskDetail] = useState<any>({});
// API 返回的步骤数据
const [apiSteps, setApiSteps] = useState<any[]>([]);
// 当前展开的步骤
const [activeKey, setActiveKey] = useState<string[]>([]);
//
const baseSteps = [
@@ -76,6 +78,21 @@ function InitialInfo() {
engineId: apiSteps[index]?.input?.payload?.videoConfig?.engineId || '',
}));
// 当apiSteps更新时,逆向遍历找到第一个已完成或失败的步骤并展开
useEffect(() => {
for (let i = steps.length - 1; i >= 0; i--) {
if (steps[i].status === 'completed' || steps[i].status === 'failed') {
setActiveKey([String(steps[i].childId)]);
return;
}
}
setActiveKey([]);
}, [apiSteps]);
// console.log('steps', steps);
@@ -367,7 +384,7 @@ function InitialInfo() {
}
const agincreatevideo = () => {
let params = {
engine_id: steps[2].engineId,
engine_id: steps[3].engineId,
}
@@ -553,13 +570,14 @@ function InitialInfo() {
</div>
<div style={{ flex: 1, overflowY: 'auto', padding: '8px 16px 16px' }}>
<Collapse
defaultActiveKey={['']}
activeKey={activeKey}
onChange={setActiveKey}
ghost
bordered={false}
style={{ background: 'transparent' }}
expandIconPlacement="end"
items={steps.map((step) => ({
key: String(step.id),
key: String(step.childId),
label: (
<div style={{ display: 'flex', alignItems: 'center', gap: 8, width: '100%', justifyContent: 'space-between' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
+30 -8
View File
@@ -17,6 +17,7 @@ export default function VideoFrameExtractor() {
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);
@@ -81,8 +82,21 @@ export default function VideoFrameExtractor() {
idempotency_key: `shot_${Date.now()}`,
};
await createShotReplication(params);
message.success('任务创建成功');
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('任务创建失败,请重试');
@@ -91,9 +105,9 @@ export default function VideoFrameExtractor() {
}
};
const fetchList = async (page: number, size: number) => {
const fetchList = async (page: number, size: number, keyword?: string) => {
try {
const res = await getShotReplicationList(page, size);
const res = await getShotReplicationList(page, size, keyword);
setTableData(res.items || []);
setTotal(res.total || 0);
setCurrentPage(page);
@@ -104,12 +118,16 @@ export default function VideoFrameExtractor() {
};
const handlePageChange = (page: number, size: number) => {
fetchList(page, size);
fetchList(page, size, searchKeyword);
};
const handleSearch = () => {
fetchList(1, 10, searchKeyword);
};
const handleOpenModal = () => {
setIsModalOpen(true);
fetchList(1, 10);
fetchList(1, 10, searchKeyword);
};
return (
@@ -127,7 +145,7 @@ export default function VideoFrameExtractor() {
<div style={{ maxWidth: 1200, margin: '50px auto', position: 'relative', zIndex: 10 }}>
<div style={{ marginBottom: 50,height: 160, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div style={{ marginBottom: 50, height: 160, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<div style={{ width: 40, height: 2, background: 'linear-gradient(90deg, transparent, #6366f1, #8b5cf6, transparent)', borderRadius: 1 }} />
<div>
@@ -440,13 +458,16 @@ export default function VideoFrameExtractor() {
width={800}
footer={null}
styles={{
body: { background: 'linear-gradient(180deg, #f8fafc 0%, #eef2ff 100%)' },
body: { background: 'linear-gradient(180deg, #f8fafc 0%, #eef2ff 100%)' },
header: { background: '#fff', borderBottom: '1px solid rgba(99, 102, 241, 0.1)', padding: '20px 24px' },
}}
>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 16, gap: 8 }}>
<Input
placeholder="搜索产品名称"
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
onPressEnter={handleSearch}
style={{
width: 200,
borderRadius: 10,
@@ -456,6 +477,7 @@ export default function VideoFrameExtractor() {
/>
<Button
type="primary"
onClick={handleSearch}
style={{
borderRadius: 10,
height: 40,
+32 -8
View File
@@ -52,10 +52,13 @@ function InitialInfo() {
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(20);
const [total, setTotal] = useState(0);
const [searchKeyword, setSearchKeyword] = useState('');
// 任务详情数据
const [taskDetail, setTaskDetail] = useState<any>({});
// API 返回的步骤数据
const [apiSteps, setApiSteps] = useState<any[]>([]);
// 当前展开的步骤
const [activeKey, setActiveKey] = useState<string[]>([]);
//
const baseSteps = [
@@ -75,6 +78,17 @@ function InitialInfo() {
engineId: apiSteps[index]?.input?.payload?.videoConfig?.engineId || '',
}));
// 当apiSteps更新时,逆向遍历找到第一个已完成或失败的步骤并展开
useEffect(() => {
for (let i = steps.length - 1; i >= 0; i--) {
if (steps[i].status === 'completed' || steps[i].status === 'failed') {
setActiveKey([String(steps[i].childId)]);
return;
}
}
setActiveKey([]);
}, [apiSteps]);
@@ -82,8 +96,8 @@ function InitialInfo() {
// 获取列表数据的函数
const fetchList = (page: number, size: number) => {
getShotReplicationList(page, size).then((res: any) => {
const fetchList = (page: number, size: number, keyword?: string) => {
getShotReplicationList(page, size, keyword).then((res: any) => {
if (res.items) {
setTableData(res.items);
}
@@ -98,12 +112,17 @@ function InitialInfo() {
const handlePageChange = (page: number, size: number) => {
setCurrentPage(page);
setPageSize(size);
fetchList(page, size);
fetchList(page, size, searchKeyword);
};
// 搜索处理
const handleSearch = () => {
fetchList(1, 20, searchKeyword);
};
// 获取复刻列表数据
useEffect(() => {
fetchList(currentPage, pageSize);
fetchList(currentPage, pageSize, searchKeyword);
}, []);
// 获取引擎列表
@@ -371,7 +390,7 @@ function InitialInfo() {
}
const agincreatevideo = () => {
let params = {
engine_id: steps[2].engineId,
engine_id: steps[3].engineId,
}
@@ -422,7 +441,7 @@ function InitialInfo() {
<Button
type="text"
icon={<ArrowLeftOutlined />}
onClick={() => navigate(-1)}
onClick={() => navigate('/removelens')}
style={{
color: '#64748b',
borderRadius: 10,
@@ -551,13 +570,14 @@ function InitialInfo() {
</div>
<div style={{ flex: 1, overflowY: 'auto', padding: '8px 16px 16px' }}>
<Collapse
defaultActiveKey={['']}
activeKey={activeKey}
onChange={setActiveKey}
ghost
bordered={false}
style={{ background: 'transparent' }}
expandIconPlacement="end"
items={steps.map((step) => ({
key: String(step.id),
key: String(step.childId),
label: (
<div style={{ display: 'flex', alignItems: 'center', gap: 8, width: '100%', justifyContent: 'space-between' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
@@ -1199,6 +1219,9 @@ function InitialInfo() {
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 16, gap: 8 }}>
<Input
placeholder="搜索产品名称"
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
onPressEnter={handleSearch}
style={{
width: 200,
borderRadius: 10,
@@ -1208,6 +1231,7 @@ function InitialInfo() {
/>
<Button
type="primary"
onClick={handleSearch}
style={{
borderRadius: 10,
height: 40,