返回 VideoClaw
StageActions.tsx
根目录 / video-claw / video-claw / frontend / components / stages / StageActions.tsx
1 'use client';
2
3 import React, { useState, useCallback } from 'react';
4 import { CheckCircle, Edit3, Save, RefreshCw, Check, Loader2, Play } from 'lucide-react';
5 import type { StageStatus } from './types';
6
7 // 需要"继续生成"的阶段(2、3、4、5)
8 const STAGES_WITH_CONTINUE = ['character_design', 'storyboard', 'reference_generation', 'video_generation'];
9 // 需要检查后续阶段的阶段(1)
10 const STAGES_WITH_NEXT_CHECK = ['script_generation'];
11
12 interface StageActionsProps {
13 status: StageStatus;
14 onConfirm: () => void;
15 onEdit?: () => void;
16 onSave?: () => void;
17 onSaveSelections?: () => Promise<void>;
18 onRegenerate?: () => void;
19 /** 阶段ID */
20 stageId?: string;
21 /** 是否有待生成的项(用于阶段2、3、4、5) */
22 hasPendingItems?: boolean;
23 /** 后续阶段是否已开始(用于阶段1) */
24 hasNextStageStarted?: boolean;
25 /** 是否显示"确认并继续"按钮(后续阶段已执行过时隐藏) */
26 showConfirm?: boolean;
27 isRunning: boolean;
28 }
29
30 export default function StageActions({
31 status,
32 onConfirm,
33 onEdit,
34 onSave,
35 onSaveSelections,
36 onRegenerate,
37 stageId = '',
38 hasPendingItems = true,
39 hasNextStageStarted = false,
40 showConfirm = true,
41 isRunning
42 }: StageActionsProps) {
43 const [saveState, setSaveState] = useState<'idle' | 'saving' | 'saved'>('idle');
44
45 // 是否是"继续生成"阶段(2、3、4、5)
46 const isContinueStage = STAGES_WITH_CONTINUE.includes(stageId);
47 // 是否是"重新生成"阶段(1)
48 const isRegenStage = STAGES_WITH_NEXT_CHECK.includes(stageId);
49
50 // 重新生成/继续生成按钮:在 != 'running' 状态下显示 (pending时也不会到这个组件)
51 const showRegen = !!onRegenerate && status !== 'running';
52
53 // 按钮是否禁用逻辑
54 let isButtonDisabled = isRunning;
55 if (!isRunning && stageId) {
56 // 除了第一阶段和第六阶段外,completed 状态下禁用继续生成
57 if (stageId !== 'script_generation' && stageId !== 'post_production' && status === 'completed') {
58 isButtonDisabled = true;
59 }
60 }
61
62 // 其余按钮在 waiting、running、completed、stopped 状态显示
63 const showActions = status === 'waiting' || status === 'running' || status === 'completed' || status === 'stopped';
64 // 保存选项:在 waiting、completed 和 stopped 状态下都显示
65 const showSaveSelections = onSaveSelections && (status === 'waiting' || status === 'completed' || status === 'stopped');
66 // "确认并继续"在 showConfirm=true 且 waiting/running/completed 时显示(stopped 状态不显示确认)
67 const showConfirmBtn = showConfirm && (status === 'waiting' || status === 'running' || status === 'completed');
68
69 const handleSaveClick = useCallback(async () => {
70 if (!onSaveSelections || saveState !== 'idle') return;
71 setSaveState('saving');
72 try {
73 await onSaveSelections();
74 setSaveState('saved');
75 setTimeout(() => setSaveState('idle'), 1500);
76 } catch {
77 setSaveState('idle');
78 }
79 }, [onSaveSelections, saveState]);
80
81 if (!showRegen && !showActions && !showSaveSelections) return null;
82
83 return (
84 <div className="border-t border-gray-200 bg-white px-4 sm:px-6 py-4 flex flex-wrap items-center justify-between gap-3 flex-shrink-0">
85 {/* 左侧:重新生成/继续生成按钮 */}
86 <div>
87 {showRegen && (
88 <button
89 onClick={onRegenerate}
90 disabled={isButtonDisabled}
91 className="flex items-center gap-2 px-4 py-2 bg-white border border-orange-300 text-orange-600 rounded-lg text-sm font-medium hover:bg-orange-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
92 title={isButtonDisabled ? (status === 'error' ? '重新尝试生成' : (isContinueStage && !hasPendingItems ? '所有项已生成完毕' : (isRegenStage && hasNextStageStarted ? '后续阶段已开始,无法重新生成' : ''))) : ''}
93 >
94 {isContinueStage ? <Play className="w-4 h-4" /> : <RefreshCw className="w-4 h-4" />}
95 {isContinueStage ? '继续生成' : '重新生成'}
96 </button>
97 )}
98 </div>
99
100 {/* 右侧:操作按钮组 */}
101 <div className="flex flex-wrap items-center justify-end gap-3">
102 {showActions && onEdit && (
103 <button
104 onClick={onEdit}
105 disabled={isRunning}
106 className="flex items-center gap-2 px-4 py-2 bg-white border border-gray-200 text-gray-700 rounded-lg text-sm font-medium hover:bg-gray-50 transition-colors disabled:opacity-50"
107 >
108 <Edit3 className="w-4 h-4" />
109 修改
110 </button>
111 )}
112
113 {showActions && onSave && (
114 <button
115 onClick={onSave}
116 disabled={isRunning}
117 className="flex items-center gap-2 px-4 py-2 bg-white border border-blue-300 text-blue-600 rounded-lg text-sm font-medium hover:bg-blue-50 transition-colors disabled:opacity-50"
118 >
119 <Save className="w-4 h-4" />
120 保存
121 </button>
122 )}
123
124 {showSaveSelections && (
125 <button
126 onClick={handleSaveClick}
127 disabled={saveState !== 'idle' || isRunning}
128 className="flex items-center gap-2 px-4 py-2 bg-white border border-blue-600 text-blue-600 rounded-lg text-sm font-medium hover:bg-blue-50 transition-colors disabled:opacity-50 min-w-[120px] justify-center"
129 >
130 {saveState === 'saving' ? (
131 <Loader2 className="w-4 h-4 animate-spin" />
132 ) : saveState === 'saved' ? (
133 <Check className="w-4 h-4 text-green-600" />
134 ) : (
135 <Save className="w-4 h-4" />
136 )}
137 {saveState === 'saving' ? '正在保存...' : saveState === 'saved' ? '保存成功' : '保存选择'}
138 </button>
139 )}
140
141 {showConfirmBtn && (
142 <button
143 onClick={onConfirm}
144 disabled={isRunning}
145 className="flex items-center gap-2 px-6 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:bg-blue-300"
146 >
147 <CheckCircle className="w-4 h-4" />
148 确认并继续
149 </button>
150 )}
151 </div>
152 </div>
153 );
154 }
155
155 lines Plain Text