Files
shanghai-residences/src/components/Modal.tsx
T
zhongyidian 4ce7520066 feat: enhance modal functionality and integrate new components
- Added keyframes and animation for a smooth scale-in effect in the Tailwind configuration.
- Updated Image component to improve quality handling and style definitions.
- Introduced WordViewer and WordViewerModal components for document viewing capabilities.
- Enhanced Modal component to support embedded mode with customizable position and dimensions.
- Integrated IframeModal across various pages for user assistance and content display.
- Updated Content and Menu pages to utilize new components and improve user interaction.
- Added utility function to retrieve public paths for resources.
2025-10-29 00:18:06 +08:00

246 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import clsx from "clsx";
import { useState } from "react";
type DisplayMode = "modal" | "embedded";
type EmbeddedPosition =
| "bottom-right"
| "bottom-left"
| "top-right"
| "top-left";
interface ModalProps {
isOpen: boolean;
onClose: () => void;
children?: React.ReactNode;
className?: string;
mode?: DisplayMode;
position?: EmbeddedPosition;
width?: string | number;
height?: string | number;
showMask?: boolean;
}
export const Modal = ({
isOpen,
onClose,
children,
className,
mode = "modal",
position = "bottom-right",
width,
height,
showMask = true,
}: ModalProps) => {
if (!isOpen) return null;
// 嵌入式客服对话框模式
if (mode === "embedded") {
const getPosition = () => {
switch (position) {
case "bottom-right":
return "bottom-6 right-6";
case "bottom-left":
return "bottom-6 left-6";
case "top-right":
return "top-6 right-6";
case "top-left":
return "top-6 left-6";
}
};
const defaultWidth = width || "400px";
const defaultHeight = height || "600px";
return (
<div
className={clsx(
"fixed z-[9999] bg-transparent overflow-hidden border border-transparent",
"transition-all duration-300 ease-out",
"animate-scale-in",
getPosition(),
className
)}
style={{
width: defaultWidth,
height: defaultHeight,
maxHeight: "calc(100vh - 3rem)",
maxWidth: "calc(100vw - 3rem)",
}}
>
{/* 关闭/最小化按钮 */}
<button
onClick={onClose}
className="absolute top-3 right-3 z-10 w-7 h-7 flex items-center justify-center bg-white hover:bg-gray-100 rounded-full transition-colors shadow-sm border border-gray-200"
title="最小化"
>
<span className="text-gray-500 text-lg font-bold leading-none">
−
</span>
</button>
{/* 内容 */}
<div className="h-full w-full">{children}</div>
</div>
);
}
// 传统弹窗模式
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* 背景遮罩 */}
{showMask && (
<div
className="absolute inset-0 bg-black/80 backdrop-blur-sm"
onClick={onClose}
/>
)}
{/* 弹窗内容 */}
<div
className={clsx(
"relative bg-white rounded-lg shadow-xl max-w-7xl max-h-[100vh] w-full mx-4",
className
)}
style={{
width: width,
height: height,
}}
>
{/* 关闭按钮 */}
<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;
mode?: DisplayMode;
position?: EmbeddedPosition;
width?: string | number;
height?: string | number;
showMask?: boolean;
}
export const IframeModal = ({
isOpen,
onClose,
url,
title,
mode = "modal",
position = "bottom-right",
width,
height,
showMask = true,
}: 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}
mode={mode}
position={position}
width={width}
height={height}
showMask={showMask}
className={mode === "modal" ? "max-w-5xl" : ""}
>
<div className="h-full flex flex-col">
{title && mode === "modal" && (
<div className="px-6 py-4 border-b border-transparent">
<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-transparent">
<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-transparent">
<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 bg-transparent"
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>
);
};