加载样式的优化

This commit is contained in:
海龙 2025-08-20 23:39:15 +08:00
parent 140448c3a1
commit d13f2a3363
4 changed files with 234 additions and 24 deletions

View File

@ -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:""
}

View File

@ -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 */

View File

@ -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>
);
};

View 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>
);