forked from 77media/video-flow
加载样式的优化
This commit is contained in:
parent
140448c3a1
commit
d13f2a3363
@ -70,21 +70,24 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
|
||||
{
|
||||
id: '1',
|
||||
name: '奇幻冒险故事',
|
||||
imageUrl: ['/assets/3dr_howlcastle.png', '/assets/3dr_spirited.jpg'],
|
||||
image_url: ['/assets/3dr_howlcastle.png', '/assets/3dr_spirited.jpg'],
|
||||
generateText: '一个关于勇气与友谊的奇幻冒险故事,主角在神秘世界中寻找失落的宝藏,结识了各种奇特的生物和伙伴。',
|
||||
storyRole: [
|
||||
{
|
||||
role_name: '艾莉娅',
|
||||
role_description: '一个勇敢的女孩,拥有强大的魔法力量,她的冒险旅程充满了危险和挑战。',
|
||||
photo_url: '/assets/3dr_chihiro.png',
|
||||
voice_url:""
|
||||
},
|
||||
{
|
||||
role_name: '魔法师梅林',
|
||||
role_description: '一个智慧的魔法师,拥有强大的魔法力量,他的冒险旅程充满了危险和挑战。',
|
||||
photo_url: '/assets/3dr_mono.png',
|
||||
voice_url:""
|
||||
},
|
||||
{
|
||||
role_name: '守护者龙',
|
||||
role_description: '一个强大的守护者,拥有强大的魔法力量,他的冒险旅程充满了危险和挑战。',
|
||||
photo_url: '/assets/3dr_howlbg.jpg',
|
||||
voice_url:""
|
||||
}
|
||||
@ -93,39 +96,44 @@ export const useTemplateStoryServiceHook = (): UseTemplateStoryService => {
|
||||
{
|
||||
id: '2',
|
||||
name: '科幻探索之旅',
|
||||
imageUrl: ['/assets/3dr_monobg.jpg'],
|
||||
image_url: ['/assets/3dr_monobg.jpg'],
|
||||
generateText: '未来世界的太空探索故事,人类在浩瀚宇宙中寻找新的家园,面对未知的挑战和外星文明的接触。',
|
||||
storyRole: [
|
||||
{
|
||||
role_name: '船长凯特',
|
||||
role_description: '一个勇敢的船长,拥有强大的魔法力量,他的冒险旅程充满了危险和挑战。',
|
||||
photo_url: '/assets/3dr_chihiro.png',
|
||||
voice_url:""
|
||||
},
|
||||
{
|
||||
role_name: 'AI助手诺娃',
|
||||
role_description: '一个强大的AI助手,拥有强大的魔法力量,他的冒险旅程充满了危险和挑战。',
|
||||
photo_url: '/assets/3dr_mono.png',
|
||||
voice_url:""
|
||||
voice_url: ''
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: '温馨家庭喜剧',
|
||||
imageUrl: ['/assets/3dr_spirited.jpg'],
|
||||
image_url: ['/assets/3dr_spirited.jpg'],
|
||||
generateText: '一个充满欢笑和温情的家庭故事,讲述家庭成员之间的日常趣事,以及如何一起面对生活中的小挑战。',
|
||||
storyRole: [
|
||||
{
|
||||
role_name: '妈妈莉莉',
|
||||
role_description: '一个温柔的妈妈,拥有强大的魔法力量,她的冒险旅程充满了危险和挑战。',
|
||||
photo_url: '/assets/3dr_chihiro.png',
|
||||
voice_url:""
|
||||
},
|
||||
{
|
||||
role_name: '爸爸汤姆',
|
||||
role_description: '一个勇敢的爸爸,拥有强大的魔法力量,他的冒险旅程充满了危险和挑战。',
|
||||
photo_url: '/assets/3dr_mono.png',
|
||||
voice_url:""
|
||||
},
|
||||
{
|
||||
role_name: '孩子小杰',
|
||||
role_description: '一个聪明的孩子,拥有强大的魔法力量,他的冒险旅程充满了危险和挑战。',
|
||||
photo_url: '/assets/3dr_howlbg.jpg',
|
||||
voice_url:""
|
||||
}
|
||||
|
||||
@ -137,13 +137,15 @@ export interface StoryTemplateEntity {
|
||||
/** 故事模板名称 */
|
||||
name: string;
|
||||
/** 故事模板图片 */
|
||||
imageUrl: string[];
|
||||
image_url: string[];
|
||||
/** 故事模板概览*/
|
||||
generateText: string;
|
||||
/**故事角色 */
|
||||
storyRole: {
|
||||
/**角色名 */
|
||||
role_name: string;
|
||||
/**角色描述 */
|
||||
role_description: string;
|
||||
/**照片URL */
|
||||
photo_url: string;
|
||||
/**声音URL */
|
||||
|
||||
@ -18,7 +18,7 @@ import {
|
||||
Sparkles,
|
||||
Settings,
|
||||
} from "lucide-react";
|
||||
import { Dropdown, Modal, Tooltip, Upload, Spin } from "antd";
|
||||
import { Dropdown, Modal, Tooltip, Upload, Spin, Popconfirm } from "antd";
|
||||
import { UploadOutlined } from "@ant-design/icons";
|
||||
import { StoryTemplateEntity } from "@/app/service/domain/Entities";
|
||||
import { useImageStoryServiceHook } from "@/app/service/Interaction/ImageStoryService";
|
||||
@ -30,6 +30,7 @@ import { createMovieProjectV1 } from "@/api/video_flow";
|
||||
import { useLoadScriptText, useUploadFile } from "@/app/service/domain/service";
|
||||
import { ActionButton } from "../common/ActionButton";
|
||||
import { HighlightEditor } from "../common/HighlightEditor";
|
||||
import GlobalLoad from "../common/GlobalLoad";
|
||||
|
||||
/**模板故事模式弹窗组件 */
|
||||
const RenderTemplateStoryMode = ({
|
||||
@ -55,7 +56,7 @@ const RenderTemplateStoryMode = ({
|
||||
} = useTemplateStoryServiceHook();
|
||||
|
||||
// 本地加载状态,用于 UI 反馈
|
||||
const [localLoading, setLocalLoading] = useState(false);
|
||||
const [localLoading, setLocalLoading] = useState(0);
|
||||
|
||||
// 组件挂载时获取模板列表
|
||||
useEffect(() => {
|
||||
@ -73,9 +74,19 @@ const RenderTemplateStoryMode = ({
|
||||
// 处理确认操作
|
||||
const handleConfirm = async () => {
|
||||
if (!selectedTemplate) return;
|
||||
|
||||
let timer = setInterval(() => {
|
||||
setLocalLoading((prev) => {
|
||||
if (prev >= 95) {
|
||||
clearInterval(timer);
|
||||
return 95;
|
||||
}
|
||||
return prev + 1;
|
||||
});
|
||||
}, 100);
|
||||
try {
|
||||
setLocalLoading(true);
|
||||
setLocalLoading(1);
|
||||
// 假性的增加进度条
|
||||
|
||||
const projectId = await actionStory();
|
||||
console.log("Story action created:", projectId);
|
||||
onClose();
|
||||
@ -86,7 +97,8 @@ const RenderTemplateStoryMode = ({
|
||||
console.error("Failed to create story action:", error);
|
||||
// 这里可以添加 toast 提示
|
||||
} finally {
|
||||
setLocalLoading(false);
|
||||
setLocalLoading(0);
|
||||
clearInterval(timer);
|
||||
}
|
||||
};
|
||||
|
||||
@ -119,7 +131,7 @@ const RenderTemplateStoryMode = ({
|
||||
onClick={() => handleTemplateSelect(template)}
|
||||
>
|
||||
<TemplateCard
|
||||
imageUrl={template.imageUrl[0]}
|
||||
imageUrl={template.image_url[0]}
|
||||
imageAlt={template.name}
|
||||
title={template.name}
|
||||
description={template.generateText}
|
||||
@ -150,7 +162,7 @@ const RenderTemplateStoryMode = ({
|
||||
className="relative w-full aspect-square rounded-xl overflow-hidden group cursor-pointer"
|
||||
>
|
||||
<img
|
||||
src={selectedTemplate.imageUrl[0]}
|
||||
src={selectedTemplate.image_url[0]}
|
||||
alt={selectedTemplate.name}
|
||||
className="w-full h-full object-cover transition-all duration-500 group-hover:scale-105 group-hover:rotate-1"
|
||||
/>
|
||||
@ -322,7 +334,7 @@ const RenderTemplateStoryMode = ({
|
||||
</div> */}
|
||||
<div className=" absolute bottom-0 right-0">
|
||||
<ActionButton
|
||||
isCreating={localLoading}
|
||||
isCreating={localLoading>0}
|
||||
handleCreateVideo={handleConfirm}
|
||||
icon={<Clapperboard className="w-5 h-5" />}
|
||||
/>
|
||||
@ -775,15 +787,21 @@ const PhotoStoryModal = ({
|
||||
uploadCharacterAvatarAndAnalyzeFeatures,
|
||||
} = useImageStoryServiceHook();
|
||||
const { loadingText } = useLoadScriptText(isLoading);
|
||||
const { uploadFile } = useUploadFile();
|
||||
const [localLoading, setLocalLoading] = useState(0);
|
||||
// 重置状态
|
||||
const handleClose = () => {
|
||||
resetImageStory();
|
||||
// resetImageStory();
|
||||
onClose();
|
||||
};
|
||||
const router = useRouter();
|
||||
// 处理图片上传
|
||||
const handleImageUpload = async () => {
|
||||
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) {
|
||||
@ -820,6 +838,35 @@ const PhotoStoryModal = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleAnalyzeImage = async () => {
|
||||
let timeout = 100
|
||||
let timer: NodeJS.Timeout;
|
||||
timer = setInterval(() => {
|
||||
setLocalLoading((prev) => {
|
||||
if (prev >= 95) {
|
||||
return 95;
|
||||
}
|
||||
return prev + 0.1;
|
||||
});
|
||||
}, timeout);
|
||||
try {
|
||||
await uploadAndAnalyzeImage();
|
||||
} finally {
|
||||
timeout=10
|
||||
clearInterval(timer);
|
||||
timer = setInterval(() => {
|
||||
setLocalLoading((prev) => {
|
||||
if (prev >= 100) {
|
||||
clearInterval(timer);
|
||||
return 0;
|
||||
}
|
||||
return prev + 1;
|
||||
});
|
||||
}, timeout);
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={isOpen}
|
||||
@ -836,7 +883,7 @@ const PhotoStoryModal = ({
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Spin spinning={isLoading} tip={loadingText}>
|
||||
<GlobalLoad show={localLoading>0} progress={localLoading}>
|
||||
<div className="rounded-2xl">
|
||||
{/* 弹窗头部 */}
|
||||
<div className="flex items-center gap-3 p-2 border-b border-white/[0.1]">
|
||||
@ -865,18 +912,33 @@ const PhotoStoryModal = ({
|
||||
alt="Story inspiration"
|
||||
className="w-full h-full object-cover rounded-lg"
|
||||
/>
|
||||
<Tooltip title="Clear all content !!! " placement="top">
|
||||
<Popconfirm
|
||||
title="Clear all content"
|
||||
description="Are you sure you want to clear all content? This action cannot be undone."
|
||||
onConfirm={() => {
|
||||
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",
|
||||
}}
|
||||
>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
resetImageStory();
|
||||
}}
|
||||
className="absolute top-1 right-1 w-4 h-4 bg-black/60 hover:bg-black/80 rounded-full flex items-center justify-center text-white/80 hover:text-white transition-colors"
|
||||
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"
|
||||
>
|
||||
<Trash2 className="w-2.5 h-2.5" />
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center text-white/60">
|
||||
@ -1035,7 +1097,7 @@ const PhotoStoryModal = ({
|
||||
>
|
||||
<ActionButton
|
||||
isCreating={isLoading}
|
||||
handleCreateVideo={uploadAndAnalyzeImage}
|
||||
handleCreateVideo={handleAnalyzeImage}
|
||||
icon={<Sparkles className="w-5 h-5" />}
|
||||
/>
|
||||
</Tooltip>
|
||||
@ -1055,7 +1117,7 @@ const PhotoStoryModal = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Spin>
|
||||
</GlobalLoad>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
138
components/common/GlobalLoad.tsx
Normal file
138
components/common/GlobalLoad.tsx
Normal file
@ -0,0 +1,138 @@
|
||||
import { Spin } from "antd";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface GlobalLoadProps {
|
||||
/** 是否显示加载状态 */
|
||||
show: boolean;
|
||||
/** 子元素内容 */
|
||||
children: ReactNode;
|
||||
/** 进度条数值 0-100,非必须 */
|
||||
progress?: number;
|
||||
/** 旋转圆环直径,非必须 */
|
||||
spinnerDiameter?: number;
|
||||
/** 进度条宽度,非必须 */
|
||||
progressWidth?: number;
|
||||
/** 是否显示旋转圆圈,默认显示 */
|
||||
showSpinner?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局加载组件,支持标签包裹形式渲染子元素
|
||||
* @param props - 组件属性
|
||||
* @returns 加载组件
|
||||
*/
|
||||
export default function GlobalLoad({
|
||||
show,
|
||||
children,
|
||||
progress,
|
||||
spinnerDiameter,
|
||||
progressWidth,
|
||||
showSpinner = true,
|
||||
}: GlobalLoadProps) {
|
||||
if (!show) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
// 自定义加载图标:组合两个组件
|
||||
const customIndicator = (
|
||||
<div
|
||||
data-alt="custom-loading-indicator"
|
||||
className="!w-max !h-max !flex flex-col items-center justify-center gap-4 -translate-x-1/2 -translate-y-1/2"
|
||||
>
|
||||
{showSpinner && <TailwindSpinner diameter={spinnerDiameter} />}
|
||||
{progress && <TailwindLinearLoader progress={progress} width={progressWidth} />}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div data-alt="loading-container" className="relative">
|
||||
<Spin spinning={true} tip="" indicator={customIndicator}>
|
||||
{children}
|
||||
</Spin>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tailwind CSS loading spinner component for global loading overlay.
|
||||
* @param diameter - Diameter of the spinner in pixels
|
||||
* @returns {JSX.Element} - Spinner visual.
|
||||
*/
|
||||
export const TailwindSpinner = ({ diameter = 50 }: { diameter?: number }) => {
|
||||
const radius = diameter / 2;
|
||||
|
||||
return (
|
||||
<div data-alt="loading-spinner" className="relative" style={{ width: `${diameter}px`, height: `${diameter}px` }}>
|
||||
{/* 主旋转圆环 */}
|
||||
<div
|
||||
className="w-full h-full animate-spin"
|
||||
style={{
|
||||
width: `${diameter}px`,
|
||||
height: `${diameter}px`,
|
||||
borderRadius: `${radius}px`,
|
||||
backgroundImage:
|
||||
"linear-gradient(rgb(186, 66, 255) 35%, rgb(0, 225, 255))",
|
||||
filter: "blur(1px)",
|
||||
boxShadow:
|
||||
"0px -5px 20px 0px rgb(186, 66, 255), 0px 5px 20px 0px rgb(0, 225, 255)",
|
||||
animation: "spinning82341 1.7s linear infinite",
|
||||
}}
|
||||
data-alt="spinner-main"
|
||||
/>
|
||||
{/* 背景模糊圆环 */}
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
width: `${diameter}px`,
|
||||
height: `${diameter}px`,
|
||||
borderRadius: `${radius}px`,
|
||||
backgroundColor: "rgb(36, 36, 36)",
|
||||
filter: "blur(10px)",
|
||||
}}
|
||||
data-alt="spinner-blur-bg"
|
||||
/>
|
||||
{/* 自定义动画关键帧 */}
|
||||
<style>
|
||||
{`
|
||||
@keyframes spinning82341 {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Tailwind CSS linear progress bar loader with animated light sweep.
|
||||
* @param progress - Progress value from 0 to 100
|
||||
* @param width - Width of the progress bar in pixels
|
||||
* @returns {JSX.Element} - Linear loader visual.
|
||||
*/
|
||||
export const TailwindLinearLoader = ({
|
||||
progress,
|
||||
width = 160
|
||||
}: {
|
||||
progress: number;
|
||||
width?: number;
|
||||
}) => (
|
||||
<div
|
||||
data-alt="linear-loader-container"
|
||||
className="relative h-[2px] bg-gray-200 dark:bg-gray-700 overflow-hidden rounded-full"
|
||||
style={{ width: `${width}px` }}
|
||||
>
|
||||
{/* Animated light sweep - position controlled by progress */}
|
||||
<div
|
||||
data-alt="linear-loader-light"
|
||||
className="absolute top-0 h-full w-[70px]"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(87deg, rgba(0, 0, 0, 0) 0%, rgb(0, 204, 255) 40%, rgb(0, 204, 255) 60%, rgba(0, 0, 0, 0) 100%)",
|
||||
left: `${progress - 10}%`,
|
||||
}}
|
||||
/>
|
||||
{/* Keyframes for light sweep - no longer needed since position is controlled by progress */}
|
||||
</div>
|
||||
);
|
||||
Loading…
x
Reference in New Issue
Block a user