forked from 77media/video-flow
776 lines
32 KiB
TypeScript
776 lines
32 KiB
TypeScript
"use client";
|
||
|
||
import { useState, useEffect } from "react";
|
||
import {
|
||
Clapperboard,
|
||
Sparkles,
|
||
LayoutTemplate,
|
||
} from "lucide-react";
|
||
import {
|
||
Modal,
|
||
Tooltip,
|
||
Upload,
|
||
Image,
|
||
Dropdown,
|
||
} from "antd";
|
||
import { UploadOutlined } from "@ant-design/icons";
|
||
import { StoryTemplateEntity } from "@/app/service/domain/Entities";
|
||
import { useTemplateStoryServiceHook } from "@/app/service/Interaction/templateStoryService";
|
||
import TemplateCard from "./templateCard";
|
||
import { useRouter } from "next/navigation";
|
||
import { useUploadFile } from "@/app/service/domain/service";
|
||
import { ActionButton } from "../common/ActionButton";
|
||
import GlobalLoad from "../common/GlobalLoad";
|
||
import { AspectRatioSelector, AspectRatioValue } from "./AspectRatioSelector";
|
||
|
||
/**
|
||
* 防抖函数
|
||
* @param {Function} func - 需要防抖的函数
|
||
* @param {number} wait - 等待时间(ms)
|
||
* @returns {Function} - 防抖后的函数
|
||
*/
|
||
const debounce = (func: Function, wait: number) => {
|
||
let timeout: NodeJS.Timeout;
|
||
return function executedFunction(...args: any[]) {
|
||
const later = () => {
|
||
clearTimeout(timeout);
|
||
func(...args);
|
||
};
|
||
clearTimeout(timeout);
|
||
timeout = setTimeout(later, wait);
|
||
};
|
||
};
|
||
|
||
interface PcTemplateModalProps {
|
||
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;
|
||
isOpen: boolean;
|
||
onClose: () => void;
|
||
configOptions: {
|
||
mode: "auto" | "manual";
|
||
resolution: "720p" | "1080p" | "4k";
|
||
language: string;
|
||
videoDuration: string;
|
||
};
|
||
}
|
||
|
||
/**
|
||
* PC端模板故事模式弹窗组件
|
||
*/
|
||
export const PcTemplateModal = ({
|
||
isTemplateCreating,
|
||
setIsTemplateCreating,
|
||
isRoleGenerating,
|
||
setIsRoleGenerating,
|
||
isItemGenerating,
|
||
setIsItemGenerating,
|
||
isOpen,
|
||
onClose,
|
||
configOptions = {
|
||
mode: "auto" as "auto" | "manual",
|
||
resolution: "720p" as "720p" | "1080p" | "4k",
|
||
language: "english",
|
||
videoDuration: "1min",
|
||
},
|
||
}: PcTemplateModalProps) => {
|
||
// 使用 hook 管理状态
|
||
const {
|
||
templateStoryList,
|
||
selectedTemplate,
|
||
isLoading,
|
||
getTemplateStoryList,
|
||
actionStory,
|
||
setSelectedTemplate,
|
||
AvatarAndAnalyzeFeatures,
|
||
updateRoleImage,
|
||
updateItemImage,
|
||
handleRoleFieldBlur,
|
||
handleItemFieldBlur,
|
||
clearData,
|
||
} = useTemplateStoryServiceHook();
|
||
|
||
// 使用上传文件hook
|
||
const { uploadFile, isUploading } = useUploadFile();
|
||
// 本地加载状态,用于 UI 反馈
|
||
const [localLoading, setLocalLoading] = useState(0);
|
||
// 控制输入框显示状态
|
||
const [inputVisible, setInputVisible] = useState<{ [key: string]: boolean }>(
|
||
{}
|
||
);
|
||
// 自由输入框布局
|
||
const [freeInputLayout, setFreeInputLayout] = useState('bottom');
|
||
const router = useRouter();
|
||
const [aspectUI, setAspectUI] = useState<AspectRatioValue>("VIDEO_ASPECT_RATIO_LANDSCAPE");
|
||
|
||
// 组件挂载时获取模板列表
|
||
useEffect(() => {
|
||
if (isOpen) {
|
||
getTemplateStoryList();
|
||
}
|
||
}, [isOpen, getTemplateStoryList]);
|
||
|
||
// 自由输入框布局
|
||
useEffect(() => {
|
||
if (selectedTemplate?.storyRole && selectedTemplate.storyRole.length > 0 ||
|
||
selectedTemplate?.storyItem && selectedTemplate.storyItem.length > 0
|
||
) {
|
||
setFreeInputLayout('bottom');
|
||
} else {
|
||
setFreeInputLayout('top');
|
||
}
|
||
}, [selectedTemplate])
|
||
|
||
// 监听点击外部区域关闭输入框
|
||
useEffect(() => {
|
||
const handleClickOutside = (event: MouseEvent) => {
|
||
const target = event.target as Element;
|
||
// 检查是否点击了输入框相关的元素
|
||
if (
|
||
!target.closest(".ant-tooltip") &&
|
||
!target.closest('[data-alt*="field-ai-button"]')
|
||
) {
|
||
// 关闭所有打开的输入框
|
||
setInputVisible({});
|
||
}
|
||
};
|
||
|
||
document.addEventListener("mousedown", handleClickOutside);
|
||
return () => {
|
||
document.removeEventListener("mousedown", handleClickOutside);
|
||
};
|
||
}, []);
|
||
|
||
// 处理模板选择
|
||
const handleTemplateSelect = (template: StoryTemplateEntity) => {
|
||
setSelectedTemplate(template);
|
||
};
|
||
|
||
// 处理确认操作
|
||
const handleConfirm = async () => {
|
||
if (!selectedTemplate) return;
|
||
if (isTemplateCreating) return;
|
||
|
||
setIsTemplateCreating(true);
|
||
let timer: NodeJS.Timeout | null = null;
|
||
|
||
try {
|
||
// 获取当前用户信息
|
||
const User = JSON.parse(localStorage.getItem("currentUser") || "{}");
|
||
|
||
if (!User.id) {
|
||
console.error("用户未登录");
|
||
return;
|
||
}
|
||
|
||
// 启动进度条动画
|
||
timer = setInterval(() => {
|
||
setLocalLoading((prev) => {
|
||
if (prev >= 95) {
|
||
return 95;
|
||
}
|
||
return prev + 0.1;
|
||
});
|
||
}, 100);
|
||
|
||
setLocalLoading(1);
|
||
|
||
const projectId = await actionStory(
|
||
String(User.id),
|
||
configOptions.mode,
|
||
configOptions.resolution,
|
||
configOptions.language,
|
||
aspectUI as AspectRatioValue
|
||
);
|
||
|
||
if (projectId) {
|
||
// 跳转到电影详情页
|
||
router.push(`/movies/work-flow?episodeId=${projectId}`);
|
||
onClose();
|
||
// 重置状态
|
||
setSelectedTemplate(null);
|
||
}
|
||
console.log("Story action created:", projectId);
|
||
} catch (error) {
|
||
console.log("Failed to create story action:", error);
|
||
window.msg.error(error instanceof Error ? error.message : "Failed to create story action");
|
||
setIsTemplateCreating(false);
|
||
setLocalLoading(0);
|
||
} finally {
|
||
setLocalLoading(0);
|
||
if (timer) {
|
||
clearInterval(timer);
|
||
}
|
||
}
|
||
};
|
||
|
||
// 模板列表渲染
|
||
const templateListRender = () => {
|
||
return (
|
||
<div className="w-1/3 p-4 border-r border-white/[0.1] overflow-y-auto">
|
||
<div className="space-y-4 overflow-y-auto template-list-scroll">
|
||
{templateStoryList.map((template, index) => (
|
||
<div
|
||
key={template.id}
|
||
data-alt={`template-card-${index}`}
|
||
className="flex justify-center"
|
||
onClick={() => handleTemplateSelect(template)}
|
||
>
|
||
<TemplateCard
|
||
imageUrl={template.image_url[0]}
|
||
imageAlt={template.name}
|
||
title={template.name}
|
||
description={template.generateText}
|
||
isSelected={selectedTemplate?.id === template.id}
|
||
width={200}
|
||
height={280}
|
||
/>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// 故事编辑器渲染
|
||
const storyEditorRender = () => {
|
||
return selectedTemplate ? (
|
||
<div className="relative h-full">
|
||
{/* 模板信息头部 - 增加顶部空间 */}
|
||
<div className="flex gap-3 py-4 border-b border-white/[0.1] h-[300px]">
|
||
{/* 左侧图片 */}
|
||
<div className="w-1/4">
|
||
<Image
|
||
src={selectedTemplate.image_url[0]}
|
||
alt={selectedTemplate.name}
|
||
className="w-4 h-5 !object-contain transition-all duration-500 group-hover:scale-105 group-hover:rotate-1"
|
||
/>
|
||
</div>
|
||
|
||
{/* 右侧信息 - 增加文本渲染空间 */}
|
||
<div className="flex-1 flex flex-col">
|
||
<h2
|
||
data-alt="template-title"
|
||
className="text-2xl font-bold text-white mb-2"
|
||
>
|
||
{selectedTemplate.name}
|
||
</h2>
|
||
<div
|
||
className="flex-1 overflow-y-auto max-h-96 "
|
||
style={{
|
||
scrollbarWidth: "thin",
|
||
scrollbarColor: "rgba(156,163,175,0.2) rgba(0,0,0,0)",
|
||
}}
|
||
>
|
||
<p
|
||
data-alt="template-description"
|
||
className="text-gray-300 text-sm leading-relaxed"
|
||
>
|
||
{selectedTemplate.generateText}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 角色配置区域 */}
|
||
{selectedTemplate?.storyRole &&
|
||
selectedTemplate.storyRole.length > 0 && (
|
||
<div className="pt-2 border-t border-white/10">
|
||
<h3
|
||
data-alt="roles-section-title"
|
||
className="text-lg font-semibold text-white mb-4"
|
||
>
|
||
Character Configuration
|
||
</h3>
|
||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||
{selectedTemplate.storyRole.map((role, index) => (
|
||
<div
|
||
key={index}
|
||
data-alt={`role-field-${index}`}
|
||
className="flex flex-col items-center space-y-3"
|
||
>
|
||
{/* 图片容器 */}
|
||
<div className="relative group">
|
||
<Tooltip
|
||
title={
|
||
<div className="relative">
|
||
<input
|
||
type="text"
|
||
value={role.role_description || ""}
|
||
onChange={(e) => {
|
||
// 更新角色的描述字段
|
||
const updatedTemplate = {
|
||
...selectedTemplate!,
|
||
storyRole: selectedTemplate!.storyRole.map(
|
||
(r) =>
|
||
r.role_name === role.role_name
|
||
? {
|
||
...r,
|
||
role_description: e.target.value,
|
||
}
|
||
: r
|
||
),
|
||
};
|
||
setSelectedTemplate(updatedTemplate);
|
||
}}
|
||
placeholder={role.user_tips}
|
||
className="w-[30rem] px-3 py-2 pr-16 bg-white/0 border border-white/10 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/30 transition-all duration-200 text-sm"
|
||
/>
|
||
<div className="absolute right-2 top-1/2 -translate-y-1/2">
|
||
{/* AI生成按钮 */}
|
||
<ActionButton
|
||
isCreating={isRoleGenerating[role.role_name] || false}
|
||
handleCreateVideo={async () => {
|
||
if (
|
||
role.role_description &&
|
||
role.role_description.trim()
|
||
) {
|
||
setIsRoleGenerating(prev => ({...prev, [role.role_name]: true}));
|
||
try {
|
||
await handleRoleFieldBlur(
|
||
role.role_name,
|
||
role.role_description.trim()
|
||
);
|
||
} finally {
|
||
setIsRoleGenerating(prev => ({...prev, [role.role_name]: false}));
|
||
}
|
||
}
|
||
setInputVisible((prev) => ({
|
||
...prev,
|
||
[role.role_name]: false,
|
||
}));
|
||
}}
|
||
icon={<Sparkles className="w-4 h-4" />}
|
||
width="w-8"
|
||
height="h-8"
|
||
disabled={isRoleGenerating[role.role_name] || false}
|
||
/>
|
||
</div>
|
||
</div>
|
||
}
|
||
placement="top"
|
||
classNames={{
|
||
root: "max-w-none",
|
||
}}
|
||
open={inputVisible[role.role_name]}
|
||
onOpenChange={(visible) =>
|
||
setInputVisible((prev) => ({
|
||
...prev,
|
||
[role.role_name]: visible,
|
||
}))
|
||
}
|
||
trigger="contextMenu"
|
||
styles={{ root: { zIndex: 1000 } }}
|
||
>
|
||
{/* 图片 */}
|
||
<div
|
||
data-alt={`role-thumbnail-${index}`}
|
||
className="w-24 h-24 rounded-xl overflow-hidden border border-white/10 bg-white/0 flex items-center justify-center cursor-pointer hover:scale-105 transition-all duration-200"
|
||
>
|
||
<Image
|
||
src={role.photo_url || "/assets/empty_video.png"}
|
||
alt={role.role_name}
|
||
className="w-full h-full object-cover"
|
||
preview={{
|
||
mask: null,
|
||
maskClassName: "hidden",
|
||
}}
|
||
fallback="/assets/empty_video.png"
|
||
/>
|
||
</div>
|
||
</Tooltip>
|
||
|
||
{/* 角色名称 - 图片下方 */}
|
||
<div className="text-center mt-2">
|
||
<span className="text-white text-sm font-medium">
|
||
{role.role_name}
|
||
</span>
|
||
</div>
|
||
|
||
{/* 按钮组 - 右上角 */}
|
||
<div className="absolute -top-8 left-[1.2rem] flex gap-3 opacity-0 group-hover:opacity-100 transition-all duration-200">
|
||
{/* AI生成按钮 */}
|
||
<Tooltip title="AI generate image" placement="top">
|
||
<button
|
||
data-alt={`role-ai-button-${index}`}
|
||
onClick={() =>
|
||
setInputVisible((prev) => ({
|
||
...prev,
|
||
[role.role_name]: !prev[role.role_name],
|
||
}))
|
||
}
|
||
className="w-6 h-6 bg-purple-500 hover:bg-purple-600 text-white rounded-full flex items-center justify-center transition-all duration-200 hover:scale-110 shadow-lg"
|
||
>
|
||
<Sparkles className="w-3.5 h-3.5" />
|
||
</button>
|
||
</Tooltip>
|
||
|
||
{/* 上传按钮 */}
|
||
<Upload
|
||
name="roleImage"
|
||
showUploadList={false}
|
||
beforeUpload={(file) => {
|
||
const isImage = file.type.startsWith("image/");
|
||
if (!isImage) {
|
||
console.error("只能上传图片文件");
|
||
return false;
|
||
}
|
||
const isLt5M = file.size / 1024 / 1024 < 5;
|
||
if (!isLt5M) {
|
||
console.error("图片大小不能超过5MB");
|
||
return false;
|
||
}
|
||
return true;
|
||
}}
|
||
customRequest={async ({
|
||
file,
|
||
onSuccess,
|
||
onError,
|
||
}) => {
|
||
try {
|
||
const fileObj = file as File;
|
||
const uploadedUrl = await uploadFile(
|
||
fileObj,
|
||
(progress) => {
|
||
console.log(`上传进度: ${progress}%`);
|
||
}
|
||
);
|
||
await AvatarAndAnalyzeFeatures(
|
||
uploadedUrl,
|
||
role.role_name
|
||
);
|
||
onSuccess?.(uploadedUrl);
|
||
} catch (error) {
|
||
console.error("角色图片上传失败:", error);
|
||
onError?.(error as Error);
|
||
}
|
||
}}
|
||
>
|
||
<Tooltip title="upload your image" placement="top">
|
||
<button
|
||
data-alt={`role-upload-button-${index}`}
|
||
className="w-6 h-6 bg-blue-500 hover:bg-blue-600 text-white rounded-full flex items-center justify-center transition-all duration-200 hover:scale-110 shadow-lg"
|
||
>
|
||
<UploadOutlined className="w-3.5 h-3.5" />
|
||
</button>
|
||
</Tooltip>
|
||
</Upload>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 道具配置区域 */}
|
||
{selectedTemplate?.storyItem &&
|
||
selectedTemplate.storyItem.length > 0 && (
|
||
<div className="pt-2 border-t border-white/10">
|
||
<h3
|
||
data-alt="items-section-title"
|
||
className="text-lg font-semibold text-white mb-4"
|
||
>
|
||
props Configuration
|
||
</h3>
|
||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||
{selectedTemplate.storyItem.map((item, index) => (
|
||
<div
|
||
key={index}
|
||
data-alt={`item-field-${index}`}
|
||
className="flex flex-col items-center space-y-3"
|
||
>
|
||
{/* 图片容器 */}
|
||
<div className="relative group">
|
||
<Tooltip
|
||
title={
|
||
<div className="relative">
|
||
<input
|
||
type="text"
|
||
value={item.item_description || ""}
|
||
onChange={(e) => {
|
||
// 更新道具的描述字段
|
||
const updatedTemplate = {
|
||
...selectedTemplate!,
|
||
storyItem: selectedTemplate!.storyItem.map(
|
||
(i) =>
|
||
i.item_name === item.item_name
|
||
? {
|
||
...i,
|
||
item_description: e.target.value,
|
||
}
|
||
: i
|
||
),
|
||
};
|
||
setSelectedTemplate(updatedTemplate);
|
||
}}
|
||
placeholder="Enter description for AI image generation..."
|
||
className="w-[30rem] px-3 py-2 pr-16 bg-white/0 border border-white/10 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/30 transition-all duration-200 text-sm"
|
||
/>
|
||
<div className="absolute right-2 top-1/2 -translate-y-1/2">
|
||
{/* AI生成按钮 */}
|
||
<ActionButton
|
||
isCreating={isItemGenerating[item.item_name] || false}
|
||
handleCreateVideo={async () => {
|
||
if (
|
||
item.item_description &&
|
||
item.item_description.trim()
|
||
) {
|
||
setIsItemGenerating(prev => ({...prev, [item.item_name]: true}));
|
||
try {
|
||
await handleItemFieldBlur(
|
||
item.item_name,
|
||
item.item_description.trim()
|
||
);
|
||
} finally {
|
||
setIsItemGenerating(prev => ({...prev, [item.item_name]: false}));
|
||
}
|
||
}
|
||
setInputVisible((prev) => ({
|
||
...prev,
|
||
[item.item_name]: false,
|
||
}));
|
||
}}
|
||
icon={<Sparkles className="w-4 h-4" />}
|
||
width="w-8"
|
||
height="h-8"
|
||
disabled={isItemGenerating[item.item_name] || false}
|
||
/>
|
||
</div>
|
||
</div>
|
||
}
|
||
placement="top"
|
||
classNames={{
|
||
root: "max-w-none",
|
||
}}
|
||
open={inputVisible[item.item_name]}
|
||
onOpenChange={(visible) =>
|
||
setInputVisible((prev) => ({
|
||
...prev,
|
||
[item.item_name]: visible,
|
||
}))
|
||
}
|
||
trigger="contextMenu"
|
||
styles={{ root: { zIndex: 1000 } }}
|
||
>
|
||
{/* 图片 */}
|
||
<div
|
||
data-alt={`item-thumbnail-${index}`}
|
||
className="w-24 h-24 rounded-xl overflow-hidden border border-white/10 bg-white/0 flex items-center justify-center cursor-pointer hover:scale-105 transition-all duration-200"
|
||
>
|
||
<Image
|
||
src={item.photo_url || "/assets/empty_video.png"}
|
||
alt={item.item_name}
|
||
className="w-full h-full object-cover"
|
||
preview={{
|
||
mask: null,
|
||
maskClassName: "hidden",
|
||
}}
|
||
fallback="/assets/empty_video.png"
|
||
/>
|
||
</div>
|
||
</Tooltip>
|
||
|
||
{/* 道具名称 - 图片下方 */}
|
||
<div className="text-center mt-2">
|
||
<span className="text-white text-sm font-medium">
|
||
{item.item_name}
|
||
</span>
|
||
</div>
|
||
|
||
{/* 按钮组 - 右上角 */}
|
||
<div className="absolute -top-8 left-[1.2rem] flex gap-3 opacity-0 group-hover:opacity-100 transition-all duration-200">
|
||
{/* AI生成按钮 */}
|
||
<Tooltip title="AI generate image" placement="top">
|
||
<button
|
||
data-alt={`item-ai-button-${index}`}
|
||
onClick={() =>
|
||
setInputVisible((prev) => ({
|
||
...prev,
|
||
[item.item_name]: !prev[item.item_name],
|
||
}))
|
||
}
|
||
className="w-6 h-6 bg-purple-500 hover:bg-purple-600 text-white rounded-full flex items-center justify-center transition-all duration-200 hover:scale-110 shadow-lg"
|
||
>
|
||
<Sparkles className="w-3.5 h-3.5" />
|
||
</button>
|
||
</Tooltip>
|
||
|
||
{/* 上传按钮 */}
|
||
<Upload
|
||
name="itemImage"
|
||
showUploadList={false}
|
||
beforeUpload={(file) => {
|
||
const isImage = file.type.startsWith("image/");
|
||
if (!isImage) {
|
||
console.error("只能上传图片文件");
|
||
return false;
|
||
}
|
||
const isLt5M = file.size / 1024 / 1024 < 5;
|
||
if (!isLt5M) {
|
||
console.error("图片大小不能超过5MB");
|
||
return false;
|
||
}
|
||
return true;
|
||
}}
|
||
customRequest={async ({
|
||
file,
|
||
onSuccess,
|
||
onError,
|
||
}) => {
|
||
try {
|
||
const fileObj = file as File;
|
||
const uploadedUrl = await uploadFile(
|
||
fileObj,
|
||
(progress) => {
|
||
console.log(`上传进度: ${progress}%`);
|
||
}
|
||
);
|
||
updateItemImage(item.item_name, uploadedUrl);
|
||
onSuccess?.(uploadedUrl);
|
||
} catch (error) {
|
||
console.error("道具图片上传失败:", error);
|
||
onError?.(error as Error);
|
||
}
|
||
}}
|
||
>
|
||
<Tooltip title="upload your image" placement="top">
|
||
<button
|
||
data-alt={`item-upload-button-${index}`}
|
||
className="w-6 h-6 bg-blue-500 hover:bg-blue-600 text-white rounded-full flex items-center justify-center transition-all duration-200 hover:scale-110 shadow-lg"
|
||
>
|
||
<UploadOutlined className="w-3.5 h-3.5" />
|
||
</button>
|
||
</Tooltip>
|
||
</Upload>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/** 自由输入文字 */}
|
||
{freeInputLayout === 'top' && selectedTemplate?.freeInput && selectedTemplate.freeInput.length > 0 && (
|
||
<div className="py-2 flex-1 flex flex-col" style={{
|
||
height: 'calc(70vh - 300px - 8rem)'
|
||
}}>
|
||
<h3
|
||
data-alt="items-section-title"
|
||
className="text-lg font-semibold text-white mb-4"
|
||
>
|
||
input Configuration
|
||
</h3>
|
||
<textarea
|
||
value={selectedTemplate?.freeInput[0].free_input_text || ""}
|
||
placeholder={selectedTemplate?.freeInput[0].user_tips || ""}
|
||
className="w-full flex-1 px-3 py-2 pr-16 bg-white/0 border border-white/10 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/30 transition-all duration-200 text-sm"
|
||
onChange={(e) => {
|
||
// 更新自由输入文字字段
|
||
const updatedTemplate = {
|
||
...selectedTemplate!,
|
||
freeInput: selectedTemplate!.freeInput.map((item) => ({
|
||
...item,
|
||
free_input_text: e.target.value
|
||
})),
|
||
};
|
||
setSelectedTemplate(updatedTemplate as StoryTemplateEntity);
|
||
}}
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
<div className=" absolute -bottom-8 right-0 w-full flex items-center justify-end gap-2">
|
||
{/** 自由输入文字 */}
|
||
{freeInputLayout === 'bottom' && selectedTemplate?.freeInput && selectedTemplate.freeInput.length > 0 && (
|
||
<div className="py-2 flex-1">
|
||
<input
|
||
type="text"
|
||
value={selectedTemplate?.freeInput[0].free_input_text || ""}
|
||
placeholder={selectedTemplate?.freeInput[0].user_tips}
|
||
className="w-full px-3 py-2 pr-16 bg-white/0 border border-white/10 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/30 transition-all duration-200 text-sm"
|
||
onChange={(e) => {
|
||
// 更新自由输入文字字段
|
||
const updatedTemplate = {
|
||
...selectedTemplate!,
|
||
freeInput: selectedTemplate!.freeInput.map((item) => ({
|
||
...item,
|
||
free_input_text: e.target.value
|
||
})),
|
||
}
|
||
setSelectedTemplate(updatedTemplate as StoryTemplateEntity);
|
||
}}
|
||
/>
|
||
</div>
|
||
)}
|
||
{/* 横/竖屏选择 上线暂时不开放 */}
|
||
{/* <AspectRatioSelector
|
||
value={aspectUI}
|
||
onChange={setAspectUI}
|
||
placement="top"
|
||
/> */}
|
||
<ActionButton
|
||
isCreating={isTemplateCreating || localLoading > 0}
|
||
handleCreateVideo={handleConfirm}
|
||
icon={<Clapperboard className="w-5 h-5" />}
|
||
disabled={isTemplateCreating || localLoading > 0}
|
||
/>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="flex items-center justify-center h-64">
|
||
<div className="text-center text-white/60">
|
||
<LayoutTemplate className="w-16 h-16 mx-auto mb-4 opacity-50" />
|
||
<p className="text-lg">No templates available</p>
|
||
<p className="text-sm">Please try again later</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
return (
|
||
<>
|
||
<Modal
|
||
open={isOpen}
|
||
onCancel={() => {
|
||
// 清空所有选中的内容数据
|
||
clearData();
|
||
onClose();
|
||
}}
|
||
footer={null}
|
||
width="60%"
|
||
closable={false}
|
||
maskClosable={false}
|
||
style={{ maxWidth: "800px", marginTop: "0vh" }}
|
||
className="photo-story-modal !pb-0 rounded-lg bg-white/[0.08] backdrop-blur-[20px] [&_.ant-modal-content]:bg-white/[0.00]"
|
||
>
|
||
<GlobalLoad show={isLoading} progress={localLoading}>
|
||
<div className="rounded-2xl h-[70vh] overflow-y-hidden flex flex-col">
|
||
{/* 弹窗头部 */}
|
||
<div className="flex items-center justify-between px-4 pb-6 border-b border-white/[0.1]">
|
||
<h2 className="text-2xl font-bold text-white">
|
||
Template Story Selection
|
||
</h2>
|
||
<button
|
||
onClick={onClose}
|
||
className="w-6 h-6 bg-white/10 rounded-full flex items-center justify-center hover:bg-white/20 transition-colors"
|
||
>
|
||
<span className="text-white/70 text-lg leading-none">×</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div className="flex gap-4 pb-8 flex-1 overflow-y-hidden">
|
||
{templateListRender()}
|
||
<div className="flex-1">{storyEditorRender()}</div>
|
||
</div>
|
||
</div>
|
||
</GlobalLoad>
|
||
</Modal>
|
||
</>
|
||
);
|
||
};
|