Merge branch 'dev'

This commit is contained in:
孙佳艺
2026-06-18 14:34:43 +08:00
9 changed files with 634 additions and 216 deletions
+15
View File
@@ -379,6 +379,11 @@ 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 updateImagePrompt(projectId: string, stepId: string, params: any): Promise<any> {
return api.put(`/hot-opening-replications/tasks/${projectId}/steps/${stepId}/image-prompt`, params);
}
// 第二步,生成图片
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);
@@ -534,6 +539,16 @@ export async function removeone(projectId: string, stepId: string): Promise<any>
}
// 修改图片提示词
export async function updateShotImagePrompt(projectId: string, stepId: string, params: any): Promise<any> {
return api.put(`/shot-replications/projects/${projectId}/steps/${stepId}/image-prompt`, params);
}
// 修改视频提示词
export async function updateShotVideoPromptSchema(projectId: string, stepId: string, params: any): Promise<any> {
return api.put(`/shot-replications/projects/${projectId}/steps/${stepId}/video-prompt-schema`, params);
}
// 第二步,生成图片
export async function removetwo(projectId: string, stepId: string ,params: any): Promise<any> {
return api.post(`/shot-replications/projects/${projectId}/steps/${stepId}/generate-image`, params);
@@ -20,6 +20,12 @@ const LOCKED_TOP_LEVEL_KEYS = new Set([
'质量控制',
]);
const HIDDEN_TOP_LEVEL_KEYS = new Set([
'schemaUsage',
'schemaVersion',
'输出规格限制',
]);
const LOCKED_FRAME_KEYS = new Set([
'视频时长',
'视频比例',
@@ -174,7 +180,7 @@ const VideoPromptSchemaEditor: React.FC<VideoPromptSchemaEditorProps> = ({ value
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8 }}>
<Space>
<Text strong style={{ color: '#334155', fontSize: 13 }}>{fieldTitle(key)}</Text>
{!editableArray && <Tag color="default"></Tag>}
{/* {!editableArray && <Tag color="default">锁定结构</Tag>} */}
</Space>
{editableArray && (
<Button size="small" type="link" onClick={() => onChange(addArrayItem(safeValue, path, sample))}>
@@ -211,11 +217,11 @@ const VideoPromptSchemaEditor: React.FC<VideoPromptSchemaEditorProps> = ({ value
)}
</div>
{(rootKey === '动作流程' || rootKey === '镜头流程') && (
{/* {(rootKey === '动作流程' || rootKey === '镜头流程') && (
<Text style={{ display: 'block', marginTop: 6, color: '#94a3b8', fontSize: 12 }}>
时间段和条目数量由后端锁定,只允许编辑每段里的动作、镜头、说明等内容。
</Text>
)}
)} */}
</div>
);
}
@@ -226,7 +232,7 @@ const VideoPromptSchemaEditor: React.FC<VideoPromptSchemaEditorProps> = ({ value
<div key={path.join('.')} style={{ marginBottom: 18 }}>
<Space style={{ marginBottom: 8 }}>
<Text strong style={{ color: '#334155', fontSize: 13 }}>{fieldTitle(key)}</Text>
{lockedSection && <Tag color="default"></Tag>}
{/* {lockedSection && <Tag color="default">只读</Tag>} */}
</Space>
<div
style={{
@@ -246,10 +252,11 @@ const VideoPromptSchemaEditor: React.FC<VideoPromptSchemaEditorProps> = ({ value
<div key={path.join('.')} style={{ marginBottom: 12 }}>
<Space style={{ marginBottom: 4 }}>
<Text style={{ color: '#64748b', fontSize: 12 }}>{fieldTitle(key)}</Text>
{locked && <Tag color="default"></Tag>}
{/* {locked && <Tag color="default">只读12312</Tag>} */}
</Space>
{locked ? (
<ReadonlyBlock value={nodeValue} />
// <></>
) : (
<EditableInput value={nodeValue} onChange={(nextText) => updatePath(path, nextText)} />
)}
@@ -263,10 +270,12 @@ const VideoPromptSchemaEditor: React.FC<VideoPromptSchemaEditorProps> = ({ value
return (
<div>
<div style={{ marginBottom: 14, padding: 12, borderRadius: 10, background: '#f8fafc', color: '#64748b', fontSize: 13, lineHeight: 1.7 }}>
{/* <div style={{ marginBottom: 14, padding: 12, borderRadius: 10, background: '#f8fafc', color: '#64748b', fontSize: 13, lineHeight: 1.7 }}>
视频时长、比例、清晰度、帧率、推荐分辨率、动态时间规划、输出规格、质量控制、合规控制、schema 协议字段为只读;动作/镜头流程保留原时间段和条目数量,只允许编辑内容描述。
</div>
{Object.entries(safeValue).map(([key, childValue]) => renderNode(key, childValue, [key]))}
</div> */}
{Object.entries(safeValue)
.filter(([key]) => !HIDDEN_TOP_LEVEL_KEYS.has(key))
.map(([key, childValue]) => renderNode(key, childValue, [key]))}
</div>
);
};
@@ -599,6 +599,27 @@ const VideoTrimPicker: React.FC<VideoTrimPickerProps> = ({
onChangeComplete={handleRangeChangeComplete}
tooltip={{ formatter: (value) => `${toIntegerSecond(Number(value || 0))}s` }}
disabled={!integerDuration || disabledByDuration}
styles={{
track: {
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
height: 6,
borderRadius: 3,
},
rail: {
background: '#e2e8f0',
height: 6,
borderRadius: 3,
},
handle: {
width: 18,
height: 18,
marginTop: 0,
// backgroundColor: '#fff',
// border: '3px solid #6366f1',
boxShadow: '0 2px 8px rgba(99, 102, 241, 0.3)',
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
},
}}
/>
</div>
</div>
@@ -606,7 +627,7 @@ const VideoTrimPicker: React.FC<VideoTrimPickerProps> = ({
<div style={{ marginTop: 30, textAlign: 'center', color: '#64748b', fontSize: 16 }}>
{selectedDuration}s
<span style={{ marginLeft: 12, fontSize: 13, color: '#94a3b8' }}>
{minDuration}s {maxDuration}s/
{minDuration}s {maxDuration}s
</span>
</div>
+1 -1
View File
@@ -830,7 +830,7 @@ const AIChatPage: React.FC = () => {
// ==================== 渲染 ====================
return (
<Layout style={{ height: '90vh', background: '#fafafa', overflow: 'auto' }}>
<Layout style={{ height: 'calc(100vh - 90px)', background: '#fafafa', overflow: 'auto' }}>
{/* 左侧边栏 - 对话列表(已隐藏,保留代码) */}
{false && (
<Sider
+178 -83
View File
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
import { Button, Typography, Collapse, Space, Modal, Input, Table, message } from 'antd';
import { ArrowLeftOutlined, PlayCircleOutlined, CheckCircleOutlined, EditOutlined, DownloadOutlined, SettingOutlined, LayoutOutlined } from '@ant-design/icons';
import { useNavigate, useParams } from 'react-router-dom';
import { getReplicationList, getReplicationDetail, gettwo, getthree, getfour, getEngine, updateHotOpeningVideoPromptSchema } from '../api/index';
import { getReplicationList, getReplicationDetail, gettwo, getthree, getfour, getEngine, updateHotOpeningVideoPromptSchema, updateImagePrompt } from '../api/index';
import VideoPromptSchemaEditor from '../components/VideoPromptSchemaEditor';
import './css/InitialInfo.css';
@@ -203,8 +203,29 @@ function InitialInfo() {
};
const handleConfirm = async () => {
if (currentType !== 'video') {
setModalVisible(false);
if (currentType === 'image') {
if (!taskDetail?.id || !editingPromptStepId) {
message.warning('缺少任务或步骤 ID,无法保存图片提示词');
return;
}
if (!promptText || !promptText.trim()) {
message.warning('图片提示词不能为空');
return;
}
setPromptSaving(true);
try {
await updateImagePrompt(taskDetail.id, editingPromptStepId, {
prompt: promptText.trim(),
});
message.success('图片提示词已保存');
refreshTaskDetail();
setModalVisible(false);
setEditingPromptStepId('');
} catch (error: any) {
message.error(error?.message || '保存图片提示词失败');
} finally {
setPromptSaving(false);
}
return;
}
@@ -219,18 +240,11 @@ function InitialInfo() {
setPromptSaving(true);
try {
const res: any = await updateHotOpeningVideoPromptSchema(taskDetail.id, editingPromptStepId, {
await updateHotOpeningVideoPromptSchema(taskDetail.id, editingPromptStepId, {
prompt_schema: formData,
});
if (res?.detail) {
setTaskDetail(res.detail);
setApiSteps(res.detail.steps || []);
} else {
refreshTaskDetail();
}
message.success(res?.message || '视频提示词已保存');
message.success('视频提示词已保存');
refreshTaskDetail();
setModalVisible(false);
setEditingPromptStepId('');
} catch (error: any) {
@@ -348,10 +362,12 @@ function InitialInfo() {
return (
<React.Fragment>
<div style={{ height: '94vh', background: '#f8fafc' }}>
<div style={{ height: 'calc(94vh)', }}>
<div style={{ minHeight: 'calc(100vh - 90px)', background: '#f8fafc' }}>
<div style={{ height: '100%' }}>
<div style={{ height: '100%', display: 'flex', justifyContent: 'space-between', gap: '2%' }}>
<div style={{ width: '70%', background: '#fff', borderRadius: 12, overflowY: 'auto' }}>
<div style={{ width: '70%', background: '#fff', borderRadius: 12, overflowY: 'auto', maxHeight: 'calc(100vh - 90px)' }}>
<div style={{ borderBottom: '1px solid #e2e8f0', width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '14px', boxSizing: 'border-box' }}>
<Button
type="text"
@@ -425,7 +441,8 @@ function InitialInfo() {
)}
</div>
</div>
<div style={{ width: '28%', minWidth: '400px', background: '#fff', borderRadius: 12, overflowY: 'auto' }}>
<div style={{ width: '28%', minWidth: '400px', background: '#fff', borderRadius: 12, overflowY: 'auto', maxHeight: 'calc(100vh - 90px)' }}>
<div style={{ padding: '14px' }}> </div>
<Collapse
defaultActiveKey={['']}
@@ -531,7 +548,7 @@ function InitialInfo() {
<Button
type="default"
icon={<EditOutlined />}
onClick={() => handleOpenModal(step?.output?.payload?.prompt)}
onClick={() => handleOpenModal(step?.output?.payload?.prompt, 'image', step.id)}
style={{ flex: 1, borderRadius: 8, borderColor: '#6366f1', color: '#6366f1', height: 36 }}
disabled={step.status !== 'completed'}
>
@@ -1053,73 +1070,151 @@ function InitialInfo() {
{/* 表格 */}
<Table
columns={[
{
title: '产品名称',
dataIndex: 'targetProjectName',
key: 'targetProjectName',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (text: string) => {
const statusMap: Record<string, string> = {
'pending': '子任务待处理',
'waiting_user': '等待用户确认或触发',
'processing': '子任务处理中',
'completed': '子任务完成',
'failed': '子任务失败',
'cancelled': '子任务取消',
};
return statusMap[text] || text || '-';
},
},
{
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 `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
},
},
{
title: '操作',
key: 'action',
render: (_, record) => (
<button
onClick={() => {
setIsModalOpen(false);
navigate(`/initial/${record.id}/initialinfo`);
}}
columns={[
{
title: '产品名称',
dataIndex: 'targetProjectName',
key: 'targetProjectName',
align: 'center',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
align: 'center',
render: (text: string, record: any) => {
const status = text;
const currentStepCode = record.current_step_code || record.currentStepCode;
const getStatusText = () => {
if (currentStepCode === 'image_prompt_optimize') {
switch (status) {
case 'waiting_user':
return '等待融合图生成';
case 'processing':
return '图片提示词生成中';
case 'completed':
return '图片提示词生成成功';
case 'failed':
return '图片提示词生成失败';
default:
return status || '-';
}
} else if (currentStepCode === 'image_generate') {
switch (status) {
case 'waiting_user':
return '等待生成视频提示词';
case 'processing':
return '融合图生成中';
case 'completed':
return '融合图生成成功';
case 'failed':
return '融合图生成失败';
default:
return status || '-';
}
} else if (currentStepCode === 'video_prompt_optimize') {
switch (status) {
case 'waiting_user':
return '等待最终视频生成';
case 'processing':
return '视频提示词生成中';
case 'completed':
return '视频提示词生成成功';
case 'failed':
return '视频提示词生成失败';
default:
return status || '-';
}
} else if (currentStepCode === 'video_generate') {
switch (status) {
case 'waiting_user':
return '';
case 'processing':
return '最终视频生成中';
case 'completed':
return '最终视频生成成功';
case 'failed':
return '最终视频生成失败';
default:
return status || '-';
}
} else if (currentStepCode === 'material_input') {
switch (status) {
case 'waiting_user':
return '等待生成图片提示词';
case 'processing':
return '素材处理中';
case 'completed':
return '素材上传成功';
case 'failed':
return '素材上传失败';
default:
return status || '-';
}
} else {
const statusMap: Record<string, string> = {
'pending': '子任务待处理',
'waiting_user': '等待用户确认或触发',
'processing': '子任务处理中',
'completed': '子任务完成',
'failed': '子任务失败',
'cancelled': '子任务取消',
};
return statusMap[status] || status || '-';
}
};
return getStatusText();
},
},
{
title: '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
align: 'center',
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 `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
},
},
{
title: '操作',
key: 'action',
align: 'center',
render: (_, record) => (
<button
onClick={() => navigate(`/initial/${record.id}/initialinfo`)}
style={{ color: '#6366f1', textDecoration: 'none', fontSize: 12, border: 'none', background: 'none', cursor: 'pointer' }}
>
>
</button>
),
},
]}
dataSource={tableData}
rowKey="id"
pagination={{
current: currentPage,
pageSize: pageSize,
total: total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total}`,
onChange: handlePageChange,
}}
style={{ fontSize: 13 }}
/>
</button>
),
},
]}
dataSource={tableData}
rowKey="id"
pagination={{
current: currentPage,
pageSize: pageSize,
total: total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total}`,
onChange: handlePageChange,
}}
style={{ fontSize: 13 }}
scroll={{ y: 350 }}
/>
</Modal>
</React.Fragment>
);
@@ -18,7 +18,7 @@ import {
LoadingOutlined,
} from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { uploadVideo, uploadImage, generateReplication, getReplicationList,getone, getReplicationDetail } from '../api';
import { uploadVideo, uploadImage, generateReplication, getReplicationList, getone, getReplicationDetail } from '../api';
const { Header, Content } = Layout;
const { TextArea } = Input;
@@ -378,10 +378,10 @@ const GenerateConver: React.FC = () => {
setOwnProductName('');
setProductSellingPoints('');
fetchCardList(1,8);
fetchCardList(1, 8);
// 获取第一个的id直接进行下一步
getReplicationList(1,20).then((res: any) => {
getReplicationList(1, 20).then((res: any) => {
if (res.items) {
setTableData(res.items);
// childId = res.items[0].child_id;
@@ -406,7 +406,7 @@ const GenerateConver: React.FC = () => {
};
return (
<div className="replication-container" style={{ overflow: 'auto', height: '94vh', display: 'flex', justifyContent: 'space-between', gap: '2%' }}>
<div className="replication-container" style={{ overflow: 'auto', height: 'calc(100vh - 90px)', display: 'flex', justifyContent: 'space-between', gap: '2%' }}>
<div className="replication-preview" style={{ width: '70%', height: '100%', background: '#fff', display: 'flex', flexDirection: 'column', borderRadius: 15, overflow: 'hidden' }}>
+243 -50
View File
@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { Button, Drawer, Input, Table, Tag, Upload, message } from 'antd';
import { Button, Drawer, Input, Popconfirm, Spin, Table, Tag, Tooltip, Upload, message } from 'antd';
import { ArrowLeftOutlined, PlusOutlined, XOutlined } from '@ant-design/icons';
import { createRemoveLens, getShotReplicationDetail, Removelist, removeCreate, splitCustom, uploadImage } from '../api';
import VideoTrimPicker from '../components/VideoTrimPicker';
@@ -34,6 +34,16 @@ function RemoveInfo() {
const videoUrl = useMemo(() => buildAssetUrl(taskDetail?.videoUrl), [taskDetail?.videoUrl]);
const autoSplitButtonText = useMemo(() => {
if (tableData.length === 0) {
return 'AI自动拆分';
}
if (tableData.some(item => item.sourceMode === 'ai_suggestion')) {
return 'AI重拆';
}
return 'AI自动拆分';
}, [tableData]);
const fetchTaskDetail = useCallback(async () => {
if (!creatID) return;
try {
@@ -134,11 +144,12 @@ function RemoveInfo() {
if (!creatID) return;
setAutoSplitLoading(true);
try {
const shouldReplace = tableData.some(item => item.sourceMode === 'ai_suggestion');
await createRemoveLens(creatID, {
selected_indices: [],
replace_existing: false,
replace_existing: shouldReplace,
});
message.success('AI 拆镜任务已提交');
message.success(shouldReplace ? 'AI 重拆任务已提交' : 'AI 拆镜任务已提交');
await refreshPageData();
} catch (error: any) {
message.error(error?.message || '拆镜失败');
@@ -186,16 +197,19 @@ function RemoveInfo() {
return record?.splitStatus === 'completed' && !!record?.segmentVideoUrl;
};
const columns = [
const columns: any[] = [
{
title: '片段',
width: 100,
fixed: 'left',
align: 'center' as const,
render: (_: any, record: any) => (
<div>
<div style={{ fontSize: 16, fontWeight: 600, color: '#333' }}>{record.segmentName || `片段${record.segmentIndex || ''}`}</div>
<div style={{ fontSize: 12, color: '#999' }}>{record.timeNode}</div>
{record.sourceMode === 'custom' && <Tag color="blue" style={{ marginTop: 6 }}></Tag>}
{record.sourceMode === 'ai_suggestion' && <Tag color="orange" style={{ marginTop: 6 }}></Tag>}
</div>
),
},
@@ -221,13 +235,48 @@ function RemoveInfo() {
},
{
title: '视频内容',
width: 250,
width: 500,
align: 'left' as const,
render: (_: any, record: any) => (
<div style={{ fontSize: 14, color: '#333', lineHeight: 1.6 }}>
{record.segmentContent || record.lastError || '-'}
</div>
),
render: (_: any, record: any) => {
console.log('视频内容列数据:', {
record,
sourceMode: record.source_mode,
sourceMode2: record.sourceMode,
analysisStatus: record.analysis_status,
analysisStatus2: record.analysisStatus,
segmentContent: record.segmentContent,
lastError: record.lastError,
});
// AI建议的片段直接显示内容,不经过分析状态判断
if (record.source_mode === 'ai_suggestion' || record.sourceMode === 'ai_suggestion') {
return <div style={{ fontSize: 14, color: '#333', lineHeight: 1.6 }}>{record.segmentContent || record.lastError || '-'}</div>;
}
const analysisStatus = record.analysis_status || record.analysisStatus;
const statusMap: Record<string, string> = {
'not_required': '无需单独分析',
'pending': '等待分析',
'processing': '分析中',
'failed': '分析失败',
};
if (analysisStatus === 'processing') {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<Spin size="small" />
<span style={{ fontSize: 14, color: '#f59e0b' }}></span>
</div>
);
}
if (analysisStatus === 'failed') {
return <div style={{ fontSize: 14, color: '#ef4444', lineHeight: 1.6 }}></div>;
}
const displayText = analysisStatus && statusMap[analysisStatus] ? statusMap[analysisStatus] : (record.segmentContent || record.lastError || '-');
return <div style={{ fontSize: 14, color: '#333', lineHeight: 1.6 }}>{displayText}</div>;
},
},
{
title: '视频类型',
@@ -243,21 +292,114 @@ function RemoveInfo() {
title: '状态',
width: 120,
align: 'center' as const,
render: (_: any, record: any) => {
const statusMap: Record<string, { text: string; color: string }> = {
pending: { text: '待切割', color: 'default' },
processing: { text: '切割中', color: 'processing' },
retry_waiting: { text: '等待重试', color: 'warning' },
completed: { text: '已完成', color: 'success' },
failed: { text: '失败', color: 'error' },
};
const item = statusMap[record.splitStatus] || { text: record.splitStatus || '-', color: 'default' };
return <Tag color={item.color}>{item.text}</Tag>;
},
dataIndex: 'moduleProjectStatus',
render: (text: string, record: any) => {
const status = text;
const currentStepCode = record.moduleProjectCurrentStepCode || record.currentStepCode;
console.log('currentStepCode',status, currentStepCode);
const getStatusText = () => {
if (currentStepCode === 'image_prompt_optimize') {
switch (status) {
case 'waiting_user':
return '等待融合图生成';
case 'processing':
return '图片提示词生成中';
case 'completed':
return '图片提示词生成成功';
case 'failed':
return '图片提示词生成失败';
default:
return status || '-';
}
} else if (currentStepCode === 'image_generate') {
switch (status) {
case 'waiting_user':
return '等待生成视频提示词';
case 'processing':
return '融合图生成中';
case 'completed':
return '融合图生成成功';
case 'failed':
return '融合图生成失败';
default:
return status || '-';
}
} else if (currentStepCode === 'video_prompt_optimize') {
switch (status) {
case 'waiting_user':
return '等待最终视频生成';
case 'processing':
return '视频提示词生成中';
case 'completed':
return '视频提示词生成成功';
case 'failed':
return '视频提示词生成失败';
default:
return status || '-';
}
} else if (currentStepCode === 'video_generate') {
switch (status) {
case 'waiting_user':
return '';
case 'processing':
return '最终视频生成中';
case 'completed':
return '最终视频生成成功';
case 'failed':
return '最终视频生成失败';
default:
return status || '-';
}
} else if (currentStepCode === 'material_input') {
switch (status) {
case 'waiting_user':
return '等待生成图片提示词';
case 'processing':
return '素材处理中';
case 'completed':
return '素材上传成功';
case 'failed':
return '素材上传失败';
default:
return status || '-';
}
} else {
if (!record.moduleProjectId) {
return '待生成任务';
}
const statusMap: Record<string, string> = {
'pending': '子任务待处理',
'waiting_user': '等待用户确认或触发',
'processing': '子任务处理中',
'completed': '子任务完成',
'failed': '子任务失败',
'cancelled': '子任务取消',
};
return statusMap[status] || status || '-';
}
};
return getStatusText();
},
// render: (_: any, record: any) => {
// const statusMap: Record<string, { text: string; color: string }> = {
// pending: { text: '待切割', color: 'default' },
// processing: { text: '切割中', color: 'processing' },
// retry_waiting: { text: '等待重试', color: 'warning' },
// completed: { text: '已完成', color: 'success' },
// failed: { text: '失败', color: 'error' },
// };
// const item = statusMap[record.splitStatus] || { text: record.splitStatus || '-', color: 'default' };
// return <Tag color={item.color}>{item.text}</Tag>;
// },
},
{
title: '素材',
title: '操作',
width: 140,
fixed: 'right',
align: 'center' as const,
render: (_: any, record: any) => (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8 }}>
@@ -307,8 +449,8 @@ function RemoveInfo() {
{taskDetail ? (
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'space-between', overflow: 'hidden' }}>
<div style={{ flex: 0.4, background: '#fff', borderRadius: 12 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 30, marginBottom: 12 }}>
<div style={{ flex: 1, background: '#fff', borderRadius: 12, display: 'flex', alignItems: 'center', gap: 30, }}>
{/* <div style={{height: '100%', display: 'flex', alignItems: 'center', gap: 30,}}> */}
<div style={{ position: 'relative', width: 280, height: 160, borderRadius: 8, overflow: 'hidden', flexShrink: 0 }}>
<video
controls
@@ -331,21 +473,33 @@ function RemoveInfo() {
</div>
<div style={{ display: 'flex', marginBottom: 12 }}>
<span style={{ color: '#999', fontSize: 14, marginRight: 12, width: 80, flexShrink: 0 }}>:</span>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
{(taskDetail.originalVideoAudience?.split('、') || []).map((point: string, index: number) => (
<Tag key={index} color="purple" style={{ fontSize: 12 }}>
{point}
</Tag>
))}
</div>
{taskDetail.analysisStatus === 'processing' ? (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<Spin size="small" tip="分析中" style={{ color: '#f59e0b' }} />
</div>
) : taskDetail.analysisStatus === 'completed' ? (
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
{(taskDetail.originalVideoAudience?.split('、') || []).map((point: string, index: number) => (
<Tag key={index} color="purple" style={{ fontSize: 12 }}>
{point}
</Tag>
))}
</div>
) : null}
</div>
<div style={{ display: 'flex' }}>
<span style={{ color: '#999', fontSize: 14, marginRight: 12, width: 80, flexShrink: 0 }}>:</span>
<span style={{ color: '#333' }}>{taskDetail.originalVideoContent}</span>
{taskDetail.analysisStatus === 'processing' ? (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<Spin size="small" tip="分析中" style={{ color: '#f59e0b' }} />
</div>
) : taskDetail.analysisStatus === 'completed' ? (
<span style={{ color: '#333' }}>{taskDetail.originalVideoContent}</span>
) : null}
</div>
</div>
</div>
</div>
{/* </div> */}
</div>
<div style={{ display: 'flex', gap: 16, marginBottom: 20, marginTop: 20 }}>
@@ -364,32 +518,71 @@ function RemoveInfo() {
>
</Button>
<Button
type="primary"
onClick={handleAutoGenerate}
loading={autoSplitLoading}
disabled={autoSplitLoading}
style={{
flex: 1,
height: 48,
borderRadius: 8,
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
fontWeight: 500,
border: 'none',
}}
<Popconfirm
title={autoSplitButtonText === 'AI重拆' ? '确定要AI重拆吗?' : '确定要AI拆分吗?'}
okText="确定"
cancelText="取消"
onConfirm={handleAutoGenerate}
>
</Button>
<div style={{ display: 'flex', alignItems: 'center', flex: 1 }}>
<Button
type="primary"
loading={autoSplitLoading}
disabled={autoSplitLoading}
style={{
flex: 1,
height: 48,
borderRadius: 8,
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
fontWeight: 500,
border: 'none',
}}
>
{autoSplitButtonText}
{taskDetail?.aiSuggestions && taskDetail.aiSuggestions.length > 0 && (
<Tooltip
title={
<div>
<div style={{ fontWeight: 600, marginBottom: 8, color: '#6366f1' }}>AI拆分方案</div>
{(() => {
console.log('ai_suggestions:', taskDetail?.ai_suggestions);
console.log('ai_suggestions2:', taskDetail?.aiSuggestions);
const suggestions = taskDetail?.ai_suggestions || taskDetail?.aiSuggestions || [];
return suggestions.map((item: any) => (
<div key={item.index} style={{ marginBottom: 8, fontSize: 13, lineHeight: 1.5 }}>
<span style={{ fontWeight: 500, color: '#6366f1' }}>{item.index}.</span>
<span style={{ marginLeft: 4, color: '#000000ff' }}>{item.timeNode}</span>
<br />
<span style={{ color: '#666' }}>{item.content}</span>
</div>
));
})()}
</div>
}
placement="top"
trigger="hover"
overlayInnerStyle={{ maxWidth: 400, background: '#fff', border: '1px solid #e5e7eb', borderRadius: 12, padding: 12 }}
>
<div style={{ borderRadius: 8, background: 'rgba(99, 102, 241, 0.1)', color: '#ebebeb', fontSize: 13, fontWeight: 500, cursor: 'pointer' }}>
(AI )
</div>
</Tooltip>
)}
</Button>
</div>
</Popconfirm>
</div>
<div style={{ flex: 0.6, background: '#fff', borderRadius: 12, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
<div style={{ flex: 2, background: '#fff', borderRadius: 12, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
<Table
columns={columns}
dataSource={tableData}
pagination={false}
bordered={false}
rowKey="id"
scroll={{ y: '1005' }}
scroll={{ y: 450 }}
/>
</div>
</div>
+78 -44
View File
@@ -85,7 +85,6 @@ export default function VideoFrameExtractor() {
message.success('任务创建成功');
// 清空上传内容和输入框
cleanupResources();
navigate('/');
} catch (err) {
message.error('任务创建失败,请重试');
} finally {
@@ -224,31 +223,65 @@ export default function VideoFrameExtractor() {
accept="video/*"
beforeUpload={handleFileChange}
showUploadList={false}
disabled={loading}
style={{
background: '#faf5ff',
border: '2px dashed #c4b5fd',
background: loading ? '#f3f4f6' : '#faf5ff',
border: `2px dashed ${loading ? '#d1d5db' : '#c4b5fd'}`,
borderRadius: 24,
padding: '60px 20px'
padding: '60px 20px',
cursor: loading ? 'not-allowed' : 'pointer',
}}
>
<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>
{loading ? (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<div style={{
width: 100,
height: 100,
margin: '0 auto 20px',
background: '#e5e7eb',
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: 500 }}>
...
</p>
<p style={{ fontSize: 13, color: '#999', margin: 0 }}>
</p>
</div>
) : (
<>
<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>
)}
@@ -362,7 +395,7 @@ export default function VideoFrameExtractor() {
opacity: 0.85,
fontWeight: 400
}}>
10
{/* 预计消耗 10 个积分 */}
</span>
</button>
</div>
@@ -418,6 +451,17 @@ export default function VideoFrameExtractor() {
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 `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
},
},
{
title: '操作',
@@ -435,27 +479,17 @@ export default function VideoFrameExtractor() {
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;
},
}}
current: currentPage,
pageSize: pageSize,
total: total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total}`,
onChange: handlePageChange,
}}
style={{ fontSize: 13 }}
scroll={{ y: 350 }}
/>
</Modal>
</div>
+63 -12
View File
@@ -2,7 +2,8 @@ import React, { useState, useEffect } from 'react';
import { Button, Typography, Collapse, Space, Modal, Input, Table, message } from 'antd';
import { ArrowLeftOutlined, PlayCircleOutlined, CheckCircleOutlined, EditOutlined, DownloadOutlined, SettingOutlined, LayoutOutlined } from '@ant-design/icons';
import { useNavigate, useParams } from 'react-router-dom';
import {getShotReplicationList, removeDetail, removeone, removetwo, removethree, removefour, getEngine } from '../api/index';
import {getShotReplicationList, removeDetail, removeone, removetwo, removethree, removefour, getEngine, updateShotImagePrompt, updateShotVideoPromptSchema } from '../api/index';
import VideoPromptSchemaEditor from '../components/VideoPromptSchemaEditor';
import './css/InitialInfo.css';
const { Title, Text } = Typography;
@@ -18,6 +19,8 @@ function InitialInfo() {
const [currentType, setCurrentType] = useState<string>('image');
const [formData, setFormData] = useState<any>({});
const [pollingTimer, setPollingTimer] = useState<any>(null);
const [editingPromptStepId, setEditingPromptStepId] = useState('');
const [promptSaving, setPromptSaving] = useState(false);
// 引擎和视频参数相关状态
const [enginesele, setEnginesele] = useState<any>({});
@@ -183,9 +186,9 @@ function InitialInfo() {
}
}, [steps]);
const handleOpenModal = (prompt?: any, type?: string) => {
const handleOpenModal = (prompt?: any, type?: string, stepId?: string | number) => {
setCurrentType(type || 'image');
setEditingPromptStepId(stepId ? String(stepId) : '');
if (type === 'video' && typeof prompt === 'object') {
setFormData(prompt);
@@ -198,8 +201,56 @@ function InitialInfo() {
setModalVisible(true);
};
const handleConfirm = () => {
setModalVisible(false);
const handleConfirm = async () => {
if (currentType === 'image') {
if (!taskDetail?.id || !editingPromptStepId) {
message.warning('缺少任务或步骤 ID,无法保存图片提示词');
return;
}
if (!promptText || !promptText.trim()) {
message.warning('图片提示词不能为空');
return;
}
setPromptSaving(true);
try {
await updateShotImagePrompt(taskDetail.id, editingPromptStepId, {
prompt: promptText.trim(),
});
message.success('图片提示词已保存');
refreshTaskDetail();
setModalVisible(false);
setEditingPromptStepId('');
} catch (error: any) {
message.error(error?.message || '保存图片提示词失败');
} finally {
setPromptSaving(false);
}
return;
}
if (!taskDetail?.id || !editingPromptStepId) {
message.warning('缺少任务或步骤 ID,无法保存视频提示词');
return;
}
if (!formData || typeof formData !== 'object' || Object.keys(formData).length === 0) {
message.warning('视频提示词不能为空');
return;
}
setPromptSaving(true);
try {
await updateShotVideoPromptSchema(taskDetail.id, editingPromptStepId, {
prompt_schema: formData,
});
message.success('视频提示词已保存');
refreshTaskDetail();
setModalVisible(false);
setEditingPromptStepId('');
} catch (error: any) {
message.error(error?.message || '保存视频提示词失败');
} finally {
setPromptSaving(false);
}
};
// 轮询任务详情
@@ -319,9 +370,9 @@ function InitialInfo() {
return (
<React.Fragment>
<div style={{ minHeight: 'calc(100vh - 90px)', background: '#f8fafc' }}>
<div style={{ }}>
<div style={{ height: '100%' }}>
<div style={{ height: '100%', display: 'flex', justifyContent: 'space-between', gap: '2%' }}>
<div style={{ width: '70%', background: '#fff', borderRadius: 12, overflowY: 'auto' }}>
<div style={{ width: '70%', background: '#fff', borderRadius: 12, overflowY: 'auto', maxHeight: 'calc(100vh - 90px)' }}>
<div style={{ borderBottom: '1px solid #e2e8f0', width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '14px', boxSizing: 'border-box' }}>
<Button
type="text"
@@ -395,7 +446,7 @@ function InitialInfo() {
)}
</div>
</div>
<div style={{ width: '28%', minWidth: '400px', background: '#fff', borderRadius: 12, overflowY: 'auto' }}>
<div style={{ width: '28%', minWidth: '400px', background: '#fff', borderRadius: 12, overflowY: 'auto', maxHeight: 'calc(100vh - 90px)' }}>
<div style={{ padding: '14px' }}> </div>
<Collapse
defaultActiveKey={['']}
@@ -504,7 +555,7 @@ function InitialInfo() {
<Button
type="default"
icon={<EditOutlined />}
onClick={() => handleOpenModal(step?.output?.payload?.prompt)}
onClick={() => handleOpenModal(step?.output?.payload?.prompt, 'image', step.id)}
style={{ flex: 1, borderRadius: 8, borderColor: '#6366f1', color: '#6366f1', height: 36 }}
disabled={step.status !== 'completed'}
>
@@ -919,7 +970,7 @@ function InitialInfo() {
<Button
type="default"
icon={<EditOutlined />}
onClick={() => handleOpenModal(step?.output?.payload?.promptSchema,'video')}
onClick={() => handleOpenModal(step?.output?.payload?.promptSchema, 'video', step.id)}
style={{ flex: 1, borderRadius: 8, borderColor: '#6366f1', color: '#6366f1', height: 36 }}
disabled={step.status !== 'completed'}
>
@@ -986,7 +1037,7 @@ function InitialInfo() {
onCancel={() => setModalVisible(false)}
footer={[
<Button key="cancel" onClick={() => setModalVisible(false)}></Button>,
<Button key="confirm" type="primary" onClick={handleConfirm}></Button>,
<Button key="confirm" type="primary" onClick={handleConfirm} loading={promptSaving}></Button>,
]}
width={800}
>
@@ -1000,7 +1051,7 @@ function InitialInfo() {
/>
) : (
<div style={{ maxHeight: 500, overflowY: 'auto', paddingRight: 10 }}>
<FormRenderer data={formData} onChange={setFormData} />
<VideoPromptSchemaEditor value={formData} onChange={setFormData} />
</div>
)}
</Modal>