diff --git a/client/packages/lowcoder/package.json b/client/packages/lowcoder/package.json index 779b51784..6af544f6e 100644 --- a/client/packages/lowcoder/package.json +++ b/client/packages/lowcoder/package.json @@ -52,6 +52,7 @@ "file-saver": "^2.0.5", "github-markdown-css": "^5.1.0", "hotkeys-js": "^3.8.7", + "html5-device-mockups": "^3.2.1", "immer": "^9.0.7", "less": "^4.1.3", "lodash": "^4.17.21", @@ -67,6 +68,7 @@ "react": "^18.2.0", "react-best-gradient-color-picker": "^3.0.10", "react-colorful": "^5.5.1", + "react-device-mockups": "^0.1.12", "react-documents": "^1.2.1", "react-dom": "^18.2.0", "react-draggable": "^4.4.4", diff --git a/client/packages/lowcoder/src/comps/editorState.tsx b/client/packages/lowcoder/src/comps/editorState.tsx index 81b462201..de87b38b7 100644 --- a/client/packages/lowcoder/src/comps/editorState.tsx +++ b/client/packages/lowcoder/src/comps/editorState.tsx @@ -35,6 +35,9 @@ export type CompInfo = { type SelectSourceType = "editor" | "leftPanel" | "addComp" | "rightPanel"; +export type DeviceType = "desktop" | "tablet" | "mobile"; +export type DeviceOrientation = "landscape" | "portrait"; + /** * All editor states are placed here and are still immutable. * @@ -56,6 +59,8 @@ export class EditorState { readonly selectedBottomResType?: BottomResTypeEnum; readonly showResultCompName: string = ""; readonly selectSource?: SelectSourceType; // the source of select type + readonly deviceType: DeviceType = "desktop"; + readonly deviceOrientation: DeviceOrientation = "portrait"; private readonly setEditorState: ( fn: (editorState: EditorState) => EditorState @@ -357,6 +362,14 @@ export class EditorState { this.changeState({ editorModeStatus: newEditorModeStatus }); } + setDeviceType(type: DeviceType) { + this.changeState({ deviceType: type }); + } + + setDeviceOrientation(orientation: DeviceOrientation) { + this.changeState({ deviceOrientation: orientation }); + } + setDragging(dragging: boolean) { if (this.isDragging === dragging) { return; diff --git a/client/packages/lowcoder/src/comps/hooks/screenInfoComp.tsx b/client/packages/lowcoder/src/comps/hooks/screenInfoComp.tsx index 64ca994f3..d813749a2 100644 --- a/client/packages/lowcoder/src/comps/hooks/screenInfoComp.tsx +++ b/client/packages/lowcoder/src/comps/hooks/screenInfoComp.tsx @@ -1,5 +1,6 @@ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { hookToStateComp } from "../generators/hookToComp"; +import { CanvasContainerID } from "@lowcoder-ee/index.sdk"; enum ScreenTypes { Mobile = 'mobile', @@ -19,9 +20,13 @@ type ScreenInfo = { } function useScreenInfo() { - const getDeviceType = () => { - if (window.innerWidth < 768) return ScreenTypes.Mobile; - if (window.innerWidth < 889) return ScreenTypes.Tablet; + const canvasContainer = document.getElementById(CanvasContainerID); + const canvas = document.getElementsByClassName('lowcoder-app-canvas')?.[0]; + const canvasWidth = canvasContainer?.clientWidth || canvas?.clientWidth; + + const getDeviceType = (width: number) => { + if (width < 768) return ScreenTypes.Mobile; + if (width < 889) return ScreenTypes.Tablet; return ScreenTypes.Desktop; } const getFlagsByDeviceType = (deviceType: ScreenType) => { @@ -41,16 +46,17 @@ function useScreenInfo() { const getScreenInfo = useCallback(() => { const { innerWidth, innerHeight } = window; - const deviceType = getDeviceType(); + const deviceType = getDeviceType(canvasWidth || window.innerWidth); const flags = getFlagsByDeviceType(deviceType); return { width: innerWidth, height: innerHeight, + canvasWidth, deviceType, ...flags }; - }, []) + }, [canvasWidth]) const [screenInfo, setScreenInfo] = useState({}); @@ -64,6 +70,10 @@ function useScreenInfo() { return () => window.removeEventListener('resize', updateScreenInfo); }, [ updateScreenInfo ]) + useEffect(() => { + updateScreenInfo(); + }, [canvasWidth]); + return screenInfo; } diff --git a/client/packages/lowcoder/src/pages/common/previewHeader.tsx b/client/packages/lowcoder/src/pages/common/previewHeader.tsx index 769d3c8fc..89aafa153 100644 --- a/client/packages/lowcoder/src/pages/common/previewHeader.tsx +++ b/client/packages/lowcoder/src/pages/common/previewHeader.tsx @@ -15,12 +15,17 @@ import ProfileDropdown from "./profileDropdown"; import { trans } from "i18n"; import { Logo } from "@lowcoder-ee/assets/images"; import { AppPermissionDialog } from "../../components/PermissionDialog/AppPermissionDialog"; -import { useMemo, useState } from "react"; +import { useContext, useMemo, useState } from "react"; import { getBrandingConfig } from "../../redux/selectors/configSelectors"; import { HeaderStartDropdown } from "./headerStartDropdown"; import { useParams } from "react-router"; import { AppPathParams } from "constants/applicationConstants"; import React from "react"; +import Segmented from "antd/es/segmented"; +import MobileOutlined from "@ant-design/icons/MobileOutlined"; +import TabletOutlined from "@ant-design/icons/TabletOutlined"; +import DesktopOutlined from "@ant-design/icons/DesktopOutlined"; +import { DeviceOrientation, DeviceType, EditorContext } from "@lowcoder-ee/comps/editorState"; const HeaderFont = styled.div<{ $bgColor: string }>` font-weight: 500; @@ -130,6 +135,7 @@ export function HeaderProfile(props: { user: User }) { const PreviewHeaderComp = () => { const params = useParams(); + const editorState = useContext(EditorContext); const user = useSelector(getUser); const application = useSelector(currentApplication); const isPublicApp = useSelector(isPublicApplication); @@ -197,9 +203,42 @@ const PreviewHeaderComp = () => { ); + + const headerMiddle = ( + <> + {/* Devices */} + + options={[ + { value: 'mobile', icon: }, + { value: 'tablet', icon: }, + { value: 'desktop', icon: }, + ]} + value={editorState.deviceType} + onChange={(value) => { + editorState.setDeviceType(value); + }} + /> + + {/* Orientation */} + {editorState.deviceType !== 'desktop' && ( + + options={[ + { value: 'portrait', label: "Portrait" }, + { value: 'landscape', label: "Landscape" }, + ]} + value={editorState.deviceOrientation} + onChange={(value) => { + editorState.setDeviceOrientation(value); + }} + /> + )} + + ); + return (
diff --git a/client/packages/lowcoder/src/pages/editor/editorView.tsx b/client/packages/lowcoder/src/pages/editor/editorView.tsx index bbdbfcb99..d26c5e8ba 100644 --- a/client/packages/lowcoder/src/pages/editor/editorView.tsx +++ b/client/packages/lowcoder/src/pages/editor/editorView.tsx @@ -30,10 +30,12 @@ import { UserGuideLocationState, } from "pages/tutorials/tutorialsConstant"; import React, { + ReactNode, Suspense, lazy, useCallback, useContext, + useEffect, useLayoutEffect, useMemo, useState, @@ -58,6 +60,7 @@ import EditorSkeletonView from "./editorSkeletonView"; import { getCommonSettings } from "@lowcoder-ee/redux/selectors/commonSettingSelectors"; import { isEqual, noop } from "lodash"; import { AppSettingContext, AppSettingType } from "@lowcoder-ee/comps/utils/appSettingContext"; +import Flex from "antd/es/flex"; // import { BottomSkeleton } from "./bottom/BottomContent"; const Header = lazy( @@ -251,6 +254,13 @@ export const EditorWrapper = styled.div` flex: 1 1 0; `; +const DeviceWrapperInner = styled(Flex)` + margin: 20px 0 0; + .screen { + overflow: auto; + } +`; + interface EditorViewProps { uiComp: InstanceType; preloadComp: InstanceType; @@ -298,6 +308,64 @@ const aggregationSiderItems = [ } ]; +const DeviceWrapper = ({ + deviceType, + deviceOrientation, + children, +}: { + deviceType: string, + deviceOrientation: string, + children: ReactNode, +}) => { + const [Wrapper, setWrapper] = useState(null); + + useEffect(() => { + const loadWrapper = async () => { + if (deviceType === "tablet") { + await import('html5-device-mockups/dist/device-mockups.min.css'); + const { IPad } = await import("react-device-mockups"); + setWrapper(() => IPad); + } else if (deviceType === "mobile") { + await import('html5-device-mockups/dist/device-mockups.min.css'); + const { IPhone7 } = await import("react-device-mockups"); + setWrapper(() => IPhone7); + } else { + setWrapper(() => null); + } + }; + + loadWrapper(); + }, [deviceType]); + + const deviceWidth = useMemo(() => { + if (deviceType === 'tablet' && deviceOrientation === 'portrait') { + return 700; + } + if (deviceType === 'tablet' && deviceOrientation === 'landscape') { + return 1000; + } + if (deviceType === 'mobile' && deviceOrientation === 'portrait') { + return 400; + } + if (deviceType === 'mobile' && deviceOrientation === 'landscape') { + return 800; + } + }, [deviceType, deviceOrientation]); + + if (!Wrapper) return <>{children}; + + return ( + + + {children} + + + ); +} + function EditorView(props: EditorViewProps) { const { uiComp } = props; const params = useParams(); @@ -416,6 +484,24 @@ function EditorView(props: EditorViewProps) { uiComp, ]); + const uiCompViewWrapper = useMemo(() => { + if (isViewMode) return uiComp.getView(); + + return ( + + {uiComp.getView()} + + ) + }, [ + uiComp, + isViewMode, + editorState.deviceType, + editorState.deviceOrientation, + ]); + // we check if we are on the public cloud const isLowCoderDomain = window.location.hostname === 'app.lowcoder.cloud'; const isLocalhost = window.location.hostname === 'localhost'; @@ -455,7 +541,7 @@ function EditorView(props: EditorViewProps) { {!hideBodyHeader && } - {uiComp.getView()} + {uiCompViewWrapper}
{hookCompViews} diff --git a/client/yarn.lock b/client/yarn.lock index 79dd8bcbe..b4842f23f 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -11455,6 +11455,13 @@ coolshapes-react@lowcoder-org/coolshapes-react: languageName: node linkType: hard +"html5-device-mockups@npm:^3.2.1": + version: 3.2.1 + resolution: "html5-device-mockups@npm:3.2.1" + checksum: abba0bccc6398313102a9365203092a7c0844879d1b0492168279c516c9462d2a7e016045be565bc183e3405a1ae4929402eaceb1952abdbf16f1580afa68df3 + languageName: node + linkType: hard + "http-cache-semantics@npm:^4.1.1": version: 4.1.1 resolution: "http-cache-semantics@npm:4.1.1" @@ -14159,6 +14166,7 @@ coolshapes-react@lowcoder-org/coolshapes-react: file-saver: ^2.0.5 github-markdown-css: ^5.1.0 hotkeys-js: ^3.8.7 + html5-device-mockups: ^3.2.1 http-proxy-middleware: ^2.0.6 immer: ^9.0.7 less: ^4.1.3 @@ -14175,6 +14183,7 @@ coolshapes-react@lowcoder-org/coolshapes-react: react: ^18.2.0 react-best-gradient-color-picker: ^3.0.10 react-colorful: ^5.5.1 + react-device-mockups: ^0.1.12 react-documents: ^1.2.1 react-dom: ^18.2.0 react-draggable: ^4.4.4 @@ -17672,6 +17681,18 @@ coolshapes-react@lowcoder-org/coolshapes-react: languageName: node linkType: hard +"react-device-mockups@npm:^0.1.12": + version: 0.1.12 + resolution: "react-device-mockups@npm:0.1.12" + peerDependencies: + html5-device-mockups: ^3.2.1 + prop-types: ^15.5.4 + react: ^15.0.0 || ^16.0.0 || ^17.0.0 + react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 738e969802c32810c2ca3ca3bd6c9bacf9b3d7adda0569c4f5c7fb1d68bab860ac7bb5a50aa2677d852143cb30ab8520e556c4dc7f53be154fd16ca08a9ba32c + languageName: node + linkType: hard + "react-documents@npm:^1.2.1": version: 1.2.1 resolution: "react-documents@npm:1.2.1" pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy