123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985 |
- import React, { useEffect, useState, useRef } from 'react';
- import { useParams, useHistory } from 'react-router-dom';
- import {
- Button, Row, Col, Container, Card, Table, Badge, Form,
- Modal, InputGroup, FormControl, Dropdown, OverlayTrigger, Tooltip, Alert, ProgressBar, Spinner
- } from 'react-bootstrap';
- import { toast } from 'react-toastify';
- import { bookService, bookInfoService, initDb } from '../../db';
- import { cozeService, initCozeService, getAvailableStyles } from '../../utils/cozeExample';
- import { WORKFLOW_IDS, hasValidToken } from '../../utils/cozeConfig';
- import { getUserSettings } from '../../utils/storageUtils';
- import taskQueueManager from '../../utils/taskQueueManager';
- import './projectDetail.css';
- import path from 'path-browserify';
- // 引入AudioPlayer组件
- import AudioPlayerComponent from '../../components/audioPlayer';
- import lipSyncService from '../../services/lipSyncService';
- import cozeUploadService from '../../services/cozeUploadService';
- const ProjectDetail = () => {
- const { projectId } = useParams();
- const history = useHistory();
- const [loading, setLoading] = useState(false);
- const [silentLoading, setSilentLoading] = useState(false);
- const [project, setProject] = useState(null);
- const [segments, setSegments] = useState([]);
- const [selectedSegments, setSelectedSegments] = useState([]);
- const [showMergeModal, setShowMergeModal] = useState(false);
- const [showSplitModal, setShowSplitModal] = useState(false);
- const [splitSegment, setSplitSegment] = useState(null);
- const [newSegments, setNewSegments] = useState([{ text: '', start_time: '', end_time: '' }]);
- const [showUploadModal, setShowUploadModal] = useState(false);
- const [uploadType, setUploadType] = useState('audio');
- const [uploadSegmentId, setUploadSegmentId] = useState(null);
- const [selectedFile, setSelectedFile] = useState(null);
- const [descriptionText, setDescriptionText] = useState('');
- const [showDescriptionModal, setShowDescriptionModal] = useState(false);
- const [descriptionSegmentId, setDescriptionSegmentId] = useState(null);
- const [showImagePreviewModal, setShowImagePreviewModal] = useState(false);
- const [previewImageUrl, setPreviewImageUrl] = useState('');
- const [generatingDescriptions, setGeneratingDescriptions] = useState(false);
- const [generationProgress, setGenerationProgress] = useState({ current: 0, total: 0 });
- const [editingSegmentId, setEditingSegmentId] = useState(null);
- const [editingTextField, setEditingTextField] = useState(null);
- const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
- const [hasDescriptions, setHasDescriptions] = useState(false);
- const [stylesList, setStylesList] = useState([]);
- const [selectedStyle, setSelectedStyle] = useState('');
- const [loadingStyles, setLoadingStyles] = useState(false);
- const [generatingImages, setGeneratingImages] = useState(false);
- const [imageGenerationProgress, setImageGenerationProgress] = useState({ current: 0, total: 0 });
- const [isPaused, setIsPaused] = useState(false);
- // 在状态变量中添加项目音频相关状态
- const [projectAudioPath, setProjectAudioPath] = useState(null);
- const [showAudioPlayer, setShowAudioPlayer] = useState(false);
- const [projectVideoPath, setProjectVideoPath] = useState(null);
- const [showVideoPlayer, setShowVideoPlayer] = useState(false);
- const [showUploadProjectAudioModal, setShowUploadProjectAudioModal] = useState(false);
- const [projectAudioFile, setProjectAudioFile] = useState(null);
- const [uploadingProjectAudio, setUploadingProjectAudio] = useState(false);
- // 对口型相关状态
- const [lipSyncProcessing, setLipSyncProcessing] = useState(false);
- const [lipSyncProgress, setLipSyncProgress] = useState(0);
- const [lipSyncTaskId, setLipSyncTaskId] = useState(null);
- const [lipSyncResultUrl, setLipSyncResultUrl] = useState(null);
- // 添加导出剪映草稿相关状态
- const [exportingDraft, setExportingDraft] = useState(false);
- const fileInputRef = useRef(null);
- // 辅助函数:根据ID获取单个分镜信息
- const getSegmentById = async (segmentId) => {
- const allSegments = await bookInfoService.getBookInfoByBookId(parseInt(projectId));
- return allSegments.find(segment => segment.id === segmentId);
- };
- // 加载项目详情
- const loadProjectDetail = async (silent = false) => {
- try {
- if (!silent) {
- setLoading(true);
- } else {
- setSilentLoading(true);
- }
- const projectData = await bookService.getBookById(parseInt(projectId));
- const segmentsData = await bookInfoService.getBookInfoByBookId(parseInt(projectId));
- setProject(projectData);
- setSegments(Array.isArray(segmentsData) ? segmentsData : []);
- // 设置当前选中的画风
- if (projectData && projectData.style) {
- setSelectedStyle(projectData.style);
- }
- // 加载项目音频路径
- if (projectData && projectData.audio_path) {
- setProjectAudioPath(projectData.audio_path);
- setShowAudioPlayer(true);
- } else {
- setProjectAudioPath(null);
- setShowAudioPlayer(false);
- }
- // 加载项目视频路径
- if (projectData && projectData.video_path) {
- setProjectVideoPath(projectData.video_path);
- setShowVideoPlayer(true);
- } else {
- setProjectVideoPath(null);
- setShowVideoPlayer(false);
- }
- // 检查是否已经有描述词
- const hasAnyDescriptions = Array.isArray(segmentsData) &&
- segmentsData.some(segment => segment.description && segment.description.trim() !== '');
- setHasDescriptions(hasAnyDescriptions);
- // 检查是否有正在进行的绘图任务
- checkOngoingImageGeneration();
- } catch (error) {
- console.error('加载项目详情失败:', error);
- toast.error(`加载项目详情失败: ${error.message}`);
- } finally {
- if (!silent) {
- setLoading(false);
- } else {
- setSilentLoading(false);
- }
- }
- };
- // 获取画风列表
- const loadStylesList = async () => {
- if (!hasValidToken()) {
- toast.warning('未配置Coze API Token,无法获取画风列表');
- return;
- }
- try {
- setLoadingStyles(true);
- const cozeInstance = initCozeService();
- // 调用获取画风列表API
- const response = await cozeInstance.getStyleList();
- console.log('获取画风列表响应:', response);
- // 处理返回结果
- let styles = [];
- if (response && response.data) {
- try {
- const responseData = JSON.parse(response.data);
- if (responseData.output) {
- // 如果返回的是数组
- if (Array.isArray(responseData.output)) {
- styles = responseData.output;
- }
- // 如果返回的是对象中包含styles数组
- else if (responseData.output.styles && Array.isArray(responseData.output.styles)) {
- styles = responseData.output.styles;
- }
- }
- } catch (e) {
- console.error('解析画风列表数据失败:', e);
- }
- }
- // 如果没有获取到画风列表,使用默认值
- if (styles.length === 0) {
- styles = ['真人风格', '古风风格', '卡通风格', '水彩风格', '油画风格'];
- }
- setStylesList(styles);
- // 如果项目还没有设置画风,且有可用画风,设置第一个为默认
- if (styles.length > 0 && (!selectedStyle || selectedStyle === '')) {
- setSelectedStyle(styles[0]);
- }
- } catch (error) {
- console.error('获取画风列表失败:', error);
- toast.error(`获取画风列表失败: ${error.message}`);
- // 使用默认的画风列表
- const defaultStyles = ['真人风格', '古风风格', '卡通风格', '水彩风格', '油画风格'];
- setStylesList(defaultStyles);
- } finally {
- setLoadingStyles(false);
- }
- };
- // 选择画风
- const handleStyleSelect = async (style) => {
- try {
- setLoading(true);
- setSelectedStyle(style);
- // 保存画风到项目中
- await bookService.updateBook(parseInt(projectId), {
- style: style
- });
- toast.success(`画风已设置为: ${style}`);
- } catch (error) {
- console.error('设置画风失败:', error);
- toast.error(`设置画风失败: ${error.message}`);
- } finally {
- setLoading(false);
- }
- };
- // 返回项目列表
- const backToProjectList = () => {
- history.push('/');
- };
- // 格式化时间
- const formatTime = (timeString) => {
- if (!timeString) return '';
- return timeString.replace(',', '.');
- };
- // 计算持续时间(秒)
- const calculateDuration = (startTime, endTime) => {
- const parseTime = (time) => {
- if (!time) return 0;
- const [hours, minutes, seconds] = time.replace(',', '.').split(':').map(parseFloat);
- return hours * 3600 + minutes * 60 + seconds;
- };
- const startSeconds = parseTime(startTime);
- const endSeconds = parseTime(endTime);
- return (endSeconds - startSeconds).toFixed(2);
- };
- // 选择/取消选择分镜
- const toggleSegmentSelection = (segmentId) => {
- setSelectedSegments(prev => {
- if (prev.includes(segmentId)) {
- return prev.filter(id => id !== segmentId);
- } else {
- return [...prev, segmentId];
- }
- });
- };
- // 显示合并分镜对话框
- const showMergeSegmentsModal = () => {
- if (selectedSegments.length < 2) {
- toast.error('请至少选择两个分镜进行合并');
- return;
- }
- setShowMergeModal(true);
- };
- // 合并分镜
- const mergeSegments = async () => {
- try {
- setLoading(true);
- await bookInfoService.mergeSegments(parseInt(projectId), selectedSegments);
- toast.success('分镜合并成功');
- setSelectedSegments([]);
- setShowMergeModal(false);
- await loadProjectDetail();
- } catch (error) {
- console.error('合并分镜失败:', error);
- toast.error(`合并分镜失败: ${error.message}`);
- } finally {
- setLoading(false);
- }
- };
- // 显示拆分分镜对话框
- const showSplitSegmentModal = (segment) => {
- setSplitSegment(segment);
- setNewSegments([{
- text: segment.text || '',
- start_time: segment.start_time || '',
- end_time: segment.end_time || ''
- }]);
- setShowSplitModal(true);
- };
- // 添加新分镜表单
- const addNewSegmentForm = () => {
- setNewSegments([...newSegments, { text: '', start_time: '', end_time: '' }]);
- };
- // 删除分镜表单
- const removeSegmentForm = (index) => {
- if (newSegments.length <= 1) {
- toast.error('至少需要一个分镜');
- return;
- }
- const updatedSegments = [...newSegments];
- updatedSegments.splice(index, 1);
- setNewSegments(updatedSegments);
- };
- // 更新分镜表单数据
- const updateNewSegmentData = (index, field, value) => {
- const updatedSegments = [...newSegments];
- updatedSegments[index][field] = value;
- setNewSegments(updatedSegments);
- };
- // 拆分分镜
- const splitSegmentSubmit = async () => {
- try {
- // 验证输入
- for (const segment of newSegments) {
- if (!segment.text || !segment.start_time || !segment.end_time) {
- toast.error('请填写所有分镜的文本内容和时间信息');
- return;
- }
- // 验证时间格式
- const timePattern = /^\d{2}:\d{2}:\d{2},\d{3}$/;
- if (!timePattern.test(segment.start_time) || !timePattern.test(segment.end_time)) {
- toast.error('时间格式不正确,应为: HH:MM:SS,SSS');
- return;
- }
- }
- setLoading(true);
- await bookInfoService.splitSegment(splitSegment.id, newSegments);
- toast.success('分镜拆分成功');
- setShowSplitModal(false);
- setSplitSegment(null);
- setNewSegments([{ text: '', start_time: '', end_time: '' }]);
- await loadProjectDetail();
- } catch (error) {
- console.error('拆分分镜失败:', error);
- toast.error(`拆分分镜失败: ${error.message}`);
- } finally {
- setLoading(false);
- }
- };
- // 显示上传对话框
- const showUploadModalFor = (segmentId, type) => {
- setUploadSegmentId(segmentId);
- setUploadType(type);
- setSelectedFile(null);
- setShowUploadModal(true);
- };
- // 处理文件选择改变
- const handleFileChange = (e) => {
- setSelectedFile(e.target.files[0]);
- };
- // 上传文件
- const handleFileUpload = async () => {
- if (!selectedFile) {
- toast.error('请先选择文件');
- return;
- }
- try {
- setLoading(true);
- // 创建上传路径
- const filePath = `uploads/${uploadType}/${selectedFile.name}`;
- // 更新分镜信息
- const updateData = {
- id: uploadSegmentId
- };
- if (uploadType === 'audio') {
- updateData.audio_path = filePath;
- } else if (uploadType === 'image') {
- updateData.image_path = filePath;
- } else if (uploadType === 'video') {
- updateData.video_path = filePath;
- }
- await bookInfoService.updateBookInfo(uploadSegmentId, updateData);
- toast.success('文件上传成功');
- setShowUploadModal(false);
- await loadProjectDetail();
- } catch (error) {
- console.error('文件上传失败:', error);
- toast.error(`文件上传失败: ${error.message}`);
- } finally {
- setLoading(false);
- }
- };
- // 显示描述词对话框
- const showDescriptionModalFor = (segment) => {
- setDescriptionSegmentId(segment.id);
- setDescriptionText(segment.description || '');
- setShowDescriptionModal(true);
- };
- // 保存描述词
- const saveDescription = async (segmentId, description) => {
- try {
- setLoading(true);
- await bookInfoService.updateBookInfo(segmentId, {
- description: description
- });
- toast.success('描述词保存成功');
- await loadProjectDetail();
- } catch (error) {
- console.error('保存描述词失败:', error);
- toast.error(`保存描述词失败: ${error.message}`);
- } finally {
- setLoading(false);
- }
- };
- // 保存文本内容
- const saveText = async (segmentId, text) => {
- try {
- setLoading(true);
- await bookInfoService.updateBookInfo(segmentId, {
- text: text
- });
- toast.success('文本内容保存成功');
- await loadProjectDetail();
- } catch (error) {
- console.error('保存文本内容失败:', error);
- toast.error(`保存文本内容失败: ${error.message}`);
- } finally {
- setLoading(false);
- }
- };
- // 一键生成描述词
- const generateAllDescriptions = async () => {
- if (!hasValidToken()) {
- toast.error('未配置Coze API Token,请先在首页点击"配置Coze API"按钮进行设置');
- return;
- }
- try {
- setGeneratingDescriptions(true);
- // 统计所有有文本的分镜数量
- const segmentsWithText = segments.filter(segment => segment.text && segment.text.trim() !== '');
- setGenerationProgress({ current: 0, total: segmentsWithText.length });
- toast.info(`开始生成描述词,共${segmentsWithText.length}个分镜需要处理...`);
- // 初始化Coze服务
- const cozeInstance = initCozeService();
- // 处理每个分镜
- let successCount = 0;
- let errorCount = 0;
- for (const segment of segments) {
- if (segment.text && segment.text.trim() !== '') {
- try {
- setGenerationProgress(prev => ({ ...prev, current: prev.current + 1 }));
- console.log(`处理分镜ID ${segment.id} 的描述词生成 (${successCount + errorCount + 1}/${segmentsWithText.length})...`);
- // 调用Coze的文本转描述词工作流
- const response = await cozeInstance.runWorkflow(WORKFLOW_IDS.textToDescription, {
- prompt: segment.text,
- });
- console.log(`分镜ID ${segment.id} 的API响应:`, JSON.parse(response.data).output);
- // 从响应中提取描述词
- let description = '';
- if (response && response.data) {
- description = JSON.parse(response.data).output;
- }
- // 保存到数据库
- await bookInfoService.updateBookInfo(segment.id, {
- description: description
- });
- // 直接更新状态而不是重新加载
- setSegments(prevSegments =>
- prevSegments.map(seg =>
- seg.id === segment.id
- ? { ...seg, description: description }
- : seg
- )
- );
- console.log(`分镜ID ${segment.id} 的描述词生成成功: ${description.slice(0, 30)}...`);
- successCount++;
- } catch (error) {
- console.error(`分镜ID ${segment.id} 的描述词生成失败:`, error);
- errorCount++;
- // 尝试获取错误详情
- let errorMsg = error.message || '未知错误';
- if (error.response) {
- try {
- const errorData = error.response.data;
- errorMsg = errorData.message || errorData.error || JSON.stringify(errorData);
- } catch (e) {
- errorMsg = `API错误: ${error.response.status}`;
- }
- }
- // 更新分镜的错误信息
- try {
- // 仅在分镜没有描述词的情况下,添加错误信息
- if (!segment.description || segment.description.trim() === '') {
- await bookInfoService.updateBookInfo(segment.id, {
- description: `[生成失败] ${errorMsg}`
- });
- // 直接更新状态而不是重新加载
- setSegments(prevSegments =>
- prevSegments.map(seg =>
- seg.id === segment.id
- ? { ...seg, description: `[生成失败] ${errorMsg}` }
- : seg
- )
- );
- }
- } catch (saveError) {
- console.error('保存错误信息失败:', saveError);
- }
- }
- }
- }
- // 显示结果统计
- if (errorCount > 0 && successCount > 0) {
- toast.warning(`描述词生成完成,成功: ${successCount}个,失败: ${errorCount}个`);
- } else if (errorCount > 0 && successCount === 0) {
- toast.error(`描述词生成全部失败,请检查API配置和网络连接`);
- } else {
- toast.success(hasDescriptions ? `描述词更新完成,共更新${successCount}个分镜` : `描述词生成完成,共生成${successCount}个分镜`);
- }
- setHasDescriptions(successCount > 0 || segments.some(segment => segment.description && segment.description.trim() !== ''));
- } catch (error) {
- console.error('生成描述词失败:', error);
- toast.error(`生成描述词失败: ${error.message}`);
- } finally {
- setGeneratingDescriptions(false);
- setGenerationProgress({ current: 0, total: 0 });
- }
- };
- // 处理直接编辑描述词
- const handleDirectEditDescription = (segmentId, description) => {
- setEditingSegmentId(segmentId);
- setEditingTextField('description');
- setDescriptionText(description || '');
- };
- // 处理直接编辑文本内容
- const handleDirectEditText = (segmentId, text) => {
- setEditingSegmentId(segmentId);
- setEditingTextField('text');
- setDescriptionText(text || '');
- };
- // 保存直接编辑的内容
- const saveDirectEdit = async () => {
- if (!editingSegmentId || !editingTextField) return;
- try {
- if (editingTextField === 'description') {
- await saveDescription(editingSegmentId, descriptionText);
- } else if (editingTextField === 'text') {
- await saveText(editingSegmentId, descriptionText);
- }
- // 清除编辑状态
- setEditingSegmentId(null);
- setEditingTextField(null);
- setDescriptionText('');
- } catch (error) {
- console.error('保存编辑内容失败:', error);
- toast.error(`保存失败: ${error.message}`);
- }
- };
- // 取消直接编辑
- const cancelDirectEdit = () => {
- setEditingSegmentId(null);
- setEditingTextField(null);
- setDescriptionText('');
- };
- // 显示删除确认对话框
- const showDeleteConfirmation = () => {
- setShowDeleteConfirm(true);
- };
- // 删除项目
- const deleteProject = async () => {
- try {
- setLoading(true);
- // 调用删除API
- await bookService.deleteBook(parseInt(projectId));
- toast.success('项目删除成功');
- // 返回首页
- history.push('/');
- } catch (error) {
- console.error('删除项目失败:', error);
- toast.error(`删除项目失败: ${error.message}`);
- } finally {
- setLoading(false);
- setShowDeleteConfirm(false);
- }
- };
- // 删除分镜
- const deleteSegment = async (segmentId) => {
- try {
- setLoading(true);
- // 调用删除API
- await bookInfoService.deleteBookInfo(segmentId);
- toast.success('分镜删除成功');
- // 重新加载分镜列表
- await loadProjectDetail();
- } catch (error) {
- console.error('删除分镜失败:', error);
- toast.error(`删除分镜失败: ${error.message}`);
- } finally {
- setLoading(false);
- }
- };
- // 为单个分镜生成描述词
- const generateSingleDescription = async (segment) => {
- if (!hasValidToken()) {
- toast.error('未配置Coze API Token,请先在首页点击"配置Coze API"按钮进行设置');
- return;
- }
- if (!segment.text || segment.text.trim() === '') {
- toast.warning('分镜文本为空,无法生成描述词');
- return;
- }
- try {
- setGeneratingDescriptions(true);
- // 设置当前正在处理的分镜ID
- setDescriptionSegmentId(segment.id);
- toast.info('开始生成描述词,请稍候...');
- // 初始化Coze服务
- const cozeInstance = initCozeService();
- console.log(`为分镜ID ${segment.id} 生成描述词...`);
- // 调用Coze的文本转描述词工作流
- const response = await cozeInstance.runWorkflow(WORKFLOW_IDS.textToDescription, {
- text: segment.text,
- style: 'detailed',
- language: 'zh'
- });
- console.log(`分镜ID ${segment.id} 的API响应:`, response);
- // 从响应中提取描述词
- let description = '';
- if (response && response.description) {
- description = response.description;
- } else if (response && typeof response === 'string') {
- description = response;
- } else if (response && response.result) {
- description = response.result;
- } else if (response && response.content) {
- description = response.content;
- } else {
- // 如果API返回格式不一致,尝试从整个响应对象提取有意义的内容
- try {
- description = JSON.stringify(response);
- } catch (e) {
- description = `描述词生成成功,但无法解析结果: ${e.message}`;
- }
- }
- // 保存到数据库
- await bookInfoService.updateBookInfo(segment.id, {
- description: description
- });
- console.log(`分镜ID ${segment.id} 的描述词生成成功: ${description.slice(0, 30)}...`);
- toast.success('描述词生成成功');
- // 刷新数据
- await loadProjectDetail();
- // 设置有描述词的状态
- setHasDescriptions(true);
- } catch (error) {
- console.error(`为分镜ID ${segment.id} 生成描述词失败:`, error);
- // 尝试获取错误详情
- let errorMsg = error.message || '未知错误';
- if (error.response) {
- try {
- const errorData = error.response.data;
- errorMsg = errorData.message || errorData.error || JSON.stringify(errorData);
- } catch (e) {
- errorMsg = `API错误: ${error.response.status}`;
- }
- }
- toast.error(`描述词生成失败: ${errorMsg}`);
- } finally {
- setGeneratingDescriptions(false);
- setDescriptionSegmentId(null);
- }
- };
- // 检查是否有正在进行的绘图任务
- const checkOngoingImageGeneration = async () => {
- try {
- console.log('检查是否有正在进行的绘图任务');
- // 从数据库获取所有分镜
- const segmentsData = await bookInfoService.getBookInfoByBookId(parseInt(projectId));
- // 检查是否有暂停状态(-1)的分镜
- const pausedSegments = segmentsData.filter(segment => segment.draw_status === -1);
- // 检查有排队状态(0)或绘画中状态(1)的分镜
- const pendingSegments = segmentsData.filter(segment =>
- segment.draw_status === 0 || segment.draw_status === 1
- );
- // 如果既没有暂停的也没有待处理的,确保状态重置为默认
- if (pausedSegments.length === 0 && pendingSegments.length === 0) {
- // 如果重启后没有正在进行的任务,重置所有状态
- setGeneratingImages(false);
- setIsPaused(false);
- localStorage.removeItem(`project_${projectId}_image_generation`);
- return null;
- }
- // 计算已完成的分镜数量(固定值)
- const completedCount = segmentsData.filter(segment => segment.draw_status === 2 && segment.image_path).length;
- // 计算总数(需要处理的分镜 + 已完成的分镜)
- // 对于暂停状态,总数是:已完成的 + 暂停的 + 排队的 + 绘画中的
- const totalSegments = segmentsData.filter(segment =>
- segment.description && segment.description.trim() !== '' && (
- segment.draw_status === -1 ||
- segment.draw_status === 0 ||
- segment.draw_status === 1 ||
- (segment.draw_status === 2 && segment.image_path)
- )
- ).length;
- if (pausedSegments.length > 0) {
- console.log(`检测到${pausedSegments.length}个处于暂停状态的分镜`);
- // 设置UI状态为暂停
- setGeneratingImages(true);
- setIsPaused(true);
- setImageGenerationProgress({
- current: completedCount,
- total: totalSegments
- });
- // 保存到localStorage
- const currentProgress = {
- current: completedCount,
- total: totalSegments
- };
- // 保存到localStorage
- localStorage.setItem(`project_${projectId}_image_generation`, JSON.stringify({
- progress: currentProgress,
- timestamp: new Date().getTime(),
- isPaused: true
- }));
- return;
- }
- if (pendingSegments.length > 0) {
- console.log(`检测到${pendingSegments.length}个待处理的分镜,继续执行绘图任务`);
- // 设置UI状态
- setGeneratingImages(true);
- setIsPaused(false);
- setImageGenerationProgress({
- current: completedCount,
- total: totalSegments
- });
- // 启动绘图任务
- setTimeout(() => {
- generateAllImages(false);
- }, 1000);
- return;
- }
- } catch (error) {
- console.error('检查绘图状态失败:', error);
- // 出现错误时确保重置状态为默认
- localStorage.removeItem(`project_${projectId}_image_generation`);
- setGeneratingImages(false);
- setIsPaused(false);
- }
- return null;
- };
- // 添加回一键生成所有分镜图片函数
- const generateAllImages = async (isForceRedraw = false) => {
- // 检查是否有有效的Coze API token
- if (!hasValidToken()) {
- toast.error('未配置Coze API Token,请先在首页点击"配置Coze API"按钮进行设置');
- return;
- }
- // 检查是否已选择绘画风格
- if (!selectedStyle || selectedStyle.trim() === '') {
- toast.error('请先选择绘画风格');
- return;
- }
- if (isForceRedraw) {
- // 重绘:将所有有描述词的分镜设置为排队状态(0)
- const segmentsToRedraw = segments.filter(
- segment => segment.description && segment.description.trim() !== ''
- );
- console.log(`重绘: 将${segmentsToRedraw.length}个有描述词的分镜设置为排队状态`);
- for (const segment of segmentsToRedraw) {
- await bookInfoService.updateBookInfo(segment.id, {
- draw_status: 0 // 排队状态
- });
- }
- // 刷新数据
- await loadProjectDetail(true);
- // 获取刷新后的最新数据
- const updatedSegmentsData = await bookInfoService.getBookInfoByBookId(parseInt(projectId));
- // 重新获取需要处理的分镜 (排队状态的分镜)
- let segmentsToProcess = updatedSegmentsData.filter(segment => segment.draw_status === 0);
- if (segmentsToProcess.length === 0) {
- toast.info('没有需要处理的分镜');
- return;
- }
- // 计算已完成的分镜数量
- const completedCount = updatedSegmentsData.filter(segment => segment.draw_status === 2 && segment.image_path).length;
- // 计算有描述词并且有效的分镜总数(待处理 + 已完成)
- // 这个总数在整个处理过程中应该保持不变
- const totalCount = updatedSegmentsData.filter(segment =>
- segment.description && segment.description.trim() !== '' && (
- segment.draw_status === -1 || // 暂停状态
- segment.draw_status === 0 || // 排队状态
- segment.draw_status === 1 || // 绘画中状态
- (segment.draw_status === 2 && segment.image_path) // 已完成状态
- )
- ).length;
- // 设置生成状态
- setGeneratingImages(true);
- const progress = { current: completedCount, total: totalCount };
- setImageGenerationProgress(progress);
- // 保存生成状态到localStorage (仅用于UI显示,实际控制由数据库字段控制)
- localStorage.setItem(`project_${projectId}_image_generation`, JSON.stringify({
- progress: progress,
- timestamp: new Date().getTime(),
- isPaused: false
- }));
- // 初始化Coze服务
- const cozeInstance = initCozeService({
- timeout: 60000 // 设置60秒超时
- });
- // 处理每个分镜
- let successCount = 0;
- let errorCount = 0;
- // 为每个分镜创建任务并添加到队列
- for (let i = 0; i < segmentsToProcess.length; i++) {
- const segment = segmentsToProcess[i];
- // 每次处理前检查分镜的当前状态
- const currentSegment = await getSegmentById(segment.id);
-
- // 如果分镜状态不是排队状态(0),或者已经有图片,跳过处理
- if (currentSegment.draw_status !== 0 || currentSegment.image_path) {
- console.log(`分镜ID ${segment.id} 当前状态不是排队状态或已有图片,跳过处理`);
- continue;
- }
- // 每次处理前检查是否有分镜处于暂停状态(-1)
- // 获取最新的分镜状态
- const latestSegments = await bookInfoService.getBookInfoByBookId(parseInt(projectId));
- const anyPaused = latestSegments.some(segment => segment.draw_status === -1);
- if (anyPaused) {
- console.log('检测到暂停状态,停止图片生成');
- toast.info('图片生成已暂停');
- // 更新UI状态
- setIsPaused(true);
- localStorage.setItem(`project_${projectId}_image_generation`, JSON.stringify({
- progress: { current: progress.current, total: totalCount },
- timestamp: new Date().getTime(),
- isPaused: true
- }));
- // 停止后续处理
- break;
- }
- // 更新进度 - 当前完成数量应该是初始完成数量 + 当前处理的索引
- // 重新从数据库获取已完成的图片数量,以确保准确性
- const currentCompletedCount = (await bookInfoService.getBookInfoByBookId(parseInt(projectId)))
- .filter(segment => segment.draw_status === 2 && segment.image_path).length;
- progress.current = currentCompletedCount;
- setImageGenerationProgress({ ...progress });
- // 再次检查分镜状态,确保没有被其他进程处理
- const segmentBeforeUpdate = await getSegmentById(segment.id);
- if (segmentBeforeUpdate.draw_status !== 0 || segmentBeforeUpdate.image_path) {
- console.log(`分镜ID ${segment.id} 已被其他进程处理,跳过`);
- continue;
- }
- // 更新状态为绘画中(1)
- await bookInfoService.updateBookInfo(segment.id, {
- draw_status: 1 // 绘画中状态
- });
- console.log(`处理分镜ID ${segment.id} 的图片生成 (${i + 1}/${segmentsToProcess.length})...`);
- try {
- // 创建图像生成任务
- const imageGenerationTask = async () => {
- console.log(`执行分镜ID ${segment.id} 的图片生成任务`);
- // 再次检查当前分镜状态,确保没有被暂停
- const currentSegment = await getSegmentById(segment.id);
- if (currentSegment.draw_status === -1 || currentSegment.image_path) {
- console.log('检测到当前分镜已被设置为暂停状态或已有图片,跳过处理');
- return null;
- }
- // 调用Coze的图片生成工作流
- console.log(`发送API请求,生成分镜ID ${segment.id} 的图片...`);
- const response = await cozeInstance.generateImage(
- segment.description, // 描述词作为prompt
- selectedStyle, // 项目设置的画风
- {
- width: 1024,
- height: 1024,
- num_images: 1
- }
- );
- console.log(`API请求成功完成,分镜ID ${segment.id} 已获得响应`);
- return response;
- };
- // 添加任务到队列并等待其完成
- const response = await taskQueueManager.addTask(
- imageGenerationTask,
- `分镜ID ${segment.id} 批量图片生成`
- );
- // 如果返回null,表示任务被跳过
- if (response === null) {
- continue;
- }
- // 检查分镜当前状态,如果已设置为暂停状态或已有图片,则不保存结果
- const segmentAfterApiCall = await getSegmentById(segment.id);
- if (segmentAfterApiCall.draw_status === -1 || segmentAfterApiCall.image_path) {
- console.log('API请求完成后检测到分镜已被设置为暂停状态或已有图片,不保存结果');
- setGeneratingImages(false);
- // 重置进度状态
- setImageGenerationProgress({ current: 0, total: 0 });
- continue;
- }
- // 从响应中提取图片URL
- let imagePath = '';
- if (response && response.data) {
- try {
- const responseData = JSON.parse(response.data);
- if (responseData.output && responseData.output) {
- imagePath = responseData.output;
- }
- } catch (e) {
- console.error('解析图片URL失败:', e);
- }
- }
- console.log('提取的图片路径11:', imagePath);
- // 再次检查暂停状态
- const segmentBeforeSave = await getSegmentById(segment.id);
- if (segmentBeforeSave.draw_status === -1) {
- console.log('保存前检测到分镜已被设置为暂停状态,不保存结果');
- continue;
- }
- // 保存到数据库
- if (imagePath) {
- await bookInfoService.updateBookInfo(segment.id, {
- id: segment.id,
- image_path: imagePath,
- draw_status: 2 // 已完成状态
- });
- console.log(`分镜ID ${segment.id} 的图片生成成功: ${imagePath}`);
- successCount++; // 增加成功计数
- // 更新进度
- const currentCompletedCount = (await bookInfoService.getBookInfoByBookId(parseInt(projectId)))
- .filter(segment => segment.draw_status === 2 && segment.image_path).length;
- progress.current = currentCompletedCount;
- setImageGenerationProgress({ ...progress });
- // 实时更新图片显示
- setSegments(prevSegments =>
- prevSegments.map(seg =>
- seg.id === segment.id
- ? { ...seg, image_path: imagePath, draw_status: 2 }
- : seg
- )
- );
- } else {
- // 如果没有成功获取图片URL,将状态设置回排队状态(0)
- await bookInfoService.updateBookInfo(segment.id, {
- draw_status: 0 // 重新设为排队状态
- });
- throw new Error('无法从响应中提取图片URL');
- }
- } catch (error) {
- errorCount++;
- // 检查分镜当前状态,如果不是暂停状态,则更新为排队状态(0),允许重试
- const segmentAfterError = await getSegmentById(segment.id);
- if (segmentAfterError.draw_status !== -1) {
- await bookInfoService.updateBookInfo(segment.id, {
- draw_status: 0 // 重新设为排队状态
- });
- }
- // 尝试获取错误详情
- let errorMsg = error.message || '未知错误';
- if (error.response) {
- try {
- const errorData = error.response.data;
- errorMsg = errorData.message || errorData.error || JSON.stringify(errorData);
- } catch (e) {
- errorMsg = `API错误: ${error.response.status}`;
- }
- }
- }
- }
- // 处理完所有分镜后更新状态
- // 检查最终状态
- const finalSegments = await bookInfoService.getBookInfoByBookId(parseInt(projectId));
- const stillPaused = finalSegments.some(segment => segment.draw_status === -1);
- if (!stillPaused) {
- // 只有在非暂停状态下才清除生成状态
- setGeneratingImages(false);
- setIsPaused(false);
- localStorage.removeItem(`project_${projectId}_image_generation`);
- if (errorCount === 0) {
- toast.success(`图片生成完成,成功生成${successCount}张图片`);
- } else {
- toast.warning(`图片生成结束,成功${successCount}张,失败${errorCount}张`);
- }
- } else {
- toast.info('图片生成已暂停,可以稍后继续');
- }
- // 刷新数据
- await loadProjectDetail(true);
- } else {
- // 继续绘画: 将没有图片的、有描述词的分镜设置为排队状态(0)
- const segmentsToQueue = segments.filter(
- segment => segment.description &&
- segment.description.trim() !== '' &&
- !segment.image_path
- );
- console.log(`继续绘画: 将${segmentsToQueue.length}个没有图片的分镜设置为排队状态`);
- for (const segment of segmentsToQueue) {
- await bookInfoService.updateBookInfo(segment.id, {
- draw_status: 0 // 排队状态
- });
- }
- // 刷新数据
- await loadProjectDetail(true);
- // 获取刷新后的最新数据
- const updatedSegmentsData = await bookInfoService.getBookInfoByBookId(parseInt(projectId));
- // 重新获取需要处理的分镜 (排队状态的分镜)
- let segmentsToProcess = updatedSegmentsData.filter(segment => segment.draw_status === 0);
- if (segmentsToProcess.length === 0) {
- toast.info('没有需要处理的分镜');
- return;
- }
- // 计算已完成的分镜数量
- const completedCount = updatedSegmentsData.filter(segment => segment.draw_status === 2 && segment.image_path).length;
- // 计算有描述词并且有效的分镜总数(待处理 + 已完成)
- // 这个总数在整个处理过程中应该保持不变
- const totalCount = updatedSegmentsData.filter(segment =>
- segment.description && segment.description.trim() !== '' && (
- segment.draw_status === -1 || // 暂停状态
- segment.draw_status === 0 || // 排队状态
- segment.draw_status === 1 || // 绘画中状态
- (segment.draw_status === 2 && segment.image_path) // 已完成状态
- )
- ).length;
- // 设置生成状态
- setGeneratingImages(true);
- const progress = { current: completedCount, total: totalCount };
- setImageGenerationProgress(progress);
- // 保存生成状态到localStorage (仅用于UI显示,实际控制由数据库字段控制)
- localStorage.setItem(`project_${projectId}_image_generation`, JSON.stringify({
- progress: progress,
- timestamp: new Date().getTime(),
- isPaused: false
- }));
- // 初始化Coze服务
- const cozeInstance = initCozeService({
- timeout: 60000 // 设置60秒超时
- });
- // 处理每个分镜
- let successCount = 0;
- let errorCount = 0;
- // 为每个分镜创建任务并添加到队列
- for (let i = 0; i < segmentsToProcess.length; i++) {
- const segment = segmentsToProcess[i];
- // 每次处理前检查分镜的当前状态
- const currentSegment = await getSegmentById(segment.id);
-
- // 如果分镜状态不是排队状态(0),或者已经有图片,跳过处理
- if (currentSegment.draw_status !== 0 || currentSegment.image_path) {
- console.log(`分镜ID ${segment.id} 当前状态不是排队状态或已有图片,跳过处理`);
- continue;
- }
- // 每次处理前检查是否有分镜处于暂停状态(-1)
- // 获取最新的分镜状态
- const latestSegments = await bookInfoService.getBookInfoByBookId(parseInt(projectId));
- const anyPaused = latestSegments.some(segment => segment.draw_status === -1);
- if (anyPaused) {
- console.log('检测到暂停状态,停止图片生成');
- toast.info('图片生成已暂停');
- // 更新UI状态
- setIsPaused(true);
- localStorage.setItem(`project_${projectId}_image_generation`, JSON.stringify({
- progress: { current: progress.current, total: totalCount },
- timestamp: new Date().getTime(),
- isPaused: true
- }));
- // 停止后续处理
- break;
- }
- // 更新进度 - 当前完成数量应该是初始完成数量 + 当前处理的索引
- // 重新从数据库获取已完成的图片数量,以确保准确性
- const currentCompletedCount = (await bookInfoService.getBookInfoByBookId(parseInt(projectId)))
- .filter(segment => segment.draw_status === 2 && segment.image_path).length;
- progress.current = currentCompletedCount;
- setImageGenerationProgress({ ...progress });
- // 再次检查分镜状态,确保没有被其他进程处理
- const segmentBeforeUpdate = await getSegmentById(segment.id);
- if (segmentBeforeUpdate.draw_status !== 0 || segmentBeforeUpdate.image_path) {
- console.log(`分镜ID ${segment.id} 已被其他进程处理,跳过`);
- continue;
- }
- // 更新状态为绘画中(1)
- await bookInfoService.updateBookInfo(segment.id, {
- draw_status: 1 // 绘画中状态
- });
- console.log(`处理分镜ID ${segment.id} 的图片生成 (${i + 1}/${segmentsToProcess.length})...`);
- try {
- // 创建图像生成任务
- const imageGenerationTask = async () => {
- console.log(`执行分镜ID ${segment.id} 的图片生成任务`);
- // 再次检查当前分镜状态,确保没有被暂停
- const currentSegment = await getSegmentById(segment.id);
- if (currentSegment.draw_status === -1 || currentSegment.image_path) {
- console.log('检测到当前分镜已被设置为暂停状态或已有图片,跳过处理');
- return null;
- }
- // 调用Coze的图片生成工作流
- console.log(`发送API请求,生成分镜ID ${segment.id} 的图片...`);
- const response = await cozeInstance.generateImage(
- segment.description, // 描述词作为prompt
- selectedStyle, // 项目设置的画风
- {
- width: 1024,
- height: 1024,
- num_images: 1
- }
- );
- console.log(`API请求成功完成,分镜ID ${segment.id} 已获得响应`);
- return response;
- };
- // 添加任务到队列并等待其完成
- const response = await taskQueueManager.addTask(
- imageGenerationTask,
- `分镜ID ${segment.id} 批量图片生成`
- );
- // 如果返回null,表示任务被跳过
- if (response === null) {
- continue;
- }
- // 检查分镜当前状态,如果已设置为暂停状态或已有图片,则不保存结果
- const segmentAfterApiCall = await getSegmentById(segment.id);
- if (segmentAfterApiCall.draw_status === -1 || segmentAfterApiCall.image_path) {
- console.log('API请求完成后检测到分镜已被设置为暂停状态或已有图片,不保存结果');
- setGeneratingImages(false);
- // 重置进度状态
- setImageGenerationProgress({ current: 0, total: 0 });
- continue;
- }
- // 从响应中提取图片URL
- let imagePath = '';
- if (response && response.data) {
- try {
- const responseData = JSON.parse(response.data);
- if (responseData.output && responseData.output) {
- imagePath = responseData.output;
- }
- } catch (e) {
- console.error('解析图片URL失败:', e);
- }
- }
- // 再次检查暂停状态
- const segmentBeforeSave = await getSegmentById(segment.id);
- if (segmentBeforeSave.draw_status === -1) {
- console.log('保存前检测到分镜已被设置为暂停状态,不保存结果');
- continue;
- }
- // 保存到数据库
- if (imagePath) {
- await bookInfoService.updateBookInfo(segment.id, {
- id: segment.id,
- image_path: imagePath,
- draw_status: 2 // 已完成状态
- });
- console.log(`分镜ID ${segment.id} 的图片生成成功: ${imagePath}`);
- successCount++; // 增加成功计数
- // 更新进度
- const currentCompletedCount = (await bookInfoService.getBookInfoByBookId(parseInt(projectId)))
- .filter(segment => segment.draw_status === 2 && segment.image_path).length;
- progress.current = currentCompletedCount;
- setImageGenerationProgress({ ...progress });
- // 实时更新图片显示
- setSegments(prevSegments =>
- prevSegments.map(seg =>
- seg.id === segment.id
- ? { ...seg, image_path: imagePath, draw_status: 2 }
- : seg
- )
- );
- } else {
- // 如果没有成功获取图片URL,将状态设置回排队状态(0)
- await bookInfoService.updateBookInfo(segment.id, {
- draw_status: 0 // 重新设为排队状态
- });
- throw new Error('无法从响应中提取图片URL');
- }
- } catch (error) {
- errorCount++;
- // 检查分镜当前状态,如果不是暂停状态,则更新为排队状态(0),允许重试
- const segmentAfterError = await getSegmentById(segment.id);
- if (segmentAfterError.draw_status !== -1) {
- await bookInfoService.updateBookInfo(segment.id, {
- draw_status: 0 // 重新设为排队状态
- });
- }
- // 尝试获取错误详情
- let errorMsg = error.message || '未知错误';
- if (error.response) {
- try {
- const errorData = error.response.data;
- errorMsg = errorData.message || errorData.error || JSON.stringify(errorData);
- } catch (e) {
- errorMsg = `API错误: ${error.response.status}`;
- }
- }
- }
- }
- // 处理完所有分镜后更新状态
- // 检查最终状态
- const finalSegments = await bookInfoService.getBookInfoByBookId(parseInt(projectId));
- const stillPaused = finalSegments.some(segment => segment.draw_status === -1);
- if (!stillPaused) {
- // 只有在非暂停状态下才清除生成状态
- setGeneratingImages(false);
- setIsPaused(false);
- localStorage.removeItem(`project_${projectId}_image_generation`);
- if (errorCount === 0) {
- toast.success(`图片生成完成,成功生成${successCount}张图片`);
- } else {
- toast.warning(`图片生成结束,成功${successCount}张,失败${errorCount}张`);
- }
- } else {
- toast.info('图片生成已暂停,可以稍后继续');
- }
- // 刷新数据
- await loadProjectDetail(true);
- }
- };
- // 暂停图片生成
- const pauseImageGeneration = async () => {
- console.log('点击暂停按钮');
- try {
- // 1. 清空任务队列
- const canceledTasks = taskQueueManager.clearQueue();
- console.log(`已取消队列中的 ${canceledTasks} 个任务`);
- // 2. 获取当前项目的所有分镜
- const allSegments = await bookInfoService.getBookInfoByBookId(parseInt(projectId));
- // 3. 找出所有正在绘制(1)或排队(0)的分镜
- const activeSegments = allSegments.filter(segment =>
- segment.draw_status === 0 || segment.draw_status === 1
- );
- if (activeSegments.length === 0) {
- console.log('没有需要暂停的任务');
- return;
- }
- console.log(`找到${activeSegments.length}个活跃分镜需要暂停`);
- // 4. 将它们的状态设置为暂停(-1)
- for (const segment of activeSegments) {
- await bookInfoService.updateBookInfo(segment.id, {
- draw_status: -1 // 暂停状态
- });
- }
- // 5. 设置UI状态
- setIsPaused(true);
- // 保持当前进度状态的total不变,只更新current
- // 6. 更新localStorage,保存进度和暂停状态
- // 确保current是已完成图片的数量
- const completedCount = allSegments.filter(segment => segment.draw_status === 2 && segment.image_path).length;
- // 重新计算需要显示的总数,包括有描述词的并且状态有效的分镜
- const totalCount = allSegments.filter(segment =>
- segment.description && segment.description.trim() !== '' && (
- segment.draw_status === -1 || // 暂停状态
- segment.draw_status === 0 || // 排队状态
- segment.draw_status === 1 || // 绘画中状态
- (segment.draw_status === 2 && segment.image_path) // 已完成状态
- )
- ).length;
- const currentProgress = {
- current: completedCount,
- total: totalCount
- };
- // 保存到localStorage
- localStorage.setItem(`project_${projectId}_image_generation`, JSON.stringify({
- progress: currentProgress,
- timestamp: new Date().getTime(),
- isPaused: true
- }));
- // 更新UI中显示的进度
- setImageGenerationProgress(currentProgress);
- // 只显示一个暂停成功的提示
- toast.info('绘图任务已暂停');
- } catch (error) {
- console.error('暂停图片生成失败:', error);
- toast.error(`暂停失败: ${error.message}`);
- }
- };
- // 继续图片生成
- const resumeImageGeneration = async () => {
- console.log('用户点击继续绘制按钮');
- try {
- // 1. 获取当前项目的所有分镜
- const allSegments = await bookInfoService.getBookInfoByBookId(parseInt(projectId));
- // 2. 找出所有暂停(-1)状态的分镜
- const pausedSegments = allSegments.filter(segment =>
- segment.draw_status === -1
- );
- console.log(`找到${pausedSegments.length}个暂停的分镜需要恢复`);
- if (pausedSegments.length === 0) {
- console.log('没有已暂停的绘图任务需要恢复');
- return;
- }
- // 3. 将它们的状态设置为排队(0)
- for (const segment of pausedSegments) {
- await bookInfoService.updateBookInfo(segment.id, {
- draw_status: 0 // 排队状态
- });
- }
- // 4. 设置UI状态
- setIsPaused(false);
- // 5. 获取刷新后的数据
- await loadProjectDetail(true);
- // 6. 调用generateAllImages继续处理
- console.log('开始继续生成图片');
- await generateAllImages(false); // 使用false参数表示继续绘制而非重绘
- } catch (error) {
- console.error('恢复图片生成失败:', error);
- toast.error(`恢复失败: ${error.message}`);
- }
- };
- // 取消图片生成
- const cancelImageGeneration = async () => {
- console.log('用户点击取消按钮');
- try {
- // 1. 清空任务队列
- const canceledTasks = taskQueueManager.clearQueue();
- console.log(`已取消队列中的 ${canceledTasks} 个任务`);
- // 2. 获取当前项目的所有分镜
- const allSegments = await bookInfoService.getBookInfoByBookId(parseInt(projectId));
- // 3. 找出所有非完成状态的分镜(状态不为2的)
- const activeSegments = allSegments.filter(segment =>
- segment.draw_status !== 2
- );
- if (activeSegments.length === 0) {
- console.log('没有需要取消的任务');
- return;
- }
- console.log(`找到${activeSegments.length}个非完成状态的分镜需要取消`);
- // 4. 将它们的状态设置为已完成(2)
- for (const segment of activeSegments) {
- await bookInfoService.updateBookInfo(segment.id, {
- draw_status: 2 // 已完成状态
- });
- }
- // 5. 清除UI状态
- setGeneratingImages(false);
- setIsPaused(false);
- // 重新计算并设置进度,可以看出所有任务都已完成
- const completedCount = allSegments.filter(segment =>
- segment.draw_status === 2 && segment.image_path
- ).length;
- // 在取消状态下,只计算已完成的分镜,因为其他都已被设置为完成状态
- setImageGenerationProgress({
- current: completedCount,
- total: completedCount
- });
- // 6. 清除localStorage
- localStorage.removeItem(`project_${projectId}_image_generation`);
- // 7. 刷新数据
- await loadProjectDetail(true);
- // 只显示一个取消成功的提示
- toast.info('绘图任务已取消');
- } catch (error) {
- console.error('取消图片生成失败:', error);
- toast.error(`取消失败: ${error.message}`);
- // 即使出错也尝试清除状态
- setGeneratingImages(false);
- setIsPaused(false);
- localStorage.removeItem(`project_${projectId}_image_generation`);
- }
- };
- // 单个分镜生成图片函数,使用draw_status字段控制
- const generateSingleImage = async (segment, forceRedraw = false) => {
- if (!hasValidToken()) {
- toast.error('未配置Coze API Token,请先在首页点击"配置Coze API"按钮进行设置');
- return;
- }
- if (!selectedStyle) {
- toast.warning('请先选择项目画风');
- return;
- }
- if (!segment.description || segment.description.trim() === '') {
- toast.warning('分镜描述词为空,无法生成图片');
- return;
- }
- // 如果已有图片且不是强制重绘,则不处理
- if (segment.image_path && !forceRedraw) {
- toast.info('该分镜已有图片,如需重新生成,请使用"重绘"按钮');
- return;
- }
- // 设置分镜为绘画中状态(1)
- await bookInfoService.updateBookInfo(segment.id, {
- draw_status: 1 // 绘画中状态
- });
- // 更新UI状态
- setGeneratingImages(true);
- // 设置进度为0/1,表示开始为单个分镜生成图片
- setImageGenerationProgress({ current: 0, total: 1 });
- toast.info(`开始${forceRedraw ? '重新' : ''}生成图片,请稍候...`);
- // 初始化Coze服务
- const cozeInstance = initCozeService({
- timeout: 60000 // 设置60秒超时
- });
- console.log(`为分镜ID ${segment.id} ${forceRedraw ? '重新' : ''}生成图片...`);
- // 使用任务队列管理器添加任务,确保一次只处理一个API请求
- try {
- // 创建一个异步任务函数
- const imageGenerationTask = async () => {
- console.log(`执行分镜ID ${segment.id} 的图片生成任务`);
- // 再次检查当前分镜状态,确保没有被暂停
- const currentSegment = await getSegmentById(segment.id);
- if (currentSegment.draw_status === -1) {
- console.log('检测到当前分镜已被设置为暂停状态,跳过处理');
- return null;
- }
- // 调用Coze的生成图片工作流
- console.log(`发送API请求,生成分镜ID ${segment.id} 的图片...`);
- const response = await cozeInstance.generateImage(
- segment.description, // 描述词作为prompt
- selectedStyle, // 项目设置的画风
- {
- width: 1024,
- height: 1024,
- num_images: 1
- }
- );
- console.log(`API请求成功完成,分镜ID ${segment.id} 已获得响应`);
- return response;
- };
- // 添加任务到队列,并等待其完成
- const response = await taskQueueManager.addTask(
- imageGenerationTask,
- `分镜ID ${segment.id} 图片生成`
- );
- // 如果返回null,表示任务被跳过
- if (response === null) {
- setGeneratingImages(false);
- // 重置进度状态
- setImageGenerationProgress({ current: 0, total: 0 });
- return;
- }
- // 检查分镜当前状态,如果已设置为暂停状态,则不保存结果
- const segmentAfterApiCall = await getSegmentById(segment.id);
- if (segmentAfterApiCall.draw_status === -1) {
- console.log('API请求完成后检测到分镜已被设置为暂停状态,不保存结果');
- setGeneratingImages(false);
- // 重置进度状态
- setImageGenerationProgress({ current: 0, total: 0 });
- return;
- }
- // 从响应中提取图片URL
- let imagePath = '';
- if (response && response.data) {
- try {
- const responseData = JSON.parse(response.data);
- if (responseData.output && responseData.output) {
- imagePath = responseData.output;
- }
- } catch (e) {
- console.error('解析图片URL失败:', e);
- }
- }
- // 保存到数据库
- if (imagePath) {
- await bookInfoService.updateBookInfo(segment.id, {
- id: segment.id,
- image_path: imagePath,
- draw_status: 2 // 已完成状态
- });
- console.log(`分镜ID ${segment.id} 的图片生成成功: ${imagePath}`);
- // toast.success(`图片${forceRedraw ? '重新生成' : '生成'}成功`);
- // 更新进度为1/1,表示图片生成100%完成
- setImageGenerationProgress({ current: 1, total: 1 });
- // 实时更新图片显示
- setSegments(prevSegments =>
- prevSegments.map(seg =>
- seg.id === segment.id
- ? { ...seg, image_path: imagePath, draw_status: 2 }
- : seg
- )
- );
- } else {
- // 如果未成功获取图片,将状态设置为已完成(2)以避免卡在绘画中状态
- await bookInfoService.updateBookInfo(segment.id, {
- draw_status: 2 // 已完成状态
- });
- throw new Error('无法从响应中提取图片URL');
- }
- // 静默刷新,避免闪烁
- await loadProjectDetail(true);
- } catch (error) {
- console.error(`为分镜ID ${segment.id} 生成图片失败:`, error);
- // 将状态设置为已完成(2)以避免卡在绘画中状态
- await bookInfoService.updateBookInfo(segment.id, {
- draw_status: 2 // 已完成状态
- });
- // 尝试获取错误详情
- let errorMsg = error.message || '未知错误';
- if (error.response) {
- try {
- const errorData = error.response.data;
- errorMsg = errorData.message || errorData.error || JSON.stringify(errorData);
- } catch (e) {
- errorMsg = `API错误: ${error.response.status}`;
- }
- }
- toast.error(`图片生成失败: ${errorMsg}`);
- } finally {
- // 释放状态
- setGeneratingImages(false);
- // 延迟重置进度状态,给用户一个视觉反馈的时间
- setTimeout(() => {
- setImageGenerationProgress({ current: 0, total: 0 });
- }, 1000);
- }
- };
- // 显示图片预览
- const showImagePreview = (imageUrl) => {
- if (imageUrl) {
- setPreviewImageUrl(imageUrl);
- setShowImagePreviewModal(true);
- }
- };
- // 组件加载时初始化
- useEffect(() => {
- let isMounted = true; // 用于防止组件卸载后设置状态
- const initialize = async () => {
- try {
- // 初始化数据库并强制更新结构以确保有draw_status字段
- await initDb(true);
- if (!isMounted) return;
- // 检查是否有任何活动的绘图任务(状态为0或1),将其暂停
- const segmentsData = await bookInfoService.getBookInfoByBookId(parseInt(projectId));
- const activeSegments = segmentsData.filter(
- segment => segment.draw_status === 0 || segment.draw_status === 1
- );
- // 如果有活动的任务,将它们设置为暂停状态
- if (activeSegments.length > 0) {
- console.log(`检测到${activeSegments.length}个正在进行的绘图任务,已自动暂停`);
- // 将活动任务设置为暂停状态
- for (const segment of activeSegments) {
- await bookInfoService.updateBookInfo(segment.id, {
- draw_status: -1 // 暂停状态
- });
- }
- // 更新本地存储,标记为暂停状态
- const completedCount = segmentsData.filter(
- segment => segment.draw_status === 2 && segment.image_path
- ).length;
- const totalCount = segmentsData.filter(segment =>
- segment.description && segment.description.trim() !== '' && (
- segment.draw_status === -1 ||
- segment.draw_status === 0 ||
- segment.draw_status === 1 ||
- (segment.draw_status === 2 && segment.image_path)
- )
- ).length;
- localStorage.setItem(`project_${projectId}_image_generation`, JSON.stringify({
- progress: {
- current: completedCount,
- total: totalCount
- },
- timestamp: new Date().getTime(),
- isPaused: true
- }));
- // 显示通知,告知用户绘图任务已被自动暂停
- setTimeout(() => {
- toast.info(`检测到${activeSegments.length}个正在进行的绘图任务,已自动暂停`);
- }, 1500); // 稍微延迟显示,以便不与其他初始化消息重叠
- }
- // 清除可能存在的过期状态
- if (localStorage.getItem(`project_${projectId}_image_generation`)) {
- // 检查时间戳是否超过2小时
- const savedState = JSON.parse(localStorage.getItem(`project_${projectId}_image_generation`));
- const currentTime = new Date().getTime();
- // 如果上次保存时间超过2小时,视为过期状态
- if (currentTime - savedState.timestamp > 2 * 60 * 60 * 1000) {
- console.log('发现过期的绘图状态,重置状态');
- localStorage.removeItem(`project_${projectId}_image_generation`);
- // 重置所有非完成状态的分镜
- const segmentsData = await bookInfoService.getBookInfoByBookId(parseInt(projectId));
- const incompleteSegments = segmentsData.filter(segment => segment.draw_status !== 2);
- for (const segment of incompleteSegments) {
- await bookInfoService.updateBookInfo(segment.id, {
- draw_status: 2 // 设置为已完成状态
- });
- }
- }
- }
- // 加载项目详情和画风列表
- await loadProjectDetail();
- await loadStylesList();
- // 检查是否有正在进行的绘图任务
- await checkOngoingImageGeneration();
- } catch (error) {
- console.error('初始化失败:', error);
- if (!isMounted) return;
- toast.error(`初始化失败: ${error.message}`);
- // 尽量加载项目详情和画风列表
- try {
- await loadProjectDetail();
- await loadStylesList();
- } catch (e) {
- console.error('备用加载失败:', e);
- }
- }
- };
- initialize();
- // 清理函数
- return () => {
- console.log('组件卸载,检查是否需要清理状态');
- isMounted = false;
- // 清空任务队列
- const canceledTasks = taskQueueManager.clearQueue();
- console.log(`组件卸载时清空任务队列,取消了 ${canceledTasks} 个任务`);
- // 获取当前分镜状态
- bookInfoService.getBookInfoByBookId(parseInt(projectId))
- .then(segmentsData => {
- // 检查是否有非完成状态(不是2)的分镜且不是暂停状态(-1)
- const activeSegments = segmentsData.filter(
- segment => segment.draw_status !== 2 && segment.draw_status !== -1
- );
- if (activeSegments.length > 0) {
- console.log(`存在${activeSegments.length}个非暂停的活动绘图任务,设置为已完成状态`);
- // 将所有非暂停的活动任务设置为已完成
- activeSegments.forEach(segment => {
- bookInfoService.updateBookInfo(segment.id, {
- draw_status: 2 // 已完成状态
- }).catch(e => console.error(`更新分镜 ${segment.id} 状态失败:`, e));
- });
- // 清除localStorage
- localStorage.removeItem(`project_${projectId}_image_generation`);
- } else {
- // 检查是否有暂停状态的分镜
- const pausedSegments = segmentsData.filter(segment => segment.draw_status === -1);
- if (pausedSegments.length > 0) {
- console.log(`存在${pausedSegments.length}个暂停的绘图任务,保留暂停状态`);
- // 确保localStorage中的状态与数据库一致
- localStorage.setItem(`project_${projectId}_image_generation`, JSON.stringify({
- progress: {
- current: segmentsData.filter(segment => segment.draw_status === 2 && segment.image_path).length,
- total: pausedSegments.length + segmentsData.filter(segment => segment.draw_status === 2 && segment.image_path).length
- },
- timestamp: new Date().getTime(),
- isPaused: true
- }));
- } else {
- // 没有需要保留的状态,清除localStorage
- localStorage.removeItem(`project_${projectId}_image_generation`);
- }
- }
- })
- .catch(error => {
- console.error('组件卸载时检查分镜状态失败:', error);
- // 清除localStorage
- localStorage.removeItem(`project_${projectId}_image_generation`);
- });
- };
- }, [projectId]);
- // 处理音频文件选择
- const handleProjectAudioFileChange = (event) => {
- if (event.target.files && event.target.files.length > 0) {
- setProjectAudioFile(event.target.files[0]);
- }
- };
- // 上传项目音频
- const uploadProjectAudio = async () => {
- if (!projectAudioFile) return;
- try {
- setUploadingProjectAudio(true);
- // 读取文件
- const reader = new FileReader();
- const fileReadPromise = new Promise((resolve, reject) => {
- reader.onload = () => resolve(reader.result);
- reader.onerror = (error) => reject(error);
- reader.readAsArrayBuffer(projectAudioFile);
- });
- // 等待文件读取完成
- const fileBuffer = await fileReadPromise;
- // 通过 IPC 通信保存文件
- const result = await window.electron.ipcRenderer.invoke('save-audio-file', {
- projectId,
- fileBuffer: Array.from(new Uint8Array(fileBuffer))
- });
- if (!result.success) {
- throw new Error(result.error || '保存音频文件失败');
- }
- // 将本地文件上传到Coze获取可访问链接
- toast.info('正在将音频上传到云端...');
- const audioUrl = await cozeUploadService.uploadFileAndGetLink(projectAudioFile);
- // 更新项目数据库记录 - 同时保存本地路径和云端链接
- await bookService.updateBook(parseInt(projectId), {
- audio_path: result.filePath,
- audio_url: audioUrl // 新增字段保存云端链接
- });
- // 更新状态
- setProjectAudioPath(result.filePath);
- setShowAudioPlayer(true);
- setProjectAudioFile(null);
- setShowUploadProjectAudioModal(false);
- if (audioUrl) {
- toast.success('项目音频已成功上传并保存云端链接');
- } else {
- toast.success(`项目音频${projectAudioPath ? '更新' : '上传'}成功`);
- toast.warning('云端链接保存失败,将在生成对口型时重新上传');
- }
- } catch (error) {
- console.error('上传项目音频失败:', error);
- toast.error(`上传项目音频失败: ${error.message}`);
- } finally {
- setUploadingProjectAudio(false);
- }
- };
- /**
- * 处理对口型生成
- */
- const handleLipSyncGeneration = async () => {
- try {
- // 检查是否有音频和视频
- if (!project.audio_path || !project.video_path) {
- toast.error('请先上传音频和视频文件');
- return;
- }
- // 确认是否开始处理
- if (!window.confirm('确定要开始生成对口型视频吗?此过程可能需要几分钟时间。')) {
- return;
- }
- setLipSyncProcessing(true);
- setLipSyncProgress(0);
- let audioUrl = project.audio_url; // 尝试使用已保存的云端链接
- let videoUrl = project.video_url; // 尝试使用已保存的云端链接
- // 如果没有保存的云端链接,则进行上传
- if (!audioUrl || !videoUrl) {
- toast.info('正在上传音频和视频文件...');
- // 异步上传音频和视频
- const uploadResults = await Promise.all([
- !audioUrl ? cozeUploadService.uploadProjectMedia(project.audio_path) : Promise.resolve(audioUrl),
- !videoUrl ? cozeUploadService.uploadProjectMedia(project.video_path) : Promise.resolve(videoUrl)
- ]);
- audioUrl = uploadResults[0];
- videoUrl = uploadResults[1];
- // 如果成功获取到链接,保存到数据库中以便下次使用
- if (audioUrl && audioUrl !== project.audio_url) {
- try {
- await bookService.updateBook(project.id, { audio_url: audioUrl });
- console.log('已保存音频云端链接');
- } catch (error) {
- console.error('保存音频云端链接失败:', error);
- }
- }
- if (videoUrl && videoUrl !== project.video_url) {
- try {
- await bookService.updateBook(project.id, { video_url: videoUrl });
- console.log('已保存视频云端链接');
- } catch (error) {
- console.error('保存视频云端链接失败:', error);
- }
- }
- } else {
- toast.info('使用已保存的音频和视频链接...');
- }
- if (!audioUrl || !videoUrl) {
- throw new Error('上传音频或视频文件失败');
- }
- toast.success('文件准备完成,开始处理对口型...');
- // 开始处理对口型任务
- lipSyncService.processLipSyncTask(
- videoUrl,
- audioUrl,
- // 进度更新回调
- (progress, status) => {
- console.log(`对口型进度: ${progress}%, 状态: ${status}`);
- setLipSyncProgress(progress);
- },
- // 完成回调
- async (resultUrl, taskInfo) => {
- console.log('对口型处理完成:', resultUrl);
- setLipSyncResultUrl(resultUrl);
- setLipSyncProcessing(false);
- // 保存结果到项目
- try {
- await bookService.updateBook(project.id, {
- lip_sync_video_path: resultUrl,
- // video_path: resultUrl, // 同时更新视频路径
- lip_sync_task_id: taskInfo.task_id
- });
- toast.success('对口型视频已保存到项目');
- // 重新加载项目详情
- await loadProjectDetail(true);
- } catch (error) {
- console.error('保存对口型结果失败:', error);
- toast.error(`保存失败: ${error.message}`);
- }
- },
- // 错误回调
- (error) => {
- console.error('对口型处理失败:', error);
- toast.error(`对口型处理失败: ${error.message}`);
- setLipSyncProcessing(false);
- }
- );
- } catch (error) {
- console.error('处理对口型失败:', error);
- toast.error(`处理失败: ${error.message}`);
- setLipSyncProcessing(false);
- }
- };
- /**
- * 处理导出剪映草稿
- */
- const handleExportDraft = async () => {
- try {
- // 检查是否有音频和视频
- if (!project.audio_url || !project.lip_sync_video_path) {
- toast.error('请先完成对口型视频生成');
- return;
- }
- // 检查是否有分镜数据
- if (!segments || segments.length === 0) {
- toast.error('没有可导出的分镜数据');
- return;
- }
- // 检查所有分镜是否都有必要的字段
- const invalidSegments = segments.filter(segment =>
- !segment.start_time ||
- !segment.end_time ||
- !segment.text ||
- !segment.image_path
- );
- if (invalidSegments.length > 0) {
- toast.error(`有${invalidSegments.length}个分镜缺少必要信息,请完善后再导出`);
- return;
- }
- setExportingDraft(true);
- // 转换时间格式为微秒
- const textList = segments.map(segment => {
- const startTime = convertTimeToMicroseconds(segment.start_time);
- const endTime = convertTimeToMicroseconds(segment.end_time);
- const duration = endTime - startTime;
- return {
- start_time: startTime,
- end_time: endTime,
- duration: duration,
- text: segment.text,
- image_path: segment.image_path
- };
- });
- // 调用工作流API
- const cozeInstance = initCozeService();
- const response = await cozeInstance.runWorkflow(WORKFLOW_IDS.exportJianyingDraft, {
- audio_url: project.audio_url,
- video_url: project.lip_sync_video_path,
- text_list: textList
- });
- if (response && response.data) {
- try {
- const result = JSON.parse(response.data);
- if (result.output) {
- toast.success('剪映草稿导出成功');
- // 这里可以添加下载草稿文件的逻辑
- } else {
- throw new Error('导出结果格式不正确');
- }
- } catch (e) {
- throw new Error('解析导出结果失败');
- }
- } else {
- throw new Error('导出失败,未收到有效响应');
- }
- } catch (error) {
- console.error('导出剪映草稿失败:', error);
- toast.error(`导出失败: ${error.message}`);
- } finally {
- setExportingDraft(false);
- }
- };
- // 辅助函数:将时间格式转换为微秒
- const convertTimeToMicroseconds = (timeStr) => {
- if (!timeStr) return 0;
-
- // 处理时间格式 "HH:MM:SS,SSS"
- const [time, milliseconds] = timeStr.split(',');
- const [hours, minutes, seconds] = time.split(':').map(Number);
- const ms = milliseconds ? parseInt(milliseconds) : 0;
-
- // 转换为微秒
- return (hours * 3600 + minutes * 60 + seconds) * 1000000 + ms * 1000;
- };
- if (loading && !silentLoading) {
- return <div className="text-center p-5">加载中...</div>;
- }
- return (
- <div className="project-detail-page">
- <Container fluid>
- <div className="d-flex justify-content-between align-items-center project-header">
- <div>
- <Button
- variant="outline-secondary"
- onClick={backToProjectList}
- className="me-2"
- >
- 返回列表
- </Button>
- </div>
- <div className="d-flex">
- {selectedSegments.length > 1 && (
- <Button
- variant="primary"
- onClick={showMergeSegmentsModal}
- className="me-2"
- >
- 合并所选分镜
- </Button>
- )}
- </div>
- </div>
- {project ? (
- <div className="project-detail-container mt-4">
- {/* 项目资源和画风设置 */}
- <Card className="mb-4">
- <Card.Header>
- <h5 className="mb-0">项目资源与画风设置</h5>
- </Card.Header>
- <Card.Body>
- <Row className="align-items-center">
- {/* 项目音频 */}
- <Col md={6}>
- <Form.Group>
- <Form.Label>项目音频</Form.Label>
- {projectAudioPath && projectAudioPath.trim() !== '' ? (
- <div className="project-audio-container">
- <AudioPlayerComponent
- audioPath={projectAudioPath}
- label="音频文件"
- onPlay={() => console.log('项目音频开始播放')}
- onPause={() => console.log('项目音频暂停播放')}
- />
- </div>
- ) : (
- <Alert variant="warning" className="mb-0">
- <small>暂无项目音频</small>
- </Alert>
- )}
- </Form.Group>
- </Col>
- {/* 项目视频 */}
- <Col md={3}>
- <Form.Group>
- <Form.Label>项目视频</Form.Label>
- {project.video_path && project.video_path.trim() !== '' ? (
- <div className="project-video-container">
- <video
- className="w-100"
- controls
- src={project.video_path}
- style={{ maxHeight: '150px' }}
- >
- 您的浏览器不支持视频播放
- </video>
- </div>
- ) : (
- <Alert variant="warning" className="mb-0">
- <small>暂无项目视频</small>
- </Alert>
- )}
- </Form.Group>
- </Col>
- {/* 对口型生成区域 */}
- <Col md={3}>
- <Form.Group>
- <Form.Label>对口型视频</Form.Label>
- {lipSyncProcessing ? (
- <div className="lip-sync-progress">
- <ProgressBar
- animated
- now={lipSyncProgress}
- label={`${Math.round(lipSyncProgress)}%`}
- className="mb-2"
- />
- <div className="text-center">
- <small className="text-muted">正在生成对口型视频,请耐心等待...</small>
- </div>
- </div>
- ) : project.lip_sync_video_path ? (
- <div className="lip-sync-video-container">
- <video
- className="w-100"
- controls
- src={project.lip_sync_video_path}
- style={{ maxHeight: '150px' }}
- >
- 您的浏览器不支持视频播放
- </video>
- </div>
- ) : (
- <Alert variant="info" className="mb-0">
- <small>
- 暂无对口型视频
- {(project.audio_path && project.video_path) ?
- <div className="mt-1">
- <small>
- 已上传音频和视频文件,可以点击"生成对口型"按钮开始处理
- </small>
- </div> :
- <div className="mt-1">
- <small>
- 需要先上传音频和视频文件才能生成对口型
- </small>
- </div>
- }
- </small>
- </Alert>
- )}
- </Form.Group>
- <div className="w-100">
- <Button
- variant="primary"
- className="w-100"
- disabled={lipSyncProcessing || !project.audio_path || !project.video_path}
- onClick={handleLipSyncGeneration}
- >
- {lipSyncProcessing ? (
- <>
- <Spinner
- as="span"
- animation="border"
- size="sm"
- role="status"
- aria-hidden="true"
- className="me-1"
- />
- 处理中...
- </>
- ) : project.lip_sync_video_path ? "重新生成对口型" : "生成对口型"}
- </Button>
- </div>
- </Col>
- {/* 对口型操作按钮 */}
- {/* 项目画风选择 */}
- <Col md={4}>
- <Form.Group>
- <Form.Label>项目画风</Form.Label>
- <InputGroup>
- <Form.Select
- value={selectedStyle}
- onChange={(e) => handleStyleSelect(e.target.value)}
- disabled={loadingStyles || stylesList.length === 0}
- >
- {stylesList.length === 0 ? (
- <option>无可用画风</option>
- ) : (
- <>
- <option value="">请选择画风</option>
- {stylesList.map((style, index) => (
- <option key={index} value={style}>
- {style}
- </option>
- ))}
- </>
- )}
- </Form.Select>
- <Button
- variant="outline-secondary"
- onClick={loadStylesList}
- disabled={loadingStyles}
- >
- {loadingStyles ? '加载中...' : '刷新'}
- </Button>
- </InputGroup>
- <Form.Text className="text-muted">
- 选择的画风将应用于生成的图像中
- </Form.Text>
- </Form.Group>
- </Col>
- </Row>
- </Card.Body>
- </Card>
- <Card className="mt-4">
- <Card.Header className="d-flex justify-content-between align-items-center">
- <h5 className="mb-0">分镜列表</h5>
- <div className="d-flex align-items-center">
- <Button
- variant="success"
- size="sm"
- onClick={generateAllDescriptions}
- disabled={generatingDescriptions}
- className="me-3"
- >
- {generatingDescriptions
- ? `生成中 ${generationProgress.current}/${generationProgress.total}`
- : (hasDescriptions ? '一键更新描述词' : '一键生成描述词')}
- </Button>
- {isPaused ? (
- <div className="d-flex me-3">
- <Button
- variant="warning"
- size="sm"
- onClick={resumeImageGeneration}
- className="me-1"
- >
- 继续绘画
- </Button>
- <Button
- variant="danger"
- size="sm"
- onClick={cancelImageGeneration}
- >
- 取消绘画
- </Button>
- </div>
- ) : (
- <>
- {generatingImages ? (
- <div className="d-flex me-3">
- <Button
- variant="warning"
- size="sm"
- onClick={pauseImageGeneration}
- >
- 暂停绘画 {imageGenerationProgress.current}/{imageGenerationProgress.total}
- </Button>
- </div>
- ) : (
- <Dropdown className="me-3">
- <Dropdown.Toggle
- variant="info"
- size="sm"
- disabled={!selectedStyle}
- >
- 画图操作
- </Dropdown.Toggle>
- <Dropdown.Menu>
- <Dropdown.Item
- onClick={() => generateAllImages(false)}
- disabled={!selectedStyle}
- >
- 开始绘画
- </Dropdown.Item>
- <Dropdown.Item
- onClick={() => generateAllImages(true)}
- disabled={!selectedStyle}
- >
- 一键重绘
- </Dropdown.Item>
- </Dropdown.Menu>
- </Dropdown>
- )}
- </>
- )}
- <Button
- variant="outline-primary"
- size="sm"
- onClick={() => setSelectedSegments([])}
- className="me-2"
- >
- 清除选择
- </Button>
- <Button
- variant="outline-success"
- size="sm"
- onClick={handleExportDraft}
- disabled={exportingDraft || !project.audio_url || !project.lip_sync_video_path || !segments || segments.length === 0}
- >
- {exportingDraft ? (
- <>
- <Spinner
- as="span"
- animation="border"
- size="sm"
- role="status"
- aria-hidden="true"
- className="me-1"
- />
- 导出中...
- </>
- ) : (
- '导出剪映草稿'
- )}
- </Button>
- </div>
- </Card.Header>
- <Card.Body>
- {segments.length > 0 ? (
- <div className="segments-container">
- <Table responsive striped hover className="segments-table">
- <thead>
- <tr>
- <th width="5%">选择</th>
- <th width="5%">ID</th>
- <th width="25%">文本内容</th>
- <th width="25%">描述词</th>
- <th width="12%">图片</th>
- <th width="10%">操作</th>
- </tr>
- </thead>
- <tbody>
- {segments.map((segment) => (
- <tr key={segment.id} className={selectedSegments.includes(segment.id) ? 'selected' : ''}>
- <td>
- <Form.Check
- type="checkbox"
- checked={selectedSegments.includes(segment.id)}
- onChange={() => toggleSegmentSelection(segment.id)}
- />
- </td>
- <td>
- <Badge bg="primary">{segment.id}</Badge>
- {segment.is_merged && (
- <Badge bg="info" className="ms-1">合并</Badge>
- )}
- </td>
- <td className="segment-text-cell">
- {editingSegmentId === segment.id && editingTextField === 'text' ? (
- <div className="segment-edit-container">
- <Form.Control
- as="textarea"
- rows={3}
- value={descriptionText}
- onChange={(e) => setDescriptionText(e.target.value)}
- className="mb-2"
- />
- <div className="d-flex justify-content-end">
- <Button
- variant="outline-secondary"
- size="sm"
- onClick={cancelDirectEdit}
- className="me-2"
- >
- 取消
- </Button>
- <Button
- variant="primary"
- size="sm"
- onClick={saveDirectEdit}
- >
- 保存
- </Button>
- </div>
- </div>
- ) : (
- <div className="segment-content editable-content" onClick={() => handleDirectEditText(segment.id, segment.text)}>
- <div className="segment-times">
- <small className="text-muted">
- {formatTime(segment.start_time)} → {formatTime(segment.end_time)}
- ({segment.duration}秒)
- </small>
- </div>
- <div className="segment-text">{segment.text || <span className="text-muted">点击添加文本内容</span>}</div>
- </div>
- )}
- </td>
- <td className="segment-description-cell">
- {editingSegmentId === segment.id && editingTextField === 'description' ? (
- <div className="segment-edit-container">
- <Form.Control
- as="textarea"
- rows={3}
- value={descriptionText}
- onChange={(e) => setDescriptionText(e.target.value)}
- className="mb-2"
- />
- <div className="d-flex justify-content-end">
- <Button
- variant="outline-secondary"
- size="sm"
- onClick={cancelDirectEdit}
- className="me-2"
- >
- 取消
- </Button>
- <Button
- variant="primary"
- size="sm"
- onClick={saveDirectEdit}
- >
- 保存
- </Button>
- </div>
- </div>
- ) : (
- <div
- className={`segment-description editable-content ${segment.id === descriptionSegmentId && generatingDescriptions ? 'generating' : ''}`}
- onClick={() => handleDirectEditDescription(segment.id, segment.description)}
- >
- {segment.description || <span className="text-muted">点击添加描述词</span>}
- </div>
- )}
- </td>
- <td>
- {segment.image_path ? (
- <div className="media-cell">
- <div className="image-thumbnail mb-2">
- <img
- src={segment.image_path}
- alt="分镜图片"
- onClick={() => showImagePreview(segment.image_path)}
- style={{ width: '100%', maxWidth: '80px', height: 'auto', cursor: 'pointer' }}
- />
- </div>
- <div className="d-flex flex-wrap justify-content-center">
- <OverlayTrigger
- placement="top"
- overlay={<Tooltip>查看大图</Tooltip>}
- >
- <Button
- variant="outline-success"
- size="sm"
- onClick={() => showImagePreview(segment.image_path)}
- className="me-1 mb-1"
- >
- 查看
- </Button>
- </OverlayTrigger>
- <OverlayTrigger
- placement="top"
- overlay={<Tooltip>重新上传</Tooltip>}
- >
- <Button
- variant="outline-secondary"
- size="sm"
- onClick={() => showUploadModalFor(segment.id, 'image')}
- className="me-1 mb-1"
- >
- 更新
- </Button>
- </OverlayTrigger>
- <OverlayTrigger
- placement="top"
- overlay={<Tooltip>重新生成图片</Tooltip>}
- >
- <Button
- variant="outline-info"
- size="sm"
- onClick={() => generateSingleImage(segment, true)}
- disabled={generatingImages || !selectedStyle || !segment.description || segment.description.trim() === ''}
- className="me-1 mb-1"
- >
- 重绘
- </Button>
- </OverlayTrigger>
- </div>
- </div>
- ) : (
- <div className="media-cell">
- <Button
- variant="outline-secondary"
- size="sm"
- onClick={() => showUploadModalFor(segment.id, 'image')}
- className="mb-2"
- >
- 上传
- </Button>
- {segment.description && segment.description.trim() !== '' && (
- <Button
- variant="outline-info"
- size="sm"
- onClick={() => generateSingleImage(segment, false)}
- disabled={generatingImages || !selectedStyle}
- >
- 生成图片
- </Button>
- )}
- </div>
- )}
- </td>
- <td>
- <Dropdown>
- <Dropdown.Toggle variant="outline-secondary" size="sm" id={`dropdown-${segment.id}`}>
- 操作
- </Dropdown.Toggle>
- <Dropdown.Menu>
- <Dropdown.Item onClick={() => showSplitSegmentModal(segment)}>拆分</Dropdown.Item>
- <Dropdown.Item onClick={() => showUploadModalFor(segment.id, 'audio')}>上传音频</Dropdown.Item>
- <Dropdown.Item onClick={() => showUploadModalFor(segment.id, 'image')}>上传图片</Dropdown.Item>
- <Dropdown.Item onClick={() => showUploadModalFor(segment.id, 'video')}>上传口播视频</Dropdown.Item>
- <Dropdown.Item
- onClick={() => generateSingleDescription(segment)}
- disabled={generatingDescriptions}
- >
- {segment.description && segment.description.trim() !== '' ? '重新生成描述词' : '生成描述词'}
- </Dropdown.Item>
- <Dropdown.Item
- onClick={() => generateSingleImage(segment, segment.image_path ? true : false)}
- disabled={generatingImages || !selectedStyle || !segment.description || segment.description.trim() === ''}
- >
- {segment.image_path ? '重新生成图片' : '生成图片'}
- </Dropdown.Item>
- <Dropdown.Divider />
- <Dropdown.Item
- className="text-danger"
- onClick={() => deleteSegment(segment.id)}
- >
- 删除分镜
- </Dropdown.Item>
- </Dropdown.Menu>
- </Dropdown>
- </td>
- </tr>
- ))}
- </tbody>
- </Table>
- </div>
- ) : (
- <p className="text-center text-muted">没有分镜数据</p>
- )}
- </Card.Body>
- </Card>
- </div>
- ) : (
- <div className="text-center my-5">
- {loading ? (
- <Spinner animation="border" />
- ) : (
- <Alert variant="warning">项目不存在或已被删除</Alert>
- )}
- </div>
- )}
- </Container>
- {/* 合并分镜对话框 */}
- <Modal show={showMergeModal} onHide={() => setShowMergeModal(false)}>
- <Modal.Header closeButton>
- <Modal.Title>合并分镜</Modal.Title>
- </Modal.Header>
- <Modal.Body>
- <p>确定要合并以下分镜吗?</p>
- <ul>
- {selectedSegments.map(id => {
- const segment = segments.find(s => s.id === id);
- return segment ? (
- <li key={id}>分镜 {id}: {segment.text ? segment.text.substring(0, 30) + (segment.text.length > 30 ? '...' : '') : '无文本'}</li>
- ) : null;
- })}
- </ul>
- </Modal.Body>
- <Modal.Footer>
- <Button variant="secondary" onClick={() => setShowMergeModal(false)}>取消</Button>
- <Button variant="primary" onClick={mergeSegments} disabled={loading}>
- {loading ? '处理中...' : '确定合并'}
- </Button>
- </Modal.Footer>
- </Modal>
- {/* 拆分分镜对话框 */}
- <Modal show={showSplitModal} onHide={() => setShowSplitModal(false)} size="lg">
- <Modal.Header closeButton>
- <Modal.Title>拆分分镜</Modal.Title>
- </Modal.Header>
- <Modal.Body>
- <h6>原分镜内容:</h6>
- <p className="mb-3 p-2 bg-light">
- {splitSegment?.text}
- </p>
- <h6>拆分为:</h6>
- {newSegments.map((segment, index) => (
- <div key={index} className="mb-3 p-3 border rounded">
- <div className="d-flex justify-content-between mb-2">
- <h6>新分镜 #{index + 1}</h6>
- <Button
- variant="outline-danger"
- size="sm"
- onClick={() => removeSegmentForm(index)}
- disabled={newSegments.length <= 1}
- >
- 删除
- </Button>
- </div>
- <Form.Group className="mb-2">
- <Form.Label>文本内容</Form.Label>
- <Form.Control
- as="textarea"
- rows={3}
- value={segment.text}
- onChange={(e) => updateNewSegmentData(index, 'text', e.target.value)}
- />
- </Form.Group>
- <Row>
- <Col>
- <Form.Group>
- <Form.Label>开始时间 (HH:MM:SS,SSS)</Form.Label>
- <Form.Control
- type="text"
- value={segment.start_time}
- onChange={(e) => updateNewSegmentData(index, 'start_time', e.target.value)}
- placeholder="00:00:00,000"
- />
- </Form.Group>
- </Col>
- <Col>
- <Form.Group>
- <Form.Label>结束时间 (HH:MM:SS,SSS)</Form.Label>
- <Form.Control
- type="text"
- value={segment.end_time}
- onChange={(e) => updateNewSegmentData(index, 'end_time', e.target.value)}
- placeholder="00:00:00,000"
- />
- </Form.Group>
- </Col>
- </Row>
- </div>
- ))}
- <Button
- variant="outline-primary"
- onClick={addNewSegmentForm}
- className="mt-2"
- >
- 添加新分镜
- </Button>
- </Modal.Body>
- <Modal.Footer>
- <Button variant="secondary" onClick={() => setShowSplitModal(false)}>取消</Button>
- <Button variant="primary" onClick={splitSegmentSubmit} disabled={loading}>
- {loading ? '处理中...' : '确定拆分'}
- </Button>
- </Modal.Footer>
- </Modal>
- {/* 上传文件对话框 */}
- <Modal show={showUploadModal} onHide={() => setShowUploadModal(false)}>
- <Modal.Header closeButton>
- <Modal.Title>
- {(() => {
- const segment = segments.find(s => s.id === uploadSegmentId);
- const isUpdate = segment && (
- (uploadType === 'audio' && segment.audio_path) ||
- (uploadType === 'image' && segment.image_path) ||
- (uploadType === 'video' && segment.video_path)
- );
- if (isUpdate) {
- return uploadType === 'audio' ? '更新音频' :
- uploadType === 'image' ? '更新图片' : '更新口播视频';
- } else {
- return uploadType === 'audio' ? '上传音频' :
- uploadType === 'image' ? '上传图片' : '上传口播视频';
- }
- })()}
- </Modal.Title>
- </Modal.Header>
- <Modal.Body>
- {(() => {
- const segment = segments.find(s => s.id === uploadSegmentId);
- const existingPath = segment && (
- uploadType === 'audio' ? segment.audio_path :
- uploadType === 'image' ? segment.image_path : segment.video_path
- );
- return (
- <>
- {existingPath && (
- <div className="mb-3">
- <div className="mb-2">当前{uploadType === 'audio' ? '音频' : uploadType === 'image' ? '图片' : '视频'}:</div>
- {uploadType === 'image' && (
- <div className="text-center mb-3">
- <img
- src={existingPath}
- alt="当前图片"
- style={{ maxWidth: '100%', maxHeight: '200px' }}
- className="border rounded"
- />
- </div>
- )}
- {(uploadType === 'audio' || uploadType === 'video') && (
- <div className="text-center text-muted mb-3">
- <small>{existingPath}</small>
- </div>
- )}
- </div>
- )}
- <Form.Group className="mb-3">
- <Form.Label>
- 选择{uploadType === 'audio' ? '音频文件' :
- uploadType === 'image' ? '图片文件' : '视频文件'}
- </Form.Label>
- <Form.Control
- type="file"
- ref={fileInputRef}
- onChange={handleFileChange}
- accept={
- uploadType === 'audio' ? 'audio/*' :
- uploadType === 'image' ? 'image/*' : 'video/*'
- }
- />
- <Form.Text className="text-muted">
- {uploadType === 'audio' ? '支持的格式: MP3, WAV, OGG' :
- uploadType === 'image' ? '支持的格式: JPG, PNG, GIF' : '支持的格式: MP4, WebM, AVI'}
- </Form.Text>
- </Form.Group>
- </>
- );
- })()}
- </Modal.Body>
- <Modal.Footer>
- <Button variant="secondary" onClick={() => setShowUploadModal(false)}>取消</Button>
- <Button
- variant="primary"
- onClick={handleFileUpload}
- disabled={loading || !selectedFile}
- >
- {loading ? '上传中...' : '上传'}
- </Button>
- </Modal.Footer>
- </Modal>
- {/* 描述词对话框 */}
- <Modal show={showDescriptionModal} onHide={() => setShowDescriptionModal(false)}>
- <Modal.Header closeButton>
- <Modal.Title>编辑描述词</Modal.Title>
- </Modal.Header>
- <Modal.Body>
- <Form.Group>
- <Form.Label>描述词内容</Form.Label>
- <Form.Control
- as="textarea"
- rows={5}
- value={descriptionText}
- onChange={(e) => setDescriptionText(e.target.value)}
- placeholder="请输入描述词..."
- />
- </Form.Group>
- </Modal.Body>
- <Modal.Footer>
- <Button variant="secondary" onClick={() => setShowDescriptionModal(false)}>取消</Button>
- <Button variant="primary" onClick={() => saveDescription(descriptionSegmentId, descriptionText)} disabled={loading}>
- {loading ? '保存中...' : '保存'}
- </Button>
- </Modal.Footer>
- </Modal>
- {/* 删除项目确认对话框 */}
- <Modal show={showDeleteConfirm} onHide={() => setShowDeleteConfirm(false)}>
- <Modal.Header closeButton>
- <Modal.Title>确认删除项目</Modal.Title>
- </Modal.Header>
- <Modal.Body>
- <p>您确定要删除此项目吗?此操作将同时删除所有相关的分镜数据,且不可恢复!</p>
- {project && <p className="text-danger fw-bold">项目名称: {project.title}</p>}
- </Modal.Body>
- <Modal.Footer>
- <Button variant="secondary" onClick={() => setShowDeleteConfirm(false)}>取消</Button>
- <Button variant="danger" onClick={deleteProject} disabled={loading}>
- {loading ? '删除中...' : '确定删除'}
- </Button>
- </Modal.Footer>
- </Modal>
- {/* 图片预览对话框 */}
- <Modal show={showImagePreviewModal} onHide={() => setShowImagePreviewModal(false)}>
- <Modal.Header closeButton>
- <Modal.Title>图片预览</Modal.Title>
- </Modal.Header>
- <Modal.Body>
- <img src={previewImageUrl} alt="图片预览" style={{ width: '100%', height: 'auto' }} />
- </Modal.Body>
- <Modal.Footer>
- <Button variant="secondary" onClick={() => setShowImagePreviewModal(false)}>关闭</Button>
- </Modal.Footer>
- </Modal>
- {/* 上传项目音频对话框 */}
- <Modal show={showUploadProjectAudioModal} onHide={() => setShowUploadProjectAudioModal(false)}>
- <Modal.Header closeButton>
- <Modal.Title>
- {projectAudioPath ? '更新项目音频' : '上传项目音频'}
- </Modal.Title>
- </Modal.Header>
- <Modal.Body>
- {projectAudioPath && (
- <div className="mb-3">
- <div className="mb-2">当前项目音频:</div>
- <div className="text-center text-muted mb-3">
- <small>{projectAudioPath}</small>
- </div>
- </div>
- )}
- <div className="mb-3">
- <Form.Group>
- <Form.Label>选择音频文件</Form.Label>
- <Form.Control
- type="file"
- accept="audio/*"
- onChange={handleProjectAudioFileChange}
- />
- <Form.Text className="text-muted">
- 支持的格式: MP3, WAV, OGG等常见音频格式
- </Form.Text>
- </Form.Group>
- </div>
- </Modal.Body>
- <Modal.Footer>
- <Button variant="secondary" onClick={() => setShowUploadProjectAudioModal(false)}>取消</Button>
- <Button
- variant="primary"
- onClick={uploadProjectAudio}
- disabled={uploadingProjectAudio || !projectAudioFile}
- >
- {uploadingProjectAudio ? '上传中...' : '上传'}
- </Button>
- </Modal.Footer>
- </Modal>
- </div>
- );
- };
- export default ProjectDetail;
|