feat: add avatar interaction and modal for user assistance

- Introduced an avatar element that opens a modal when clicked, providing users with assistance.
- Integrated IframeModal component to display external content within the modal.
- Enhanced user experience with hover effects on the avatar.
This commit is contained in:
zhongyidian committed 2025-10-26 13:15:41 +08:00
1 parent 2d66abc610
commit 13dd84a8a7
2 files changed
+175

No files matched your search

+143
View File
@@ -0,0 +1,143 @@
import clsx from "clsx";
import { useState } from "react";
interface ModalProps {
isOpen: boolean;
onClose: () => void;
children?: React.ReactNode;
className?: string;
}
export const Modal = ({ isOpen, onClose, children, className }: ModalProps) => {
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* 背景遮罩 */}
<div className="absolute inset-0 bg-black/80 backdrop-blur-sm" />
{/* 弹窗内容 */}
<div
className={clsx(
"relative bg-white rounded-lg shadow-xl max-w-7xl max-h-[100vh] w-full mx-4",
className
)}
>
{/* 关闭按钮 */}
<button
onClick={onClose}
className="absolute top-4 right-4 z-10 w-8 h-8 flex items-center justify-center bg-gray-200 hover:bg-gray-300 rounded-full transition-colors"
>
<span className="text-gray-600 text-xl font-bold">×</span>
</button>
{/* 弹窗内容 */}
<div className="h-full">{children}</div>
</div>
</div>
);
};
interface IframeModalProps {
isOpen: boolean;
onClose: () => void;
url: string;
title?: string;
}
export const IframeModal = ({
isOpen,
onClose,
url,
title,
}: IframeModalProps) => {
const [hasError, setHasError] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [errorType, setErrorType] = useState<
"network" | "security" | "unknown"
>("unknown");
const handleIframeError = () => {
setHasError(true);
setIsLoading(false);
setErrorType("network");
};
const handleIframeLoad = () => {
setIsLoading(false);
setHasError(false);
// 监听iframe内部的错误
const iframe = document.querySelector("iframe");
if (iframe) {
try {
iframe.contentWindow?.addEventListener("error", (event) => {
console.error("Iframe内部错误:", event);
if (
event.message?.includes("VideoDecoder") ||
event.message?.includes("WebCodecs")
) {
setErrorType("security");
setHasError(true);
}
});
} catch {
console.log("无法访问iframe内容,这是正常的跨域限制");
}
}
};
return (
<Modal isOpen={isOpen} onClose={onClose} className="max-w-5xl">
<div className="h-[90vh] flex flex-col">
{title && (
<div className="px-6 py-4 border-b border-gray-200">
<h3 className="text-lg font-semibold text-gray-800">{title}</h3>
</div>
)}
<div className="flex-1 relative">
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-gray-50">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-4"></div>
<p className="text-gray-600">正在加载...</p>
</div>
</div>
)}
{hasError ? (
<div className="absolute inset-0 flex items-center justify-center bg-gray-50">
<div className="text-center">
<div className="text-red-500 text-6xl mb-4">⚠️</div>
<h3 className="text-lg font-semibold text-gray-800 mb-2">
{errorType === "security" ? "页面功能受限" : "页面加载失败"}
</h3>
<p className="text-gray-600 mb-4">
{errorType === "security"
? "该页面使用了高级视频处理功能,在嵌入模式下可能无法正常工作"
: "由于安全策略限制,无法在此窗口中显示该页面"}
</p>
<p className="text-sm text-gray-500">
{errorType === "security"
? "建议在支持的浏览器中直接访问该页面"
: "请检查网络连接或稍后重试"}
</p>
</div>
</div>
) : (
<iframe
src={url}
className="w-full h-full border-0"
title={title || "嵌入页面"}
allow="microphone; camera; autoplay; fullscreen; encrypted-media; picture-in-picture; web-share; accelerometer; gyroscope; magnetometer; payment; usb; serial; bluetooth; xr-spatial-tracking"
referrerPolicy="no-referrer-when-downgrade"
loading="lazy"
onError={handleIframeError}
onLoad={handleIframeLoad}
/>
)}
</div>
</div>
</Modal>
);
};
+32
View File
@@ -1,4 +1,5 @@
import { BackgroundImage, Image } from "@/components"; import { BackgroundImage, Image } from "@/components";
import { IframeModal } from "@/components/Modal";
import { getStaticImage } from "@/utils"; import { getStaticImage } from "@/utils";
import clsx from "clsx"; import clsx from "clsx";
import { useState } from "react"; import { useState } from "react";
@@ -177,11 +178,20 @@ const TabContent = ({
export default function PublicityPage() { export default function PublicityPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const [isModalOpen, setIsModalOpen] = useState(false);
const changeTab = (id: number) => { const changeTab = (id: number) => {
console.log(id); console.log(id);
}; };
const handleAvatarClick = () => {
setIsModalOpen(true);
};
const handleCloseModal = () => {
setIsModalOpen(false);
};
return ( return (
<BackgroundImage <BackgroundImage
src={getStaticImage("content/内页底图02.png")} src={getStaticImage("content/内页底图02.png")}
@@ -200,8 +210,30 @@ export default function PublicityPage() {
}} }}
/> />
{/* 数字人头像 */}
<div
className="flex flex-col items-center absolute right-4 bottom-2 cursor-pointer hover:scale-105 transition-all duration-300 z-10"
onClick={handleAvatarClick}
>
<Image
src={getStaticImage("publicity/数字人-头像框.png")}
alt="数字人头像"
/>
<span className="text-white text-2xl font-bold tracking-wide">
点击我随时为你解答
</span>
</div>
<Tab changeTab={changeTab} /> <Tab changeTab={changeTab} />
</div> </div>
{/* 弹窗 */}
<IframeModal
isOpen={isModalOpen}
onClose={handleCloseModal}
url="https://human-train.lkz.fit/ad905c35-886a-4abb-98c0-e7f894d86b4c/"
title="数字人助手"
/>
</BackgroundImage> </BackgroundImage>
); );
} }