UNPKG

@blocklet/ui-react

Version:

Some useful front-end web components that can be used in Blocklets.

201 lines (186 loc) 5.42 kB
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, };