- 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.
246 lines
6.9 KiB
TypeScript
246 lines
6.9 KiB
TypeScript
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>
|
||
);
|
||
};
|