@blocklet/ui-react
Version:
Some useful front-end web components that can be used in Blocklets.
201 lines (186 loc) • 5.42 kB
JSX
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
import { Box, CircularProgress, Dialog, Typography, useMediaQuery, useTheme } from '@mui/material';
import merge from 'lodash/merge';
import PropTypes from 'prop-types';
import { useEffect, useRef, useState } from 'react';
import { joinURL, withQuery } from 'ufo';
const DEFAULT_WIZARD_PATH = '/.well-known/service/onboard/bind-account';
export default function WizardModal({
onFinished = () => {},
show = false,
onChangeVisible = () => {},
loadingText = '',
defaultPath = DEFAULT_WIZARD_PATH,
...props
}) {
const [open, setOpen] = useState(show);
const [loaded, setLoaded] = useState(false);
const [currentUrl, setCurrentUrl] = useState(() => {
// 从 localStorage 恢复上次的 URL
const savedUrl = localStorage.getItem('wizard-current-url');
if (savedUrl?.includes('/.well-known/service/onboard')) {
return savedUrl;
}
return defaultPath;
});
const onFinishedRef = useRef(onFinished);
const handleCloseRef = useRef();
const iframeRef = useRef(null);
const { locale } = useLocaleContext();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
onFinishedRef.current = onFinished;
handleCloseRef.current = () => {
if (iframeRef.current?.contentWindow) {
try {
const url = new URL(iframeRef.current.contentWindow.location.href);
const savedUrl = url.pathname;
localStorage.setItem('wizard-current-url', savedUrl);
setCurrentUrl(savedUrl);
} catch (e) {
setCurrentUrl(defaultPath);
console.warn('Failed to save wizard URL:', e);
}
}
localStorage.setItem('wizard-completed', 'true');
setOpen(false);
onChangeVisible(false);
};
useEffect(() => {
if (show !== open) {
setOpen(show);
}
}, [show]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
if (!open && loaded) {
setLoaded(false);
}
}, [open]); // eslint-disable-line react-hooks/exhaustive-deps
// 处理 iframe 消息
useEffect(() => {
const listener = (event) => {
// 只处理来自同源的消息
if (event.origin !== window.location.origin) {
return;
}
const { type, data } = event.data || {};
switch (type) {
case 'wizard.loaded':
setLoaded(true);
break;
case 'wizard.finished': {
setOpen(false);
// 完成后重置为默认 URL
setCurrentUrl(defaultPath);
localStorage.removeItem('wizard-current-url');
localStorage.setItem('wizard-completed', 'true');
onFinishedRef.current?.(data);
break;
}
case 'wizard.close': {
handleCloseRef.current();
break;
}
default:
break;
}
};
window.addEventListener('message', listener);
return () => {
window.removeEventListener('message', listener);
};
}, []); // eslint-disable-line react-hooks/exhaustive-deps
// 控制弹窗显示
useEffect(() => {
const wizardCompleted = localStorage.getItem('wizard-completed');
if (!wizardCompleted) {
setOpen(true);
}
}, []);
if (!open) {
return null;
}
const src = withQuery(joinURL(window.location.origin, currentUrl), {
locale,
});
return (
<Dialog
id="wizard-dialog"
open={open}
onClose={() => handleCloseRef.current()}
fullWidth
maxWidth={isMobile ? false : 'md'}
fullScreen={isMobile}
{...props}
slotProps={merge(
{},
{
paper: {
sx: {
margin: 0,
borderRadius: 0,
position: 'relative',
overflow: 'hidden',
...(isMobile
? { borderRadius: 0 }
: {
borderRadius: 1,
height: '720px',
}),
},
},
},
props.slotProps
)}
sx={{
'& .MuiBackdrop-root': {
backgroundColor: 'rgba(0, 0, 0, 0.5)',
},
...props.sx,
}}>
<iframe
ref={iframeRef}
id="wizard-iframe"
src={src}
title="Setup Wizard"
style={{
width: '100%',
height: '100%',
border: 0,
padding: 0,
margin: 0,
opacity: loaded ? 1 : 0,
transition: 'opacity 0.3s ease-in-out',
}}
onLoad={() => setLoaded(true)}
/>
{loaded ? null : (
<Box
sx={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
bgcolor: 'background.paper',
}}>
<Box sx={{ display: 'flex', alignItems: 'center', flexDirection: 'column', gap: 1 }}>
<CircularProgress />
{typeof loadingText === 'string' ? <Typography variant="body1">{loadingText}</Typography> : loadingText}
</Box>
</Box>
)}
</Dialog>
);
}
WizardModal.propTypes = {
onFinished: PropTypes.func,
show: PropTypes.bool,
onChangeVisible: PropTypes.func,
loadingText: PropTypes.node,
defaultPath: PropTypes.string,
...Dialog.propTypes,
};