修复 action 无法禁用 问题

This commit is contained in:
北枳 2025-09-03 12:03:46 +08:00
parent b7d036157b
commit 1145ee13d3
3 changed files with 126 additions and 43 deletions

View File

@ -47,6 +47,12 @@ import GlobalLoad from "../common/GlobalLoad";
/**模板故事模式弹窗组件 */ /**模板故事模式弹窗组件 */
const RenderTemplateStoryMode = ({ const RenderTemplateStoryMode = ({
isTemplateCreating,
setIsTemplateCreating,
isRoleGenerating,
setIsRoleGenerating,
isItemGenerating,
setIsItemGenerating,
isOpen, isOpen,
onClose, onClose,
configOptions = { configOptions = {
@ -58,6 +64,12 @@ const RenderTemplateStoryMode = ({
}: { }: {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
isTemplateCreating: boolean;
setIsTemplateCreating: (value: boolean) => void;
isRoleGenerating: { [key: string]: boolean };
setIsRoleGenerating: (value: { [key: string]: boolean } | ((prev: { [key: string]: boolean }) => { [key: string]: boolean })) => void;
isItemGenerating: { [key: string]: boolean };
setIsItemGenerating: (value: { [key: string]: boolean } | ((prev: { [key: string]: boolean }) => { [key: string]: boolean })) => void;
configOptions: { configOptions: {
mode: "auto" | "manual"; mode: "auto" | "manual";
resolution: "720p" | "1080p" | "4k"; resolution: "720p" | "1080p" | "4k";
@ -125,17 +137,12 @@ const RenderTemplateStoryMode = ({
// 处理确认操作 // 处理确认操作
const handleConfirm = async () => { const handleConfirm = async () => {
if (!selectedTemplate) return; if (!selectedTemplate) return;
let timer = setInterval(() => { if (isTemplateCreating) return;
setLocalLoading((prev) => {
if (prev >= 95) { setIsTemplateCreating(true);
clearInterval(timer); let timer: NodeJS.Timeout | null = null;
return 95;
}
return prev + 0.1;
});
}, 100);
try { try {
setLocalLoading(1);
// 获取当前用户信息 // 获取当前用户信息
const User = JSON.parse(localStorage.getItem("currentUser") || "{}"); const User = JSON.parse(localStorage.getItem("currentUser") || "{}");
@ -143,12 +150,26 @@ const RenderTemplateStoryMode = ({
console.error("用户未登录"); console.error("用户未登录");
return; return;
} }
// 启动进度条动画
timer = setInterval(() => {
setLocalLoading((prev) => {
if (prev >= 95) {
return 95;
}
return prev + 0.1;
});
}, 100);
setLocalLoading(1);
const projectId = await actionStory( const projectId = await actionStory(
String(User.id), String(User.id),
configOptions.mode, configOptions.mode,
configOptions.resolution, configOptions.resolution,
configOptions.language configOptions.language
); );
if (projectId) { if (projectId) {
// 跳转到电影详情页 // 跳转到电影详情页
router.push(`/movies/work-flow?episodeId=${projectId}`); router.push(`/movies/work-flow?episodeId=${projectId}`);
@ -159,14 +180,17 @@ const RenderTemplateStoryMode = ({
console.log("Story action created:", projectId); console.log("Story action created:", projectId);
} catch (error) { } catch (error) {
console.error("Failed to create story action:", error); console.error("Failed to create story action:", error);
setIsTemplateCreating(false);
// 这里可以添加 toast 提示 // 这里可以添加 toast 提示
onClose(); onClose();
// 重置状态 // 重置状态
setSelectedTemplate(null); setSelectedTemplate(null);
} finally { } finally {
setLocalLoading(0); setLocalLoading(0);
if (timer) {
clearInterval(timer); clearInterval(timer);
} }
}
}; };
// 模板列表渲染 // 模板列表渲染
const templateListRender = () => { const templateListRender = () => {
@ -281,16 +305,21 @@ const RenderTemplateStoryMode = ({
<div className="absolute right-2 top-1/2 -translate-y-1/2"> <div className="absolute right-2 top-1/2 -translate-y-1/2">
{/* AI生成按钮 */} {/* AI生成按钮 */}
<ActionButton <ActionButton
isCreating={false} isCreating={isRoleGenerating[role.role_name] || false}
handleCreateVideo={() => { handleCreateVideo={async () => {
if ( if (
role.role_description && role.role_description &&
role.role_description.trim() role.role_description.trim()
) { ) {
handleRoleFieldBlur( setIsRoleGenerating(prev => ({...prev, [role.role_name]: true}));
try {
await handleRoleFieldBlur(
role.role_name, role.role_name,
role.role_description.trim() role.role_description.trim()
); );
} finally {
setIsRoleGenerating(prev => ({...prev, [role.role_name]: false}));
}
} }
setInputVisible((prev) => ({ setInputVisible((prev) => ({
...prev, ...prev,
@ -300,6 +329,7 @@ const RenderTemplateStoryMode = ({
icon={<Sparkles className="w-4 h-4" />} icon={<Sparkles className="w-4 h-4" />}
width="w-8" width="w-8"
height="h-8" height="h-8"
disabled={isRoleGenerating[role.role_name] || false}
/> />
</div> </div>
</div> </div>
@ -466,16 +496,21 @@ const RenderTemplateStoryMode = ({
<div className="absolute right-2 top-1/2 -translate-y-1/2"> <div className="absolute right-2 top-1/2 -translate-y-1/2">
{/* AI生成按钮 */} {/* AI生成按钮 */}
<ActionButton <ActionButton
isCreating={false} isCreating={isItemGenerating[item.item_name] || false}
handleCreateVideo={() => { handleCreateVideo={async () => {
if ( if (
item.item_description && item.item_description &&
item.item_description.trim() item.item_description.trim()
) { ) {
handleItemFieldBlur( setIsItemGenerating(prev => ({...prev, [item.item_name]: true}));
try {
await handleItemFieldBlur(
item.item_name, item.item_name,
item.item_description.trim() item.item_description.trim()
); );
} finally {
setIsItemGenerating(prev => ({...prev, [item.item_name]: false}));
}
} }
setInputVisible((prev) => ({ setInputVisible((prev) => ({
...prev, ...prev,
@ -485,6 +520,7 @@ const RenderTemplateStoryMode = ({
icon={<Sparkles className="w-4 h-4" />} icon={<Sparkles className="w-4 h-4" />}
width="w-8" width="w-8"
height="h-8" height="h-8"
disabled={isItemGenerating[item.item_name] || false}
/> />
</div> </div>
</div> </div>
@ -721,9 +757,10 @@ const RenderTemplateStoryMode = ({
</div> */} </div> */}
<div className=" absolute -bottom-8 right-0"> <div className=" absolute -bottom-8 right-0">
<ActionButton <ActionButton
isCreating={localLoading > 0} isCreating={isTemplateCreating || localLoading > 0}
handleCreateVideo={handleConfirm} handleCreateVideo={handleConfirm}
icon={<Clapperboard className="w-5 h-5" />} icon={<Clapperboard className="w-5 h-5" />}
disabled={isTemplateCreating || localLoading > 0}
/> />
</div> </div>
</div> </div>
@ -798,7 +835,12 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
const router = useRouter(); const router = useRouter();
const [loadingIdea, setLoadingIdea] = useState(false); // 获取创意建议时的加载状态 const [loadingIdea, setLoadingIdea] = useState(false); // 获取创意建议时的加载状态
const [isCreating, setIsCreating] = useState(false); // 视频创建过程中的加载状态 // 各种操作的加载状态
const [isCreating, setIsCreating] = useState(false); // 主视频创建按钮的加载状态
const [isTemplateCreating, setIsTemplateCreating] = useState(false); // 模板故事创建按钮的加载状态
const [isPhotoCreating, setIsPhotoCreating] = useState(false); // 图片故事创建按钮的加载状态
const [isRoleGenerating, setIsRoleGenerating] = useState<{[key: string]: boolean}>({}); // 角色AI生成按钮的加载状态
const [isItemGenerating, setIsItemGenerating] = useState<{[key: string]: boolean}>({}); // 道具AI生成按钮的加载状态
// 配置选项状态 - 整合所有配置项到一个对象 // 配置选项状态 - 整合所有配置项到一个对象
const [configOptions, setConfigOptions] = useState<{ const [configOptions, setConfigOptions] = useState<{
@ -848,13 +890,23 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
// Handle creating video // Handle creating video
const handleCreateVideo = async () => { const handleCreateVideo = async () => {
setIsCreating(true); if (isCreating) return; // 如果正在创建中,直接返回
if (!script) { if (!script) {
setIsCreating(false); console.warn("请输入剧本内容");
return; return;
} }
setIsCreating(true); // 设置创建状态为 true按钮会显示 loading 状态
try {
const User = JSON.parse(localStorage.getItem("currentUser") || "{}"); const User = JSON.parse(localStorage.getItem("currentUser") || "{}");
if (!User.id) {
console.error("用户未登录");
return;
}
// 创建剧集数据 // 创建剧集数据
let episodeData: any = { let episodeData: any = {
user_id: String(User.id), user_id: String(User.id),
@ -866,17 +918,18 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
}; };
// 调用创建剧集API // 调用创建剧集API
try {
const result = await MovieProjectService.createProject( const result = await MovieProjectService.createProject(
MovieProjectMode.NORMAL, MovieProjectMode.NORMAL,
episodeData episodeData
); );
const episodeId = result.project_id; const episodeId = result.project_id;
router.push(`/movies/work-flow?episodeId=${episodeId}`); router.push(`/movies/work-flow?episodeId=${episodeId}`);
} catch (error) { } catch (error) {
console.error("创建剧集失败:", error); console.error("创建剧集失败:", error);
} finally { } finally {
setIsCreating(false); setIsCreating(false); // 无论成功还是失败,都重置创建状态
} }
}; };
@ -1048,6 +1101,10 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
isOpen={isPhotoStoryModalOpen} isOpen={isPhotoStoryModalOpen}
onClose={() => setIsPhotoStoryModalOpen(false)} onClose={() => setIsPhotoStoryModalOpen(false)}
configOptions={configOptions} configOptions={configOptions}
isCreating={isCreating}
setIsCreating={setIsCreating}
isPhotoCreating={isPhotoCreating}
setIsPhotoCreating={setIsPhotoCreating}
/> />
</div> </div>
@ -1077,6 +1134,12 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
configOptions={configOptions} configOptions={configOptions}
isOpen={isTemplateModalOpen} isOpen={isTemplateModalOpen}
onClose={() => setIsTemplateModalOpen(false)} onClose={() => setIsTemplateModalOpen(false)}
isTemplateCreating={isTemplateCreating}
setIsTemplateCreating={setIsTemplateCreating}
isRoleGenerating={isRoleGenerating}
setIsRoleGenerating={setIsRoleGenerating}
isItemGenerating={isItemGenerating}
setIsItemGenerating={setIsItemGenerating}
/> />
</div> </div>
); );
@ -1196,6 +1259,10 @@ const ConfigOptions = ({
* AI分析和故事生成功能UI变化 * AI分析和故事生成功能UI变化
*/ */
const PhotoStoryModal = ({ const PhotoStoryModal = ({
isCreating,
setIsCreating,
isPhotoCreating,
setIsPhotoCreating,
isOpen, isOpen,
onClose, onClose,
configOptions = { configOptions = {
@ -1207,6 +1274,10 @@ const PhotoStoryModal = ({
}: { }: {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
isCreating: boolean;
setIsCreating: (value: boolean) => void;
isPhotoCreating: boolean;
setIsPhotoCreating: (value: boolean) => void;
configOptions?: { configOptions?: {
mode: "auto" | "manual"; mode: "auto" | "manual";
resolution: "720p" | "1080p" | "4k"; resolution: "720p" | "1080p" | "4k";
@ -1272,6 +1343,7 @@ const PhotoStoryModal = ({
// 处理确认 // 处理确认
const handleConfirm = async () => { const handleConfirm = async () => {
try { try {
setIsCreating(true);
// 获取当前用户信息 // 获取当前用户信息
const User = JSON.parse(localStorage.getItem("currentUser") || "{}"); const User = JSON.parse(localStorage.getItem("currentUser") || "{}");
@ -1294,11 +1366,15 @@ const PhotoStoryModal = ({
// 成功后关闭弹窗 // 成功后关闭弹窗
handleClose(); handleClose();
} catch (error) { } catch (error) {
setIsCreating(false);
console.error("创建电影项目失败:", error); console.error("创建电影项目失败:", error);
} }
}; };
const handleAnalyzeImage = async () => { const handleAnalyzeImage = async () => {
if (isPhotoCreating || isLoading) return;
setIsPhotoCreating(true);
let timeout = 100; let timeout = 100;
let timer: NodeJS.Timeout; let timer: NodeJS.Timeout;
timer = setInterval(() => { timer = setInterval(() => {
@ -1312,6 +1388,9 @@ const PhotoStoryModal = ({
}, timeout); }, timeout);
try { try {
await uploadAndAnalyzeImage(); await uploadAndAnalyzeImage();
} catch (error) {
console.error("分析图片失败:", error);
setIsPhotoCreating(false);
} finally { } finally {
clearInterval(timer); clearInterval(timer);
setLocalLoading(0); setLocalLoading(0);
@ -1550,9 +1629,10 @@ const PhotoStoryModal = ({
placement="top" placement="top"
> >
<ActionButton <ActionButton
isCreating={isLoading} isCreating={isLoading || isPhotoCreating}
handleCreateVideo={handleAnalyzeImage} handleCreateVideo={handleAnalyzeImage}
icon={<Sparkles className="w-5 h-5" />} icon={<Sparkles className="w-5 h-5" />}
disabled={isLoading || isPhotoCreating}
/> />
</Tooltip> </Tooltip>
) : ( ) : (
@ -1563,6 +1643,7 @@ const PhotoStoryModal = ({
isCreating={isLoading} isCreating={isLoading}
handleCreateVideo={handleConfirm} handleCreateVideo={handleConfirm}
icon={<Clapperboard className="w-5 h-5" />} icon={<Clapperboard className="w-5 h-5" />}
disabled={isCreating}
/> />
</Tooltip> </Tooltip>
</> </>

View File

@ -8,6 +8,7 @@ export function ActionButton({
width = "w-12", width = "w-12",
height = "h-12", height = "h-12",
className = "", className = "",
disabled = false,
}: { }: {
isCreating: boolean; isCreating: boolean;
handleCreateVideo: () => void; handleCreateVideo: () => void;
@ -15,19 +16,21 @@ export function ActionButton({
width?: string; width?: string;
height?: string; height?: string;
className?: string; className?: string;
disabled?: boolean;
}) { }) {
return ( return (
<div className={`relative group ${className}`}> <div className={`relative group ${className}`}>
<div <div
data-alt="action-button" data-alt="action-button"
className={`relative ${width} ${height} opacity-90 cursor-pointer overflow-hidden rounded-xl bg-black z-10`} className={`relative ${width} ${height} opacity-90 cursor-pointer overflow-hidden rounded-xl bg-black z-10`}
onClick={isCreating ? undefined : handleCreateVideo}
> >
<div className={`absolute z-10 -translate-x-12 group-hover:translate-x-12 transition-all duration-700 h-full ${width} bg-gradient-to-r from-gray-500 to-white/10 opacity-30 -skew-x-12`}></div> <div className={`absolute z-10 -translate-x-12 group-hover:translate-x-12 transition-all duration-700 h-full ${width} bg-gradient-to-r from-gray-500 to-white/10 opacity-30 -skew-x-12`}></div>
<div className="absolute flex items-center justify-center text-white z-[1] opacity-90 rounded-xl inset-0.5 bg-black"> <div className="absolute flex items-center justify-center text-white z-[1] opacity-90 rounded-xl inset-0.5 bg-black">
<button <button
name="text" name="text"
className="w-full h-full opacity-90 rounded-xl bg-black flex items-center justify-center" className="w-full h-full opacity-90 rounded-xl bg-black flex items-center justify-center disabled:cursor-not-allowed"
onClick={isCreating ? undefined : handleCreateVideo}
disabled={isCreating || disabled}
> >
{isCreating ? <Loader2 className="w-5 h-5 animate-spin" /> : icon} {isCreating ? <Loader2 className="w-5 h-5 animate-spin" /> : icon}
</button> </button>

View File

@ -6,7 +6,6 @@ import { useRouter, useSearchParams } from 'next/navigation';
import './style/create-to-video2.css'; import './style/create-to-video2.css';
import { getScriptEpisodeListNew } from "@/api/script_episode"; import { getScriptEpisodeListNew } from "@/api/script_episode";
import { EmptyStateAnimation } from '@/components/common/EmptyStateAnimation2';
import { ChatInputBox } from '@/components/ChatInputBox/ChatInputBox'; import { ChatInputBox } from '@/components/ChatInputBox/ChatInputBox';
import cover_image1 from '@/public/assets/cover_image1.jpg'; import cover_image1 from '@/public/assets/cover_image1.jpg';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';