From 84caabe69a64e4eff357d2272824b0ef13f08148 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=8C=97=E6=9E=B3?=
<7854742+wang_rumeng@user.noreply.gitee.com>
Date: Mon, 15 Sep 2025 11:21:43 +0800
Subject: [PATCH] =?UTF-8?q?=E5=88=97=E8=A1=A8=E9=A1=B5=E9=80=82=E9=85=8DH5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/service/Interaction/ImageStoryService.ts | 1 +
.../Interaction/templateStoryService.ts | 1 +
components/ChatInputBox/ChatInputBox.tsx | 1539 ++---------------
.../ChatInputBox/H5PhotoStoryDrawer.tsx | 396 +++++
components/ChatInputBox/H5TemplateDrawer.tsx | 603 +++++++
components/ChatInputBox/PcPhotoStoryModal.tsx | 349 ++++
components/ChatInputBox/PcTemplateModal.tsx | 740 ++++++++
components/common/GlobalLoad.tsx | 2 +-
components/common/HighlightEditor.tsx | 5 +-
components/layout/dashboard-layout.tsx | 9 +-
components/pages/style/login.css | 55 +-
11 files changed, 2221 insertions(+), 1479 deletions(-)
create mode 100644 components/ChatInputBox/H5PhotoStoryDrawer.tsx
create mode 100644 components/ChatInputBox/H5TemplateDrawer.tsx
create mode 100644 components/ChatInputBox/PcPhotoStoryModal.tsx
create mode 100644 components/ChatInputBox/PcTemplateModal.tsx
diff --git a/app/service/Interaction/ImageStoryService.ts b/app/service/Interaction/ImageStoryService.ts
index 9ee1c4d..7ec2a46 100644
--- a/app/service/Interaction/ImageStoryService.ts
+++ b/app/service/Interaction/ImageStoryService.ts
@@ -492,6 +492,7 @@ export const useImageStoryServiceHook = (): UseImageStoryService => {
}
} catch (error) {
console.error("创建电影项目失败:", error);
+ throw error;
}
},
[
diff --git a/app/service/Interaction/templateStoryService.ts b/app/service/Interaction/templateStoryService.ts
index 7d58f7d..8fa1dc4 100644
--- a/app/service/Interaction/templateStoryService.ts
+++ b/app/service/Interaction/templateStoryService.ts
@@ -276,6 +276,7 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
return result.project_id;
} catch (error) {
console.error("创建电影项目失败:", error);
+ throw error;
} finally {
// 清除 loading 状态
setIsLoading(false);
diff --git a/components/ChatInputBox/ChatInputBox.tsx b/components/ChatInputBox/ChatInputBox.tsx
index d236085..4a48075 100644
--- a/components/ChatInputBox/ChatInputBox.tsx
+++ b/components/ChatInputBox/ChatInputBox.tsx
@@ -44,6 +44,23 @@ import { useLoadScriptText, useUploadFile } from "@/app/service/domain/service";
import { ActionButton } from "../common/ActionButton";
import { HighlightEditor } from "../common/HighlightEditor";
import GlobalLoad from "../common/GlobalLoad";
+import { useDeviceType } from '@/hooks/useDeviceType';
+import { PcTemplateModal } from "./PcTemplateModal";
+import { H5TemplateDrawer } from "./H5TemplateDrawer";
+import { PcPhotoStoryModal } from "./PcPhotoStoryModal";
+import { H5PhotoStoryDrawer } from "./H5PhotoStoryDrawer";
+
+const LauguageOptions = [
+ { value: "english", label: "English", isVip: false, code:'en' },
+ { value: "chinese", label: "Chinese", isVip: false, code:'zh' },
+ { value: "japanese", label: "Japanese", isVip: false, code:'ja' },
+ { value: "spanish", label: "Spanish", isVip: false, code:'es' },
+ { value: "portuguese", label: "Portuguese", isVip: false, code:'pt' },
+ { value: "hindi", label: "Hindi", isVip: false, code:'hi' },
+ { value: "korean", label: "Korean", isVip: false, code:'ko' },
+ { value: "arabic", label: "Arabic", isVip: false, code:'ar' },
+ { value: "russian", label: "Russian", isVip: false, code:'ru' },
+]
/**模板故事模式弹窗组件 */
/**
@@ -64,850 +81,12 @@ const debounce = (func: Function, wait: number) => {
};
};
-const RenderTemplateStoryMode = ({
- isTemplateCreating,
- setIsTemplateCreating,
- isRoleGenerating,
- setIsRoleGenerating,
- isItemGenerating,
- setIsItemGenerating,
- isOpen,
- onClose,
- configOptions = {
- mode: "auto" as "auto" | "manual",
- resolution: "720p" as "720p" | "1080p" | "4k",
- language: "english",
- videoDuration: "1min",
- },
-}: {
- isOpen: boolean;
- 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: {
- mode: "auto" | "manual";
- resolution: "720p" | "1080p" | "4k";
- language: string;
- videoDuration: string;
- };
-}) => {
- // 使用 hook 管理状态
- const {
- templateStoryList,
- selectedTemplate,
- isLoading,
- getTemplateStoryList,
- actionStory,
- setSelectedTemplate,
- AvatarAndAnalyzeFeatures,
- updateRoleImage,
- updateItemImage,
- handleRoleFieldBlur,
- handleItemFieldBlur,
- clearData,
- } = useTemplateStoryServiceHook();
-
- // 防抖处理的输入更新函数
- const debouncedUpdateInput = debounce((value: string) => {
- // 过滤特殊字符
- const sanitizedValue = value.replace(/[<>]/g, '');
- // 更新输入值
- if (!selectedTemplate?.freeInputItem) return;
- const updatedTemplate: StoryTemplateEntity = {
- ...selectedTemplate,
- freeInputItem: {
- ...selectedTemplate.freeInputItem,
- free_input_text: sanitizedValue
- }
- };
- setSelectedTemplate(updatedTemplate);
- }, 300); // 300ms 的防抖延迟
-
- // 使用上传文件hook
- const { uploadFile, isUploading } = useUploadFile();
- // 本地加载状态,用于 UI 反馈
- const [localLoading, setLocalLoading] = useState(0);
- // 控制输入框显示状态
- const [inputVisible, setInputVisible] = useState<{ [key: string]: boolean }>(
- {}
- );
- const router = useRouter();
- // 组件挂载时获取模板列表
- useEffect(() => {
- if (isOpen) {
- getTemplateStoryList();
- }
- }, [isOpen, getTemplateStoryList]);
-
- // 监听点击外部区域关闭输入框
- 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
- );
-
- if (projectId) {
- // 跳转到电影详情页
- router.push(`/movies/work-flow?episodeId=${projectId}`);
- onClose();
- // 重置状态
- setSelectedTemplate(null);
- }
- console.log("Story action created:", projectId);
- } catch (error) {
- console.error("Failed to create story action:", error);
- setIsTemplateCreating(false);
- // 这里可以添加 toast 提示
- onClose();
- // 重置状态
- setSelectedTemplate(null);
- } finally {
- setLocalLoading(0);
- if (timer) {
- clearInterval(timer);
- }
- }
- };
- // 模板列表渲染
- const templateListRender = () => {
- return (
-
-
- {templateStoryList.map((template, index) => (
-
handleTemplateSelect(template)}
- >
-
-
- ))}
-
-
- );
- };
- // 故事编辑器渲染
- const storyEditorRender = () => {
- return selectedTemplate ? (
-
- {/* 模板信息头部 - 增加顶部空间 */}
-
- {/* 左侧图片 */}
-
-
-
-
- {/* 右侧信息 - 增加文本渲染空间 */}
-
-
- {selectedTemplate.name}
-
-
-
- {selectedTemplate.generateText}
-
-
-
-
-
- {/* 角色配置区域 */}
- {selectedTemplate?.storyRole &&
- selectedTemplate.storyRole.length > 0 && (
-
-
- Character Configuration
-
-
- {selectedTemplate.storyRole.map((role, index) => (
-
- {/* 图片容器 */}
-
- }
- 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 } }}
- >
- {/* 图片 */}
-
-
-
-
-
- {/* 角色名称 - 图片下方 */}
-
-
- {role.role_name}
-
-
-
- {/* 按钮组 - 右上角 */}
-
- {/* AI生成按钮 */}
-
-
-
-
- {/* 上传按钮 */}
- {
- 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);
- }
- }}
- >
-
-
-
-
-
-
-
- ))}
-
-
- )}
-
- {/* 道具配置区域 */}
- {selectedTemplate?.storyItem &&
- selectedTemplate.storyItem.length > 0 && (
-
-
- props Configuration
-
-
- {selectedTemplate.storyItem.map((item, index) => (
-
- {/* 图片容器 */}
-
- }
- 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 } }}
- >
- {/* 图片 */}
-
-
-
-
-
- {/* 道具名称 - 图片下方 */}
-
-
- {item.item_name}
-
-
-
- {/* 按钮组 - 右上角 */}
-
- {/* AI生成按钮 */}
-
-
-
-
- {/* 上传按钮 */}
- {
- 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);
- }
- }}
- >
-
-
-
-
-
-
-
- ))}
-
-
- )}
-
- {/* * 自由输入文字
- {(selectedTemplate?.freeInputItem) && (
-
-
- input Configuration
-
-
- )} */}
-
- {/* 角色自定义部分 - 精简布局 */}
- {/*
-
- Character Customization
-
-
- {/* 紧凑布局 */}
- {/*
- {/* 左侧:音频部分 */}
- {/*
- {/* 音频操作区域 - 使用新的 AudioRecorder 组件 */}
- {/*
-
{
- setActiveRoleAudio(audioUrl);
- }}
- onAudioDeleted={() => {
- setActiveRoleAudio("");
- }}
- />
-
-
-
- {/* 右侧:角色图片缩略图列表 - 精简 */}
- {/*
- {selectedTemplate.storyRole.map((role, index: number) => (
-
-
-
-
-
- {/* 上传按钮 - 右上角 */}
- {/*
- {
- // 验证文件类型
- const isImage = file.type.startsWith("image/");
- if (!isImage) {
- console.error("只能上传图片文件");
- return false;
- }
-
- // 验证文件大小 (10MB)
- const isLt10M = file.size / 1024 / 1024 < 10;
- if (!isLt10M) {
- console.error("图片大小不能超过10MB");
- return false;
- }
-
- return true;
- }}
- customRequest={async ({ file, onSuccess, onError }) => {
- try {
- const fileObj = file as File;
- console.log(
- "开始上传图片文件:",
- fileObj.name,
- fileObj.type,
- fileObj.size
- );
-
- // 使用 hook 上传文件到七牛云
- const uploadedUrl = await uploadFile(
- fileObj,
- (progress) => {
- console.log(`上传进度: ${progress}%`);
- }
- );
- console.log("图片上传成功,URL:", uploadedUrl);
-
- // 上传成功后,更新角色图片
- await AvatarAndAnalyzeFeatures(uploadedUrl);
- onSuccess?.(uploadedUrl);
- } catch (error) {
- console.error("图片上传失败:", error);
- onError?.(error as Error);
- }
- }}
- >
-
-
-
-
- ))}
-
-
-
*/}
-
- {/** 自由输入文字 */}
- {(selectedTemplate?.freeInputItem) && (
-
- {
- // 更新自由输入文字字段
- const updatedTemplate = {
- ...selectedTemplate!,
- freeInputItem: {
- ...selectedTemplate!.freeInputItem,
- free_input_text: e.target.value
- }
- };
- setSelectedTemplate(updatedTemplate as StoryTemplateEntity);
- }}
- />
-
- )}
-
0}
- handleCreateVideo={handleConfirm}
- icon={}
- disabled={isTemplateCreating || localLoading > 0}
- />
-
-
- ) : (
-
-
-
-
No templates available
-
Please try again later
-
-
- );
- };
- return (
- <>
- {
- // 清空所有选中的内容数据
- 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]"
- >
-
-
- {/* 弹窗头部 */}
-
-
- Template Story Selection
-
-
-
-
-
- {templateListRender()}
-
{storyEditorRender()}
-
-
-
-
- >
- );
-};
-
/**
* 视频工具面板组件
* 提供脚本输入和视频克隆两种模式,支持展开/收起功能
*/
export function ChatInputBox({ noData }: { noData: boolean }) {
+ const { isMobile, isDesktop } = useDeviceType();
// 控制面板展开/收起状态
const [isExpanded, setIsExpanded] = useState(false);
@@ -960,22 +139,13 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
}
}, []);
- // 配置项显示控制状态
- const [showConfigOptions, setShowConfigOptions] = useState(false);
-
- const handleGetIdea = () => {
- if (loadingIdea) return;
- setLoadingIdea(true);
- const ideaText =
- "a cute capybara with an orange on its head, staring into the distance and walking forward";
- setTimeout(() => {
- setScript(ideaText);
- setLoadingIdea(false);
- }, 3000);
+ const onConfigChange = (key: string, value: string) => {
+ setConfigOptions((prev) => ({
+ ...prev,
+ [key]: value,
+ }));
};
- // Handle creating video
-
const handleCreateVideo = async () => {
if (isCreating) return; // 如果正在创建中,直接返回
@@ -1043,7 +213,7 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
onClick={() => setIsExpanded(false)}
>
-
+
Click to action
@@ -1063,51 +233,13 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
- {/* 右上角齿轮图标和配置项 */}
- {!isExpanded && (
-
- {/* 使用 Dropdown 替代手动控制显示/隐藏 */}
-
(
-
- {
- setConfigOptions((prev) => {
- const newConfig = { ...prev, [key]: value };
- // 保存到 localStorage
- if (typeof window !== 'undefined') {
- localStorage.setItem('videoFlowConfig', JSON.stringify(newConfig));
- }
- return newConfig;
- });
- }}
- />
-
- )}
- placement={"left" as any}
- trigger={["click"]}
- >
-
-
-
-
-
- )}
{/* 输入框和Action按钮 - 只在展开状态显示 */}
{!isExpanded && (
-
+
{/* 第一行:输入框 */}
{/* 文本输入框 - 改为textarea */}
@@ -1159,10 +291,10 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
{/*
*/}
{/* 模板故事按钮 */}
-
+
{/* 右侧Action按钮 */}
}
+ icon={
}
className="mr-1 mb-1"
+ width={isMobile ? "w-10" : "w-12"}
+ height={isMobile ? "h-10" : "h-12"}
/>
@@ -1208,538 +387,34 @@ export function ChatInputBox({ noData }: { noData: boolean }) {
- {/* 配置选项区域 - 已移至右上角 */}
- {/*
- setConfigOptions((prev) => ({ ...prev, [key]: value }))
- }
- /> */}
-
{/* 模板故事弹窗 */}
- setIsTemplateModalOpen(false)}
- isTemplateCreating={isTemplateCreating}
- setIsTemplateCreating={setIsTemplateCreating}
- isRoleGenerating={isRoleGenerating}
- setIsRoleGenerating={setIsRoleGenerating}
- isItemGenerating={isItemGenerating}
- setIsItemGenerating={setIsItemGenerating}
- />
+ {isDesktop ? (
+ setIsTemplateModalOpen(false)}
+ isTemplateCreating={isTemplateCreating}
+ setIsTemplateCreating={setIsTemplateCreating}
+ isRoleGenerating={isRoleGenerating}
+ setIsRoleGenerating={setIsRoleGenerating}
+ isItemGenerating={isItemGenerating}
+ setIsItemGenerating={setIsItemGenerating}
+ />
+ ) : (
+ setIsTemplateModalOpen(false)}
+ isTemplateCreating={isTemplateCreating}
+ setIsTemplateCreating={setIsTemplateCreating}
+ isRoleGenerating={isRoleGenerating}
+ setIsRoleGenerating={setIsRoleGenerating}
+ isItemGenerating={isItemGenerating}
+ setIsItemGenerating={setIsItemGenerating}
+ />
+ )}
);
}
-/**
- * 配置选项组件
- * 提供视频创建的各种配置选项,位于输入框下方
- */
-const ConfigOptions = ({
- config,
- onConfigChange,
-}: {
- config: {
- mode: string;
- resolution: string;
- language: string;
- videoDuration: string;
- };
- onConfigChange: (key: string, value: string) => void;
-}) => {
- const configItems = [
- // {
- // key: "mode",
- // icon: Package,
- // options: [
- // { value: "auto", label: "Auto", isVip: false },
- // { value: "manual", label: "Manual", isVip: true },
- // ],
- // },
- // {
- // key: "resolution",
- // icon: Video,
- // options: [
- // { value: "720p", label: "720P", isVip: false },
- // { value: "1080p", label: "1080P", isVip: true },
- // { value: "2k", label: "2K", isVip: true },
- // { value: "4k", label: "4K", isVip: true },
- // ],
- // },
- {
- key: "language",
- icon: Globe,
- options: [
- { value: "english", label: "English", isVip: false },
- { value: "chinese", label: "Chinese", isVip: false },
- { value: "japanese", label: "Japanese", isVip: false },
- { value: "spanish", label: "Spanish", isVip: false },
- { value: "portuguese", label: "Portuguese", isVip: false },
- { value: "hindi", label: "Hindi", isVip: false },
- { value: "korean", label: "Korean", isVip: false },
- { value: "arabic", label: "Arabic", isVip: false },
- { value: "russian", label: "Russian", isVip: false },
- ],
- },
- // {
- // key: "videoDuration",
- // icon: Clock,
- // options: [
- // { value: "1min", label: "1 Min", isVip: false },
- // { value: "2min", label: "2 Min", isVip: true },
- // { value: "3min", label: "3 Min", isVip: true },
- // ],
- // },
- ];
-
- return (
-
- {configItems.map((item) => {
- const IconComponent = item.icon;
- const currentOption = item.options.find(
- (opt) => opt.value === config[item.key as keyof typeof config]
- );
- return (
-
({
- key: option.value,
- label: (
-
- {option.label}
- {option.isVip && (
-
- )}
-
- ),
- })),
- onClick: ({ key }) => onConfigChange(item.key, key),
- }}
- trigger={["click"]}
- placement="topRight"
- >
-
-
- {currentOption?.label}
- {currentOption?.isVip && (
-
- )}
-
-
-
- );
- })}
-
- );
-};
-
-/**
- * 图片故事弹窗组件
- * 提供图片上传、AI分析和故事生成功能,支持动态UI变化
- */
-const PhotoStoryModal = ({
- isCreating,
- setIsCreating,
- isPhotoCreating,
- setIsPhotoCreating,
- isOpen,
- onClose,
- configOptions = {
- mode: "auto" as "auto" | "manual",
- resolution: "720p" as "720p" | "1080p" | "4k",
- language: "english",
- videoDuration: "1min",
- },
-}: {
- isOpen: boolean;
- onClose: () => void;
- isCreating: boolean;
- setIsCreating: (value: boolean) => void;
- isPhotoCreating: boolean;
- setIsPhotoCreating: (value: boolean) => void;
- configOptions?: {
- mode: "auto" | "manual";
- resolution: "720p" | "1080p" | "4k";
- language: string;
- videoDuration: string;
- };
-}) => {
- // 使用图片故事服务hook管理状态
- const {
- activeImageUrl,
- storyContent,
- potentialGenres,
- selectedCategory,
- isLoading,
- hasAnalyzed,
- taskProgress,
- updateStoryType,
- updateStoryContent,
- updateCharacterName,
- resetImageStory,
- triggerFileSelection,
- avatarComputed,
- uploadAndAnalyzeImage,
- setCharactersAnalysis,
- originalUserDescription,
- actionMovie,
- uploadCharacterAvatarAndAnalyzeFeatures,
- } = useImageStoryServiceHook();
- const { loadingText } = useLoadScriptText(isLoading);
- const [localLoading, setLocalLoading] = useState(0);
- // 重置状态
- const handleClose = () => {
- // resetImageStory();
- onClose();
- };
- const router = useRouter();
- const taskProgressRef = useRef(taskProgress);
- const [cursorPosition, setCursorPosition] = useState(0);
-
- const handleCursorPositionChange = (position: number) => {
- setCursorPosition(position);
- };
- useEffect(() => {
- taskProgressRef.current = taskProgress;
- }, [taskProgress]);
- // 处理图片上传
- const handleImageUpload = async (e: any) => {
- const target = e.target as HTMLImageElement;
- if (
- !(target.tagName == "IMG" || e.target.dataset.alt == "image-upload-area")
- ) {
- return;
- }
- e.preventDefault();
- e.stopPropagation();
- try {
- await triggerFileSelection();
- } catch (error) {
- console.error("Failed to upload image:", error);
- }
- };
-
- // 处理确认
- const handleConfirm = async () => {
- try {
- setIsCreating(true);
- // 获取当前用户信息
- const User = JSON.parse(localStorage.getItem("currentUser") || "{}");
-
- if (!User.id) {
- console.error("用户未登录");
- return;
- }
-
- // 调用actionMovie接口
- const episodeResponse = await actionMovie(
- String(User.id),
- configOptions.mode as "auto" | "manual",
- configOptions.resolution as "720p" | "1080p" | "4k",
- configOptions.language
- );
- if (!episodeResponse) return;
- let episodeId = episodeResponse.project_id;
- // let episodeId = '9c34fcc4-c8d8-44fc-879e-9bd56f608c76';
- router.push(`/movies/work-flow?episodeId=${episodeId}`);
- // 成功后关闭弹窗
- handleClose();
- } catch (error) {
- setIsCreating(false);
- console.error("创建电影项目失败:", error);
- }
- };
-
- const handleAnalyzeImage = async () => {
- if (isPhotoCreating || isLoading) return;
-
- setIsPhotoCreating(true);
- let timeout = 100;
- let timer: NodeJS.Timeout;
- timer = setInterval(() => {
- const currentProgress = taskProgressRef.current;
- setLocalLoading((prev) => {
- if (prev >= currentProgress && currentProgress != 0) {
- return currentProgress;
- }
- return prev + 0.1;
- });
- }, timeout);
- try {
- await uploadAndAnalyzeImage();
- } catch (error) {
- console.error("分析图片失败:", error);
- setIsPhotoCreating(false);
- } finally {
- clearInterval(timer);
- setLocalLoading(0);
- }
- };
-
- return (
-
-
- ×
-
-
- }
- >
- 0} progress={localLoading}>
-
- {/* 弹窗头部 */}
-
-
-
- Movie Generation from Image
-
-
-
-
- {/* 左侧:图片上传 */}
-
-
- {activeImageUrl ? (
-
-

-
{
- resetImageStory();
- }}
- okText="Yes"
- cancelText="No"
- showCancel={false}
- okType="default"
- placement="top"
- classNames={{
- root: "text-white event-pointer",
- body: "text-white border rounded-lg bg-white/[0.04] [&_.ant-popconfirm-description]:!text-white [&_.ant-popconfirm-title]:!text-white [&_.ant-btn]:!text-white",
- }}
- >
- {
- e.preventDefault();
- e.stopPropagation();
- }}
- className="absolute top-1 right-1 w-4 h-4 bg-white/20 hover:bg-white/30 backdrop-blur-sm rounded-md flex items-center justify-center text-white/90 hover:text-white border border-white/20 hover:border-white/30 transition-all duration-200 z-10 shadow-sm hover:shadow-md"
- data-alt="clear-all-button"
- >
-
-
-
-
- ) : (
-
- )}
-
-
-
- {/* 中间:头像展示(分析后显示) */}
- {hasAnalyzed && avatarComputed.length > 0 && (
-
- {avatarComputed.map((avatar, index) => (
-
-
-

{
- // 如果裁剪的头像加载失败,回退到原图
- const target = e.target as HTMLImageElement;
- target.src = activeImageUrl;
- }}
- />
- {/* 删除角色按钮 - 使用Tooltip并调整z-index避免被遮挡 */}
-
- {
- e.stopPropagation();
- // 从角色分析中删除该角色
- setCharactersAnalysis((charactersAnalysis) => {
- const updatedCharacters =
- charactersAnalysis.filter(
- (char) => char.role_name !== avatar.name
- );
- return updatedCharacters;
- });
- // 从故事内容中删除该角色的所有标签和引用
- const updatedStory = storyContent
- .replace(
- new RegExp(
- `]*>${avatar.name}<\/role>`,
- "g"
- ),
- ""
- )
- .replace(
- new RegExp(`\\b${avatar.name}\\b`, "g"),
- ""
- )
- .replace(/\s+/g, " ")
- .trim();
- // 更新状态
- updateStoryContent(updatedStory);
- }}
- className="absolute top-0.5 right-0.5 w-4 h-4 bg-black/[0.4] border border-black/[0.1] text-white rounded-full flex items-center justify-center transition-colors opacity-0 group-hover:opacity-100 z-10"
- >
-
-
-
- {/* 上传新图片按钮 - 悬停时显示 */}
-
- {
- e.stopPropagation();
- // 使用新的上传人物头像并分析特征方法
- uploadCharacterAvatarAndAnalyzeFeatures(
- avatar.name
- ).catch((error) => {
- console.error("上传人物头像失败:", error);
- });
- }}
- className="absolute inset-0 bg-black/20 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex items-center justify-center"
- >
-
-
-
-
-
-
{
- const newName = e.target.value.trim();
- if (newName && newName !== avatar.name) {
- updateCharacterName(avatar.name, newName);
- }
- }}
- className="w-16 text-center text-sm text-white/80 bg-transparent border-none outline-none focus:ring-1 focus:ring-blue-400/50 rounded px-1 py-0.5 transition-all duration-200"
- style={{ textAlign: "center" }}
- />
-
-
-
- ))}
-
- )}
-
- {/* 右侧:分类选择(分析后显示) */}
- {hasAnalyzed && potentialGenres.length > 0 && (
-
-
- {[...potentialGenres].map((genre) => (
- updateStoryType(genre)}
- className={`px-3 py-1.5 text-xs rounded-lg transition-all duration-200 whitespace-nowrap ${
- selectedCategory === genre
- ? "bg-blue-500/20 border border-blue-500/40 text-blue-300"
- : "bg-white/[0.05] border border-white/[0.1] text-white/60 hover:bg-white/[0.08] hover:text-white/80"
- }`}
- >
- {genre}
-
- ))}
-
-
- )}
-
- {/* 原始用户描述的展示 */}
- {originalUserDescription && (
-
- Your Provided Text:{originalUserDescription}
-
- )}
-
-
- {/* 文本输入框 */}
-
-
- {!hasAnalyzed ? (
- // 分析按钮 - 使用ActionButton样式
-
- }
- disabled={isLoading || isPhotoCreating}
- />
-
- ) : (
- <>
- {/* Action按钮 - 使用ActionButton样式 */}
-
- }
- disabled={isCreating}
- />
-
- >
- )}
-
-
-
-
-
-
- );
-};
diff --git a/components/ChatInputBox/H5PhotoStoryDrawer.tsx b/components/ChatInputBox/H5PhotoStoryDrawer.tsx
new file mode 100644
index 0000000..c41fd10
--- /dev/null
+++ b/components/ChatInputBox/H5PhotoStoryDrawer.tsx
@@ -0,0 +1,396 @@
+"use client";
+
+import { useEffect, useRef, useState } from "react";
+import { Drawer, Popconfirm, Tooltip, Upload } from "antd";
+import { ImagePlay, Sparkles, Trash2 } from "lucide-react";
+import { useRouter } from "next/navigation";
+import GlobalLoad from "../common/GlobalLoad";
+import { ActionButton } from "../common/ActionButton";
+import { HighlightEditor } from "../common/HighlightEditor";
+import { useImageStoryServiceHook } from "@/app/service/Interaction/ImageStoryService";
+import { useLoadScriptText } from "@/app/service/domain/service";
+
+type ConfigOptions = {
+ mode: "auto" | "manual";
+ resolution: "720p" | "1080p" | "4k";
+ language: string;
+ videoDuration: string;
+};
+
+/**
+ * 移动端/平板端全屏抽屉版本:基于 PcPhotoStoryModal 的功能与交互
+ * - 顶部固定标题栏
+ * - 中间可滚动内容
+ * - 底部固定操作条
+ * - 人物头像采用网格布局,避免横向滚动误触返回手势
+ *
+ * @param {boolean} isOpen - 是否打开抽屉
+ * @param {() => void} onClose - 关闭回调
+ * @param {boolean} isCreating - 是否正在创建视频
+ * @param {(v: boolean) => void} setIsCreating - 设置创建状态
+ * @param {boolean} isPhotoCreating - 是否正在分析图片
+ * @param {(v: boolean) => void} setIsPhotoCreating - 设置分析状态
+ * @param {ConfigOptions} configOptions - 配置项,默认与 PC 版一致
+ */
+export const H5PhotoStoryDrawer = ({
+ isMobile,
+ isOpen,
+ onClose,
+ isCreating,
+ setIsCreating,
+ isPhotoCreating,
+ setIsPhotoCreating,
+ configOptions = {
+ mode: "auto",
+ resolution: "720p",
+ language: "english",
+ videoDuration: "1min",
+ },
+}: {
+ isMobile: boolean;
+ isOpen: boolean;
+ onClose: () => void;
+ isCreating: boolean;
+ setIsCreating: (value: boolean) => void;
+ isPhotoCreating: boolean;
+ setIsPhotoCreating: (value: boolean) => void;
+ configOptions?: ConfigOptions;
+}) => {
+ const {
+ activeImageUrl,
+ storyContent,
+ potentialGenres,
+ selectedCategory,
+ isLoading,
+ hasAnalyzed,
+ taskProgress,
+ updateStoryType,
+ updateStoryContent,
+ updateCharacterName,
+ resetImageStory,
+ triggerFileSelection,
+ avatarComputed,
+ uploadAndAnalyzeImage,
+ setCharactersAnalysis,
+ originalUserDescription,
+ actionMovie,
+ uploadCharacterAvatarAndAnalyzeFeatures,
+ } = useImageStoryServiceHook();
+
+ const { loadingText } = useLoadScriptText(isLoading);
+ const [localLoading, setLocalLoading] = useState(0);
+ const router = useRouter();
+ const taskProgressRef = useRef(taskProgress);
+ const [cursorPosition, setCursorPosition] = useState(0);
+
+ const handleCursorPositionChange = (position: number) => {
+ setCursorPosition(position);
+ };
+
+ useEffect(() => {
+ taskProgressRef.current = taskProgress;
+ }, [taskProgress]);
+
+ const handleImageUpload = async (e: any) => {
+ const target = e.target as HTMLImageElement;
+ if (!(target.tagName == "IMG" || e.target.dataset.alt == "image-upload-area")) {
+ return;
+ }
+ e.preventDefault();
+ e.stopPropagation();
+ try {
+ await triggerFileSelection();
+ } catch (error) {
+ // 保持静默失败,避免打断用户
+ }
+ };
+
+ const handleConfirm = async () => {
+ try {
+ setIsCreating(true);
+ const User = JSON.parse(localStorage.getItem("currentUser") || "{}");
+ if (!User.id) {
+ setIsCreating(false);
+ return;
+ }
+ const episodeResponse = await actionMovie(
+ String(User.id),
+ configOptions.mode as "auto" | "manual",
+ configOptions.resolution as "720p" | "1080p" | "4k",
+ configOptions.language
+ );
+ if (!episodeResponse) return;
+ const episodeId = episodeResponse.project_id;
+ router.push(`/movies/work-flow?episodeId=${episodeId}`);
+ onClose();
+ } catch (error) {
+ setIsCreating(false);
+ }
+ };
+
+ const handleAnalyzeImage = async () => {
+ if (isPhotoCreating || isLoading) return;
+ setIsPhotoCreating(true);
+ let timeout = 100;
+ let timer: NodeJS.Timeout;
+ timer = setInterval(() => {
+ const currentProgress = taskProgressRef.current;
+ setLocalLoading((prev) => {
+ if (prev >= currentProgress && currentProgress != 0) {
+ return currentProgress;
+ }
+ return prev + 0.1;
+ });
+ }, timeout);
+ try {
+ await uploadAndAnalyzeImage();
+ } catch (error) {
+ setIsPhotoCreating(false);
+ } finally {
+ clearInterval(timer);
+ setLocalLoading(0);
+ }
+ };
+
+ return (
+
+ 0} progress={localLoading}>
+
+
+
+
+
Movie Generation from Image
+
+
+
+
+
+ {/* 上传卡片 */}
+
+
+
+
+ {activeImageUrl ? (
+
+

+
{
+ resetImageStory();
+ }}
+ okText="Yes"
+ cancelText="No"
+ showCancel={false}
+ okType="default"
+ placement="top"
+ classNames={{
+ root: "text-white event-pointer",
+ body: "text-white border rounded-lg bg-white/[0.04] [&_.ant-popconfirm-description]:!text-white [&_.ant-popconfirm-title]:!text-white [&_.ant-btn]:!text-white",
+ }}
+ >
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ }}
+ className="absolute top-1 right-1 w-5 h-5 bg-white/20 hover:bg-white/30 backdrop-blur-sm rounded-md flex items-center justify-center text-white/90 border border-white/20 transition-all duration-200 z-10"
+ data-alt="clear-all-button"
+ aria-label="clear-all"
+ >
+
+
+
+
+ ) : (
+
+
+ Upload
+
+ )}
+
+
+
+
+
+
+ {/* 人物头像网格 */}
+ {hasAnalyzed && avatarComputed.length > 0 && (
+
+
+ {avatarComputed.map((avatar, index) => (
+
+
+

{
+ const target = e.target as HTMLImageElement;
+ target.src = activeImageUrl;
+ }}
+ />
+
+ {
+ e.stopPropagation();
+ setCharactersAnalysis((charactersAnalysis) => {
+ const updatedCharacters = charactersAnalysis.filter((char) => char.role_name !== avatar.name);
+ return updatedCharacters;
+ });
+ const updatedStory = storyContent
+ .replace(new RegExp(`]*>${avatar.name}<\/role>`, "g"), "")
+ .replace(new RegExp(`\\b${avatar.name}\\b`, "g"), "")
+ .replace(/\s+/g, " ")
+ .trim();
+ updateStoryContent(updatedStory);
+ }}
+ className="absolute top-0.5 right-0.5 w-4 h-4 bg-black/40 border border-black/20 text-white rounded-full flex items-center justify-center transition-opacity opacity-0 group-hover:opacity-100 z-10"
+ aria-label="remove-character"
+ >
+
+
+
+
+ {
+ e.stopPropagation();
+ uploadCharacterAvatarAndAnalyzeFeatures(avatar.name).catch(() => {});
+ }}
+ className="absolute inset-0 bg-black/20 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex items-center justify-center"
+ aria-label="replace-character-avatar"
+ >
+
+
+
+
+
{
+ const newName = e.target.value.trim();
+ if (newName && newName !== avatar.name) {
+ updateCharacterName(avatar.name, newName);
+ }
+ }}
+ className="w-full max-w-[72px] md:max-w-[80px] text-center text-xs md:text-sm text-white/80 bg-transparent border-none outline-none focus:ring-1 focus:ring-blue-400/50 rounded px-1 py-0.5"
+ />
+
+ ))}
+
+
+ )}
+
+ {/* 题材标签(自动换行,避免横向滚动) */}
+ {hasAnalyzed && potentialGenres.length > 0 && (
+
+
+ {[...potentialGenres].map((genre) => (
+ updateStoryType(genre)}
+ className={`px-3 py-1.5 text-xs rounded-lg transition-all duration-200 ${
+ selectedCategory === genre
+ ? "bg-blue-500/20 border border-blue-500/40 text-blue-300"
+ : "bg-white/[0.05] border border-white/[0.1] text-white/60 hover:bg-white/[0.08] hover:text-white/80"
+ }`}
+ aria-pressed={selectedCategory === genre}
+ >
+ {genre}
+
+ ))}
+
+
+ )}
+
+ {/* 用户原始文本信息 */}
+ {originalUserDescription && (
+
+ Your Provided Text: {originalUserDescription}
+
+ )}
+
+ {/* 文本编辑器 */}
+
+
+
+
+
+
+
+
+ {!hasAnalyzed ? (
+
+
+
}
+ disabled={isLoading || isPhotoCreating}
+ width="w-10"
+ height="h-10"
+ />
+
+
+ ) : (
+
+
+
}
+ disabled={isCreating}
+ width="w-10"
+ height="h-10"
+ />
+
+
+ )}
+
+
+
+
+
+ );
+};
+
+export default H5PhotoStoryDrawer;
+
+
diff --git a/components/ChatInputBox/H5TemplateDrawer.tsx b/components/ChatInputBox/H5TemplateDrawer.tsx
new file mode 100644
index 0000000..693fc3c
--- /dev/null
+++ b/components/ChatInputBox/H5TemplateDrawer.tsx
@@ -0,0 +1,603 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { Drawer, Tooltip, Upload, Image } from "antd";
+import { UploadOutlined } from "@ant-design/icons";
+import { Clapperboard, Sparkles, LayoutTemplate, ChevronDown, ChevronUp, CheckCircle2 } from "lucide-react";
+import { useRouter } from "next/navigation";
+import { StoryTemplateEntity } from "@/app/service/domain/Entities";
+import { useTemplateStoryServiceHook } from "@/app/service/Interaction/templateStoryService";
+import { useUploadFile } from "@/app/service/domain/service";
+import { ActionButton } from "../common/ActionButton";
+import GlobalLoad from "../common/GlobalLoad";
+import { motion, AnimatePresence } from "framer-motion";
+
+interface H5TemplateDrawerProps {
+ isMobile: boolean;
+ 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;
+ };
+}
+
+export const H5TemplateDrawer = ({
+ isMobile,
+ isTemplateCreating,
+ setIsTemplateCreating,
+ isRoleGenerating,
+ setIsRoleGenerating,
+ isItemGenerating,
+ setIsItemGenerating,
+ isOpen,
+ onClose,
+ configOptions,
+}: H5TemplateDrawerProps) => {
+ const router = useRouter();
+
+ const {
+ templateStoryList,
+ selectedTemplate,
+ isLoading,
+ getTemplateStoryList,
+ actionStory,
+ setSelectedTemplate,
+ AvatarAndAnalyzeFeatures,
+ updateItemImage,
+ handleRoleFieldBlur,
+ handleItemFieldBlur,
+ clearData,
+ } = useTemplateStoryServiceHook();
+
+ const { uploadFile } = useUploadFile();
+ const [localLoading, setLocalLoading] = useState(0);
+ const [inputVisible, setInputVisible] = useState<{ [key: string]: boolean }>({});
+ const [isBottomExpanded, setIsBottomExpanded] = useState(true);
+ const [isDescExpanded, setIsDescExpanded] = useState(false);
+
+ useEffect(() => {
+ if (isOpen) {
+ getTemplateStoryList();
+ }
+ }, [isOpen, getTemplateStoryList]);
+
+ 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 handleConfirm = async () => {
+ if (!selectedTemplate || isTemplateCreating) return;
+ setIsTemplateCreating(true);
+ let timer: NodeJS.Timeout | null = null;
+ try {
+ const User = JSON.parse(localStorage.getItem("currentUser") || "{}");
+ if (!User.id) return;
+
+ timer = setInterval(() => {
+ setLocalLoading((prev) => (prev >= 95 ? 95 : prev + 0.1));
+ }, 100);
+ setLocalLoading(1);
+
+ const projectId = await actionStory(
+ String(User.id),
+ configOptions.mode,
+ configOptions.resolution,
+ configOptions.language
+ );
+
+ if (projectId) {
+ router.push(`/movies/work-flow?episodeId=${projectId}`);
+ onClose();
+ setSelectedTemplate(null);
+ }
+ } catch (error) {
+ setIsTemplateCreating(false);
+ setLocalLoading(0);
+ setSelectedTemplate(null);
+ } finally {
+ setLocalLoading(0);
+ if (timer) clearInterval(timer);
+ }
+ };
+
+ const renderTopTemplateList = () => {
+ return (
+
+
+ {templateStoryList.map((template, index) => {
+ const isSelected = selectedTemplate?.id === template.id;
+ return (
+
{
+ if (!isBottomExpanded) setIsBottomExpanded(true);
+ setSelectedTemplate(template);
+ }}
+ className={`${isSelected ? "ring-1 ring-blue-500/60 bg-white/[0.06]" : "bg-white/0"} w-full flex items-center gap-3 rounded-xl border border-white/10 hover:border-white/20 hover:bg-white/[0.04] transition-colors p-2`}
+ >
+
+
+
+
+
+
+ {template.name}
+
+
+
+ {template.generateText}
+
+
+
+ {isSelected ? (
+
+ ) : null}
+
+
+ );
+ })}
+
+
+ );
+ };
+
+ const renderRoles = () => {
+ if (!selectedTemplate?.storyRole || selectedTemplate.storyRole.length === 0) return null;
+ return (
+
+
Character Configuration
+
+ {selectedTemplate.storyRole.map((role, index) => (
+
+
+ }
+ placement="top"
+ classNames={{ root: "max-w-none" }}
+ open={inputVisible[role.role_name]}
+ onOpenChange={(visible) =>
+ setInputVisible((prev) => ({ ...prev, [role.role_name]: visible }))
+ }
+ trigger="click"
+ styles={{ root: { zIndex: 1000 } }}
+ >
+
+
+
+
+
+
+
+ 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"
+ >
+
+
+
+ {
+ const isImage = file.type.startsWith("image/");
+ if (!isImage) return false;
+ const isLt5M = file.size / 1024 / 1024 < 5;
+ if (!isLt5M) return false;
+ return true;
+ }}
+ customRequest={async ({ file, onSuccess, onError }) => {
+ try {
+ const fileObj = file as File;
+ const uploadedUrl = await uploadFile(fileObj, () => {});
+ await AvatarAndAnalyzeFeatures(uploadedUrl, role.role_name);
+ onSuccess?.(uploadedUrl as any);
+ } catch (error) {
+ onError?.(error as Error);
+ }
+ }}
+ >
+
+
+
+
+
+
+
+
+
+ {role.role_name}
+
+
+
+ ))}
+
+
+ );
+ };
+
+ const renderItems = () => {
+ if (!selectedTemplate?.storyItem || selectedTemplate.storyItem.length === 0) return null;
+ return (
+
+
props Configuration
+
+ {selectedTemplate.storyItem.map((item, index) => (
+
+
+ }
+ placement="top"
+ classNames={{ root: "max-w-none" }}
+ open={inputVisible[item.item_name]}
+ onOpenChange={(visible) =>
+ setInputVisible((prev) => ({ ...prev, [item.item_name]: visible }))
+ }
+ trigger="click"
+ styles={{ root: { zIndex: 1000 } }}
+ >
+
+
+
+
+
+
+
+ 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"
+ >
+
+
+ {
+ const isImage = file.type.startsWith("image/");
+ if (!isImage) return false;
+ const isLt5M = file.size / 1024 / 1024 < 5;
+ if (!isLt5M) return false;
+ return true;
+ }}
+ customRequest={async ({ file, onSuccess, onError }) => {
+ try {
+ const fileObj = file as File;
+ const uploadedUrl = await uploadFile(fileObj, () => {});
+ updateItemImage(item.item_name, uploadedUrl);
+ onSuccess?.(uploadedUrl as any);
+ } catch (error) {
+ onError?.(error as Error);
+ }
+ }}
+ >
+
+
+
+
+
+
+
+
+
+ {item.item_name}
+
+
+
+ ))}
+
+
+ );
+ };
+
+ const renderBottomDetail = () => {
+ if (!selectedTemplate) {
+ return (
+
+
+
+
No templates available
+
Please try again later
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+ {selectedTemplate.name}
+
+
+
+ {selectedTemplate.generateText}
+
+ {!isDescExpanded && (
+
+ )}
+
+
+ setIsDescExpanded((v) => !v)}
+ className="inline-flex items-center gap-1 text-[11px] text-blue-400 hover:text-blue-300"
+ >
+ {isDescExpanded ? "Collapse" : "Expand"}
+ {isDescExpanded ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ {renderRoles()}
+ {renderItems()}
+
+
+
+
+ {selectedTemplate?.freeInputItem && (
+
+ {
+ const updatedTemplate = {
+ ...selectedTemplate!,
+ freeInputItem: {
+ ...selectedTemplate!.freeInputItem,
+ free_input_text: e.target.value,
+ },
+ } as StoryTemplateEntity;
+ setSelectedTemplate(updatedTemplate);
+ }}
+ />
+
+ )}
+
0}
+ handleCreateVideo={handleConfirm}
+ icon={}
+ disabled={isTemplateCreating || localLoading > 0}
+ width="w-10"
+ height="h-10"
+ />
+
+
+ );
+ };
+
+ return (
+ {
+ clearData();
+ onClose();
+ }}
+ className="h5-template-drawer [&_.ant-drawer-body]:!p-0 bg-white/[0.02]"
+ styles={{
+ body: {
+ height: `calc(100vh - 3rem)`,
+ overflow: "hidden",
+ display: "flex",
+ flexDirection: "column",
+ position: "relative",
+ },
+ }}
+ >
+
+
+
+
Template Story
+
+
+
+
+ {renderTopTemplateList()}
+
+
+
+ setIsBottomExpanded((v) => !v)}
+ className="w-10 h-10 rounded-full bg-white/10 hover:bg-white/20 border border-white/20 text-white flex items-center justify-center shadow-xl"
+ whileTap={{ scale: 0.96 }}
+ >
+
+
+
+
+
+
+ {isBottomExpanded && (
+
+ {renderBottomDetail()}
+
+ )}
+
+
+
+
+
+
+ );
+};
+
+export default H5TemplateDrawer;
+
+
diff --git a/components/ChatInputBox/PcPhotoStoryModal.tsx b/components/ChatInputBox/PcPhotoStoryModal.tsx
new file mode 100644
index 0000000..cea4cd2
--- /dev/null
+++ b/components/ChatInputBox/PcPhotoStoryModal.tsx
@@ -0,0 +1,349 @@
+"use client";
+
+import { useEffect, useRef, useState } from "react";
+import { Modal, Tooltip, Popconfirm, Upload } from "antd";
+import { ImagePlay, Sparkles, Trash2 } from "lucide-react";
+import { useRouter } from "next/navigation";
+import GlobalLoad from "../common/GlobalLoad";
+import { ActionButton } from "../common/ActionButton";
+import { HighlightEditor } from "../common/HighlightEditor";
+import { useImageStoryServiceHook } from "@/app/service/Interaction/ImageStoryService";
+import { useLoadScriptText } from "@/app/service/domain/service";
+
+type ConfigOptions = {
+ mode: "auto" | "manual";
+ resolution: "720p" | "1080p" | "4k";
+ language: string;
+ videoDuration: string;
+};
+
+export const PcPhotoStoryModal = ({
+ isCreating,
+ setIsCreating,
+ isPhotoCreating,
+ setIsPhotoCreating,
+ isOpen,
+ onClose,
+ configOptions = {
+ mode: "auto",
+ resolution: "720p",
+ language: "english",
+ videoDuration: "1min",
+ },
+}: {
+ isOpen: boolean;
+ onClose: () => void;
+ isCreating: boolean;
+ setIsCreating: (value: boolean) => void;
+ isPhotoCreating: boolean;
+ setIsPhotoCreating: (value: boolean) => void;
+ configOptions?: ConfigOptions;
+}) => {
+ const {
+ activeImageUrl,
+ storyContent,
+ potentialGenres,
+ selectedCategory,
+ isLoading,
+ hasAnalyzed,
+ taskProgress,
+ updateStoryType,
+ updateStoryContent,
+ updateCharacterName,
+ resetImageStory,
+ triggerFileSelection,
+ avatarComputed,
+ uploadAndAnalyzeImage,
+ setCharactersAnalysis,
+ originalUserDescription,
+ actionMovie,
+ uploadCharacterAvatarAndAnalyzeFeatures,
+ } = useImageStoryServiceHook();
+ const { loadingText } = useLoadScriptText(isLoading);
+ const [localLoading, setLocalLoading] = useState(0);
+ const router = useRouter();
+ const taskProgressRef = useRef(taskProgress);
+ const [cursorPosition, setCursorPosition] = useState(0);
+
+ const handleCursorPositionChange = (position: number) => {
+ setCursorPosition(position);
+ };
+
+ useEffect(() => {
+ taskProgressRef.current = taskProgress;
+ }, [taskProgress]);
+
+ const handleClose = () => {
+ onClose();
+ };
+
+ const handleImageUpload = async (e: any) => {
+ const target = e.target as HTMLImageElement;
+ if (!(target.tagName == "IMG" || e.target.dataset.alt == "image-upload-area")) {
+ return;
+ }
+ e.preventDefault();
+ e.stopPropagation();
+ try {
+ await triggerFileSelection();
+ } catch (error) {
+ console.error("Failed to upload image:", error);
+ }
+ };
+
+ const handleConfirm = async () => {
+ try {
+ setIsCreating(true);
+ const User = JSON.parse(localStorage.getItem("currentUser") || "{}");
+ if (!User.id) {
+ console.error("用户未登录");
+ return;
+ }
+ const episodeResponse = await actionMovie(
+ String(User.id),
+ configOptions.mode as "auto" | "manual",
+ configOptions.resolution as "720p" | "1080p" | "4k",
+ configOptions.language
+ );
+ if (!episodeResponse) return;
+ const episodeId = episodeResponse.project_id;
+ router.push(`/movies/work-flow?episodeId=${episodeId}`);
+ handleClose();
+ } catch (error) {
+ setIsCreating(false);
+ console.error("创建电影项目失败:", error);
+ }
+ };
+
+ const handleAnalyzeImage = async () => {
+ if (isPhotoCreating || isLoading) return;
+ setIsPhotoCreating(true);
+ let timeout = 100;
+ let timer: NodeJS.Timeout;
+ timer = setInterval(() => {
+ const currentProgress = taskProgressRef.current;
+ setLocalLoading((prev) => {
+ if (prev >= currentProgress && currentProgress != 0) {
+ return currentProgress;
+ }
+ return prev + 0.1;
+ });
+ }, timeout);
+ try {
+ await uploadAndAnalyzeImage();
+ } catch (error) {
+ console.error("分析图片失败:", error);
+ setIsPhotoCreating(false);
+ } finally {
+ clearInterval(timer);
+ setLocalLoading(0);
+ }
+ };
+
+ return (
+
+ ×
+
+ }
+ >
+ 0} progress={localLoading}>
+
+
+
+
Movie Generation from Image
+
+
+
+
+
+ {activeImageUrl ? (
+
+

+
{
+ resetImageStory();
+ }}
+ okText="Yes"
+ cancelText="No"
+ showCancel={false}
+ okType="default"
+ placement="top"
+ classNames={{
+ root: "text-white event-pointer",
+ body: "text-white border rounded-lg bg-white/[0.04] [&_.ant-popconfirm-description]:!text-white [&_.ant-popconfirm-title]:!text-white [&_.ant-btn]:!text-white",
+ }}
+ >
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ }}
+ className="absolute top-1 right-1 w-4 h-4 bg-white/20 hover:bg-white/30 backdrop-blur-sm rounded-md flex items-center justify-center text-white/90 hover:text-white border border-white/20 hover:border-white/30 transition-all duration-200 z-10 shadow-sm hover:shadow-md"
+ data-alt="clear-all-button"
+ >
+
+
+
+
+ ) : (
+
+ )}
+
+
+
+ {hasAnalyzed && avatarComputed.length > 0 && (
+
+ {avatarComputed.map((avatar, index) => (
+
+
+

{
+ const target = e.target as HTMLImageElement;
+ target.src = activeImageUrl;
+ }}
+ />
+
+ {
+ e.stopPropagation();
+ setCharactersAnalysis((charactersAnalysis) => {
+ const updatedCharacters = charactersAnalysis.filter((char) => char.role_name !== avatar.name);
+ return updatedCharacters;
+ });
+ const updatedStory = storyContent
+ .replace(new RegExp(`]*>${avatar.name}<\/role>`, "g"), "")
+ .replace(new RegExp(`\\b${avatar.name}\\b`, "g"), "")
+ .replace(/\s+/g, " ")
+ .trim();
+ updateStoryContent(updatedStory);
+ }}
+ className="absolute top-0.5 right-0.5 w-4 h-4 bg-black/[0.4] border border-black/[0.1] text-white rounded-full flex items-center justify-center transition-colors opacity-0 group-hover:opacity-100 z-10"
+ >
+
+
+
+
+ {
+ e.stopPropagation();
+ uploadCharacterAvatarAndAnalyzeFeatures(avatar.name).catch((error) => {
+ console.error("上传人物头像失败:", error);
+ });
+ }}
+ className="absolute inset-0 bg-black/20 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex items-center justify-center"
+ >
+
+
+
+
+
+
{
+ const newName = e.target.value.trim();
+ if (newName && newName !== avatar.name) {
+ updateCharacterName(avatar.name, newName);
+ }
+ }}
+ className="w-16 text-center text-sm text-white/80 bg-transparent border-none outline-none focus:ring-1 focus:ring-blue-400/50 rounded px-1 py-0.5 transition-all duration-200"
+ style={{ textAlign: "center" }}
+ />
+
+
+
+ ))}
+
+ )}
+
+ {hasAnalyzed && potentialGenres.length > 0 && (
+
+
+ {[...potentialGenres].map((genre) => (
+ updateStoryType(genre)}
+ className={`px-3 py-1.5 text-xs rounded-lg transition-all duration-200 whitespace-nowrap ${
+ selectedCategory === genre
+ ? "bg-blue-500/20 border border-blue-500/40 text-blue-300"
+ : "bg-white/[0.05] border border-white/[0.1] text-white/60 hover:bg-white/[0.08] hover:text-white/80"
+ }`}
+ >
+ {genre}
+
+ ))}
+
+
+ )}
+
+ {originalUserDescription && (
+
Your Provided Text:{originalUserDescription}
+ )}
+
+
+
+ {!hasAnalyzed ? (
+
+ }
+ disabled={isLoading || isPhotoCreating}
+ />
+
+ ) : (
+
+ }
+ disabled={isCreating}
+ />
+
+ )}
+
+
+
+
+
+
+ );
+};
+
+
diff --git a/components/ChatInputBox/PcTemplateModal.tsx b/components/ChatInputBox/PcTemplateModal.tsx
new file mode 100644
index 0000000..ee2ebbe
--- /dev/null
+++ b/components/ChatInputBox/PcTemplateModal.tsx
@@ -0,0 +1,740 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import {
+ Clapperboard,
+ Sparkles,
+ LayoutTemplate,
+} from "lucide-react";
+import {
+ Modal,
+ Tooltip,
+ Upload,
+ Image,
+} 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";
+
+/**
+ * 防抖函数
+ * @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();
+
+ // 防抖处理的输入更新函数
+ const debouncedUpdateInput = debounce((value: string) => {
+ // 过滤特殊字符
+ const sanitizedValue = value.replace(/[<>]/g, '');
+ // 更新输入值
+ if (!selectedTemplate?.freeInputItem) return;
+ const updatedTemplate: StoryTemplateEntity = {
+ ...selectedTemplate,
+ freeInputItem: {
+ ...selectedTemplate.freeInputItem,
+ free_input_text: sanitizedValue
+ }
+ };
+ setSelectedTemplate(updatedTemplate);
+ }, 300); // 300ms 的防抖延迟
+
+ // 使用上传文件hook
+ const { uploadFile, isUploading } = useUploadFile();
+ // 本地加载状态,用于 UI 反馈
+ const [localLoading, setLocalLoading] = useState(0);
+ // 控制输入框显示状态
+ const [inputVisible, setInputVisible] = useState<{ [key: string]: boolean }>(
+ {}
+ );
+ const router = useRouter();
+
+ // 组件挂载时获取模板列表
+ useEffect(() => {
+ if (isOpen) {
+ getTemplateStoryList();
+ }
+ }, [isOpen, getTemplateStoryList]);
+
+ // 监听点击外部区域关闭输入框
+ 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
+ );
+
+ if (projectId) {
+ // 跳转到电影详情页
+ router.push(`/movies/work-flow?episodeId=${projectId}`);
+ onClose();
+ // 重置状态
+ setSelectedTemplate(null);
+ }
+ console.log("Story action created:", projectId);
+ } catch (error) {
+ console.error("Failed to create story action:", error);
+ setIsTemplateCreating(false);
+ setLocalLoading(0);
+ // 这里可以添加 toast 提示
+ // 重置状态
+ setSelectedTemplate(null);
+ } finally {
+ setLocalLoading(0);
+ if (timer) {
+ clearInterval(timer);
+ }
+ }
+ };
+
+ // 模板列表渲染
+ const templateListRender = () => {
+ return (
+
+
+ {templateStoryList.map((template, index) => (
+
handleTemplateSelect(template)}
+ >
+
+
+ ))}
+
+
+ );
+ };
+
+ // 故事编辑器渲染
+ const storyEditorRender = () => {
+ return selectedTemplate ? (
+
+ {/* 模板信息头部 - 增加顶部空间 */}
+
+ {/* 左侧图片 */}
+
+
+
+
+ {/* 右侧信息 - 增加文本渲染空间 */}
+
+
+ {selectedTemplate.name}
+
+
+
+ {selectedTemplate.generateText}
+
+
+
+
+
+ {/* 角色配置区域 */}
+ {selectedTemplate?.storyRole &&
+ selectedTemplate.storyRole.length > 0 && (
+
+
+ Character Configuration
+
+
+ {selectedTemplate.storyRole.map((role, index) => (
+
+ {/* 图片容器 */}
+
+ }
+ 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 } }}
+ >
+ {/* 图片 */}
+
+
+
+
+
+ {/* 角色名称 - 图片下方 */}
+
+
+ {role.role_name}
+
+
+
+ {/* 按钮组 - 右上角 */}
+
+ {/* AI生成按钮 */}
+
+
+ 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"
+ >
+
+
+
+
+ {/* 上传按钮 */}
+ {
+ 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);
+ }
+ }}
+ >
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+ )}
+
+ {/* 道具配置区域 */}
+ {selectedTemplate?.storyItem &&
+ selectedTemplate.storyItem.length > 0 && (
+
+
+ props Configuration
+
+
+ {selectedTemplate.storyItem.map((item, index) => (
+
+ {/* 图片容器 */}
+
+ }
+ 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 } }}
+ >
+ {/* 图片 */}
+
+
+
+
+
+ {/* 道具名称 - 图片下方 */}
+
+
+ {item.item_name}
+
+
+
+ {/* 按钮组 - 右上角 */}
+
+ {/* AI生成按钮 */}
+
+
+ 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"
+ >
+
+
+
+
+ {/* 上传按钮 */}
+ {
+ 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);
+ }
+ }}
+ >
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+ )}
+
+
+ {/** 自由输入文字 */}
+ {(selectedTemplate?.freeInputItem) && (
+
+ {
+ // 更新自由输入文字字段
+ const updatedTemplate = {
+ ...selectedTemplate!,
+ freeInputItem: {
+ ...selectedTemplate!.freeInputItem,
+ free_input_text: e.target.value
+ }
+ };
+ setSelectedTemplate(updatedTemplate as StoryTemplateEntity);
+ }}
+ />
+
+ )}
+
0}
+ handleCreateVideo={handleConfirm}
+ icon={}
+ disabled={isTemplateCreating || localLoading > 0}
+ />
+
+
+ ) : (
+
+
+
+
No templates available
+
Please try again later
+
+
+ );
+ };
+
+ return (
+ <>
+ {
+ // 清空所有选中的内容数据
+ 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]"
+ >
+
+
+ {/* 弹窗头部 */}
+
+
+ Template Story Selection
+
+
+ ×
+
+
+
+
+ {templateListRender()}
+
{storyEditorRender()}
+
+
+
+
+ >
+ );
+};
diff --git a/components/common/GlobalLoad.tsx b/components/common/GlobalLoad.tsx
index c03e213..64d20a6 100644
--- a/components/common/GlobalLoad.tsx
+++ b/components/common/GlobalLoad.tsx
@@ -37,7 +37,7 @@ export default function GlobalLoad({
const customIndicator = (
{showSpinner &&
}
{Boolean(progress) &&
}
diff --git a/components/common/HighlightEditor.tsx b/components/common/HighlightEditor.tsx
index c69b9d7..53f9192 100644
--- a/components/common/HighlightEditor.tsx
+++ b/components/common/HighlightEditor.tsx
@@ -16,6 +16,7 @@ export const HighlightEditor = ({
placeholder,
cursorPosition,
onCursorPositionChange,
+ className
}: {
/** 内容 */
content: string;
@@ -29,6 +30,8 @@ export const HighlightEditor = ({
cursorPosition?: number;
/** 光标位置变化回调 */
onCursorPositionChange?: (position: number) => void;
+ /** 样式 */
+ className?: string;
}) => {
console.log(44444);
@@ -120,7 +123,7 @@ export const HighlightEditor = ({
}, [content, editor, cursorPosition]);
return (
-