Ver código fonte

修复导出剪映草稿

黎海 2 meses atrás
pai
commit
accd7df6e0
2 arquivos alterados com 301 adições e 112 exclusões
  1. 76 58
      src/pages/home/home.js
  2. 225 54
      src/pages/projectDetail/projectDetail.js

+ 76 - 58
src/pages/home/home.js

@@ -64,12 +64,12 @@ const Home = forwardRef((props, ref) => {
64 64
       setLoading(true);
65 65
       // 获取项目详情
66 66
       const segments = await bookInfoService.getBookInfoByBookId(projectId);
67
-      
67
+
68 68
       if (!segments || segments.length === 0) {
69 69
         toast.error('该项目没有字幕内容可下载');
70 70
         return;
71 71
       }
72
-      
72
+
73 73
       // 生成SRT格式内容
74 74
       const srtContent = segments
75 75
         .sort((a, b) => a.segment_id - b.segment_id)
@@ -77,24 +77,24 @@ const Home = forwardRef((props, ref) => {
77 77
           return `${segment.segment_id}\n${segment.start_time} --> ${segment.end_time}\n${segment.text}\n`;
78 78
         })
79 79
         .join('\n');
80
-      
80
+
81 81
       // 创建Blob对象
82 82
       const blob = new Blob([srtContent], { type: 'text/plain;charset=utf-8' });
83
-      
83
+
84 84
       // 创建下载链接
85 85
       const url = URL.createObjectURL(blob);
86 86
       const link = document.createElement('a');
87 87
       link.href = url;
88 88
       link.download = `${projectTitle || '项目'}.srt`;
89
-      
89
+
90 90
       // 触发下载
91 91
       document.body.appendChild(link);
92 92
       link.click();
93
-      
93
+
94 94
       // 清理
95 95
       document.body.removeChild(link);
96 96
       URL.revokeObjectURL(url);
97
-      
97
+
98 98
       toast.success('字幕下载成功');
99 99
     } catch (error) {
100 100
       console.error('下载字幕失败:', error);
@@ -110,35 +110,35 @@ const Home = forwardRef((props, ref) => {
110 110
       setLoading(true);
111 111
       // 获取项目详情
112 112
       const segments = await bookInfoService.getBookInfoByBookId(projectId);
113
-      
113
+
114 114
       if (!segments || segments.length === 0) {
115 115
         toast.error('该项目没有文本内容可下载');
116 116
         return;
117 117
       }
118
-      
118
+
119 119
       // 提取所有纯文本内容(不包含时间信息)
120 120
       const textContent = segments
121 121
         .sort((a, b) => a.segment_id - b.segment_id)
122 122
         .map(segment => segment.text)
123 123
         .join('\n\n');
124
-      
124
+
125 125
       // 创建Blob对象
126 126
       const blob = new Blob([textContent], { type: 'text/plain;charset=utf-8' });
127
-      
127
+
128 128
       // 创建下载链接
129 129
       const url = URL.createObjectURL(blob);
130 130
       const link = document.createElement('a');
131 131
       link.href = url;
132 132
       link.download = `${projectTitle || '项目'}_文案.txt`;
133
-      
133
+
134 134
       // 触发下载
135 135
       document.body.appendChild(link);
136 136
       link.click();
137
-      
137
+
138 138
       // 清理
139 139
       document.body.removeChild(link);
140 140
       URL.revokeObjectURL(url);
141
-      
141
+
142 142
       toast.success('文案下载成功');
143 143
     } catch (error) {
144 144
       console.error('下载文案失败:', error);
@@ -154,21 +154,21 @@ const Home = forwardRef((props, ref) => {
154 154
       setLoading(true);
155 155
       // 获取项目详情
156 156
       const segments = await bookInfoService.getBookInfoByBookId(projectId);
157
-      
157
+
158 158
       if (!segments || segments.length === 0) {
159 159
         toast.error('该项目没有文本内容可复制');
160 160
         return;
161 161
       }
162
-      
162
+
163 163
       // 提取所有纯文本内容(不包含时间信息)
164 164
       const textContent = segments
165 165
         .sort((a, b) => a.segment_id - b.segment_id)
166 166
         .map(segment => segment.text)
167 167
         .join('\n\n');
168
-      
168
+
169 169
       // 复制到剪贴板
170 170
       await navigator.clipboard.writeText(textContent);
171
-      
171
+
172 172
       toast.success('文案已复制到剪贴板');
173 173
     } catch (error) {
174 174
       console.error('复制文案失败:', error);
@@ -212,18 +212,18 @@ const Home = forwardRef((props, ref) => {
212 212
           toast.error('无法获取数据库路径');
213 213
           return;
214 214
         }
215
-        
215
+
216 216
         console.log('数据库路径:', dbPath);
217
-        
217
+
218 218
         // 获取数据库所在目录
219 219
         let dbDir = dbPath;
220 220
         const lastSlashIndex = Math.max(dbPath.lastIndexOf('/'), dbPath.lastIndexOf('\\'));
221 221
         if (lastSlashIndex !== -1) {
222 222
           dbDir = dbPath.substring(0, lastSlashIndex);
223 223
         }
224
-        
224
+
225 225
         console.log('数据库目录:', dbDir);
226
-        
226
+
227 227
         // 打开文件夹
228 228
         window.electron.files.showItemInFolder(dbDir);
229 229
         toast.success('已打开数据库所在文件夹');
@@ -246,9 +246,9 @@ const Home = forwardRef((props, ref) => {
246 246
           toast.error(`无法获取资源目录路径: ${result.error}`);
247 247
           return;
248 248
         }
249
-        
249
+
250 250
         console.log('资源目录路径:', result.path);
251
-        
251
+
252 252
         // 打开文件夹
253 253
         window.electron.files.showItemInFolder(result.path);
254 254
         toast.success('已打开资源文件夹');
@@ -270,7 +270,7 @@ const Home = forwardRef((props, ref) => {
270 270
   // 确认删除项目
271 271
   const confirmDeleteProject = async () => {
272 272
     if (!projectToDelete) return;
273
-    
273
+
274 274
     try {
275 275
       setLoading(true);
276 276
       await bookService.deleteBook(projectToDelete.id);
@@ -344,7 +344,7 @@ const Home = forwardRef((props, ref) => {
344 344
                   >
345 345
                     添加项目
346 346
                   </Button>
347
-                  
347
+
348 348
                   <Button
349 349
                     variant={hasValidToken() ? "outline-success" : "outline-warning"}
350 350
                     onClick={handleOpenApiSettings}
@@ -354,7 +354,7 @@ const Home = forwardRef((props, ref) => {
354 354
                   </Button>
355 355
                 </div>
356 356
               </div>
357
-              
357
+
358 358
               {!hasValidToken() && (
359 359
                 <Alert variant="warning" className="mt-3">
360 360
                   <Alert.Heading>请配置Coze API Token</Alert.Heading>
@@ -364,40 +364,41 @@ const Home = forwardRef((props, ref) => {
364 364
                   </p>
365 365
                 </Alert>
366 366
               )}
367
-              
367
+
368 368
               <div className="welcome-container">
369 369
                 {projects.length > 0 ? (
370 370
                   <div className="project-list-container">
371 371
                     <h4 className="mb-3">项目列表</h4>
372
-                    
372
+
373 373
                     <div className="mb-3 d-flex gap-2">
374
-                      <Button 
375
-                        variant="outline-secondary" 
374
+                      <Button
375
+                        variant="outline-secondary"
376 376
                         size="sm"
377 377
                         onClick={openDatabasePath}
378 378
                       >
379 379
                         <i className="bi bi-folder"></i> 打开数据库文件夹
380 380
                       </Button>
381
-                      
382
-                      <Button 
383
-                        variant="outline-secondary" 
381
+
382
+                      <Button
383
+                        variant="outline-secondary"
384 384
                         size="sm"
385 385
                         onClick={openResourcesPath}
386 386
                       >
387 387
                         <i className="bi bi-folder"></i> 打开资源文件夹
388 388
                       </Button>
389 389
                     </div>
390
-                    
390
+
391 391
                     <div className="table-responsive">
392 392
                       <Table striped hover className="project-table">
393 393
                         <thead>
394 394
                           <tr>
395
-                            <th>项目名称</th>
395
+                            <th>名称</th>
396 396
                             <th>创建时间</th>
397
-                            <th>字幕文件</th>
398
-                            <th>音频文件</th>
399
-                            <th>视频文件</th>
397
+                            <th>字幕</th>
398
+                            <th>音频</th>
399
+                            <th>视频</th>
400 400
                             <th>对口型视频</th>
401
+                            <th>剪映草稿</th>
401 402
                             <th>操作</th>
402 403
                           </tr>
403 404
                         </thead>
@@ -434,6 +435,13 @@ const Home = forwardRef((props, ref) => {
434 435
                                   <span className="text-muted">未生成</span>
435 436
                                 )}
436 437
                               </td>
438
+                              <td>
439
+                                {project.draft_url ? (
440
+                                  <Badge bg="success">已导出</Badge>
441
+                                ) : (
442
+                                  <span className="text-muted">未导出</span>
443
+                                )}
444
+                              </td>
437 445
                               <td className="action-buttons">
438 446
                                 <Button
439 447
                                   variant="outline-primary"
@@ -474,6 +482,18 @@ const Home = forwardRef((props, ref) => {
474 482
                                     播放对口型
475 483
                                   </Button>
476 484
                                 )}
485
+                                {project.draft_url && (
486
+                                  <Button
487
+                                    variant="outline-success"
488
+                                    size="sm"
489
+                                    onClick={() => {
490
+                                      navigator.clipboard.writeText(project.draft_url);
491
+                                      toast.success('剪映草稿链接已复制到剪贴板');
492
+                                    }}
493
+                                  >
494
+                                    复制草稿链接
495
+                                  </Button>
496
+                                )}
477 497
                                 <Button
478 498
                                   variant="outline-danger"
479 499
                                   size="sm"
@@ -505,12 +525,10 @@ const Home = forwardRef((props, ref) => {
505 525
                 </Button>
506 526
               </div>
507 527
 
508
-              <AudioUpload onAudioSelected={(audioFile) => setProjectAudioFile(audioFile)} />
509
-              
510 528
               <VideoUpload onVideoSelected={(videoFile) => setProjectVideoFile(videoFile)} />
511
-
512
-              <SubtitleUpload 
513
-                onProjectCreated={backToHome} 
529
+              <AudioUpload onAudioSelected={(audioFile) => setProjectAudioFile(audioFile)} />
530
+              <SubtitleUpload
531
+                onProjectCreated={backToHome}
514 532
                 projectAudioFile={projectAudioFile}
515 533
                 projectVideoFile={projectVideoFile}
516 534
               />
@@ -520,10 +538,10 @@ const Home = forwardRef((props, ref) => {
520 538
 
521 539
         </Container>
522 540
       </div>
523
-      
541
+
524 542
       {/* API设置对话框 */}
525
-      <Modal 
526
-        show={showApiSettings} 
543
+      <Modal
544
+        show={showApiSettings}
527 545
         onHide={handleCloseApiSettings}
528 546
         size="lg"
529 547
         centered
@@ -535,7 +553,7 @@ const Home = forwardRef((props, ref) => {
535 553
           <CozeApiSettings onSave={handleCloseApiSettings} />
536 554
         </Modal.Body>
537 555
       </Modal>
538
-      
556
+
539 557
       {/* 删除确认对话框 */}
540 558
       <Modal
541 559
         show={showDeleteConfirm}
@@ -554,8 +572,8 @@ const Home = forwardRef((props, ref) => {
554 572
           <Button variant="secondary" onClick={cancelDeleteProject}>
555 573
             取消
556 574
           </Button>
557
-          <Button 
558
-            variant="danger" 
575
+          <Button
576
+            variant="danger"
559 577
             onClick={confirmDeleteProject}
560 578
             disabled={loading}
561 579
           >
@@ -563,7 +581,7 @@ const Home = forwardRef((props, ref) => {
563 581
           </Button>
564 582
         </Modal.Footer>
565 583
       </Modal>
566
-      
584
+
567 585
       {/* 音频播放器模态框 */}
568 586
       <Modal
569 587
         show={showAudioPlayer}
@@ -578,9 +596,9 @@ const Home = forwardRef((props, ref) => {
578 596
             <div className="audio-player-container">
579 597
               <p>项目音频: {currentAudio.fileName}</p>
580 598
               <div className="audio-player">
581
-                <audio 
582
-                  controls 
583
-                  autoPlay 
599
+                <audio
600
+                  controls
601
+                  autoPlay
584 602
                   className="w-100"
585 603
                   src={`${currentAudio.fileName}`} // 实际开发中需要提供真实的音频文件URL
586 604
                 >
@@ -599,7 +617,7 @@ const Home = forwardRef((props, ref) => {
599 617
           </Button>
600 618
         </Modal.Footer>
601 619
       </Modal>
602
-      
620
+
603 621
       {/* 视频播放模态框 */}
604 622
       <Modal show={showVideoPlayer} onHide={closeVideoPlayer} size="lg" centered>
605 623
         <Modal.Header closeButton>
@@ -608,9 +626,9 @@ const Home = forwardRef((props, ref) => {
608 626
         <Modal.Body>
609 627
           {currentVideo && (
610 628
             <div className="video-player-container">
611
-              <video 
612
-                className="w-100" 
613
-                controls 
629
+              <video
630
+                className="w-100"
631
+                controls
614 632
                 autoPlay
615 633
                 src={currentVideo.path}
616 634
               >

+ 225 - 54
src/pages/projectDetail/projectDetail.js

@@ -72,6 +72,9 @@ const ProjectDetail = () => {
72 72
   // 添加一个标志位来防止重复调用
73 73
   const [isGenerating, setIsGenerating] = useState(false);
74 74
 
75
+  // 添加一个状态变量防止重复检查
76
+  const [isCheckingGeneration, setIsCheckingGeneration] = useState(false);
77
+
75 78
   const fileInputRef = useRef(null);
76 79
 
77 80
   // 辅助函数:根据ID获取单个分镜信息
@@ -721,7 +724,14 @@ const ProjectDetail = () => {
721 724
 
722 725
   // 检查是否有正在进行的绘图任务
723 726
   const checkOngoingImageGeneration = async () => {
727
+    // 防止重复检查
728
+    if (isCheckingGeneration || isGenerating) {
729
+      console.log('已经在检查或正在生成图片中,跳过重复检查');
730
+      return null;
731
+    }
732
+
724 733
     try {
734
+      setIsCheckingGeneration(true);
725 735
       console.log('检查是否有正在进行的绘图任务');
726 736
 
727 737
       // 从数据库获取所有分镜
@@ -796,8 +806,8 @@ const ProjectDetail = () => {
796 806
         });
797 807
 
798 808
         // 检查是否已经在生成图片
799
-        if (!generatingImages) {
800
-          // 启动绘图任务
809
+        if (!generatingImages && !isGenerating) {
810
+          // 启动绘图任务,使用setTimeout避免可能的状态更新问题
801 811
           setTimeout(() => {
802 812
             generateAllImages(false);
803 813
           }, 1000);
@@ -812,6 +822,8 @@ const ProjectDetail = () => {
812 822
       localStorage.removeItem(`project_${projectId}_image_generation`);
813 823
       setGeneratingImages(false);
814 824
       setIsPaused(false);
825
+    } finally {
826
+      setIsCheckingGeneration(false);
815 827
     }
816 828
 
817 829
     return null;
@@ -1097,9 +1109,10 @@ const ProjectDetail = () => {
1097 1109
         // 检查最终状态
1098 1110
         const finalSegments = await bookInfoService.getBookInfoByBookId(parseInt(projectId));
1099 1111
         const stillPaused = finalSegments.some(segment => segment.draw_status === -1);
1112
+        const anyPending = finalSegments.some(segment => segment.draw_status === 0 || segment.draw_status === 1);
1100 1113
 
1101
-        if (!stillPaused) {
1102
-          // 只有在非暂停状态才清除生成状态
1114
+        if (!stillPaused && !anyPending) {
1115
+          // 只有在非暂停状态并且没有待处理的分镜时才清除生成状态
1103 1116
           setGeneratingImages(false);
1104 1117
           setIsPaused(false);
1105 1118
           localStorage.removeItem(`project_${projectId}_image_generation`);
@@ -1109,11 +1122,13 @@ const ProjectDetail = () => {
1109 1122
           } else {
1110 1123
             toast.warning(`图片生成结束,成功${successCount}张,失败${errorCount}张`);
1111 1124
           }
1112
-        } else {
1125
+        } else if (stillPaused) {
1113 1126
           toast.info('图片生成已暂停,可以稍后继续');
1127
+        } else if (anyPending) {
1128
+          console.log('还有待处理的分镜,等待下一轮处理');
1114 1129
         }
1115 1130
 
1116
-        // 刷新数据
1131
+        // 刷新数据,不触发检查
1117 1132
         await loadProjectDetail(true);
1118 1133
       } else {
1119 1134
         // 继续绘画: 将没有图片的、有描述词的分镜设置为排队状态(0)
@@ -1365,9 +1380,10 @@ const ProjectDetail = () => {
1365 1380
         // 检查最终状态
1366 1381
         const finalSegments = await bookInfoService.getBookInfoByBookId(parseInt(projectId));
1367 1382
         const stillPaused = finalSegments.some(segment => segment.draw_status === -1);
1383
+        const anyPending = finalSegments.some(segment => segment.draw_status === 0 || segment.draw_status === 1);
1368 1384
 
1369
-        if (!stillPaused) {
1370
-          // 只有在非暂停状态才清除生成状态
1385
+        if (!stillPaused && !anyPending) {
1386
+          // 只有在非暂停状态并且没有待处理的分镜时才清除生成状态
1371 1387
           setGeneratingImages(false);
1372 1388
           setIsPaused(false);
1373 1389
           localStorage.removeItem(`project_${projectId}_image_generation`);
@@ -1377,11 +1393,13 @@ const ProjectDetail = () => {
1377 1393
           } else {
1378 1394
             toast.warning(`图片生成结束,成功${successCount}张,失败${errorCount}张`);
1379 1395
           }
1380
-        } else {
1396
+        } else if (stillPaused) {
1381 1397
           toast.info('图片生成已暂停,可以稍后继续');
1398
+        } else if (anyPending) {
1399
+          console.log('还有待处理的分镜,等待下一轮处理');
1382 1400
         }
1383 1401
 
1384
-        // 刷新数据
1402
+        // 刷新数据,不触发检查
1385 1403
         await loadProjectDetail(true);
1386 1404
       }
1387 1405
     } finally {
@@ -2086,68 +2104,105 @@ const ProjectDetail = () => {
2086 2104
    * 处理导出剪映草稿
2087 2105
    */
2088 2106
   const handleExportDraft = async () => {
2089
-    try {
2090
-      // 检查是否有音频和视频
2091
-      if (!project.audio_url || !project.lip_sync_video_path) {
2092
-        toast.error('请先完成对口型视频生成');
2093
-        return;
2094
-      }
2107
+    if (!hasValidToken()) {
2108
+      toast.error('未配置Coze API Token,请先在首页点击"配置Coze API"按钮进行设置');
2109
+      return;
2110
+    }
2095 2111
 
2096
-      // 检查是否有分镜数据
2097
-      if (!segments || segments.length === 0) {
2098
-        toast.error('没有可导出的分镜数据');
2099
-        return;
2100
-      }
2112
+    // 检查必要条件
2113
+    if (!project.audio_path) {
2114
+      toast.error('项目缺少音频文件,无法导出剪映草稿');
2115
+      return;
2116
+    }
2101 2117
 
2102
-      // 检查所有分镜是否都有必要的字段
2103
-      const invalidSegments = segments.filter(segment => 
2104
-        !segment.start_time || 
2105
-        !segment.end_time || 
2106
-        !segment.text || 
2107
-        !segment.image_path
2108
-      );
2118
+    setExportingDraft(true);
2119
+    try {
2120
+      // 整理分镜数据
2121
+      const orderedSegments = [...segments].sort((a, b) => a.segment_id - b.segment_id);
2109 2122
 
2110
-      if (invalidSegments.length > 0) {
2111
-        toast.error(`有${invalidSegments.length}个分镜缺少必要信息,请完善后再导出`);
2123
+      // 检查分镜是否有必要的信息
2124
+      const incompleteSegments = orderedSegments.filter(
2125
+        segment => !segment.text || !segment.start_time || !segment.end_time
2126
+      );
2127
+      
2128
+      if (incompleteSegments.length > 0) {
2129
+        toast.error(`有${incompleteSegments.length}个分镜缺少必要信息(文本、开始时间或结束时间),请完善后再导出`);
2112 2130
         return;
2113 2131
       }
2114 2132
 
2115
-      setExportingDraft(true);
2116
-
2117
-      // 转换时间格式为微秒
2118
-      const textList = segments.map(segment => {
2119
-        const startTime = convertTimeToMicroseconds(segment.start_time);
2120
-        const endTime = convertTimeToMicroseconds(segment.end_time);
2121
-        const duration = endTime - startTime;
2122
-
2133
+      // 构建文本列表 - 按照API要求格式化时间
2134
+      const textList = orderedSegments.map((segment) => {
2135
+        // 转换时间格式为微秒
2136
+        const startTimeMs = convertTimeToMicroseconds(segment.start_time);
2137
+        const endTimeMs = convertTimeToMicroseconds(segment.end_time);
2138
+        
2123 2139
         return {
2124
-          start_time: startTime,
2125
-          end_time: endTime,
2126
-          duration: duration,
2127 2140
           text: segment.text,
2128
-          image_path: segment.image_path
2141
+          start_time: startTimeMs,
2142
+          end_time: endTimeMs,
2143
+          duration: endTimeMs - startTimeMs,
2144
+          // 添加可选的图片路径
2145
+          image_path: segment.image_path || ""
2129 2146
         };
2130 2147
       });
2131 2148
 
2132
-      // 调用工作流API
2133
-      const cozeInstance = initCozeService();
2134
-      const response = await cozeInstance.runWorkflow(WORKFLOW_IDS.exportJianyingDraft, {
2135
-        audio_url: project.audio_url,
2136
-        video_url: project.lip_sync_video_path,
2137
-        text_list: textList
2149
+      const cozeInstance = initCozeService({
2150
+        timeout: 120000 // 设置120秒超时
2138 2151
       });
2139 2152
 
2153
+      // 准备所有必要参数
2154
+      const params = {
2155
+        // 必填参数
2156
+        project_id: projectId,
2157
+        text_list: textList,
2158
+        // 添加音频链接参数
2159
+        audio_url: project.audio_url || project.audio_path,
2160
+        // 添加可选参数
2161
+        video_url: project.lip_sync_video_path || project.video_path || "",
2162
+        project_title: project.title || "项目" + projectId
2163
+      };
2164
+
2165
+      console.log("导出剪映草稿参数:", JSON.stringify(params, null, 2));
2166
+
2167
+      // 调用导出API - 使用runWorkflow替代exportToDraft
2168
+      const response = await cozeInstance.runWorkflow(WORKFLOW_IDS.exportJianyingDraft, params);
2169
+
2140 2170
       if (response && response.data) {
2141 2171
         try {
2142
-          const result = JSON.parse(response.data);
2143
-          if (result.output) {
2144
-            toast.success('剪映草稿导出成功');
2145
-            // 这里可以添加下载草稿文件的逻辑
2172
+          // 解析响应数据
2173
+          const responseObj = JSON.parse(response.data);
2174
+          console.log("导出剪映草稿响应:", responseObj);
2175
+          
2176
+          // 检查状态码
2177
+          if (response.code === 0) {
2178
+            if (responseObj && responseObj.draft_url) {
2179
+              const draftUrl = responseObj.draft_url;
2180
+              
2181
+              // 保存草稿链接到数据库
2182
+              await bookService.updateBook(parseInt(projectId), {
2183
+                draft_url: draftUrl
2184
+              });
2185
+              
2186
+              // 更新本地状态
2187
+              setProject(prev => ({
2188
+                ...prev,
2189
+                draft_url: draftUrl
2190
+              }));
2191
+              
2192
+              toast.success('剪映草稿导出成功');
2193
+              
2194
+              // 刷新项目数据
2195
+              await loadProjectDetail(true);
2196
+            } else {
2197
+              throw new Error('未找到有效的草稿链接');
2198
+            }
2146 2199
           } else {
2147
-            throw new Error('导出结果格式不正确');
2200
+            // 导出失败
2201
+            throw new Error(response.msg || '导出失败');
2148 2202
           }
2149 2203
         } catch (e) {
2150
-          throw new Error('解析导出结果失败');
2204
+          console.error('解析导出结果失败:', e);
2205
+          throw new Error('解析导出结果失败: ' + e.message);
2151 2206
         }
2152 2207
       } else {
2153 2208
         throw new Error('导出失败,未收到有效响应');
@@ -2702,6 +2757,122 @@ const ProjectDetail = () => {
2702 2757
                 )}
2703 2758
               </Card.Body>
2704 2759
             </Card>
2760
+
2761
+            <Row className="mt-3">
2762
+              {/* 项目媒体文件区域 */}
2763
+              <Col md={12}>
2764
+                <Card className="mb-3">
2765
+                  <Card.Header>
2766
+                    <div className="d-flex justify-content-between align-items-center">
2767
+                      <h5 className="mb-0">项目媒体</h5>
2768
+                      <div>
2769
+                        {project.draft_url && (
2770
+                          <Button
2771
+                            variant="outline-success"
2772
+                            size="sm"
2773
+                            className="ms-2"
2774
+                            onClick={() => {
2775
+                              navigator.clipboard.writeText(project.draft_url);
2776
+                              toast.success('剪映草稿链接已复制到剪贴板');
2777
+                            }}
2778
+                          >
2779
+                            <i className="bi bi-clipboard"></i> 复制剪映草稿链接
2780
+                          </Button>
2781
+                        )}
2782
+                        <Button
2783
+                          variant="outline-primary"
2784
+                          size="sm"
2785
+                          className="ms-2"
2786
+                          onClick={handleExportDraft}
2787
+                          disabled={exportingDraft}
2788
+                        >
2789
+                          {exportingDraft ? (
2790
+                            <>
2791
+                              <Spinner
2792
+                                as="span"
2793
+                                animation="border"
2794
+                                size="sm"
2795
+                                role="status"
2796
+                                aria-hidden="true"
2797
+                              />
2798
+                              <span className="ms-2">导出中...</span>
2799
+                            </>
2800
+                          ) : (
2801
+                            '导出剪映草稿'
2802
+                          )}
2803
+                        </Button>
2804
+                      </div>
2805
+                    </div>
2806
+                  </Card.Header>
2807
+                  <Card.Body>
2808
+                    <Row>
2809
+                      {/* 项目画风 */}
2810
+                      <Col md={3}>
2811
+                        <Form.Group>
2812
+                          <Form.Label>项目画风</Form.Label>
2813
+                          <Dropdown>
2814
+                            <Dropdown.Toggle 
2815
+                              variant="outline-primary" 
2816
+                              id="dropdown-style"
2817
+                              disabled={loadingStyles}
2818
+                              className="w-100 text-start"
2819
+                            >
2820
+                              {selectedStyle || '请选择画风'}
2821
+                              {loadingStyles && (
2822
+                                <Spinner
2823
+                                  as="span"
2824
+                                  animation="border"
2825
+                                  size="sm"
2826
+                                  role="status"
2827
+                                  aria-hidden="true"
2828
+                                  className="ms-2"
2829
+                                />
2830
+                              )}
2831
+                            </Dropdown.Toggle>
2832
+
2833
+                            <Dropdown.Menu className="w-100">
2834
+                              {stylesList.map((style, index) => (
2835
+                                <Dropdown.Item
2836
+                                  key={`style-${index}`}
2837
+                                  onClick={() => handleStyleSelect(style)}
2838
+                                >
2839
+                                  {style}
2840
+                                </Dropdown.Item>
2841
+                              ))}
2842
+                            </Dropdown.Menu>
2843
+                          </Dropdown>
2844
+                        </Form.Group>
2845
+                      </Col>
2846
+
2847
+                      {/* 剪映草稿 */}
2848
+                      <Col md={3}>
2849
+                        <Form.Group>
2850
+                          <Form.Label>剪映草稿</Form.Label>
2851
+                          {project.draft_url ? (
2852
+                            <div className="border rounded p-2 bg-light">
2853
+                              <div className="text-truncate small" style={{maxWidth: '100%'}}>
2854
+                                <a 
2855
+                                  href={project.draft_url} 
2856
+                                  target="_blank" 
2857
+                                  rel="noopener noreferrer"
2858
+                                  title={project.draft_url}
2859
+                                >
2860
+                                  {project.draft_url}
2861
+                                </a>
2862
+                              </div>
2863
+                            </div>
2864
+                          ) : (
2865
+                            <Alert variant="warning" className="mb-0">
2866
+                              <small>尚未导出剪映草稿</small>
2867
+                            </Alert>
2868
+                          )}
2869
+                        </Form.Group>
2870
+                      </Col>
2871
+                    </Row>
2872
+                  </Card.Body>
2873
+                </Card>
2874
+              </Col>
2875
+            </Row>
2705 2876
           </div>
2706 2877
         ) : (
2707 2878
           <div className="text-center my-5">