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:
1 parent
2d66abc610
commit
13dd84a8a7
2 files changed
+175
No files matched your search
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user