Feat: edit and view - support simulated Android device preview

This commit is contained in:
2024-03-22 16:54:43 +08:00
parent 3e34ae7616
commit 3ac302c14b
14 changed files with 298 additions and 14 deletions

View File

@@ -1,19 +1,30 @@
import { ChangeEvent } from 'react'
import '@/components/Playground/Output/Preview/render.scss'
import { COLOR_FONT_MAIN } from '@/constants/common.constants'
import iframeRaw from '@/components/Playground/Output/Preview/iframe.html?raw'
import HideScrollbar from '@/components/common/HideScrollbar'
interface RenderProps {
iframeKey: string
compiledCode: string
mobileMode?: boolean
}
interface IMessage {
type: 'LOADED' | 'ERROR' | 'UPDATE' | 'DONE'
type: 'LOADED' | 'ERROR' | 'UPDATE' | 'DONE' | 'SCALE'
msg: string
data: {
compiledCode: string
compiledCode?: string
zoom?: number
}
}
interface IDevice {
name: string
width: number
height: number
}
const getIframeUrl = (iframeRaw: string) => {
const shimsUrl = '//unpkg.com/es-module-shims/dist/es-module-shims.js'
// 判断浏览器是否支持esm 不支持esm就引入es-module-shims
@@ -29,9 +40,112 @@ const getIframeUrl = (iframeRaw: string) => {
const iframeUrl = getIframeUrl(iframeRaw)
const Render = ({ iframeKey, compiledCode }: RenderProps) => {
const Render = ({ iframeKey, compiledCode, mobileMode = false }: RenderProps) => {
const iframeRef = useRef<HTMLIFrameElement>(null)
const [loaded, setLoaded] = useState(false)
const [selectedDevice, setSelectedDevice] = useState('Pixel 7')
const [zoom, setZoom] = useState(1)
const [isRotate, setIsRotate] = useState(false)
const devices: IDevice[] = [
{
name: 'iPhone SE',
width: 375,
height: 667
},
{
name: 'iPhone XR',
width: 414,
height: 896
},
{
name: 'iPhone 12 Pro',
width: 390,
height: 844
},
{
name: 'iPhone 14 Pro Max',
width: 430,
height: 932
},
{
name: 'Pixel 7',
width: 412,
height: 915
},
{
name: 'Samsung Galaxy S8+',
width: 360,
height: 740
},
{
name: 'Samsung Galaxy S20 Ultra',
width: 412,
height: 915
},
{
name: 'iPad Mini',
width: 768,
height: 1024
},
{
name: 'iPad Air',
width: 820,
height: 1180
},
{
name: 'iPad Pro',
width: 1024,
height: 1366
},
{
name: 'Surface Pro 7',
width: 912,
height: 1368
},
{
name: 'Surface Duo',
width: 540,
height: 720
},
{
name: 'Galaxy Fold',
width: 280,
height: 653
},
{
name: 'Asus Zenbook Fold',
width: 853,
height: 1280
},
{
name: 'Samsung Galaxy A51/71',
width: 412,
height: 914
},
{
name: 'Nest Hub',
width: 1024,
height: 600
},
{
name: 'Nest Hub Max',
width: 1280,
height: 800
}
]
const handleOnChangeDevice = (e: ChangeEvent<HTMLSelectElement>) => {
setSelectedDevice(e.target.value)
}
const handleOnChangeZoom = (e: ChangeEvent<HTMLInputElement>) => {
setZoom(Number(e.target.value))
}
const handleOnRotateDevice = () => {
setIsRotate(!isRotate)
}
useEffect(() => {
if (loaded) {
@@ -43,9 +157,88 @@ const Render = ({ iframeKey, compiledCode }: RenderProps) => {
'*'
)
}
}, [compiledCode, loaded])
}, [loaded, compiledCode])
return (
useEffect(() => {
if (loaded) {
iframeRef.current?.contentWindow?.postMessage(
{
type: 'SCALE',
data: { zoom: zoom }
} as IMessage,
'*'
)
}
}, [loaded, zoom])
return mobileMode ? (
<>
<HideScrollbar
className={'mobile-mode-background'}
isShowVerticalScrollbar
isShowHorizontalScrollbar
autoHideWaitingTime={1000}
>
<div className={'mobile-mode-content'} style={{ zoom }}>
<div className={`device${isRotate ? ' rotate' : ''}`}>
<div className={`device-header${isRotate ? ' rotate' : ''}`} />
<div
className={`device-content${isRotate ? ' rotate' : ''}`}
style={{
width: isRotate
? devices.find((value) => value.name === selectedDevice)
?.height ?? 915
: devices.find((value) => value.name === selectedDevice)
?.width ?? 412,
height: isRotate
? devices.find((value) => value.name === selectedDevice)
?.width ?? 412
: devices.find((value) => value.name === selectedDevice)
?.height ?? 915
}}
>
<iframe
data-component={'playground-output-preview-render'}
key={iframeKey}
ref={iframeRef}
src={iframeUrl}
onLoad={() => setLoaded(true)}
sandbox="allow-downloads allow-forms allow-modals allow-scripts"
/>
</div>
<div className={`device-footer${isRotate ? ' rotate' : ''}`} />
</div>
</div>
</HideScrollbar>
<div className={'switch-device'}>
<IconOxygenMobile fill={COLOR_FONT_MAIN} />
<select value={selectedDevice} onChange={handleOnChangeDevice}>
{devices.map((value) => (
<option value={value.name}>{value.name}</option>
))}
</select>
<div className={'rotate-device'} title={'旋转屏幕'} onClick={handleOnRotateDevice}>
{isRotate ? (
<IconOxygenRotateRight fill={COLOR_FONT_MAIN} />
) : (
<IconOxygenRotateLeft fill={COLOR_FONT_MAIN} />
)}
</div>
</div>
<div className={'switch-zoom'}>
<IconOxygenZoom fill={COLOR_FONT_MAIN} />
<input
type={'range'}
min={0.5}
max={2}
step={0.1}
value={zoom}
onChange={handleOnChangeZoom}
/>
</div>
</>
) : (
<iframe
data-component={'playground-output-preview-render'}
key={iframeKey}

View File

@@ -36,6 +36,10 @@
document.body.appendChild(script);
URL.revokeObjectURL(oldSrc);
}
if (data?.type === "SCALE") {
document.getElementById("root").style.zoom = data.data.zoom
}
});
</script>
<script type="module" id="appSrc"></script>

View File

@@ -10,6 +10,7 @@ interface PreviewProps {
entryPoint: string
preExpansionCode?: string
postExpansionCode?: string
mobileMode?: boolean
}
const Preview = ({
@@ -18,7 +19,8 @@ const Preview = ({
importMap,
entryPoint,
preExpansionCode = '',
postExpansionCode = ''
postExpansionCode = '',
mobileMode = false
}: PreviewProps) => {
const [errorMsg, setErrorMsg] = useState('')
const [compiledCode, setCompiledCode] = useState('')
@@ -41,7 +43,7 @@ const Preview = ({
return (
<div data-component={'playground-preview'}>
<Render iframeKey={iframeKey} compiledCode={compiledCode} />
<Render iframeKey={iframeKey} compiledCode={compiledCode} mobileMode={mobileMode} />
{errorMsg && <div className={'playground-error-message'}>{errorMsg}</div>}
</div>
)

View File

@@ -1,8 +1,7 @@
[data-component=playground-preview] {
display: flex;
position: relative;
width: 100%;
height: 100%;
height: 0;
.playground-error-message {
position: absolute;
@@ -13,4 +12,4 @@
padding: 5px 10px;
font-size: 1.2em;
}
}
}

View File

@@ -1,6 +1,80 @@
@use '@/assets/css/constants' as constants;
[data-component=playground-output-preview-render] {
border: none;
height: 100%;
width: 100%;
flex: 1;
}
}
.mobile-mode-background {
background-color: rgba(204, 204, 204, 0.66);
.mobile-mode-content {
padding: 80px;
}
.device {
display: flex;
flex-direction: column;
background-color: #EEEFF2;
width: fit-content;
margin: 0 auto;
border-radius: 40px;
&.rotate {
flex-direction: row;
}
.device-header {
margin: 20px auto;
width: 60px;
height: 10px;
border-radius: 5px;
background-color: #C8C9CC;
&.rotate {
margin: auto 20px;
width: 10px;
height: 60px;
}
}
.device-content {
margin: 0 10px;
background-color: white;
&.rotate {
margin: 10px 0;
}
}
.device-footer {
margin: 20px auto;
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #C8C9CC;
&.rotate {
margin: auto 20px;
}
}
}
}
.switch-device, .switch-zoom {
display: flex;
position: absolute;
top: 10px;
align-items: center;
gap: 4px;
}
.switch-device {
left: 10px;
}
.switch-zoom {
right: 10px;
}