├── docs ├── en │ ├── case.md │ └── index.md ├── static │ └── img │ │ ├── framework.png │ │ └── project-layer.png └── zh │ ├── case.md │ └── index.md ├── src ├── controller │ ├── const.ts │ ├── code │ │ ├── index.ts │ │ └── app-storage.ts │ ├── react │ │ ├── index.ts │ │ └── container.tsx │ ├── browser │ │ ├── const.ts │ │ ├── index.ts │ │ ├── event.ts │ │ └── document.ts │ ├── util │ │ ├── index.ts │ │ └── proxy.ts │ ├── index.ts │ ├── keyboard-event.ts │ └── service │ │ ├── app.ts │ │ ├── project.ts │ │ ├── history.ts │ │ ├── node.ts │ │ └── page.ts ├── pages │ ├── App │ │ ├── components │ │ │ ├── Drawer │ │ │ │ ├── component │ │ │ │ │ └── index.tsx │ │ │ │ ├── attribute │ │ │ │ │ ├── components │ │ │ │ │ │ ├── Display │ │ │ │ │ │ │ ├── img │ │ │ │ │ │ │ │ ├── wrap.png │ │ │ │ │ │ │ │ ├── nowrap.png │ │ │ │ │ │ │ │ ├── stretch.png │ │ │ │ │ │ │ │ ├── baseline.png │ │ │ │ │ │ │ │ ├── flex-end.png │ │ │ │ │ │ │ │ ├── flex-start.png │ │ │ │ │ │ │ │ ├── item-end.png │ │ │ │ │ │ │ │ ├── item-start.png │ │ │ │ │ │ │ │ ├── item-center.png │ │ │ │ │ │ │ │ ├── space-around.png │ │ │ │ │ │ │ │ ├── piccenter-fill.png │ │ │ │ │ │ │ │ └── space-between.png │ │ │ │ │ │ │ ├── index.module.scss │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ └── flex.tsx │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ ├── className.tsx │ │ │ │ │ │ ├── content.tsx │ │ │ │ │ │ ├── component.tsx │ │ │ │ │ │ ├── width.tsx │ │ │ │ │ │ └── height.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── index.module.scss │ │ │ │ │ ├── config.tsx │ │ │ │ │ ├── base-attribute.tsx │ │ │ │ │ └── style.tsx │ │ │ │ ├── css-edit.tsx │ │ │ │ ├── index.module.scss │ │ │ │ ├── index.tsx │ │ │ │ └── component-edit.tsx │ │ │ ├── Content │ │ │ │ ├── Header │ │ │ │ │ ├── index.module.scss │ │ │ │ │ ├── component │ │ │ │ │ │ ├── code │ │ │ │ │ │ │ ├── base-info.tsx │ │ │ │ │ │ │ ├── index.module.scss │ │ │ │ │ │ │ ├── util │ │ │ │ │ │ │ │ ├── template.ts │ │ │ │ │ │ │ │ └── index.tsx │ │ │ │ │ │ │ ├── code-edit.tsx │ │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ │ └── config.tsx │ │ │ │ │ │ ├── preview.tsx │ │ │ │ │ │ └── size.tsx │ │ │ │ │ ├── index.tsx │ │ │ │ │ ├── keep.tsx │ │ │ │ │ ├── Operation.tsx │ │ │ │ │ └── new-build.tsx │ │ │ │ ├── Body │ │ │ │ │ ├── index.module.scss │ │ │ │ │ ├── container.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── index.module.scss │ │ │ ├── Sider │ │ │ │ ├── Menu │ │ │ │ │ └── index.tsx │ │ │ │ ├── index.module.scss │ │ │ │ └── index.tsx │ │ │ └── Header │ │ │ │ ├── index.module.scss │ │ │ │ ├── components │ │ │ │ ├── Home │ │ │ │ │ ├── index.module.scss │ │ │ │ │ └── index.tsx │ │ │ │ └── Project │ │ │ │ │ ├── index.module.scss │ │ │ │ │ └── index.tsx │ │ │ │ └── index.tsx │ │ ├── slider-components │ │ │ ├── index.tsx │ │ │ ├── Layout │ │ │ │ ├── index.module.scss │ │ │ │ ├── index.tsx │ │ │ │ └── const.ts │ │ │ ├── NodeTree │ │ │ │ ├── index.module.scss │ │ │ │ └── index.tsx │ │ │ ├── Component │ │ │ │ ├── index.module.scss │ │ │ │ ├── index.tsx │ │ │ │ └── const.ts │ │ │ └── History │ │ │ │ ├── index.module.scss │ │ │ │ └── index.tsx │ │ ├── index.tsx │ │ └── config.tsx │ ├── index.tsx │ └── components │ │ ├── Collapse │ │ ├── index.module.scss │ │ └── index.tsx │ │ └── Visible │ │ └── index.tsx ├── static │ └── img │ │ ├── forward.png │ │ └── revoke.png ├── theme │ └── _colors.scss ├── setupTests.ts ├── model │ ├── app.ts │ ├── history.ts │ ├── project.ts │ ├── page.ts │ ├── node.ts │ └── index.ts ├── react-app-env.d.ts ├── reportWebVitals.ts ├── const │ ├── index.ts │ └── container.ts ├── index.css ├── index.tsx ├── context │ └── index.tsx ├── project-config.ts └── util │ └── index.ts ├── public ├── favicon.ico ├── logo192.png ├── logo512.png ├── robots.txt ├── manifest.json └── index.html ├── config-overrides.js ├── .gitignore ├── .vscode └── settings.json ├── tsconfig.json ├── .github └── workflows │ └── publish-website.yml ├── prettier.config.js ├── LICENSE ├── package.json └── README.md /docs/en/case.md: -------------------------------------------------------------------------------- 1 | ## Case 2 | -------------------------------------------------------------------------------- /src/controller/const.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /src/pages/App/components/Drawer/component/index.tsx: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import App from "./App"; 2 | 3 | export { App }; 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loo41/visual-layout/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loo41/visual-layout/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loo41/visual-layout/HEAD/public/logo512.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/controller/code/index.ts: -------------------------------------------------------------------------------- 1 | import AppStorage from './app-storage'; 2 | 3 | export { AppStorage }; 4 | -------------------------------------------------------------------------------- /src/static/img/forward.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loo41/visual-layout/HEAD/src/static/img/forward.png -------------------------------------------------------------------------------- /src/static/img/revoke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loo41/visual-layout/HEAD/src/static/img/revoke.png -------------------------------------------------------------------------------- /docs/static/img/framework.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loo41/visual-layout/HEAD/docs/static/img/framework.png -------------------------------------------------------------------------------- /docs/static/img/project-layer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loo41/visual-layout/HEAD/docs/static/img/project-layer.png -------------------------------------------------------------------------------- /src/controller/react/index.ts: -------------------------------------------------------------------------------- 1 | import { Container, render } from './container'; 2 | 3 | export { Container, render }; 4 | -------------------------------------------------------------------------------- /src/controller/browser/const.ts: -------------------------------------------------------------------------------- 1 | export enum NODE_TYPE { 2 | ELEMENT = 1, 3 | ATTRIBUTE = 2, 4 | TEXT = 3, 5 | COMMENT = 8, 6 | DOCUMENT = 9, 7 | } 8 | -------------------------------------------------------------------------------- /src/pages/App/components/Content/Header/index.module.scss: -------------------------------------------------------------------------------- 1 | .inputNumbers { 2 | display: flex; 3 | height: 50px; 4 | align-items: flex-end; 5 | .close { 6 | margin: 0 10px; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/pages/App/components/Drawer/attribute/components/Display/img/wrap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loo41/visual-layout/HEAD/src/pages/App/components/Drawer/attribute/components/Display/img/wrap.png -------------------------------------------------------------------------------- /src/pages/App/components/Drawer/attribute/components/Display/img/nowrap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loo41/visual-layout/HEAD/src/pages/App/components/Drawer/attribute/components/Display/img/nowrap.png -------------------------------------------------------------------------------- /src/pages/App/components/Drawer/attribute/components/Display/img/stretch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loo41/visual-layout/HEAD/src/pages/App/components/Drawer/attribute/components/Display/img/stretch.png -------------------------------------------------------------------------------- /src/pages/App/components/Drawer/attribute/components/Display/img/baseline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loo41/visual-layout/HEAD/src/pages/App/components/Drawer/attribute/components/Display/img/baseline.png -------------------------------------------------------------------------------- /src/pages/App/components/Drawer/attribute/components/Display/img/flex-end.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loo41/visual-layout/HEAD/src/pages/App/components/Drawer/attribute/components/Display/img/flex-end.png -------------------------------------------------------------------------------- /src/pages/App/components/Drawer/attribute/components/Display/img/flex-start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loo41/visual-layout/HEAD/src/pages/App/components/Drawer/attribute/components/Display/img/flex-start.png -------------------------------------------------------------------------------- /src/pages/App/components/Drawer/attribute/components/Display/img/item-end.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loo41/visual-layout/HEAD/src/pages/App/components/Drawer/attribute/components/Display/img/item-end.png -------------------------------------------------------------------------------- /src/pages/App/components/Drawer/attribute/components/Display/img/item-start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loo41/visual-layout/HEAD/src/pages/App/components/Drawer/attribute/components/Display/img/item-start.png -------------------------------------------------------------------------------- /src/pages/App/components/Drawer/attribute/components/Display/img/item-center.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loo41/visual-layout/HEAD/src/pages/App/components/Drawer/attribute/components/Display/img/item-center.png -------------------------------------------------------------------------------- /src/pages/App/components/Drawer/attribute/components/Display/img/space-around.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loo41/visual-layout/HEAD/src/pages/App/components/Drawer/attribute/components/Display/img/space-around.png -------------------------------------------------------------------------------- /src/pages/App/components/Drawer/attribute/components/Display/img/piccenter-fill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loo41/visual-layout/HEAD/src/pages/App/components/Drawer/attribute/components/Display/img/piccenter-fill.png -------------------------------------------------------------------------------- /src/pages/App/components/Drawer/attribute/components/Display/img/space-between.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loo41/visual-layout/HEAD/src/pages/App/components/Drawer/attribute/components/Display/img/space-between.png -------------------------------------------------------------------------------- /src/controller/browser/index.ts: -------------------------------------------------------------------------------- 1 | import Doc from './document'; 2 | import DocEvent from './event'; 3 | 4 | export enum EventType { 5 | layout = 'layout', 6 | container = 'container', 7 | } 8 | 9 | export { Doc, DocEvent }; 10 | -------------------------------------------------------------------------------- /src/pages/App/components/Drawer/attribute/components/index.tsx: -------------------------------------------------------------------------------- 1 | import Width from './width'; 2 | import Height from './height'; 3 | import Display from './Display'; 4 | import Component from './component'; 5 | 6 | export { Width, Height, Display, Component }; 7 | -------------------------------------------------------------------------------- /src/theme/_colors.scss: -------------------------------------------------------------------------------- 1 | $colors: ( 2 | border: #f0f0f0, 3 | primary: #000000, 4 | disabled: #bfbfbf, 5 | background: #f5f5f5, 6 | text: #595959, 7 | status: rgb(24, 144, 255), 8 | ); 9 | 10 | .disable-text { 11 | user-select: none; 12 | } 13 | -------------------------------------------------------------------------------- /src/pages/App/slider-components/index.tsx: -------------------------------------------------------------------------------- 1 | import LayoutComponent from './Layout'; 2 | import History from './History'; 3 | import Component from './Component'; 4 | import NodeTree from './NodeTree'; 5 | 6 | export { LayoutComponent, History, Component, NodeTree }; 7 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /config-overrides.js: -------------------------------------------------------------------------------- 1 | const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin'); 2 | 3 | module.exports = function override(config, env) { 4 | config.plugins.push( 5 | new MonacoWebpackPlugin({ 6 | languages: ['json', 'javascript', 'css', 'less', 'scss', 'html'], 7 | }), 8 | ); 9 | return config; 10 | }; 11 | -------------------------------------------------------------------------------- /src/model/app.ts: -------------------------------------------------------------------------------- 1 | import { Components, ProjectService } from 'src/controller'; 2 | 3 | export default class App { 4 | public static _idx: number = 1; 5 | public project!: ProjectService; 6 | public static components: Components = new Map(); 7 | 8 | static get idx() { 9 | return App._idx++; 10 | } 11 | 12 | static set idx(idx: number) { 13 | App._idx = idx; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/pages/components/Collapse/index.module.scss: -------------------------------------------------------------------------------- 1 | @import 'src/theme/_colors.scss'; 2 | 3 | .header { 4 | display: flex; 5 | align-items: center; 6 | cursor: pointer; 7 | background-color: map-get($colors, 'background'); 8 | height: 40px; 9 | padding: 5px; 10 | border-radius: 5px; 11 | margin: 5px 0; 12 | .icon { 13 | width: 25px; 14 | } 15 | .content { 16 | flex: 1; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/pages/App/slider-components/Layout/index.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | padding: 10px; 4 | height: 100%; 5 | overflow: auto; 6 | .layoutWarper { 7 | width: 100%; 8 | display: flex; 9 | flex-wrap: wrap; 10 | margin: 10px 0 10px 10px; 11 | .item { 12 | width: 100%; 13 | min-height: 100px; 14 | margin-right: 10px; 15 | margin-top: 10px; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/pages/App/components/Drawer/attribute/index.tsx: -------------------------------------------------------------------------------- 1 | import { PageService } from 'src/controller'; 2 | import BaseAttribute from './base-attribute'; 3 | import StyleComponent from './style'; 4 | 5 | const Attribute: React.FC<{ page: PageService }> = ({ page }) => { 6 | return ( 7 | <> 8 | 9 | 10 | 11 | ); 12 | }; 13 | 14 | export default Attribute; 15 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.css' { 4 | const classes: { readonly [key: string]: string }; 5 | export default classes; 6 | } 7 | 8 | declare module '*.scss' { 9 | const classes: { readonly [key: string]: string }; 10 | export default classes; 11 | } 12 | 13 | declare module '*.less' { 14 | const classes: { readonly [key: string]: string }; 15 | export default classes; 16 | } 17 | -------------------------------------------------------------------------------- /src/pages/components/Visible/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | const Visible = ({ 4 | children, 5 | }: { 6 | children: ({ 7 | visible, 8 | setVisible, 9 | }: { 10 | visible: boolean; 11 | setVisible: (visible: boolean) => void; 12 | }) => React.ReactElement; 13 | }) => { 14 | const [visible, setVisible] = useState(false); 15 | 16 | return children({ visible, setVisible }); 17 | }; 18 | 19 | export default Visible; 20 | -------------------------------------------------------------------------------- /src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /src/const/index.ts: -------------------------------------------------------------------------------- 1 | const DragEleId = 'application/layout'; 2 | 3 | const SelectStyle = [ 4 | { 5 | key: 'border', 6 | value: '1px solid #1890ff', 7 | }, 8 | ]; 9 | 10 | const PreviewStyle = [ 11 | { 12 | key: 'border', 13 | value: '1px dashed #000000', 14 | title: '虚线边框', 15 | isCanUse: true, 16 | }, 17 | { 18 | key: 'padding', 19 | value: '5px', 20 | title: '外边距', 21 | isCanUse: true, 22 | }, 23 | ]; 24 | export { DragEleId, SelectStyle, PreviewStyle }; 25 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'lucida grande', 'lucida sans unicode', lucida, helvetica, 3 | 'Hiragino Sans GB', 'MicrosoftYaHei', 'WenQuanYi Micro Hei', sans-serif; 4 | } 5 | 6 | ::-webkit-scrollbar { 7 | background-color: white; 8 | border-radius: 10px; 9 | width: 3px; 10 | height: 3px; 11 | } 12 | 13 | ::-webkit-scrollbar-thumb { 14 | border-radius: 10px; 15 | background: #bfbfbf; 16 | } 17 | 18 | ::-webkit-scrollbar-track { 19 | border-radius: 10px; 20 | background: white; 21 | } 22 | -------------------------------------------------------------------------------- /src/pages/App/components/Content/Body/index.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | min-height: 100%; 3 | min-width: 100%; 4 | background-color: rgb(240, 242, 245); 5 | padding: 20px; 6 | display: flex; 7 | overflow: auto; 8 | transform-origin: 0 0; 9 | 10 | .canvas { 11 | display: flex; 12 | justify-content: center; 13 | align-items: center; 14 | flex: 1; 15 | } 16 | } 17 | 18 | .body { 19 | margin: 20px; 20 | flex: 1; 21 | overflow: auto; 22 | .innerBody { 23 | height: 100%; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/pages/App/components/Sider/Menu/index.tsx: -------------------------------------------------------------------------------- 1 | import styles from '../index.module.scss'; 2 | import { Menu } from 'src/pages/App/config'; 3 | 4 | const MenuComponent: React.FC<{ onChange: (id: string) => void; menus: Menu[] }> = ({ 5 | onChange, 6 | menus, 7 | }) => { 8 | return ( 9 |
10 | {menus.map(({ id, icon }) => ( 11 |
onChange(id)}> 12 | {icon} 13 |
14 | ))} 15 |
16 | ); 17 | }; 18 | 19 | export default MenuComponent; 20 | -------------------------------------------------------------------------------- /src/pages/App/slider-components/NodeTree/index.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: column; 4 | height: 100%; 5 | width: 100%; 6 | overflow: auto; 7 | .search { 8 | height: 50px; 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | padding: 0 20px; 13 | border-bottom: 1px solid #f0f0f0; 14 | } 15 | .scrollWarper { 16 | flex: 1; 17 | padding: 20px; 18 | white-space: nowrap; 19 | overflow: auto; 20 | } 21 | } 22 | 23 | .match { 24 | color: #1890ff; 25 | } 26 | -------------------------------------------------------------------------------- /src/pages/App/components/Drawer/attribute/index.module.scss: -------------------------------------------------------------------------------- 1 | @import 'src/theme/_colors.scss'; 2 | 3 | .container { 4 | display: flex; 5 | align-items: center; 6 | justify-content: space-between; 7 | min-height: 50px; 8 | border-bottom: 1px solid map-get($colors, 'border'); 9 | flex-wrap: wrap; 10 | padding: 10px 0; 11 | } 12 | 13 | .header { 14 | display: flex; 15 | justify-content: space-between; 16 | align-items: center; 17 | } 18 | 19 | .rightContainer { 20 | display: flex; 21 | align-items: center; 22 | input { 23 | width: 100px; 24 | margin-left: 10px; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/pages/App/slider-components/Component/index.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | height: 100%; 4 | flex-direction: column; 5 | .search { 6 | height: 50px; 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | padding: 0 20px; 11 | border-bottom: 1px solid #f0f0f0; 12 | margin: 0; 13 | } 14 | .components { 15 | flex: 1; 16 | padding-left: 20px; 17 | padding-bottom: 20px; 18 | padding-right: 20px; 19 | height: 100%; 20 | overflow: auto; 21 | & > div { 22 | margin-top: 10px; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { App } from './pages'; 4 | import reportWebVitals from './reportWebVitals'; 5 | import 'antd/dist/antd.css'; 6 | import './index.css'; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | , 12 | document.getElementById('root'), 13 | ); 14 | 15 | // If you want to start measuring performance in your app, pass a function 16 | // to log results (for example: reportWebVitals(console.log)) 17 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 18 | reportWebVitals(); 19 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "editor.formatOnSave": true, 4 | "[typescript]": { 5 | "editor.defaultFormatter": "esbenp.prettier-vscode" 6 | }, 7 | "[typescriptreact]": { 8 | "editor.defaultFormatter": "esbenp.prettier-vscode" 9 | }, 10 | "[json]": { 11 | "editor.defaultFormatter": "esbenp.prettier-vscode" 12 | }, 13 | "[javascript]": { 14 | "editor.defaultFormatter": "esbenp.prettier-vscode" 15 | }, 16 | "[html]": { 17 | "editor.defaultFormatter": "esbenp.prettier-vscode" 18 | }, 19 | "editor.codeActionsOnSave": { 20 | "source.fixAll.eslint": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/pages/App/index.tsx: -------------------------------------------------------------------------------- 1 | import { Layout } from 'antd'; 2 | import Content from './components/Content'; 3 | import Header from './components/Header'; 4 | import Sider from './components/Sider'; 5 | import Drawer from './components/Drawer'; 6 | import { PagesProvider } from 'src/context'; 7 | 8 | function App() { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 | ); 21 | } 22 | 23 | export default App; 24 | -------------------------------------------------------------------------------- /src/pages/App/components/Drawer/attribute/config.tsx: -------------------------------------------------------------------------------- 1 | import { Style } from 'src/model'; 2 | import { Width, Height, Display } from './components'; 3 | 4 | export interface CssProps { 5 | style?: Style[]; 6 | onChange?: (styles: Style[]) => void; 7 | } 8 | 9 | const Css = [ 10 | { 11 | key: 'width', 12 | component: (props: CssProps) => , 13 | }, 14 | { 15 | key: 'height', 16 | component: (props: CssProps) => , 17 | }, 18 | { 19 | key: 'display', 20 | component: (props: CssProps) => , 21 | }, 22 | ]; 23 | 24 | export { Css }; 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "es5", 5 | "lib": [ 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ], 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "module": "esnext", 18 | "moduleResolution": "node", 19 | "resolveJsonModule": true, 20 | "isolatedModules": true, 21 | "noEmit": true, 22 | "jsx": "react-jsx" 23 | }, 24 | "include": [ 25 | "src" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /src/context/index.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useState } from 'react'; 2 | import { AppService } from 'src/controller'; 3 | import appConfig from 'src/project-config'; 4 | 5 | const appService = new AppService(appConfig); 6 | export const AppContext = createContext<{ 7 | appService: AppService; 8 | refresh: boolean; 9 | }>({ 10 | appService, 11 | refresh: false, 12 | }); 13 | 14 | export const PagesProvider: React.FC<{}> = ({ children }) => { 15 | const [refresh, setRefresh] = useState(false); 16 | 17 | AppService.updateView = () => setRefresh(!refresh); 18 | 19 | return ( 20 | 21 | {children} 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/pages/App/components/Header/index.module.scss: -------------------------------------------------------------------------------- 1 | @import 'src/theme/_colors.scss'; 2 | 3 | .header { 4 | background-color: map-get($colors, 'primary') !important; 5 | height: 40px !important; 6 | display: flex; 7 | justify-content: space-between; 8 | align-items: center; 9 | .leftWarper { 10 | display: flex; 11 | } 12 | .item { 13 | display: flex; 14 | box-sizing: border-box; 15 | justify-content: center; 16 | align-items: center; 17 | line-height: 20px; 18 | padding: 5px 10px; 19 | cursor: pointer; 20 | color: white; 21 | &:hover { 22 | border-radius: 5px; 23 | border: 1px solid map-get($colors, 'border'); 24 | } 25 | } 26 | .itemHome { 27 | padding: 0; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/controller/util/index.ts: -------------------------------------------------------------------------------- 1 | import { Children } from 'src/model'; 2 | import { NodeService } from '..'; 3 | 4 | const strikeToCamel = (str: string) => { 5 | return (str + '').replace(/-\D/g, function (match) { 6 | return match.charAt(1).toUpperCase(); 7 | }); 8 | }; 9 | 10 | const isFunction = (obj: any) => 11 | Object.prototype.toString.call(obj) === '[object Function]'; 12 | 13 | function isString( 14 | children?: Children | Children | T, 15 | ): children is string { 16 | return typeof children === 'string'; 17 | } 18 | 19 | const isElement = (value: unknown) => { 20 | return value && typeof value === 'object' && (value as NodeService)?._name; 21 | }; 22 | 23 | export { strikeToCamel, isFunction, isString, isElement }; 24 | -------------------------------------------------------------------------------- /src/pages/App/components/Header/components/Home/index.module.scss: -------------------------------------------------------------------------------- 1 | @import 'src/theme/_colors.scss'; 2 | 3 | .newBuild { 4 | height: 250px; 5 | div:nth-child(1) { 6 | height: 100%; 7 | width: 100%; 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | flex-direction: column; 12 | cursor: pointer; 13 | border-radius: 5px; 14 | background-color: rgba($color: map-get($colors, 'background'), $alpha: 0.6); 15 | span:nth-child(1) { 16 | font-size: 24px; 17 | } 18 | span:nth-child(2) { 19 | font-size: 18px; 20 | } 21 | &:hover { 22 | background-color: rgba($color: map-get($colors, 'background'), $alpha: 1); 23 | } 24 | } 25 | } 26 | 27 | .projects { 28 | height: 250px; 29 | } 30 | -------------------------------------------------------------------------------- /src/pages/App/components/Drawer/attribute/base-attribute.tsx: -------------------------------------------------------------------------------- 1 | import { PageService } from 'src/controller'; 2 | import Collapse from 'src/pages/components/Collapse'; 3 | import ClassName from './components/className'; 4 | import Content from './components/content'; 5 | import Component from './components/component'; 6 | import styles from './index.module.scss'; 7 | 8 | const BaseAttribute: React.FC<{ page: PageService }> = ({ page }) => { 9 | return ( 10 | 13 | 节点属性 14 | 15 | } 16 | > 17 | 18 | 19 | 20 | 21 | ); 22 | }; 23 | 24 | export default BaseAttribute; 25 | -------------------------------------------------------------------------------- /.github/workflows/publish-website.yml: -------------------------------------------------------------------------------- 1 | name: Deploy GitHub Pages 2 | 3 | # 触发条件:在 push 到 master 分支后 4 | on: 5 | push: 6 | branches: [master] 7 | pull_request: 8 | branches: [master] 9 | 10 | # 任务 11 | jobs: 12 | build-and-deploy: 13 | # 服务器环境:最新版 Ubuntu 14 | runs-on: ubuntu-latest 15 | steps: 16 | # 拉取代码 17 | - name: Checkout 18 | uses: actions/checkout@v2.3.1 19 | 20 | # 生成静态文件 21 | - name: Install and Test and Build 22 | run: | 23 | npm install 24 | npm run build 25 | 26 | # 部署到 GitHub Pages 27 | - name: Deploy 28 | uses: JamesIves/github-pages-deploy-action@4.1.4 29 | with: 30 | ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }} 31 | BRANCH: gh-pages 32 | FOLDER: build 33 | -------------------------------------------------------------------------------- /src/project-config.ts: -------------------------------------------------------------------------------- 1 | import { cloneDeep } from 'lodash'; 2 | import { PreviewStyle, SelectStyle } from 'src/const'; 3 | import { computer } from 'src/const/container'; 4 | import { ProjectOptions } from 'src/controller'; 5 | import * as components from 'antd'; 6 | import { AppConfig } from 'src/controller'; 7 | 8 | export const options: ProjectOptions = { 9 | target: cloneDeep(computer), 10 | selectStyle: SelectStyle, 11 | previewStyle: PreviewStyle, 12 | }; 13 | 14 | const Components = new Map(); 15 | 16 | for (const [key, value] of Object.entries(components)) { 17 | Components.set(key, { 18 | from: 'antd', 19 | to: value, 20 | }); 21 | } 22 | 23 | const appConfig: AppConfig = { 24 | project: { 25 | options: options, 26 | }, 27 | components: Components, 28 | }; 29 | 30 | export default appConfig; 31 | -------------------------------------------------------------------------------- /src/pages/components/Collapse/index.tsx: -------------------------------------------------------------------------------- 1 | import { DownCircleOutlined, UpCircleOutlined } from '@ant-design/icons'; 2 | import React from 'react'; 3 | import { useState } from 'react'; 4 | import styles from './index.module.scss'; 5 | 6 | const Collapse: React.FC<{ header?: React.ReactNode }> = ({ header, children }) => { 7 | const [visible, setVisible] = useState(true); 8 | 9 | return ( 10 |
11 |
12 |
setVisible(!visible)} className={styles.icon}> 13 | {visible ? : } 14 |
15 |
{header}
16 |
17 |
{children}
18 |
19 | ); 20 | }; 21 | 22 | export default Collapse; 23 | -------------------------------------------------------------------------------- /src/pages/App/components/Content/Body/container.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import { PageService } from 'src/controller'; 3 | import { Options } from '..'; 4 | import styles from './index.module.scss'; 5 | 6 | const Container: React.FC<{ 7 | page: PageService; 8 | curPage: PageService; 9 | options: Options; 10 | }> = ({ page, curPage, options }) => { 11 | const cacheView = useRef(<>); 12 | 13 | const canvasSize = { 14 | transform: `scale(${options.zoom},${options.zoom})`, 15 | }; 16 | 17 | if (page.id !== curPage.id) { 18 | return cacheView.current; 19 | } 20 | 21 | const view = ( 22 |
23 |
{page.createView()}
24 |
25 | ); 26 | 27 | cacheView.current = view; 28 | 29 | return view; 30 | }; 31 | 32 | export default Container; 33 | -------------------------------------------------------------------------------- /src/model/history.ts: -------------------------------------------------------------------------------- 1 | import { HistoryLog } from '.'; 2 | 3 | const MAX_HISTORY = 100; 4 | 5 | class History { 6 | private __history: HistoryLog[] = []; 7 | private _future: HistoryLog[] = []; 8 | private _id: number = 0; 9 | set history(history: HistoryLog[]) { 10 | this.__history = history; 11 | while (this.__history.length > MAX_HISTORY) { 12 | this.__history.shift(); 13 | } 14 | } 15 | get history() { 16 | return this.__history; 17 | } 18 | 19 | set future(future: HistoryLog[]) { 20 | this._future = future; 21 | while (this._future.length > MAX_HISTORY) { 22 | this._future.shift(); 23 | } 24 | } 25 | 26 | get future() { 27 | return this._future; 28 | } 29 | 30 | set id(id: number) { 31 | this._id = id; 32 | } 33 | 34 | get id() { 35 | this._id = ++this._id; 36 | return this._id; 37 | } 38 | } 39 | 40 | export default History; 41 | -------------------------------------------------------------------------------- /src/pages/App/components/Content/index.tsx: -------------------------------------------------------------------------------- 1 | import { Layout } from 'antd'; 2 | import { useState } from 'react'; 3 | import { useContext } from 'react'; 4 | import { AppContext } from 'src/context'; 5 | import Body from './Body'; 6 | import Header from './Header'; 7 | import styles from './index.module.scss'; 8 | 9 | const { Content } = Layout; 10 | 11 | export interface Options { 12 | zoom: number; 13 | } 14 | 15 | // eslint-disable-next-line 16 | export default () => { 17 | const { appService } = useContext(AppContext); 18 | 19 | const [options, setOptions] = useState({ 20 | zoom: 1, 21 | }); 22 | 23 | return ( 24 | 25 |
30 | 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/pages/App/components/Content/Header/component/code/base-info.tsx: -------------------------------------------------------------------------------- 1 | import { ProjectService } from 'src/controller'; 2 | import styles from './index.module.scss'; 3 | 4 | const BaseInfo: React.FC<{ project: ProjectService }> = ({ project }) => { 5 | const { name, description } = project; 6 | return ( 7 |
8 |
9 |

项目信息

10 |
11 |
12 |
13 |
项目名:
14 |
{name ? name : '--'}
15 |
16 |
17 |
项目描述:
18 |
{description ? description : '--'}
19 |
20 |
21 |
22 | ); 23 | }; 24 | 25 | export default BaseInfo; 26 | -------------------------------------------------------------------------------- /src/pages/App/slider-components/History/index.module.scss: -------------------------------------------------------------------------------- 1 | @import 'src/theme/_colors.scss'; 2 | 3 | .container { 4 | padding: 10px; 5 | height: 100%; 6 | overflow: scroll; 7 | .history { 8 | height: 30px; 9 | display: flex; 10 | justify-content: space-between; 11 | align-items: center; 12 | padding: 0 10px; 13 | border-radius: 5px; 14 | span:nth-child(1) { 15 | flex: 1; 16 | } 17 | span:nth-child(2) { 18 | font-size: 12px; 19 | flex: 1; 20 | } 21 | span:nth-child(3) { 22 | width: 30px; 23 | height: 100%; 24 | display: inline-flex; 25 | justify-content: center; 26 | align-items: center; 27 | img { 28 | height: 16px; 29 | width: 16px; 30 | } 31 | } 32 | &:hover { 33 | background-color: #f5f5f5; 34 | cursor: pointer; 35 | } 36 | } 37 | } 38 | 39 | .futureHistory { 40 | color: map-get($colors, 'disabled'); 41 | } 42 | -------------------------------------------------------------------------------- /docs/zh/case.md: -------------------------------------------------------------------------------- 1 | ## Case 2 | 3 | 一. 「 创建项目 」 4 | 5 | 主要是需要保存项目进度,防止刷新,页面异常等情况,需要及时保存。 6 | 7 | ![1.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ed6ec1b97c7d4fd097df2a410325666f~tplv-k3u1fbpfcp-watermark.image?) 8 | 9 | 二. 「 基本页面 」 10 | 11 | 1. 通过布局定义结构 12 | 2. 通过点击或节点树选中节点 13 | 3. 操作节点相关属性 14 | 15 | ![2.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/26f83dea3bf9491ba3d7c6b87b5e44f8~tplv-k3u1fbpfcp-watermark.image?) 16 | 17 | 三. 「 生成代码 」 18 | 19 | ![3.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/08cf62f9e3bc42c3b1a0cf75ae11c6f6~tplv-k3u1fbpfcp-watermark.image?) 20 | 21 | 四. 「 下载代码本地运行代码 」 22 | 23 | 1. 导出代码 24 | 2. 导入到项目目录 25 | 3. 引入相关依赖 26 | 4. 相关位置引入项目代码 27 | 5. 运行项目 28 | 29 | ![4.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/92ecd6d021c840638c16cbc37de021bd~tplv-k3u1fbpfcp-watermark.image?) 30 | 31 | 五. 「 运行效果 」 32 | 33 | ![5.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/19547db094af4c60839c38fdc5471099~tplv-k3u1fbpfcp-watermark.image?) 34 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | // .prettierrc.js 2 | module.exports = { 3 | // 一行最多 85 字符 4 | printWidth: 85, 5 | // 使用 2 个空格缩进 6 | tabWidth: 2, 7 | // 不使用缩进符,而使用空格 8 | useTabs: false, 9 | // 行尾需要有分号 10 | semi: true, 11 | // 使用单引号 12 | singleQuote: true, 13 | // 对象的 key 仅在必要时用引号 14 | quoteProps: 'as-needed', 15 | // jsx 不使用单引号,而使用双引号 16 | jsxSingleQuote: false, 17 | // 末尾需要有逗号 18 | trailingComma: 'all', 19 | // 大括号内的首尾需要空格 20 | bracketSpacing: true, 21 | // jsx 标签的反尖括号需要换行 22 | jsxBracketSameLine: false, 23 | // 箭头函数,只有一个参数的时候,不需要括号 24 | arrowParens: 'avoid', 25 | // 每个文件格式化的范围是文件的全部内容 26 | rangeStart: 0, 27 | rangeEnd: Infinity, 28 | // 不需要写文件开头的 @prettier 29 | requirePragma: false, 30 | // 不需要自动在文件开头插入 @prettier 31 | insertPragma: false, 32 | // 使用默认的折行标准 33 | proseWrap: 'preserve', 34 | // 根据显示样式决定 html 要不要折行 35 | htmlWhitespaceSensitivity: 'css', 36 | // 换行符使用 lf 37 | endOfLine: 'lf', 38 | // 格式化嵌入的内容 39 | embeddedLanguageFormatting: 'auto', 40 | }; 41 | -------------------------------------------------------------------------------- /src/pages/App/components/Sider/index.module.scss: -------------------------------------------------------------------------------- 1 | .slider { 2 | display: flex; 3 | height: 100%; 4 | .body { 5 | height: 100%; 6 | width: calc(100% - 50px); 7 | display: flex; 8 | flex-direction: column; 9 | .card { 10 | display: flex; 11 | flex-direction: column; 12 | height: 100%; 13 | .title { 14 | height: 50px; 15 | width: 100%; 16 | padding: 0 10px; 17 | display: flex; 18 | align-items: center; 19 | border-bottom: 1px solid #f0f0f0; 20 | margin-bottom: 0; 21 | } 22 | .component { 23 | height: calc(100% - 50px); 24 | } 25 | } 26 | } 27 | } 28 | 29 | .menu { 30 | width: 50px; 31 | height: 100%; 32 | border-right: 1px solid #f0f0f0; 33 | div { 34 | display: flex; 35 | justify-content: center; 36 | align-items: center; 37 | border-bottom: 1px solid #f0f0f0; 38 | cursor: pointer; 39 | height: 50px; 40 | } 41 | img { 42 | height: 24px; 43 | width: 24px; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/controller/index.ts: -------------------------------------------------------------------------------- 1 | import ProjectService from './service/project'; 2 | import PageService from './service/page'; 3 | import NodeService from './service/node'; 4 | import HistoryService from './service/history'; 5 | import AppService from './service/app'; 6 | 7 | import { AST, Style } from 'src/model'; 8 | export interface Options { 9 | id?: string; 10 | update?: () => void; 11 | name?: string; 12 | target: AST; 13 | selectStyle: Style[]; 14 | previewStyle: (Style & { isCanUse?: boolean })[]; 15 | } 16 | 17 | export type ComponentValue = { 18 | from: string; 19 | to: unknown; 20 | }; 21 | export type Components = Map; 22 | 23 | export type ProjectOptions = Pick< 24 | Options, 25 | 'target' | 'selectStyle' | 'previewStyle' 26 | >; 27 | export interface ProjectConfig { 28 | options: ProjectOptions; 29 | } 30 | 31 | export interface AppConfig { 32 | project: ProjectConfig; 33 | components: Components; 34 | } 35 | 36 | export { ProjectService, NodeService, PageService, HistoryService, AppService }; 37 | -------------------------------------------------------------------------------- /src/pages/App/components/Drawer/attribute/components/className.tsx: -------------------------------------------------------------------------------- 1 | import { Input } from 'antd'; 2 | import { useEffect } from 'react'; 3 | import { useState } from 'react'; 4 | import { PageService } from 'src/controller'; 5 | import styles from '../index.module.scss'; 6 | 7 | const ClassName: React.FC<{ page: PageService }> = ({ page }) => { 8 | const [value, setValue] = useState(''); 9 | 10 | useEffect(() => { 11 | if (page) { 12 | setValue(page.currentNode[0]?.className || ''); 13 | } 14 | // eslint-disable-next-line 15 | }, [page?.currentNode, page?.currentNode[0]?.className]); 16 | 17 | const updateClass = () => { 18 | if (value !== page.currentNode[0]?.className) { 19 | page.setClassName(value); 20 | } 21 | }; 22 | 23 | return ( 24 |
25 | setValue(e.target.value)} 28 | value={value} 29 | onBlur={() => updateClass()} 30 | placeholder={'类名,默认随机生成'} 31 | /> 32 |
33 | ); 34 | }; 35 | 36 | export default ClassName; 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 TCYong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/pages/App/config.tsx: -------------------------------------------------------------------------------- 1 | import { LayoutComponent, History, Component, NodeTree } from './slider-components'; 2 | import { 3 | HistoryOutlined, 4 | LayoutOutlined, 5 | AppstoreOutlined, 6 | ApartmentOutlined, 7 | } from '@ant-design/icons'; 8 | 9 | export interface Menu { 10 | icon: React.ReactNode; 11 | id: string; 12 | title: string; 13 | component: React.ReactNode; 14 | } 15 | 16 | const getSiderMenu = (): Menu[] => { 17 | return [ 18 | { 19 | id: 'layout', 20 | icon: , 21 | title: '布局', 22 | component: , 23 | }, 24 | { 25 | id: 'component', 26 | icon: , 27 | title: '组件', 28 | component: , 29 | }, 30 | { 31 | id: 'node', 32 | icon: , 33 | title: '节点树', 34 | component: , 35 | }, 36 | { 37 | id: 'history', 38 | icon: , 39 | title: '历史', 40 | component: , 41 | }, 42 | ]; 43 | }; 44 | 45 | export { getSiderMenu }; 46 | -------------------------------------------------------------------------------- /src/pages/App/components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import { ExpandOutlined, GithubOutlined } from '@ant-design/icons'; 2 | import { Layout } from 'antd'; 3 | import screenFull from 'screenfull'; 4 | import Home from './components/Home'; 5 | import styles from './index.module.scss'; 6 | 7 | const { Header } = Layout; 8 | 9 | // eslint-disable-next-line 10 | export default () => { 11 | const requestFullScreen = () => { 12 | if (screenFull.isEnabled) { 13 | screenFull.request(); 14 | } 15 | }; 16 | 17 | return ( 18 |
19 |
20 |
requestFullScreen()}> 21 | 22 |
23 |
24 | 25 |
26 |
27 |
28 |
{ 31 | window.open('https://github.com/loo41/visual-layout'); 32 | }} 33 | > 34 | 35 |
36 |
37 |
38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /src/pages/App/components/Drawer/attribute/components/content.tsx: -------------------------------------------------------------------------------- 1 | import { Input } from 'antd'; 2 | import { isString } from 'lodash'; 3 | import { useEffect } from 'react'; 4 | import { useState } from 'react'; 5 | import { PageService } from 'src/controller'; 6 | import styles from '../index.module.scss'; 7 | 8 | const Content: React.FC<{ page: PageService }> = ({ page }) => { 9 | const [value, setValue] = useState(''); 10 | 11 | useEffect(() => { 12 | if (page) { 13 | const children = page.currentNode[0]?.children; 14 | const content = isString(children) 15 | ? children 16 | : isString(children?.[0]) 17 | ? children?.[0] || '' 18 | : ''; 19 | 20 | setValue(content); 21 | } 22 | // eslint-disable-next-line 23 | }, [page?.currentNode[0]]); 24 | 25 | const updateContent = () => { 26 | page.setContent(value); 27 | }; 28 | 29 | return ( 30 |
31 | setValue(e.target.value)} 34 | value={value} 35 | onBlur={() => updateContent()} 36 | /> 37 |
38 | ); 39 | }; 40 | 41 | export default Content; 42 | -------------------------------------------------------------------------------- /src/pages/App/components/Drawer/attribute/components/Display/index.module.scss: -------------------------------------------------------------------------------- 1 | @import 'src/pages/App/components/Drawer/attribute/index.module.scss'; 2 | @import 'src/theme/_colors.scss'; 3 | 4 | .clear { 5 | display: flex; 6 | justify-content: flex-end; 7 | cursor: pointer; 8 | div { 9 | width: 50px; 10 | height: 20px; 11 | border-radius: 5px; 12 | background-color: white; 13 | display: flex; 14 | justify-content: center; 15 | align-items: center; 16 | } 17 | } 18 | 19 | .flexWarper { 20 | display: flex; 21 | flex-direction: column; 22 | margin: 5px 0; 23 | font-size: 12px; 24 | .flexItem { 25 | display: flex; 26 | height: 25px; 27 | margin-top: 5px; 28 | font-size: 16px; 29 | div { 30 | flex: 1; 31 | background-color: white; 32 | margin: 0 5px; 33 | display: flex; 34 | align-items: center; 35 | justify-content: center; 36 | cursor: pointer; 37 | border-radius: 5px; 38 | img { 39 | height: 16px; 40 | width: 16px; 41 | } 42 | } 43 | } 44 | } 45 | 46 | .flexContainer { 47 | width: 100%; 48 | padding: 10px; 49 | background: map-get($colors, 'background'); 50 | margin-top: 10px; 51 | border-radius: 5px; 52 | } 53 | -------------------------------------------------------------------------------- /src/controller/util/proxy.ts: -------------------------------------------------------------------------------- 1 | import { isObject } from 'src/util'; 2 | 3 | export type Get = ( 4 | target: object, 5 | propertyKey: string, 6 | receiver: ProxyConstructor, 7 | ) => void; 8 | 9 | export type Set = ( 10 | target: object, 11 | propertyKey: string, 12 | value: unknown, 13 | receiver: ProxyConstructor, 14 | ) => void; 15 | 16 | function proxy(targe: T, get?: Get, set?: Set): T { 17 | const handler = () => { 18 | return { 19 | get(target: T, propertyKey: string, receiver: ProxyConstructor) { 20 | const value = Reflect.get(target, propertyKey, receiver); 21 | if (Array.isArray(value) || isObject(value)) { 22 | return setProxy(value); 23 | } 24 | 25 | get?.(target, propertyKey, receiver); 26 | return value; 27 | }, 28 | set(target: T, propertyKey: string, value: U, receiver: ProxyConstructor) { 29 | set?.(target, propertyKey, value, receiver); 30 | return Reflect.set(target, propertyKey, value, receiver); 31 | }, 32 | }; 33 | }; 34 | 35 | const setProxy = (targe: T): T => { 36 | return new Proxy(targe, handler()); 37 | }; 38 | 39 | return setProxy(targe); 40 | } 41 | 42 | export default proxy; 43 | -------------------------------------------------------------------------------- /src/const/container.ts: -------------------------------------------------------------------------------- 1 | import { AST } from 'src/model'; 2 | 3 | const computer: AST = { 4 | _name: 'div', 5 | type: 'Element', 6 | styles: [ 7 | { 8 | key: 'height', 9 | value: '800px', 10 | }, 11 | { 12 | key: 'width', 13 | value: '1280px', 14 | }, 15 | { 16 | key: 'background', 17 | value: 'white', 18 | }, 19 | ], 20 | children: [], 21 | }; 22 | 23 | const models = [ 24 | { 25 | height: '926', 26 | width: '428', 27 | key: 'iPhone 12 Pro Max', 28 | }, 29 | { 30 | height: '844', 31 | width: '390', 32 | key: 'iPhone 12 Pro', 33 | }, 34 | { 35 | height: '896', 36 | width: '414', 37 | key: 'iPhone 11', 38 | }, 39 | { 40 | height: '640', 41 | width: '360', 42 | key: 'Nexus 5', 43 | }, 44 | { 45 | height: '800', 46 | width: '360', 47 | key: 'Samsung Galaxy A70', 48 | }, 49 | { 50 | height: '780', 51 | width: '360', 52 | key: 'Oppo Find X', 53 | }, 54 | { 55 | height: '1366', 56 | width: '1024', 57 | key: 'iPadOS', 58 | }, 59 | { 60 | height: '1024', 61 | width: '768', 62 | key: '小米平板', 63 | }, 64 | { 65 | height: '800', 66 | width: '1280', 67 | key: 'MacBook', 68 | }, 69 | ]; 70 | 71 | export { computer, models }; 72 | -------------------------------------------------------------------------------- /src/pages/App/components/Sider/index.tsx: -------------------------------------------------------------------------------- 1 | import { Layout } from 'antd'; 2 | import { useState, useMemo } from 'react'; 3 | import { getSiderMenu, Menu } from 'src/pages/App/config'; 4 | import MenuComponent from './Menu'; 5 | import styles from './index.module.scss'; 6 | 7 | const { Sider } = Layout; 8 | 9 | // eslint-disable-next-line 10 | export default () => { 11 | const menus: Menu[] = useMemo(() => { 12 | return getSiderMenu(); 13 | }, []); 14 | 15 | const [curMenu, setCurMenu] = useState(menus[0].id); 16 | 17 | return ( 18 | 19 |
20 | setCurMenu(menu)} menus={menus} /> 21 |
22 | {menus.map(({ component, id, title }) => { 23 | const isShow = curMenu === id; 24 | return ( 25 |
30 |

{title}

31 |
{component}
32 |
33 | ); 34 | })} 35 |
36 |
37 |
38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /src/pages/App/components/Drawer/attribute/style.tsx: -------------------------------------------------------------------------------- 1 | import { Input } from 'antd'; 2 | import { useState } from 'react'; 3 | import { PageService } from 'src/controller'; 4 | import { Css } from './config'; 5 | import _ from 'lodash'; 6 | import Collapse from 'src/pages/components/Collapse'; 7 | import { Style } from 'src/model'; 8 | import styles from './index.module.scss'; 9 | 10 | const StyleComponent: React.FC<{ page: PageService }> = ({ page }) => { 11 | const [value, setValue] = useState(''); 12 | 13 | const onChange = (styles: Style[]) => { 14 | page.setStyles(styles); 15 | }; 16 | 17 | return ( 18 | 21 | 样式 22 | setValue(e.target.value)} 25 | style={{ width: 100 }} 26 | /> 27 | 28 | } 29 | > 30 | <> 31 | {Css.filter( 32 | ({ key }) => !value || new RegExp(_.escapeRegExp(value), 'ig').test(key), 33 | ).map(({ component }) => { 34 | return component({ 35 | style: page?.currentNode[0]?.styles, 36 | onChange, 37 | }); 38 | })} 39 | 40 | 41 | ); 42 | }; 43 | 44 | export default StyleComponent; 45 | -------------------------------------------------------------------------------- /src/pages/App/components/Drawer/attribute/components/Display/index.tsx: -------------------------------------------------------------------------------- 1 | import { Select } from 'antd'; 2 | import Flex from './flex'; 3 | import styles from './index.module.scss'; 4 | import { CssProps } from '../../config'; 5 | 6 | const { Option } = Select; 7 | 8 | const KEY = 'display'; 9 | const Display: React.FC = ({ style = [], onChange }) => { 10 | const display = style.filter(css => css.key === KEY)[0]; 11 | 12 | return ( 13 |
14 |

展示

15 |
16 | 35 |
36 | {['flex', 'inline-flex'].includes(display?.value) && ( 37 | 38 | )} 39 |
40 | ); 41 | }; 42 | 43 | export default Display; 44 | -------------------------------------------------------------------------------- /src/pages/App/components/Drawer/css-edit.tsx: -------------------------------------------------------------------------------- 1 | import { Input } from 'antd'; 2 | import { useEffect } from 'react'; 3 | import { useState } from 'react'; 4 | import { PageService } from 'src/controller'; 5 | import styles from './index.module.scss'; 6 | 7 | const Css: React.FC<{ page: PageService }> = ({ page }) => { 8 | const cssString = 9 | page?.currentNode[0]?.styles 10 | ?.map(({ key, value }) => { 11 | return `${key}: ${value};\n`; 12 | }) 13 | .join('') || ''; 14 | 15 | const [css, setCss] = useState(cssString); 16 | 17 | useEffect(() => { 18 | setCss(cssString); 19 | }, [cssString]); 20 | 21 | const setStyle = () => { 22 | if (css !== cssString) { 23 | const styles = css 24 | .replace(/\\n/, '') 25 | .split(';') 26 | .map(style => { 27 | const [key, value] = style.split(':').map(key => key.trim()); 28 | return { 29 | key, 30 | value, 31 | }; 32 | }) 33 | .filter(({ key, value }) => key && value); 34 | page.setStyles(styles); 35 | } 36 | }; 37 | 38 | return ( 39 |
40 | { 45 | setCss(e.target.value); 46 | }} 47 | onBlur={() => setStyle()} 48 | /> 49 |
50 | ); 51 | }; 52 | 53 | export default Css; 54 | -------------------------------------------------------------------------------- /docs/en/index.md: -------------------------------------------------------------------------------- 1 | ## Files Overview 2 | 3 | ```txt 4 | .github ----> github Action config 5 | .vscode ----> vscode config(prettier) 6 | src ---| 7 | | 8 | | 9 | context ---> global context (Inject Page Context) 10 | model ---> model layer 11 | controller ---> controller layer (Controller Service) 12 | pages ---> view layer (UI View) 13 | theme ---> theme .scss file 14 | ``` 15 | 16 | ## Framework 17 | 18 | ```txt 19 | Model ---- Update --> View 20 | ↑ | 21 | Change Interactive 22 | | | 23 | <————— Controller ————— 24 | ``` 25 | 26 | ## Project layer 27 | 28 | ```txt 29 | ______________________ 30 | | Project | 31 | | ________________ | 32 | | | Page | Page | | 33 | | | _____ | _____ | | 34 | | || Node||| Node|| | 35 | | ||_Node_||_Node|| | 36 | | |_______|_______| | 37 | |______________________| 38 | ``` 39 | 40 | ## Features 41 | 42 | - Keyboard Event ✅ 43 | - [Ctrl + c] (Copy Select) 44 | - [Ctrl + v] (Paste Select) 45 | - [Ctrl + Backspace] (Delete Select) 46 | - [Ctrl + z] (Step Back) 47 | - [Ctrl + y] (Step Forward) 48 | - Multi Page ✅ 49 | - Layout ✅ 50 | - History Operation ✅ 51 | - Visual Component ✅ 52 | - Visual Styles 53 | - Export Code 54 | - History 55 | -------------------------------------------------------------------------------- /src/model/project.ts: -------------------------------------------------------------------------------- 1 | import { AppService, PageService, ProjectOptions } from 'src/controller'; 2 | import { App, ProjectObject } from '.'; 3 | export default class Project { 4 | protected pages: { [key: string]: PageService } = {}; 5 | public _idx: number = 1; 6 | public currentId?: string; 7 | public name: string = ''; 8 | public description: string = ''; 9 | public id: string = String(App.idx); 10 | 11 | constructor(options?: ProjectObject & ProjectOptions) { 12 | if (options) { 13 | const { 14 | id, 15 | currentId, 16 | idx, 17 | name, 18 | description, 19 | pages, 20 | selectStyle, 21 | previewStyle, 22 | } = options; 23 | this.idx = idx; 24 | this.id = id; 25 | this.currentId = currentId; 26 | this.name = name || ''; 27 | this.description = description || ''; 28 | this.pages = Object.entries(pages).reduce((pages, [key, value]) => { 29 | pages[key] = new PageService({ 30 | ...value, 31 | selectStyle, 32 | previewStyle, 33 | update: this.update, 34 | }); 35 | return pages; 36 | }, {} as { [props: string]: PageService }); 37 | } 38 | } 39 | 40 | get idx() { 41 | return this._idx++; 42 | } 43 | 44 | set idx(idx: number) { 45 | this._idx = idx; 46 | } 47 | 48 | update = () => { 49 | Promise.resolve().then(() => AppService.updateView()); 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /src/pages/App/components/Content/Body/index.tsx: -------------------------------------------------------------------------------- 1 | import { AppService, PageService } from 'src/controller'; 2 | import styles from './index.module.scss'; 3 | import Container from './container'; 4 | import { Options } from '..'; 5 | import { useState } from 'react'; 6 | import { useEffect } from 'react'; 7 | import { useRef } from 'react'; 8 | 9 | const Body: React.FC<{ appService: AppService; options: Options }> = ({ 10 | appService, 11 | options, 12 | }) => { 13 | const [pages, setPages] = useState([]); 14 | const project = useRef(appService.project); 15 | 16 | const curPage = appService.project.getCurrentPage(); 17 | 18 | useEffect(() => { 19 | if (appService.project === project.current) { 20 | if (curPage && pages.every(({ id }) => id !== curPage.id)) { 21 | setPages(pages.concat([curPage])); 22 | } 23 | } else { 24 | project.current = appService.project; 25 | setPages([curPage]); 26 | } 27 | // eslint-disable-next-line 28 | }, [curPage, appService.project]); 29 | 30 | return ( 31 |
32 | {pages.map(page => { 33 | const style = { display: page.id === curPage.id ? 'block' : 'none' }; 34 | return ( 35 |
36 | 37 |
38 | ); 39 | })} 40 |
41 | ); 42 | }; 43 | 44 | export default Body; 45 | -------------------------------------------------------------------------------- /src/model/page.ts: -------------------------------------------------------------------------------- 1 | import { NodeService, Options } from 'src/controller'; 2 | import { isString } from 'src/controller/util'; 3 | import { AST, PageObject } from '.'; 4 | 5 | export default class Page { 6 | public id: string; 7 | public name: string; 8 | public _page!: NodeService; 9 | public currentNode: NodeService[] = []; 10 | protected _idx: number = 1; 11 | protected target: AST; 12 | constructor(options: Required & Partial) { 13 | const { id, name, target, idx } = options; 14 | this.id = id; 15 | this.idx = idx || 1; 16 | this.name = name; 17 | this.target = target; 18 | } 19 | setPage(target: NodeService) { 20 | this._page = target; 21 | } 22 | set page(page: NodeService) { 23 | this._page = page; 24 | this.clearDeleteNode(this._page); 25 | // clear select node 26 | this.currentNode = []; 27 | } 28 | get page() { 29 | this.clearDeleteNode(this._page); 30 | return this._page; 31 | } 32 | 33 | get idx() { 34 | return this._idx++; 35 | } 36 | 37 | set idx(idx: number) { 38 | this._idx = idx; 39 | } 40 | 41 | clearDeleteNode = (node: NodeService): NodeService | null => { 42 | if (node.isDelete) { 43 | if (node.isRoot) { 44 | node.children = []; 45 | } 46 | return null; 47 | } else { 48 | node.children = isString(node.children) 49 | ? node.children 50 | : (node.children 51 | ?.map(node => (isString(node) ? node : this.clearDeleteNode(node))) 52 | .filter(_ => _) as NodeService[]); 53 | return node; 54 | } 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /src/pages/App/components/Content/Header/component/preview.tsx: -------------------------------------------------------------------------------- 1 | import { EyeOutlined } from '@ant-design/icons'; 2 | import { Checkbox, Popover, Row } from 'antd'; 3 | import { CheckboxValueType } from 'antd/lib/checkbox/Group'; 4 | import { ProjectService } from 'src/controller'; 5 | 6 | const Preview: React.FC<{ projectService: ProjectService }> = ({ 7 | projectService, 8 | }) => { 9 | const page = projectService.getCurrentPage(); 10 | const options = page?.options; 11 | 12 | return ( 13 | isCanUse) 18 | .map(({ key }) => key)} 19 | style={{ width: '100%' }} 20 | onChange={(checkedValue: CheckboxValueType[]) => { 21 | page.setOptions({ 22 | previewStyle: options?.previewStyle.map(option => { 23 | return { 24 | ...option, 25 | isCanUse: checkedValue.includes(option.key) ? true : false, 26 | }; 27 | }), 28 | }); 29 | }} 30 | > 31 | {options?.previewStyle.map(style => { 32 | return ( 33 | 34 | {style?.title} 35 | 36 | ); 37 | })} 38 | 39 | } 40 | title="预览样式设置" 41 | placement="bottom" 42 | > 43 | 44 | 45 | ); 46 | }; 47 | 48 | export default Preview; 49 | -------------------------------------------------------------------------------- /src/pages/App/components/Content/Header/component/code/index.module.scss: -------------------------------------------------------------------------------- 1 | @import 'src/theme/_colors.scss'; 2 | 3 | .container { 4 | display: flex; 5 | height: 100%; 6 | .leftContainer { 7 | width: 40%; 8 | padding-right: 20px; 9 | border-right: 1px solid map-get($colors, 'border'); 10 | } 11 | .rightContainer { 12 | width: 70%; 13 | } 14 | } 15 | 16 | .download { 17 | height: 35px; 18 | display: flex; 19 | justify-content: flex-end; 20 | align-items: center; 21 | } 22 | 23 | .warper { 24 | .mainTitle { 25 | font-weight: bold; 26 | letter-spacing: 1px; 27 | border-left: 3px solid map-get($colors, 'status'); 28 | padding-left: 10px; 29 | } 30 | .body { 31 | display: flex; 32 | flex-wrap: wrap; 33 | .item { 34 | width: 100%; 35 | min-height: 30px; 36 | display: flex; 37 | align-items: flex-end; 38 | margin: 5px 0; 39 | label { 40 | width: 100px; 41 | color: #595959; 42 | } 43 | .title { 44 | min-width: 100px; 45 | font-size: 14px; 46 | color: #262626; 47 | } 48 | .content { 49 | width: 1px; 50 | flex: 1; 51 | margin-left: 10px; 52 | color: map-get($colors, 'text'); 53 | } 54 | } 55 | } 56 | } 57 | 58 | .codeContainer { 59 | padding: 0 20px; 60 | height: 100%; 61 | display: flex; 62 | flex-direction: column; 63 | .titleWarper { 64 | height: 35px; 65 | display: flex; 66 | align-items: center; 67 | margin-bottom: 10px; 68 | } 69 | :global { 70 | .ant-tabs-content { 71 | height: 100%; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/controller/keyboard-event.ts: -------------------------------------------------------------------------------- 1 | import keyboard from 'keyboardjs'; 2 | import { NodeService, ProjectService } from '.'; 3 | import { isString } from 'src/controller/util'; 4 | 5 | export default class Keyboard { 6 | private copyNode: NodeService[] = []; 7 | constructor(public projectService: ProjectService) { 8 | this.bindKeyboardEvent(); 9 | } 10 | bindKeyboardEvent = () => { 11 | keyboard.bind('ctrl + c', () => { 12 | console.log('Copy'); 13 | this.copyNode = this.projectService.getCurrentPage().currentNode; 14 | }); 15 | keyboard.bind('ctrl + v', () => { 16 | console.log('Paste'); 17 | this.projectService.getCurrentPage().currentNode.forEach(node => { 18 | !isString(node.children) && 19 | node.children?.push(...this.copyNode.map(node => node.copy({}))); 20 | }); 21 | this.projectService.getCurrentPage().update({ description: '复制元素' }); 22 | }); 23 | keyboard.bind('ctrl + backspace', () => { 24 | console.log('Delete'); 25 | const currentNode = this.projectService.getCurrentPage().currentNode; 26 | if (currentNode.some(({ isRoot }) => isRoot)) { 27 | return; 28 | } 29 | currentNode.forEach(node => { 30 | node.isDelete = true; 31 | }); 32 | this.projectService.getCurrentPage().update({ description: '删除元素' }); 33 | }); 34 | keyboard.bind('ctrl + z', () => { 35 | console.log('BackOff'); 36 | this.projectService.getCurrentPage().backOffHistory(); 37 | }); 38 | keyboard.bind('ctrl + y', () => { 39 | console.log('Forward'); 40 | this.projectService.getCurrentPage().forwardHistory(); 41 | }); 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /src/util/index.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import React from 'react'; 3 | 4 | const isObject = (target: unknown) => { 5 | return Object.prototype.toString.call(target) === '[object Object]'; 6 | }; 7 | 8 | const cloneDeep = (object: T): T => _.cloneDeep(object); 9 | 10 | const getDoubleTime = (time: number) => { 11 | return time > 9 ? time : `0${time}`; 12 | }; 13 | 14 | const formatTime = (time: string = new Date().toDateString()): string => { 15 | const date = new Date(time); 16 | const Y = date.getFullYear(); 17 | const M = date.getMonth() + 1; 18 | const D = date.getDate(); 19 | const H = date.getHours(); 20 | const Mi = getDoubleTime(date.getMinutes()); 21 | const Se = getDoubleTime(date.getSeconds()); 22 | 23 | return `${Y}/${M}/${D} ${H}:${Mi}:${Se}`; 24 | }; 25 | 26 | function cloneJsxObject(treeData: T): T { 27 | return _.cloneDeepWith(treeData, (value: T) => { 28 | if (React.isValidElement(value)) { 29 | return value; 30 | } 31 | }); 32 | } 33 | 34 | const randomChart = (strLen: number = 5) => { 35 | const randoms: string[] = []; 36 | return () => { 37 | const str = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; 38 | const getRandom = () => 39 | new Array(strLen) 40 | .fill(0) 41 | .map(() => str.charAt(Math.floor(Math.random() * str.length))) 42 | .join(''); 43 | let random = getRandom(); 44 | while (randoms.includes(random)) { 45 | random = getRandom(); 46 | } 47 | randoms.push(random); 48 | return random; 49 | }; 50 | }; 51 | 52 | export { 53 | isObject, 54 | cloneDeep, 55 | getDoubleTime, 56 | formatTime, 57 | cloneJsxObject, 58 | randomChart, 59 | }; 60 | -------------------------------------------------------------------------------- /src/pages/App/components/Content/Header/component/code/util/template.ts: -------------------------------------------------------------------------------- 1 | import { Dep, FileContent, templateType, Type } from '.'; 2 | 3 | const getTemplate = ( 4 | type: string, 5 | ): (({ name, dep, code }: FileContent) => string) => { 6 | switch (type) { 7 | case templateType.classComponent: 8 | return CLASS; 9 | case templateType.functionComponent: 10 | return FUNC; 11 | default: 12 | return ({ name, dep, code }) => code; 13 | } 14 | }; 15 | 16 | const getDepImport = (dep: Dep) => { 17 | return Object.entries(dep) 18 | .map(([key, value]) => { 19 | switch (value.type) { 20 | case Type.module: 21 | return `import ${value.import[0]} from "${key}";`; 22 | case Type.more: 23 | return `import ${`{ ${[ 24 | // @ts-ignore 25 | ...new Set(value.import.filter(name => !/\./.test(name))), 26 | ].join(', ')} }`} from "${key}";`; 27 | case Type.none: 28 | return `import "${key}";`; 29 | default: 30 | return null; 31 | } 32 | }) 33 | .filter(_ => _) 34 | .join('\n'); 35 | }; 36 | 37 | const CLASS = ({ name, dep, code }: FileContent) => `import React from "react"; 38 | ${getDepImport(dep)} 39 | 40 | class ${name} extends React.Component { 41 | render() { 42 | return ( 43 | ${code} 44 | ) 45 | } 46 | } 47 | 48 | export default ${name};`; 49 | 50 | const FUNC = ({ 51 | name, 52 | dep, 53 | code, 54 | suffix, 55 | }: FileContent) => `import React from 'react'; 56 | ${getDepImport(dep)} 57 | 58 | const ${name}${suffix === 'tsx' ? ': React.FC<{}>' : ''} = () => { 59 | return ( 60 | ${code} 61 | ); 62 | }; 63 | 64 | export default ${name};`; 65 | 66 | export { getTemplate }; 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "visual-layout", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "./", 6 | "dependencies": { 7 | "@ant-design/icons": "^4.6.3", 8 | "@testing-library/jest-dom": "^5.11.4", 9 | "@testing-library/react": "^11.1.0", 10 | "@testing-library/user-event": "^12.1.10", 11 | "@types/file-saver": "^2.0.3", 12 | "@types/jest": "^26.0.15", 13 | "@types/js-beautify": "^1.13.3", 14 | "@types/node": "^12.0.0", 15 | "@types/react": "^17.0.0", 16 | "@types/react-dom": "^17.0.0", 17 | "antd": "^4.16.12", 18 | "file-saver": "^2.0.5", 19 | "js-beautify": "^1.14.0", 20 | "jszip": "^3.7.1", 21 | "keyboardjs": "^2.6.4", 22 | "lodash": "^4.17.21", 23 | "monaco-editor-webpack-plugin": "^4.1.2", 24 | "node-sass": "^6.0.1", 25 | "react": "^17.0.2", 26 | "react-app-rewired": "^2.1.8", 27 | "react-dom": "^17.0.2", 28 | "react-monaco-editor": "^0.45.0", 29 | "react-scripts": "4.0.3", 30 | "screenfull": "^5.1.0", 31 | "typescript": "^4.1.2", 32 | "web-vitals": "^1.0.1" 33 | }, 34 | "scripts": { 35 | "start": "react-app-rewired start", 36 | "build": "react-app-rewired build", 37 | "test": "react-app-rewired test", 38 | "eject": "react-scripts eject" 39 | }, 40 | "eslintConfig": { 41 | "extends": [ 42 | "react-app", 43 | "react-app/jest" 44 | ] 45 | }, 46 | "browserslist": { 47 | "production": [ 48 | ">0.2%", 49 | "not dead", 50 | "not op_mini all" 51 | ], 52 | "development": [ 53 | "last 1 chrome version", 54 | "last 1 firefox version", 55 | "last 1 safari version" 56 | ] 57 | }, 58 | "devDependencies": { 59 | "@types/keyboardjs": "^2.5.0", 60 | "@types/lodash": "^4.14.172" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 24 | Visual Layout 25 | 26 | 27 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/pages/App/components/Drawer/index.module.scss: -------------------------------------------------------------------------------- 1 | @import 'src/theme/_colors.scss'; 2 | 3 | .drawer { 4 | width: 300px; 5 | background-color: white; 6 | padding: 20px; 7 | position: relative; 8 | .header { 9 | position: absolute; 10 | top: 0; 11 | left: 0px; 12 | right: 0; 13 | display: flex; 14 | justify-content: space-between; 15 | .slider { 16 | background-color: map-get($colors, 'background'); 17 | display: flex; 18 | justify-content: center; 19 | align-items: center; 20 | padding: 5px; 21 | margin-bottom: 5px; 22 | border-top-right-radius: 5px; 23 | border-bottom-right-radius: 5px; 24 | cursor: pointer; 25 | span { 26 | margin-right: 5px; 27 | } 28 | } 29 | .close { 30 | width: 50px; 31 | display: flex; 32 | justify-content: center; 33 | align-items: center; 34 | cursor: pointer; 35 | &:hover { 36 | background-color: map-get($colors, 'background'); 37 | } 38 | } 39 | } 40 | } 41 | 42 | .input { 43 | font-size: 12px; 44 | } 45 | 46 | .header { 47 | position: absolute; 48 | top: 0; 49 | left: 0px; 50 | right: 0; 51 | } 52 | 53 | .tabs { 54 | flex: 1; 55 | padding-top: 10px !important; 56 | :global { 57 | .ant-tabs-content { 58 | height: 100% !important; 59 | } 60 | } 61 | } 62 | 63 | .opr { 64 | margin: 10px 0; 65 | display: flex; 66 | justify-content: flex-end; 67 | border: 1px solid map-get($colors, 'border'); 68 | div { 69 | height: 100%; 70 | flex: 1; 71 | display: flex; 72 | justify-content: center; 73 | align-items: center; 74 | padding: 10px 0; 75 | cursor: pointer; 76 | &:hover { 77 | background-color: map-get($colors, 'background'); 78 | } 79 | } 80 | div:nth-child(1) { 81 | border-right: 1px solid map-get($colors, 'border'); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/model/node.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { NodeService } from 'src/controller'; 3 | import { Children, CodeConfig, NodeOption, Props, Style } from './index'; 4 | import Doc from 'src/controller/browser/document'; 5 | 6 | export const DEFAULT = { 7 | codeConfig: { 8 | isComponent: false, 9 | componentName: '', 10 | }, 11 | }; 12 | class Node extends Doc { 13 | public type: 'Element' | 'Component'; 14 | public _name: string; 15 | public hasCanChild?: boolean; 16 | public children?: Children; 17 | public isDelete: boolean = false; 18 | public isSelect: boolean = false; 19 | public codeConfig: CodeConfig = DEFAULT.codeConfig; 20 | public element?: React.ReactElement; 21 | public isRoot?: boolean = false; 22 | private _styles?: Style[]; 23 | public className?: string; 24 | public props?: Props; 25 | public id: number; 26 | public static random: number = 1; 27 | constructor(Option: NodeOption) { 28 | super(); 29 | const { 30 | type, 31 | styles, 32 | children, 33 | _name, 34 | element, 35 | className, 36 | props, 37 | id, 38 | hasCanChild, 39 | codeConfig, 40 | isSelect, 41 | isDelete, 42 | isRoot = false, 43 | } = Option; 44 | this.type = type; 45 | this._name = _name; 46 | this._styles = styles; 47 | this.children = children; 48 | this.element = element; 49 | this.isRoot = isRoot; 50 | this.props = props; 51 | this.id = id; 52 | this.hasCanChild = hasCanChild; 53 | this.className = className; 54 | this.isSelect = isSelect || false; 55 | this.codeConfig = codeConfig || DEFAULT.codeConfig; 56 | this.isDelete = isDelete || false; 57 | } 58 | 59 | set styles(styles: Style[]) { 60 | this._styles = styles; 61 | } 62 | 63 | get styles() { 64 | this._styles = _.uniqBy(this._styles, 'key'); 65 | return this._styles; 66 | } 67 | } 68 | 69 | export default Node; 70 | -------------------------------------------------------------------------------- /src/pages/App/components/Header/components/Project/index.module.scss: -------------------------------------------------------------------------------- 1 | @import 'src/theme/_colors.scss'; 2 | 3 | .container { 4 | height: 250px; 5 | cursor: pointer; 6 | border: 1px solid map-get($colors, 'border'); 7 | background-color: rgba($color: map-get($colors, 'background'), $alpha: 0.6); 8 | border-radius: 5px; 9 | display: flex; 10 | flex-direction: column; 11 | &:hover { 12 | background-color: rgba($color: map-get($colors, 'background'), $alpha: 1); 13 | } 14 | .operation { 15 | display: flex; 16 | justify-content: center; 17 | align-items: center; 18 | height: 60%; 19 | width: 100%; 20 | border-bottom: 1px solid map-get($colors, 'border'); 21 | position: relative; 22 | .select { 23 | position: absolute; 24 | top: 5px; 25 | left: 5px; 26 | width: 10px; 27 | height: 10px; 28 | border-radius: 50%; 29 | background-color: map-get($colors, 'status'); 30 | } 31 | div { 32 | .item { 33 | margin: 0 10px; 34 | height: 30px; 35 | width: 30px; 36 | border-radius: 50%; 37 | background-color: white; 38 | display: flex; 39 | justify-content: center; 40 | align-items: center; 41 | box-shadow: 1px 1px 4px rgba($color: #000000, $alpha: 0.2); 42 | &:hover { 43 | background-color: rgba($color: map-get($colors, 'background'), $alpha: 1); 44 | } 45 | } 46 | } 47 | } 48 | .info { 49 | height: 40%; 50 | background-color: white; 51 | padding: 20px; 52 | overflow: auto; 53 | div:nth-child(1) { 54 | margin-bottom: 10px; 55 | } 56 | div { 57 | display: flex; 58 | span:nth-child(1) { 59 | display: inline-block; 60 | width: 70px; 61 | } 62 | span:nth-child(2) { 63 | display: inline-block; 64 | flex: 1; 65 | width: 1px; 66 | color: map-get($colors, 'text'); 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/pages/App/slider-components/Layout/index.tsx: -------------------------------------------------------------------------------- 1 | import { AST } from 'src/model'; 2 | import { useContext, useMemo } from 'react'; 3 | import styles from './index.module.scss'; 4 | import _ from 'lodash'; 5 | import { Tooltip } from 'antd'; 6 | import { PageService } from 'src/controller'; 7 | import { EventType } from 'src/controller/browser'; 8 | import Collapse from 'src/pages/components/Collapse'; 9 | import { LayoutAST } from './const'; 10 | import { AppContext } from 'src/context'; 11 | 12 | const Layout: React.FC<{}> = () => { 13 | const { appService } = useContext(AppContext); 14 | const pageService = appService.project.getCurrentPage(); 15 | 16 | return useMemo( 17 | () => ( 18 |
19 | {LayoutAST.map(({ children, title }) => ( 20 | 24 | {title} 25 |
26 | } 27 | > 28 |
29 | {children.map(layout => ( 30 |
31 | 32 |
33 | ))} 34 |
35 | 36 | ))} 37 | 38 | ), 39 | [pageService], 40 | ); 41 | }; 42 | 43 | const Item: React.FC<{ 44 | layout: { title: string; layout: AST }; 45 | pageService: PageService; 46 | }> = ({ layout, pageService }) => { 47 | const node = useMemo(() => { 48 | return pageService?.newNode(_.cloneDeep(layout.layout)); 49 | }, [layout.layout, pageService]); 50 | 51 | const DOM = useMemo( 52 | () => node?.createElement({ eventType: EventType.layout }), 53 | [node], 54 | ); 55 | 56 | return ( 57 | 58 |
{DOM}
59 |
60 | ); 61 | }; 62 | 63 | export default Layout; 64 | -------------------------------------------------------------------------------- /src/pages/App/components/Drawer/attribute/components/component.tsx: -------------------------------------------------------------------------------- 1 | import { ExclamationCircleOutlined } from '@ant-design/icons'; 2 | import { Input, Switch, Tooltip } from 'antd'; 3 | import { useEffect, useState } from 'react'; 4 | import { PageService } from 'src/controller'; 5 | import { DEFAULT } from 'src/model/node'; 6 | import styles from '../index.module.scss'; 7 | 8 | const Component: React.FC<{ page: PageService }> = ({ page }) => { 9 | const [codeConfig, setCodeConfig] = useState(DEFAULT.codeConfig); 10 | const [name, setName] = useState(DEFAULT.codeConfig.componentName); 11 | 12 | useEffect(() => { 13 | if (page) { 14 | setCodeConfig(page.currentNode[0]?.codeConfig); 15 | setName(page.currentNode[0]?.codeConfig.componentName); 16 | } 17 | // eslint-disable-next-line 18 | }, [page?.currentNode, page?.currentNode[0]?.codeConfig]); 19 | 20 | const updateIsComponent = (value: boolean) => { 21 | if (value !== codeConfig.isComponent) { 22 | page.setCodeConfig({ 23 | ...codeConfig, 24 | isComponent: value, 25 | }); 26 | } 27 | }; 28 | 29 | const updateComponentName = (e: React.FocusEvent) => { 30 | if (e.target.value !== codeConfig.componentName) { 31 | page.setCodeConfig({ 32 | ...codeConfig, 33 | componentName: e.target.value, 34 | }); 35 | } 36 | }; 37 | 38 | return ( 39 |
40 |
41 | 组件 42 | 43 | 44 | 45 |
46 |
47 | updateIsComponent(value)} 50 | /> 51 | setName(e.target.value)} 55 | onBlur={e => updateComponentName(e)} 56 | /> 57 |
58 |
59 | ); 60 | }; 61 | 62 | export default Component; 63 | -------------------------------------------------------------------------------- /src/pages/App/components/Drawer/attribute/components/width.tsx: -------------------------------------------------------------------------------- 1 | import { Input, Select } from 'antd'; 2 | import { useEffect } from 'react'; 3 | import { useRef } from 'react'; 4 | import { useState } from 'react'; 5 | import styles from '../index.module.scss'; 6 | import { CssProps } from '../config'; 7 | 8 | const { Option } = Select; 9 | 10 | const KEY = 'width'; 11 | const Width: React.FC = ({ style = [], onChange }) => { 12 | const [option, setOption] = useState('px'); 13 | const [value, setValue] = useState(''); 14 | const unitValue = useRef<{ [props: string]: string }>(); 15 | 16 | const width = style.filter(css => css.key === KEY)[0]; 17 | 18 | useEffect(() => { 19 | const [, pixel, unit] = width?.value.trim().match(/^([0-9]*)(%|px)/) || []; 20 | setOption(unit || 'px'); 21 | setValue(pixel || ''); 22 | unitValue.current = { 23 | [unit]: pixel, 24 | }; 25 | // eslint-disable-next-line 26 | }, [style]); 27 | 28 | const setStyle = () => { 29 | if (width?.value !== `${value}${option}` && value) { 30 | onChange?.([ 31 | { 32 | key: KEY, 33 | value: `${value}${option}`, 34 | }, 35 | ...style.filter(css => css.key !== KEY), 36 | ]); 37 | } 38 | }; 39 | 40 | return ( 41 |
42 |

宽度

43 | { 50 | setOption(unit); 51 | setValue( 52 | ['custom'].includes(unit) 53 | ? '暂不支持' 54 | : unitValue.current?.[unit] || '', 55 | ); 56 | }} 57 | > 58 | 59 | 60 | 61 | 62 | } 63 | disabled={['custom'].includes(option)} 64 | onBlur={() => setStyle()} 65 | value={value} 66 | onChange={e => setValue(e.target.value)} 67 | /> 68 |
69 | ); 70 | }; 71 | 72 | export default Width; 73 | -------------------------------------------------------------------------------- /src/pages/App/components/Drawer/attribute/components/height.tsx: -------------------------------------------------------------------------------- 1 | import { Input, Select } from 'antd'; 2 | import { useEffect } from 'react'; 3 | import { useRef } from 'react'; 4 | import { useState } from 'react'; 5 | import styles from '../index.module.scss'; 6 | import { CssProps } from '../config'; 7 | 8 | const { Option } = Select; 9 | 10 | const KEY = 'height'; 11 | const Height: React.FC = ({ style = [], onChange }) => { 12 | const [option, setOption] = useState('px'); 13 | const [value, setValue] = useState(''); 14 | const unitValue = useRef<{ [props: string]: string }>(); 15 | 16 | const height = style.filter(css => css.key === KEY)[0]; 17 | 18 | useEffect(() => { 19 | const [, pixel, unit] = height?.value.trim().match(/^([0-9]*)(%|px)/) || []; 20 | setOption(unit || 'px'); 21 | setValue(pixel || ''); 22 | unitValue.current = { 23 | [unit]: pixel, 24 | }; 25 | // eslint-disable-next-line 26 | }, [style]); 27 | 28 | const setStyle = () => { 29 | if (height?.value !== `${value}${option}` && value) { 30 | onChange?.([ 31 | { 32 | key: KEY, 33 | value: `${value}${option}`, 34 | }, 35 | ...style.filter(css => css.key !== KEY), 36 | ]); 37 | } 38 | }; 39 | 40 | return ( 41 |
42 |

高度

43 | { 50 | setOption(unit); 51 | setValue( 52 | ['custom'].includes(unit) 53 | ? '暂不支持' 54 | : unitValue.current?.[unit] || '', 55 | ); 56 | }} 57 | > 58 | 59 | 60 | 61 | 62 | 63 | } 64 | disabled={['custom'].includes(option)} 65 | onBlur={() => setStyle()} 66 | value={value} 67 | onChange={e => setValue(e.target.value)} 68 | /> 69 |
70 | ); 71 | }; 72 | 73 | export default Height; 74 | -------------------------------------------------------------------------------- /src/pages/App/components/Content/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import styles from '../index.module.scss'; 2 | import { Button, Popconfirm } from 'antd'; 3 | import { ProjectService } from 'src/controller'; 4 | import Operation from './Operation'; 5 | import { CloseCircleOutlined } from '@ant-design/icons'; 6 | import { Options } from 'src/pages/App/components/Content/index'; 7 | import { openNewBuildModal } from './new-build'; 8 | 9 | const Header: React.FC<{ 10 | projectService: ProjectService; 11 | options: Options; 12 | setOptions: (options: Options) => void; 13 | }> = ({ projectService, options, setOptions }) => { 14 | return ( 15 |
16 |
17 |
18 | {Object.values(projectService.getPages()).map(({ name, id }) => { 19 | const style = { 20 | border: `1px solid ${ 21 | id === projectService.currentId ? '#1890ff' : '' 22 | }`, 23 | }; 24 | return ( 25 |
26 | projectService.setPages(id)}>{name} 27 | projectService.delete(id)} 30 | onCancel={() => {}} 31 | okText="是" 32 | cancelText="否" 33 | > 34 |
35 | 36 |
37 |
38 |
39 | ); 40 | })} 41 |
42 |
43 | 52 |
53 |
54 |
55 | 60 |
61 |
62 | ); 63 | }; 64 | 65 | export default Header; 66 | -------------------------------------------------------------------------------- /src/controller/service/app.ts: -------------------------------------------------------------------------------- 1 | import ProjectService from './project'; 2 | import App from 'src/model/app'; 3 | import { AppConfig, ComponentValue, Options } from 'src/controller'; 4 | import { AppStorage } from '../code'; 5 | 6 | export default class AppService extends App { 7 | static updateView: () => void = () => {}; 8 | private appStorage: AppStorage = new AppStorage(); 9 | public appConfig: AppConfig; 10 | 11 | constructor(appConfig: AppConfig) { 12 | super(); 13 | 14 | this.appConfig = appConfig; 15 | AppService.components = appConfig.components; 16 | 17 | this.init(); 18 | } 19 | 20 | get projects() { 21 | return this.appStorage.projects; 22 | } 23 | 24 | init = () => { 25 | const projectId = [...this.appStorage.projectID].pop(); 26 | const project = typeof projectId === 'string' && this.projects.get(projectId); 27 | if (project) { 28 | this.project = new ProjectService({ 29 | ...this.appConfig.project.options, 30 | ...project, 31 | }); 32 | } else { 33 | this.new(this.appConfig.project.options); 34 | } 35 | if (this.projects.size) { 36 | App.idx = 37 | Math.max(...Array.from(this.projects.keys()).map(_ => Number(_))) + 1; 38 | } 39 | }; 40 | 41 | keep = () => { 42 | if (this.project) { 43 | this.appStorage.keep(this.project); 44 | this.update(); 45 | } 46 | }; 47 | 48 | set = (id: string) => { 49 | const project = this.appStorage.get(id); 50 | if (project) { 51 | this.project = new ProjectService({ 52 | ...this.appConfig.project.options, 53 | ...project, 54 | }); 55 | this.update(); 56 | } 57 | }; 58 | 59 | delete = (id: string) => { 60 | this.appStorage.delete(id); 61 | this.init(); 62 | this.update(); 63 | }; 64 | 65 | new = (options?: Options) => { 66 | const project = new ProjectService(); 67 | project.ceratePage(options || this.appConfig.project.options); 68 | this.project = project; 69 | this.update(); 70 | }; 71 | 72 | update = () => { 73 | Promise.resolve().then(() => AppService.updateView()); 74 | }; 75 | 76 | static registerComponent = (key: string, value: ComponentValue) => { 77 | AppService.components.set(key, value); 78 | Promise.resolve().then(() => AppService.updateView()); 79 | }; 80 | } 81 | -------------------------------------------------------------------------------- /src/pages/App/components/Header/components/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import { FolderAddOutlined, PlusCircleOutlined } from '@ant-design/icons'; 2 | import { Col, Drawer, Row } from 'antd'; 3 | import { useContext } from 'react'; 4 | import { AppContext } from 'src/context'; 5 | import { ProjectObject } from 'src/model'; 6 | import Visible from 'src/pages/components/Visible'; 7 | import Project from '../Project'; 8 | import styles from './index.module.scss'; 9 | 10 | const Home: React.FC<{}> = () => { 11 | const { appService } = useContext(AppContext); 12 | 13 | const projects: ProjectObject[] = []; 14 | appService.projects.forEach(project => projects.unshift(project)); 15 | 16 | return ( 17 | 18 | {({ visible, setVisible }) => ( 19 | <> 20 | setVisible(false)} 26 | visible={visible} 27 | > 28 | 29 | 30 |
{ 32 | appService.new(); 33 | setVisible(false); 34 | }} 35 | > 36 | 37 | 38 | 39 | 新建项目 40 |
41 | 42 | {projects.map(project => ( 43 | 44 | 49 | 50 | ))} 51 |
52 |
53 |
setVisible(!visible)} 55 | style={{ height: '100%', width: '100%', padding: '5px 10px' }} 56 | > 57 | 项目 58 | 61 |
62 | 63 | )} 64 |
65 | ); 66 | }; 67 | 68 | export default Home; 69 | -------------------------------------------------------------------------------- /src/pages/App/components/Content/Header/component/code/code-edit.tsx: -------------------------------------------------------------------------------- 1 | import { Select, Tabs } from 'antd'; 2 | import { useMemo } from 'react'; 3 | import MonacoEditor from 'react-monaco-editor'; 4 | import { ProjectService } from 'src/controller'; 5 | import { CodeConfig } from '.'; 6 | import styles from './index.module.scss'; 7 | import { generateCodeFiles } from './util'; 8 | import { useState } from 'react'; 9 | 10 | const { TabPane } = Tabs; 11 | 12 | const CodeEdit: React.FC<{ project: ProjectService; codeConfig: CodeConfig }> = ({ 13 | project, 14 | codeConfig, 15 | }) => { 16 | const [page, setPage] = useState(project.getCurrentPage()); 17 | 18 | const options = Object.values(project.getPages()).map(page => ({ 19 | key: page.id, 20 | value: page.name, 21 | page, 22 | })); 23 | 24 | const tabPanes = useMemo(() => { 25 | const files = generateCodeFiles(page, codeConfig); 26 | 27 | return files 28 | .slice(1) 29 | .concat(files.slice(0, 1)) 30 | .map(([key, { code, name, suffix, language }]) => { 31 | return ( 32 | 33 | 46 | 47 | ); 48 | }); 49 | }, [codeConfig, page]); 50 | 51 | return ( 52 |
53 |
54 |
当前页面
55 |
56 | { 20 | const modal = models.find(modal => modal.key === key); 21 | onChange?.({ 22 | key: modal?.key || 'custom', 23 | width: modal?.width || value?.width || '0', 24 | height: modal?.height || value?.height || '0', 25 | }); 26 | }} 27 | > 28 | <> 29 | 30 | {models.map(({ key }) => { 31 | return ( 32 | 35 | ); 36 | })} 37 | 38 | 39 |
40 | `${value}px`} 44 | disabled={value?.key !== 'custom'} 45 | parser={value => value?.replace('px', '') || ''} 46 | onChange={width => { 47 | if (width) { 48 | onChange?.({ 49 | key: 'custom', 50 | width: `${String(width)}`, 51 | height: value?.height || '0', 52 | }); 53 | } 54 | }} 55 | /> 56 | 57 | 58 | 59 | `${value}px`} 63 | disabled={value?.key !== 'custom'} 64 | parser={value => value?.replace('px', '') || ''} 65 | onChange={height => { 66 | if (height) { 67 | onChange?.({ 68 | key: 'custom', 69 | width: value?.width || '0', 70 | height: `${String(height)}`, 71 | }); 72 | } 73 | }} 74 | /> 75 |
76 | 77 | ); 78 | }; 79 | 80 | export default Size; 81 | -------------------------------------------------------------------------------- /src/pages/App/components/Drawer/index.tsx: -------------------------------------------------------------------------------- 1 | import { InputNumber, Tabs, Tooltip } from 'antd'; 2 | import { useContext } from 'react'; 3 | import { AppContext } from 'src/context'; 4 | import CssEdit from './css-edit'; 5 | import styles from './index.module.scss'; 6 | import ComponentEdit from './component-edit'; 7 | import Attribute from './attribute'; 8 | import { CloseSquareOutlined, ColumnWidthOutlined } from '@ant-design/icons'; 9 | import { useState } from 'react'; 10 | import { useEffect } from 'react'; 11 | 12 | const { TabPane } = Tabs; 13 | 14 | const DrawerWidth = 'drawer-width'; 15 | 16 | const Drawer: React.FC<{}> = () => { 17 | const [width, setWidth] = useState(300); 18 | const [isShow, setShow] = useState(false); 19 | const { appService } = useContext(AppContext); 20 | 21 | const page = appService.project.getCurrentPage(); 22 | 23 | useEffect(() => { 24 | setShow(!!page?.currentNode[0]); 25 | // eslint-disable-next-line 26 | }, [page?.currentNode, page?.currentNode[0]]); 27 | 28 | useEffect(() => { 29 | setWidth(Number(localStorage.getItem(DrawerWidth) ?? 300)); 30 | }, []); 31 | 32 | return ( 33 |
37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | { 51 | setWidth(value); 52 | localStorage.setItem(DrawerWidth, String(value)); 53 | }} 54 | /> 55 |
56 |
setShow(false)}> 57 | 58 |
59 |
60 | 61 | <> 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 |
74 | ); 75 | }; 76 | 77 | export default Drawer; 78 | -------------------------------------------------------------------------------- /src/controller/service/history.ts: -------------------------------------------------------------------------------- 1 | import { HistoryLog, HistoryObject } from 'src/model'; 2 | import History from 'src/model/history'; 3 | import PageService from './page'; 4 | 5 | class HistoryService extends History { 6 | constructor(options: Partial, _this: PageService) { 7 | super(); 8 | 9 | const { history, id, future } = options; 10 | if (id) { 11 | this.id = id; 12 | } 13 | this.history = 14 | history?.map(({ node, ...rest }) => ({ 15 | ...rest, 16 | node: _this.newNode(node), 17 | })) || []; 18 | 19 | this.future = 20 | future?.map(({ node, ...rest }) => ({ 21 | ...rest, 22 | node: _this.newNode(node), 23 | })) || []; 24 | } 25 | 26 | keep = (rest: Omit) => { 27 | const history = { 28 | id: this.id, 29 | time: new Date().toUTCString(), 30 | ...rest, 31 | }; 32 | this.history.push(history); 33 | // clear future history 34 | this.future = []; 35 | }; 36 | 37 | return = (_id: number) => { 38 | const index = this.history.findIndex(({ id }) => id === _id); 39 | if (index !== -1) { 40 | this.future.unshift(...this.history.slice(index)); 41 | this.history = this.history.slice(0, index); 42 | } 43 | }; 44 | 45 | recovery = (_id: number) => { 46 | const index = this.future.findIndex(({ id }) => id === _id); 47 | if (index !== -1) { 48 | this.history.push(...this.future.slice(0, index + 1)); 49 | this.future = this.future.slice(index + 1); 50 | } 51 | }; 52 | 53 | backOff = (step: number = 1) => { 54 | while (step > 0) { 55 | const future = this.history.pop(); 56 | if (future) { 57 | this.future.unshift(future); 58 | } 59 | step--; 60 | } 61 | }; 62 | 63 | forward = (step: number = 1) => { 64 | while (step > 0) { 65 | const future = this.future.shift(); 66 | if (future) { 67 | this.history.push(future); 68 | } 69 | step--; 70 | } 71 | }; 72 | 73 | current = (): HistoryLog => { 74 | return this.history[this.history.length - 1]; 75 | }; 76 | 77 | toObject = (): HistoryObject => { 78 | return { 79 | history: this.history.map(({ node, ...rest }) => ({ 80 | ...rest, 81 | node: node.toObject(), 82 | })), 83 | future: this.future.map(({ node, ...rest }) => ({ 84 | ...rest, 85 | node: node.toObject(), 86 | })), 87 | id: this.id, 88 | }; 89 | }; 90 | } 91 | 92 | export default HistoryService; 93 | -------------------------------------------------------------------------------- /src/pages/App/components/Content/Header/keep.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Input, Popover } from 'antd'; 2 | import { useState } from 'react'; 3 | import { useEffect } from 'react'; 4 | import { useMemo } from 'react'; 5 | import { useContext } from 'react'; 6 | import { AppContext } from 'src/context'; 7 | 8 | const Keep: React.FC<{}> = () => { 9 | const { appService } = useContext(AppContext); 10 | 11 | const project = appService.project; 12 | 13 | const pages = project.getPages(); 14 | 15 | const [name, setName] = useState(project.name); 16 | const [description, setDescription] = useState(project.description); 17 | 18 | const historyProject = appService.projects.get(project.id); 19 | 20 | const isSome = useMemo(() => { 21 | return name === project.name && description === project.description; 22 | }, [name, description, project.name, project.description]); 23 | 24 | const pageIds = useMemo(() => { 25 | return Object.values(historyProject?.pages || {}).map(page => page.id); 26 | }, [historyProject]); 27 | 28 | const isDisable = Object.values(pages).every(page => { 29 | const curPageId = page.history.history.slice(-1).pop()?.id; 30 | 31 | const historyPageId = Object.values(historyProject?.pages || {}) 32 | .find(({ id }) => id === page.id) 33 | ?.history.history.slice(-1) 34 | .pop()?.id; 35 | 36 | return pageIds.includes(page.id) ? curPageId === historyPageId : false; 37 | }); 38 | 39 | useEffect(() => { 40 | setName(project.name); 41 | setDescription(project.description); 42 | }, [project]); 43 | 44 | return ( 45 | 48 |
49 | 项目名 50 | { 53 | setName(e.target.value); 54 | }} 55 | /> 56 |
57 |
58 | 项目描述 59 | { 62 | setDescription(e.target.value); 63 | }} 64 | /> 65 |
66 | 67 | } 68 | title="项目信息" 69 | > 70 | 80 |
81 | ); 82 | }; 83 | 84 | export default Keep; 85 | -------------------------------------------------------------------------------- /src/pages/App/slider-components/Component/index.tsx: -------------------------------------------------------------------------------- 1 | import { SearchOutlined } from '@ant-design/icons'; 2 | import * as components from 'antd'; 3 | import _, { isString } from 'lodash'; 4 | import React, { useContext, useMemo } from 'react'; 5 | import { useState } from 'react'; 6 | import { AppContext } from 'src/context'; 7 | import { PageService } from 'src/controller'; 8 | import { EventType } from 'src/controller/browser'; 9 | import { AST } from 'src/model'; 10 | import { ComponentsAST } from './const'; 11 | import styles from './index.module.scss'; 12 | 13 | const Components: React.FC<{}> = () => { 14 | const [value, setValue] = useState(''); 15 | const { appService } = useContext(AppContext); 16 | const pageService = appService.project.getCurrentPage(); 17 | 18 | return useMemo( 19 | () => ( 20 |
21 |
22 | setValue(e.target.value)} 25 | addonAfter={} 26 | /> 27 |
28 |
29 | {ComponentsAST.filter(component => { 30 | return ( 31 | !value || 32 | !component.children || 33 | (typeof component.children !== 'string' && 34 | component.children.every(child => { 35 | return new RegExp(`${_.escapeRegExp(value)}`, 'ig').test( 36 | isString(child) ? child : child._name, 37 | ); 38 | })) 39 | ); 40 | }).map((ast, index) => ( 41 | 42 | ))} 43 |
44 |
45 | ), 46 | [pageService, value], 47 | ); 48 | }; 49 | 50 | const Component: React.FC<{ ast: AST; pageService: PageService }> = ({ 51 | ast, 52 | pageService, 53 | }) => { 54 | const node = useMemo( 55 | () => pageService?.newNode(_.cloneDeep(ast)), 56 | [ast, pageService], 57 | ); 58 | 59 | const DOM = useMemo( 60 | () => ( 61 | 69 |
{node?.createElement({ eventType: EventType.layout })}
70 |
71 | ), 72 | [node, ast.children], 73 | ); 74 | 75 | return DOM; 76 | }; 77 | 78 | export default Components; 79 | -------------------------------------------------------------------------------- /src/controller/code/app-storage.ts: -------------------------------------------------------------------------------- 1 | import proxy from 'src/controller/util/proxy'; 2 | import { ProjectObject } from 'src/model'; 3 | import ProjectService from '../service/project'; 4 | 5 | const PROJECT_IDS = 'projectIds'; 6 | 7 | export default class AppStorage { 8 | public projectID: string[] = []; 9 | public projects: Map = new Map(); 10 | 11 | constructor() { 12 | this.init(); 13 | } 14 | 15 | init = () => { 16 | try { 17 | const ids = this.getItem(PROJECT_IDS); 18 | if (ids) { 19 | const projectIds = JSON.parse(ids); 20 | Array.isArray(projectIds) && 21 | projectIds.forEach(id => { 22 | this.projectID.push(id); 23 | const projectString = this.getItem(id); 24 | if (typeof projectString === 'string') { 25 | const projectObject: ProjectObject = JSON.parse(projectString); 26 | this.projects.set(id, projectObject); 27 | } 28 | }); 29 | } 30 | this.projectID = proxy( 31 | this.projectID, 32 | () => {}, 33 | () => { 34 | this.setItem( 35 | PROJECT_IDS, 36 | this.projectID.filter(_ => _), 37 | ); 38 | }, 39 | ); 40 | } catch (err) { 41 | console.error(err.message); 42 | } 43 | }; 44 | 45 | keep = (project: ProjectService) => { 46 | const idString = String(project.id); 47 | if (this.projectID.includes(idString)) { 48 | const index = this.projectID.findIndex(id => id === idString); 49 | this.projectID.splice(index, 1); 50 | this.projectID.push(idString); 51 | } else { 52 | this.projectID.push(idString); 53 | } 54 | const projectObject = project.toObject(); 55 | this.projects.delete(idString); 56 | this.projects.set(idString, projectObject); 57 | this.setItem(idString, projectObject); 58 | }; 59 | 60 | setItem(id: string, data: T) { 61 | localStorage.setItem(id, JSON.stringify(data)); 62 | } 63 | 64 | removeItem(id: string) { 65 | localStorage.removeItem(id); 66 | 67 | if (this.projects.has(id)) { 68 | this.projects.delete(id); 69 | } 70 | 71 | if (this.projectID.includes(id)) { 72 | const index = this.projectID.findIndex(projectID => projectID === id); 73 | this.projectID.splice(index, 1); 74 | } 75 | } 76 | 77 | getItem(id: string): string | null { 78 | return localStorage.getItem(id); 79 | } 80 | 81 | delete = (id: string) => { 82 | this.removeItem(id); 83 | }; 84 | 85 | get = (id: string) => { 86 | return this.projects.get(id); 87 | }; 88 | } 89 | -------------------------------------------------------------------------------- /src/model/index.ts: -------------------------------------------------------------------------------- 1 | import Project from './project'; 2 | import Page from './page'; 3 | import Node from './node'; 4 | import History from './history'; 5 | import App from './app'; 6 | import { NodeService } from 'src/controller'; 7 | import React from 'react'; 8 | 9 | export { Project, Page, Node, History, App }; 10 | 11 | export interface Style { 12 | key: string; 13 | value: string; 14 | title?: string; 15 | } 16 | 17 | export type Children = T[] | string | null; 18 | 19 | export type JSONComponent = Pick< 20 | AST, 21 | '_name' | 'styles' | 'className' | 'hasCanChild' 22 | > & { 23 | children: Children; 24 | _type: 'Element' | 'Component'; 25 | } & Props; 26 | 27 | export interface AST { 28 | _name: string; 29 | type: 'Element' | 'Component'; 30 | children?: Children; 31 | styles?: Style[]; 32 | element?: React.ReactElement; 33 | props?: Props; 34 | className?: string; 35 | hasCanChild?: boolean; 36 | } 37 | 38 | export type HistoryObject = { 39 | history: (Omit & { 40 | node: NodeObject; 41 | })[]; 42 | future: (Omit & { 43 | node: NodeObject; 44 | })[]; 45 | id: number; 46 | }; 47 | 48 | export interface CodeConfig { 49 | isComponent: boolean; 50 | componentName: string; 51 | } 52 | 53 | export type NodeObject = Omit & { 54 | children?: string | Children; 55 | id: number; 56 | isRoot?: boolean; 57 | isDelete: boolean; 58 | isSelect: boolean; 59 | codeConfig: CodeConfig; 60 | }; 61 | 62 | export type ASTObject = Omit & { 63 | children?: Children; 64 | }; 65 | 66 | export type PageObject = Pick & { 67 | currentNode: NodeObject[]; 68 | page: NodeObject; 69 | target: ASTObject; 70 | history: HistoryObject; 71 | }; 72 | 73 | export type ProjectObject = Pick< 74 | Project, 75 | 'id' | 'currentId' | 'name' | 'description' 76 | > & { 77 | pages: { 78 | [props: string]: PageObject; 79 | }; 80 | idx: number; 81 | }; 82 | 83 | export type NodeOption = AST & { 84 | id: number; 85 | isRoot?: boolean; 86 | isDelete?: boolean; 87 | isSelect?: boolean; 88 | codeConfig?: CodeConfig; 89 | children?: Children; 90 | }; 91 | 92 | export interface HistoryLog { 93 | id: number; 94 | time: string; 95 | node: NodeService; 96 | description?: string; 97 | } 98 | 99 | export interface Props { 100 | [props: string]: unknown; 101 | } 102 | -------------------------------------------------------------------------------- /src/pages/App/components/Content/index.module.scss: -------------------------------------------------------------------------------- 1 | @import 'src/theme/_colors.scss'; 2 | 3 | .content { 4 | margin: 20px; 5 | background-color: white; 6 | display: flex; 7 | flex-direction: column; 8 | } 9 | 10 | .header { 11 | height: 50px; 12 | border-bottom: 1px solid map-get($colors, 'border'); 13 | padding: 0 20px; 14 | display: flex; 15 | .pagesWarper { 16 | display: flex; 17 | margin: 10px 0; 18 | position: relative; 19 | padding-right: 100px; 20 | width: calc(100% - 300px); 21 | .pages { 22 | width: 100%; 23 | overflow: auto; 24 | display: flex; 25 | margin-right: 20px; 26 | .page { 27 | padding: 0 10px; 28 | padding-right: 30px; 29 | border: 1px solid map-get($colors, 'border'); 30 | border-radius: 5px; 31 | display: flex; 32 | justify-content: center; 33 | align-items: center; 34 | cursor: pointer; 35 | position: relative; 36 | margin-left: 20px; 37 | &:first-child { 38 | margin-left: 0; 39 | } 40 | .close { 41 | position: absolute; 42 | right: 2px; 43 | width: 20px; 44 | height: 100%; 45 | display: flex; 46 | justify-content: center; 47 | align-items: center; 48 | margin: 0 5px; 49 | img { 50 | height: 12px; 51 | width: 12px; 52 | } 53 | } 54 | } 55 | } 56 | .new { 57 | height: 20px; 58 | width: 100px; 59 | display: flex; 60 | position: absolute; 61 | cursor: pointer; 62 | right: 0; 63 | } 64 | } 65 | .opr { 66 | width: 500px; 67 | border-left: 1px solid map-get($colors, 'border'); 68 | } 69 | } 70 | 71 | .operation { 72 | display: flex; 73 | justify-content: flex-end; 74 | height: 100%; 75 | align-items: center; 76 | .zoom { 77 | display: flex; 78 | align-items: center; 79 | cursor: pointer; 80 | .inputNumber { 81 | width: 60px; 82 | margin: 0 10px; 83 | } 84 | } 85 | & > div { 86 | padding: 0 10px; 87 | } 88 | } 89 | 90 | .history { 91 | display: flex; 92 | div { 93 | display: flex; 94 | width: 35px; 95 | justify-content: center; 96 | align-items: center; 97 | cursor: pointer; 98 | img { 99 | width: 24px; 100 | height: 24px; 101 | } 102 | } 103 | } 104 | 105 | .code, 106 | .eye { 107 | display: flex; 108 | display: flex; 109 | justify-content: center; 110 | align-items: center; 111 | cursor: pointer; 112 | } 113 | -------------------------------------------------------------------------------- /src/pages/App/slider-components/History/index.tsx: -------------------------------------------------------------------------------- 1 | import { AppContext } from 'src/context'; 2 | import { useContext } from 'react'; 3 | import styles from './index.module.scss'; 4 | import React from 'react'; 5 | import { formatTime } from 'src/util'; 6 | import { HistoryLog } from 'src/model'; 7 | import { RedoOutlined, UndoOutlined } from '@ant-design/icons'; 8 | import { Tooltip } from 'antd'; 9 | 10 | const History: React.FC<{}> = () => { 11 | const { appService } = useContext(AppContext); 12 | 13 | const page = Object.values(appService.project.getPages()).filter( 14 | ({ id }) => id === appService.project.currentId, 15 | )[0]; 16 | 17 | const renderHistory = (): React.ReactNode => { 18 | return page?.history.history 19 | .slice() 20 | .filter(_ => _) 21 | .reverse() 22 | .map(history => ( 23 | ( 27 | { 29 | page.returnHistory(id); 30 | }} 31 | > 32 | 33 | 34 | 35 | 36 | )} 37 | /> 38 | )); 39 | }; 40 | 41 | const renderFutureHistory = (): React.ReactNode => { 42 | return page?.history.future 43 | .slice() 44 | .filter(_ => _) 45 | .reverse() 46 | .map(history => ( 47 | ( 52 | { 54 | page.recoveryHistory(id); 55 | }} 56 | > 57 | 58 | 59 | 60 | 61 | )} 62 | /> 63 | )); 64 | }; 65 | 66 | return ( 67 |
68 | {renderFutureHistory()} 69 | {renderHistory()} 70 |
71 | ); 72 | }; 73 | 74 | const HistoryList = ({ 75 | history, 76 | renderSpanIcon, 77 | className, 78 | }: { 79 | history: HistoryLog; 80 | renderSpanIcon: (id: number) => React.ReactNode; 81 | className?: string; 82 | }) => { 83 | const { id, time, description } = history; 84 | return ( 85 |
86 | {description} 87 | {formatTime(time)} 88 | {renderSpanIcon(id)} 89 |
90 | ); 91 | }; 92 | 93 | export default History; 94 | -------------------------------------------------------------------------------- /src/controller/browser/event.ts: -------------------------------------------------------------------------------- 1 | import { isNull } from 'lodash'; 2 | import React from 'react'; 3 | import { DragEleId } from 'src/const'; 4 | import { isString } from 'src/controller/util'; 5 | import { NodeService } from '..'; 6 | 7 | class DocEvent { 8 | private static dragData: { [props: string]: NodeService } = {}; 9 | 10 | onDrop = (node: NodeService) => (ev: React.DragEvent) => { 11 | ev.preventDefault(); 12 | ev.stopPropagation(); 13 | const id = ev.dataTransfer?.getData(DragEleId); 14 | if (id) { 15 | const nodeService: NodeService = Reflect.get(DocEvent.dragData, id); 16 | if (nodeService) { 17 | Reflect.deleteProperty(DocEvent.dragData, id); 18 | // up merge 19 | node.styles.push(...nodeService.styles); 20 | 21 | const children = isString(node.children) 22 | ? [node.children] 23 | : node.children || []; 24 | 25 | !isNull(children) && 26 | children?.push( 27 | ...((isString(nodeService.children) 28 | ? [nodeService.children] 29 | : nodeService.children?.filter(_ => _)) || []), 30 | ); 31 | 32 | if (!isNull(node.children)) { 33 | node.children = children; 34 | } 35 | } 36 | 37 | node.getPageServiceInstance()?.update({ description: '添加元素' }); 38 | } 39 | }; 40 | 41 | onDragOver = (node: NodeService) => (ev: React.DragEvent) => { 42 | ev.preventDefault(); 43 | if (ev.dataTransfer?.dropEffect) { 44 | ev.dataTransfer.dropEffect = 'move'; 45 | } 46 | }; 47 | 48 | onClick = (node: NodeService) => (ev: React.MouseEvent) => { 49 | try { 50 | ev.stopPropagation(); 51 | // some node click return 52 | if ( 53 | node 54 | .getPageServiceInstance() 55 | ?.currentNode.map(node => node.toString()) 56 | .join(';') === node.toString() 57 | ) { 58 | return; 59 | } 60 | 61 | if (node.type !== 'Component' && ev.currentTarget !== ev.target) { 62 | // component no click event 63 | } 64 | node.getPageServiceInstance()?.setCurrentNode([node]); 65 | } catch (err) { 66 | console.error(`onClick Event Error ${err?.message}`); 67 | } 68 | }; 69 | 70 | createContainerEvent = (node: NodeService) => { 71 | const id = `drag${Math.random()}`; 72 | return { 73 | id, 74 | draggable: true, 75 | onDragStart: (ev: React.DragEvent) => { 76 | // fix self drag 77 | Reflect.set(DocEvent.dragData, id, node.copy({})); 78 | ev.dataTransfer?.setData(DragEleId, id); 79 | if (ev.dataTransfer?.dropEffect) { 80 | ev.dataTransfer.dropEffect = 'move'; 81 | } 82 | }, 83 | }; 84 | }; 85 | } 86 | 87 | export default DocEvent; 88 | -------------------------------------------------------------------------------- /src/pages/App/components/Content/Header/component/code/index.tsx: -------------------------------------------------------------------------------- 1 | import { CodeOutlined, DownloadOutlined } from '@ant-design/icons'; 2 | import { Button, Drawer, Tooltip } from 'antd'; 3 | import { ProjectService } from 'src/controller'; 4 | import Visible from 'src/pages/components/Visible'; 5 | import BaseInfo from './base-info'; 6 | import Config from './config'; 7 | import CodeEdit from './code-edit'; 8 | import styles from './index.module.scss'; 9 | import { useState } from 'react'; 10 | import { exportCode, templateType } from './util'; 11 | 12 | export interface CodeConfig { 13 | cssLocation: 'inner' | 'link'; 14 | page: string; 15 | fileSuffix: 'jsx' | 'tsx'; 16 | component: templateType; 17 | cssFileSuffix: 'css' | 'less' | 'scss'; 18 | cssModule: boolean; 19 | } 20 | 21 | export const getInitCodeConfig = (project?: ProjectService): CodeConfig => ({ 22 | cssLocation: 'link', 23 | page: (project && Object.values(project.getPages())[0].id) || '', 24 | fileSuffix: 'jsx', 25 | component: templateType.functionComponent, 26 | cssFileSuffix: 'css', 27 | cssModule: true, 28 | }); 29 | 30 | const Code: React.FC<{ project: ProjectService }> = ({ project }) => { 31 | const [codeConfig, setCodeConfig] = useState( 32 | getInitCodeConfig(project), 33 | ); 34 | 35 | return ( 36 | 37 | {({ visible, setVisible }) => ( 38 | <> 39 | setVisible(false)} 44 | visible={visible} 45 | destroyOnClose 46 | > 47 |
48 |
49 |
50 | 58 |
59 | 60 | 61 |
62 |
63 | 64 |
65 |
66 |
67 | 68 | setVisible(true)} 71 | /> 72 | 73 | 74 | )} 75 |
76 | ); 77 | }; 78 | 79 | export default Code; 80 | -------------------------------------------------------------------------------- /src/controller/browser/document.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { EventType } from '.'; 3 | import DocEvent from './event'; 4 | import { NodeService } from '..'; 5 | import { strikeToCamel } from '../util'; 6 | import { render } from 'src/controller/react'; 7 | import { isString } from 'src/controller/util'; 8 | 9 | class Doc extends DocEvent { 10 | create = ({ 11 | node, 12 | eventType, 13 | }: { 14 | node: NodeService; 15 | eventType?: EventType; 16 | }): React.ReactElement => { 17 | const { type, className, children } = node; 18 | 19 | const iscContainer = eventType === EventType.container; 20 | 21 | const props = iscContainer 22 | ? { 23 | onDrop: this.onDrop(node), 24 | onDragOver: this.onDragOver(node), 25 | onClick: this.onClick(node), 26 | } 27 | : eventType && this.createContainerEvent(node); 28 | 29 | const getStyles = (node: NodeService) => { 30 | const { styles, isSelect } = node; 31 | 32 | const { selectStyle, previewStyle } = node.getPageServiceInstance().options; 33 | 34 | const usePreviewStyle = 35 | previewStyle.filter( 36 | ({ isCanUse }) => !eventType || !iscContainer || isCanUse, 37 | ) || []; 38 | return usePreviewStyle 39 | .concat(isSelect ? selectStyle || [] : []) 40 | .concat(styles) 41 | .reduce((styles: { [props: string]: string }, style) => { 42 | const { key, value } = style; 43 | styles[strikeToCamel(key)] = value; 44 | return styles; 45 | }, {}); 46 | }; 47 | 48 | const getChildren = () => { 49 | return isString(children) 50 | ? children 51 | : children 52 | ?.map(child => { 53 | if (child) { 54 | return isString(child) 55 | ? child 56 | : this.create({ 57 | node: child, 58 | eventType: iscContainer ? eventType : undefined, 59 | }); 60 | } 61 | return undefined; 62 | }) 63 | .filter(_ => _); 64 | }; 65 | 66 | node.element = 67 | type === 'Component' 68 | ? render({ 69 | component: node, 70 | props: { 71 | onClick: this.onClick, 72 | onDrop: this.onDrop, 73 | onDragOver: this.onDragOver, 74 | }, 75 | create: (node: NodeService) => { 76 | return this.create({ node, eventType }); 77 | }, 78 | getStyles, 79 | }) 80 | : React.createElement( 81 | node._name, 82 | { 83 | key: node.id, 84 | style: getStyles(node), 85 | className: className, 86 | ...props, 87 | }, 88 | getChildren(), 89 | ); 90 | 91 | return node.element; 92 | }; 93 | } 94 | 95 | export default Doc; 96 | -------------------------------------------------------------------------------- /src/pages/App/components/Content/Header/component/code/config.tsx: -------------------------------------------------------------------------------- 1 | import { Radio, Switch } from 'antd'; 2 | import { CodeConfig } from '.'; 3 | import styles from './index.module.scss'; 4 | import { templateType } from './util'; 5 | 6 | const Config: React.FC<{ 7 | setCodeConfig: (codeConfig: CodeConfig) => void; 8 | codeConfig: CodeConfig; 9 | }> = ({ setCodeConfig, codeConfig }) => { 10 | return ( 11 |
12 |
13 |

React 配置

14 |
15 |
16 |
17 |
组件形式
18 |
19 | { 22 | setCodeConfig({ 23 | ...codeConfig, 24 | component: e.target.value, 25 | }); 26 | }} 27 | > 28 | 类组件 29 | 函数组件 30 | 31 |
32 |
33 |
34 |
文件后缀
35 |
36 | { 39 | setCodeConfig({ 40 | ...codeConfig, 41 | fileSuffix: e.target.value, 42 | }); 43 | }} 44 | > 45 | .tsx 46 | .jsx 47 | 48 |
49 |
50 |
51 |
52 |

样式配置

53 |
54 |
55 |
56 |
CSS文件后缀
57 |
58 | { 61 | setCodeConfig({ 62 | ...codeConfig, 63 | cssFileSuffix: e.target.value, 64 | }); 65 | }} 66 | > 67 | .css 68 | .less 69 | .scss 70 | 71 |
72 |
73 |
74 |
CSS-Module
75 |
76 | { 79 | setCodeConfig({ 80 | ...codeConfig, 81 | cssModule: value, 82 | }); 83 | }} 84 | /> 85 |
86 |
87 |
88 |
89 | ); 90 | }; 91 | 92 | export default Config; 93 | -------------------------------------------------------------------------------- /src/controller/react/container.tsx: -------------------------------------------------------------------------------- 1 | import { isFunction, isString } from 'lodash'; 2 | import React from 'react'; 3 | import { NodeService } from '..'; 4 | import AppService from '../service/app'; 5 | import { isElement } from 'src/controller/util'; 6 | 7 | export type Event = (node: NodeService) => (ev: T) => void; 8 | export interface Rest { 9 | onClick: Event>; 10 | onDrop: Event>; 11 | onDragOver: Event>; 12 | } 13 | 14 | export function Container({ component, ...props }: Props) { 15 | const components = AppService.components; 16 | 17 | const { create, getStyles } = props; 18 | 19 | function render(node: NodeService, args?: Rest): React.ReactNode { 20 | const { _name, type, children, hasCanChild, id } = node; 21 | 22 | if (type === 'Element') { 23 | return create(node); 24 | } 25 | 26 | const Component = components.get(_name.split('.')[0]); 27 | 28 | // support HTML label 29 | const C = /^[A-Z]/.test(_name) 30 | ? /\./.test(_name) // support two level component 31 | ? (Component?.to as any)?.[_name.split('.')[1]] 32 | : Component?.to 33 | : _name; 34 | 35 | const props = Object.entries(node.props || {}).reduce( 36 | (props: { [props: string]: unknown }, [key, value]) => { 37 | const isComponent = isElement(value); 38 | 39 | props[key] = isComponent ? render(value as NodeService, args) : value; 40 | return props; 41 | }, 42 | {}, 43 | ); 44 | 45 | if (!C) { 46 | console.error(`${_name} component not found`); 47 | return <>; 48 | } 49 | 50 | // proxy onClick event 51 | const onClick = (ev: React.MouseEvent) => { 52 | if (props?.onClick && isFunction(props?.onClick)) { 53 | (props.onClick as any)?.(ev); 54 | } 55 | if (args?.onClick) { 56 | args.onClick?.(node)?.(ev); 57 | } 58 | }; 59 | 60 | // element children null error 61 | const childrenElement = 62 | typeof children === 'string' 63 | ? children 64 | : children?.map(child => (isString(child) ? child : render(child, args))); 65 | 66 | // component? component: string 67 | return ( 68 | ) => { 75 | args?.onDragOver(node)?.(e); 76 | }, 77 | onDrop: (e: React.DragEvent) => { 78 | args?.onDrop(node)?.(e); 79 | }, 80 | style: getStyles(node), 81 | } 82 | : {})} 83 | onClick={onClick} 84 | > 85 | {childrenElement} 86 | 87 | ); 88 | } 89 | 90 | return <>{render(component, props?.props)}; 91 | } 92 | 93 | export interface Props { 94 | component: NodeService; 95 | props?: Rest; 96 | create: (node: NodeService) => React.ReactElement; 97 | getStyles: (node: NodeService) => { [props: string]: string }; 98 | } 99 | 100 | export function render({ component, ...rest }: Props) { 101 | try { 102 | return component ? : <>; 103 | } catch (err) { 104 | console.error(`组件渲染错误,请检查相关属性是否正确。${err.message}`); 105 | return <>; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/pages/App/components/Content/Header/Operation.tsx: -------------------------------------------------------------------------------- 1 | import revoke from 'src/static/img/revoke.png'; 2 | import forward from 'src/static/img/forward.png'; 3 | import styles from '../index.module.scss'; 4 | import { ZoomInOutlined, ZoomOutOutlined } from '@ant-design/icons'; 5 | import { InputNumber, Tooltip } from 'antd'; 6 | import { Options } from 'src/pages/App/components/Content/index'; 7 | import { ProjectService } from 'src/controller'; 8 | import Preview from './component/preview'; 9 | import Keep from './keep'; 10 | import Code from './component/code'; 11 | 12 | const Max_Zoom = 3; 13 | const Min_Zoom = 1; 14 | 15 | const Operation: React.FC<{ 16 | options: Options; 17 | setOptions: (options: Options) => void; 18 | projectService: ProjectService; 19 | }> = ({ options, setOptions, projectService }) => { 20 | return ( 21 |
22 |
23 | 24 |
25 |
26 | 27 |
28 |
29 |
{ 31 | projectService.getCurrentPage().backOffHistory(); 32 | }} 33 | > 34 | 35 | 36 | 37 |
38 |
{ 40 | projectService.getCurrentPage().forwardHistory(); 41 | }} 42 | > 43 | 44 | 45 | 46 |
47 |
48 | 49 |
50 |
51 | 52 | { 59 | if (options.zoom <= Min_Zoom) return; 60 | setOptions({ 61 | ...options, 62 | zoom: options.zoom - 0.5, 63 | }); 64 | }} 65 | /> 66 | 67 |
68 | { 74 | setOptions({ 75 | ...options, 76 | zoom: value, 77 | }); 78 | }} 79 | className={styles.inputNumber} 80 | /> 81 |
82 | 83 | { 90 | if (options.zoom >= Max_Zoom) return; 91 | setOptions({ 92 | ...options, 93 | zoom: options.zoom + 0.5, 94 | }); 95 | }} 96 | /> 97 | 98 |
99 |
100 |
101 | 102 |
103 |
104 | ); 105 | }; 106 | export default Operation; 107 | -------------------------------------------------------------------------------- /src/pages/App/components/Content/Header/new-build.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Form, Input, Modal } from 'antd'; 2 | import { useState } from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import { PreviewStyle, SelectStyle } from 'src/const'; 5 | import { models } from 'src/const/container'; 6 | import { ProjectService } from 'src/controller'; 7 | import Size from './component/size'; 8 | 9 | export interface Values { 10 | name: string; 11 | canvas: { 12 | height: string; 13 | width: string; 14 | }; 15 | } 16 | 17 | export interface Canvas { 18 | height: string; 19 | width: string; 20 | key: string; 21 | } 22 | 23 | export const CreateModal: React.FC<{ projectService: ProjectService }> = ({ 24 | projectService, 25 | }) => { 26 | const [visible, setVisible] = useState(true); 27 | 28 | const checkCanvas = (_: any, value: Canvas) => { 29 | if (value.height !== '0' && value.width !== '0') { 30 | return Promise.resolve(); 31 | } 32 | return Promise.reject(new Error('未设置画布大小')); 33 | }; 34 | 35 | const createPage = (values: Values) => { 36 | projectService.ceratePage({ 37 | name: values.name, 38 | target: { 39 | _name: 'div', 40 | type: 'Element', 41 | styles: [ 42 | { 43 | key: 'height', 44 | value: `${values.canvas.height}px`, 45 | }, 46 | { 47 | key: 'width', 48 | value: `${values.canvas.width}px`, 49 | }, 50 | { 51 | key: 'background', 52 | value: 'white', 53 | }, 54 | { 55 | key: 'overflow', 56 | value: 'auto', 57 | }, 58 | ], 59 | children: [], 60 | }, 61 | selectStyle: SelectStyle, 62 | previewStyle: PreviewStyle, 63 | }); 64 | setVisible(false); 65 | }; 66 | 67 | return ( 68 | {}} 72 | onCancel={() => setVisible(false)} 73 | footer={null} 74 | > 75 |
{}} 85 | > 86 | 91 | 92 | 93 | 94 | 99 | 100 | 101 | 102 | 103 | 110 | 113 | 114 |
115 |
116 | ); 117 | }; 118 | 119 | export const openNewBuildModal = (props: { projectService: ProjectService }) => { 120 | return new Promise(() => { 121 | const el = document.createElement('div'); 122 | ReactDOM.render(, el); 123 | }); 124 | }; 125 | 126 | export default openNewBuildModal; 127 | -------------------------------------------------------------------------------- /src/pages/App/components/Drawer/component-edit.tsx: -------------------------------------------------------------------------------- 1 | import { PageService } from 'src/controller'; 2 | import MonacoEditor from 'react-monaco-editor'; 3 | import { useRef } from 'react'; 4 | import { Alert } from 'antd'; 5 | import { useState } from 'react'; 6 | import { useEffect } from 'react'; 7 | import { formatTime } from 'src/util'; 8 | import { JSONComponent } from 'src/model'; 9 | import styles from './index.module.scss'; 10 | import { isString } from 'lodash'; 11 | 12 | const ComponentEdit: React.FC<{ page: PageService }> = ({ page }) => { 13 | const [errorMessage, setErrorMessage] = useState(''); 14 | 15 | const messageTimer = useRef(); 16 | 17 | const component = page?.currentNode[0]?.getComponentConfig(); 18 | 19 | const value = JSON.stringify(component, null, 2) || ''; 20 | 21 | const [code, setCode] = useState(value); 22 | 23 | useEffect(() => { 24 | setCode(value); 25 | }, [value]); 26 | 27 | useEffect(() => { 28 | messageTimer.current = window.setTimeout(() => { 29 | if (errorMessage) { 30 | setErrorMessage(''); 31 | } 32 | }, 2000); 33 | }, [errorMessage]); 34 | 35 | const update = (code: string) => { 36 | clear(); 37 | if (isTrueComponent(code)) { 38 | const component = JSON.parse(code); 39 | page?.setComponent(component); 40 | } 41 | 42 | return () => clear(); 43 | }; 44 | 45 | const reset = () => { 46 | setCode(value); 47 | }; 48 | 49 | const clear = () => { 50 | window.clearTimeout(messageTimer.current); 51 | }; 52 | 53 | const isTrueComponent = (code: string) => { 54 | try { 55 | const component: JSONComponent = JSON.parse(code); 56 | 57 | const isComponent = (component: JSONComponent): boolean => { 58 | const isTrueChildren = 59 | (component.children && typeof component.children === 'string' 60 | ? true 61 | : Array.isArray(component.children) 62 | ? component.children.every( 63 | child => isString(child) || isComponent(child), 64 | ) 65 | : false) || !component?.children; 66 | 67 | if ( 68 | component._name && 69 | isTrueChildren && 70 | ['Element', 'Component'].includes(component._type) 71 | ) { 72 | setErrorMessage(''); 73 | return true; 74 | } else { 75 | setErrorMessage( 76 | `【${formatTime().split(' ')?.[1]}】component validation failed`, 77 | ); 78 | return false; 79 | } 80 | }; 81 | 82 | return isComponent(component); 83 | } catch (err) { 84 | console.error(err?.message); 85 | setErrorMessage(err?.message); 86 | return false; 87 | } 88 | }; 89 | 90 | return ( 91 | <> 92 | {!!errorMessage.length && ( 93 | 99 | )} 100 | <> 101 |
102 |
update(code)}>更新
103 |
reset()}>重置
104 |
105 | setCode(code)} 116 | /> 117 | 118 | 119 | ); 120 | }; 121 | 122 | export default ComponentEdit; 123 | -------------------------------------------------------------------------------- /docs/zh/index.md: -------------------------------------------------------------------------------- 1 | ## 文件概览 2 | 3 | ```txt 4 | .github ----> GitHub CI 配置 5 | .vscode ----> vscode 配置(prettier) 6 | src ---| 7 | | 8 | | 9 | context ---> React Context 10 | model ---> 数据层 11 | controller ---> 控制层 12 | pages ---> 视图层 13 | theme ---> 主题 14 | ``` 15 | 16 | ## 框架 17 | 18 | ![](/docs/static/img/framework.png) 19 | 20 | ## 项目分层 21 | 22 | ![](/docs/static/img/project-layer.png) 23 | 24 | ## 功能 25 | 26 | 「 功能演示视频 」 27 | 28 | 理念: 通过选中节点对节点进行增删查改 29 | 30 | | 功能 | 说明 | 31 | | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 32 | | 快捷键 |
操作 说明
Ctrl + c 「 复制 」
Ctrl + v「 粘贴 」
Ctrl + Backspace 「 删除 」
Ctrl + z 「 返回 」
Ctrl + y 「 前进 」
| 33 | | 创建项目 | - | 34 | | 布局 | - | 35 | | 选中节点 | 点击选中,节点会高亮,不能点击的组件通过树选中。 | 36 | | 更改样式 | 可通过可视化界面更改,也可以直接修改 CSS | 37 | | 添加文本 | 文本输入框 | 38 | | 修改组件 | 组件 Tab 对比组件属性修改后点击更新 | 39 | | 导出内容 | 点击代码导出功 | 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Visual Layout 2 | 3 | Low Code Platform, Create visible UI code. 4 | 5 | [![](https://img.shields.io/badge/language-typescript-orange.svg)](https://img.shields.io/badge/language-typescript-orange.svg) [![](https://img.shields.io/badge/author-TCYong-1890ff.svg)](https://img.shields.io/badge/author-TCYong-1890ff.svg) [![](https://img.shields.io/badge/License-MIT-1890ff.svg)](https://img.shields.io/badge/License-MIT-1890ff.svg) 6 | 7 | 「 Function Display Video 」 8 | 9 | Documentation: [中文 🇨🇳](./docs/zh/index.md) | [English 🇺🇸]() 10 | 11 | Use(Secondary development): [中文 🇨🇳]() | [English 🇺🇸]() 12 | 13 | Case: [中文 🇨🇳](./docs/zh/case.md) | [English 🇺🇸](./docs/en/case.md) 14 | 15 | ## 🚀 Features 16 | 17 | | Feature | Explain | 18 | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 19 | | Keyboard Event |
Operation Explain
Ctrl + c 「 Copy 」
Ctrl + v「 Paste 」
Ctrl + Backspace 「 Delete 」
Ctrl + z 「 Back 」
Ctrl + y 「 Forward 」
| 20 | | Multi Page | - | 21 | | Layout | - | 22 | | History Operation | - | 23 | | Visual Component | - | 24 | | Visual Styles | - | 25 | | Export Code | - | 26 | -------------------------------------------------------------------------------- /src/pages/App/slider-components/NodeTree/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | AppstoreOutlined, 3 | BuildOutlined, 4 | CheckCircleTwoTone, 5 | ProfileOutlined, 6 | } from '@ant-design/icons'; 7 | import { Input, Tooltip, Tree } from 'antd'; 8 | import _ from 'lodash'; 9 | import { isString } from 'lodash'; 10 | import { DataNode, Key } from 'rc-tree/lib/interface'; 11 | import { useEffect, useMemo } from 'react'; 12 | import { useState } from 'react'; 13 | import { useContext } from 'react'; 14 | import { AppContext } from 'src/context'; 15 | import { NodeService } from 'src/controller'; 16 | import { cloneJsxObject } from 'src/util'; 17 | import styles from './index.module.scss'; 18 | 19 | const nodeKeyId = new Map(); 20 | const NodeTree = () => { 21 | const [search, setSearch] = useState(''); 22 | const [expandedKeys, setExpandedKeys] = useState([]); 23 | const [autoExpandParent, setAutoExpandParent] = useState(true); 24 | const { appService, refresh } = useContext(AppContext); 25 | 26 | const page = appService.project.getCurrentPage(); 27 | useEffect(() => { 28 | setAutoExpandParent(true); 29 | setExpandedKeys([ 30 | // @ts-ignore 31 | ...new Set([ 32 | ...expandedKeys, 33 | ...(page?.currentNode.map(({ id }) => id) || []), 34 | ]), 35 | ]); 36 | // eslint-disable-next-line 37 | }, [appService.project, page?.currentNode[0]]); 38 | 39 | const trees = useMemo((): DataNode[] => { 40 | const getTree = (node: NodeService | string, id?: number): DataNode => { 41 | if (isString(node)) { 42 | return { 43 | title: node, 44 | key: `${id}:${node}`, 45 | icon: ( 46 | 47 | 48 | 49 | ), 50 | }; 51 | } else { 52 | const { id, _name, type, children } = node; 53 | nodeKeyId.set(id, node); 54 | return { 55 | title: `${_name}`, 56 | key: id, 57 | icon: ({ selected }) => 58 | selected ? ( 59 | 60 | ) : type === 'Component' ? ( 61 | 62 | 63 | 64 | ) : ( 65 | 66 | 67 | 68 | ), 69 | 70 | children: isString(children) 71 | ? [ 72 | { 73 | title: children, 74 | key: `${id}:${children}`, 75 | icon: ( 76 | 77 | 78 | 79 | ), 80 | }, 81 | ] 82 | : (children 83 | ?.map(child => child && getTree(child, id)) 84 | .filter(_ => _) as DataNode[]), 85 | }; 86 | } 87 | }; 88 | return page?.page ? [getTree(page.page)] : []; 89 | // eslint-disable-next-line 90 | }, [refresh, page?.page]); 91 | 92 | const filter = (treeData: DataNode[]): DataNode[] => { 93 | function matchSearch(title: T): boolean { 94 | return !search || new RegExp(_.escapeRegExp(search), 'ig').test(title); 95 | } 96 | 97 | return treeData 98 | .map(tree => { 99 | const { title, children } = tree; 100 | tree.children = children && filter(children); 101 | if (tree.children?.length || matchSearch(title as string)) { 102 | return tree; 103 | } 104 | return false; 105 | }) 106 | .filter(_ => _) as DataNode[]; 107 | }; 108 | 109 | return ( 110 |
111 |
112 | setSearch(e.target.value)} 115 | /> 116 |
117 |
118 | { 121 | if (node) { 122 | const nodeService = nodeKeyId.get(node.key); 123 | if (nodeService) { 124 | page.setCurrentNode([nodeService]); 125 | } 126 | } 127 | }} 128 | showLine={{ showLeafIcon: false }} 129 | selectedKeys={ 130 | appService.project.getCurrentPage()?.currentNode.map(({ id }) => id) || 131 | [] 132 | } 133 | autoExpandParent={autoExpandParent} 134 | expandedKeys={expandedKeys} 135 | onExpand={expandedKeysValue => { 136 | setAutoExpandParent(false); 137 | setExpandedKeys(expandedKeysValue); 138 | }} 139 | treeData={filter(cloneJsxObject(trees))} 140 | /> 141 |
142 |
143 | ); 144 | }; 145 | 146 | export default NodeTree; 147 | -------------------------------------------------------------------------------- /src/controller/service/node.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { CodeConfig, JSONComponent, NodeObject, Props, Style } from 'src/model'; 3 | import { NodeOption } from 'src/model'; 4 | import { PageService } from '..'; 5 | import { EventType } from '../browser'; 6 | import { isString } from 'src/controller/util'; 7 | import { isNull } from 'lodash'; 8 | import Node from 'src/model/node'; 9 | 10 | export type GetPageServiceInstance = () => PageService; 11 | export default class NodeService extends Node { 12 | public getPageServiceInstance: GetPageServiceInstance; 13 | // eslint-disable-next-line 14 | constructor(Options: NodeOption, getPageServiceInstance: GetPageServiceInstance) { 15 | super(Options); 16 | this.getPageServiceInstance = getPageServiceInstance; 17 | } 18 | 19 | createElement = ({ eventType }: { eventType: EventType }): React.ReactElement => { 20 | return this.create({ node: this, eventType }); 21 | }; 22 | 23 | copy = ({ isCopyID }: { isCopyID?: boolean }): NodeService => { 24 | const { children, element, id } = this; 25 | return new NodeService( 26 | { 27 | ...this.getBaseAttribute(this), 28 | element, 29 | id: isCopyID ? id : this.getPageServiceInstance().getIdx(), 30 | children: isString(children) 31 | ? children 32 | : children?.map(children => 33 | isString(children) ? children : children.copy({ isCopyID }), 34 | ), 35 | }, 36 | this.getPageServiceInstance, 37 | ); 38 | }; 39 | 40 | setStyles = (styles: Style[]) => { 41 | this.styles = styles; 42 | }; 43 | 44 | setContent = (content: string) => { 45 | if (Array.isArray(this.children)) { 46 | if (isString(this.children[0])) { 47 | this.children[0] = content; 48 | } else { 49 | this.children?.unshift(content); 50 | } 51 | } else { 52 | if (!isNull(this.children)) { 53 | this.children = [content]; 54 | } else { 55 | console.error(`Error: ${this._name} can not add content`); 56 | } 57 | } 58 | }; 59 | 60 | setComponent = (component: JSONComponent, _this: PageService) => { 61 | const { _name, children, _type, styles, className, hasCanChild, ...props } = 62 | component; 63 | this._name = _name; 64 | this.props = props; 65 | this.type = _type; 66 | this.styles = styles || []; 67 | this.className = className; 68 | this.hasCanChild = hasCanChild; 69 | 70 | const newNodeService = (options: JSONComponent): NodeService => { 71 | const { _name, children, _type, styles, className, hasCanChild, ...rest } = 72 | options; 73 | return new NodeService( 74 | { 75 | _name: options._name, 76 | type: _type, 77 | styles: styles, 78 | className: className, 79 | hasCanChild: hasCanChild, 80 | props: rest, 81 | id: _this.idx, 82 | children: isString(children) 83 | ? children 84 | : children?.map(child => 85 | isString(child) ? child : newNodeService(child), 86 | ), 87 | }, 88 | this.getPageServiceInstance, 89 | ); 90 | }; 91 | 92 | this.children = isString(children) 93 | ? children 94 | : children?.map(child => (isString(child) ? child : newNodeService(child))); 95 | }; 96 | 97 | getComponentConfig = (node: NodeService = this): Props => { 98 | return { 99 | _name: node._name, 100 | _type: node.type, 101 | styles: node.styles, 102 | className: node.className, 103 | hasCanChild: node.hasCanChild, 104 | ...node.props, 105 | children: isString(node.children) 106 | ? node.children 107 | : node.children?.map(child => 108 | isString(child) ? child : this.getComponentConfig(child), 109 | ), 110 | }; 111 | }; 112 | 113 | clearEffect = () => { 114 | this.isSelect = false; 115 | if (!isString(this.children)) { 116 | this.children?.forEach(node => !isString(node) && node.clearEffect()); 117 | } 118 | }; 119 | 120 | toString = (): string => { 121 | const { id, children } = this; 122 | // why isSelect (toString can`t change component no`t update) 123 | return `${id}:${ 124 | isString(children) ? children : children?.map(node => node.toString()) 125 | }`; 126 | }; 127 | 128 | setClassName = (className: string) => { 129 | this.className = className; 130 | }; 131 | 132 | setCodeConfig = (codeConfig: CodeConfig) => { 133 | this.codeConfig = codeConfig; 134 | }; 135 | 136 | getBaseAttribute = (node: NodeService) => { 137 | const { 138 | type, 139 | _name, 140 | hasCanChild, 141 | isDelete, 142 | isSelect, 143 | isRoot, 144 | codeConfig, 145 | styles, 146 | className, 147 | props, 148 | id, 149 | } = node; 150 | return { 151 | type, 152 | _name, 153 | hasCanChild, 154 | isDelete, 155 | isSelect, 156 | isRoot, 157 | styles, 158 | codeConfig, 159 | className, 160 | props, 161 | id, 162 | }; 163 | }; 164 | 165 | toObject = (): NodeObject => { 166 | return Object.assign(this.getBaseAttribute(this), { 167 | children: isString(this.children) 168 | ? this.children 169 | : this.children?.map(child => (isString(child) ? child : child.toObject())), 170 | random: Node.random, 171 | element: null, 172 | }); 173 | }; 174 | } 175 | -------------------------------------------------------------------------------- /src/controller/service/page.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AST, 3 | Style, 4 | JSONComponent, 5 | PageObject, 6 | ASTObject, 7 | CodeConfig, 8 | } from 'src/model'; 9 | import { EventType } from '../browser'; 10 | import { HistoryService, NodeService, Options } from '..'; 11 | import { isString } from 'src/controller/util'; 12 | import Page from 'src/model/page'; 13 | 14 | export interface Update { 15 | // eslint-disable-next-line 16 | ({}: { description?: string; isKeepHistory?: boolean }): void; 17 | } 18 | 19 | export default class PageService extends Page { 20 | public update: Update; 21 | public options: Options; 22 | public history: HistoryService; 23 | public updateSign: boolean = false; 24 | constructor(options: Required & Partial) { 25 | super(options); 26 | const { id, name, history } = options; 27 | this.options = options; 28 | this.id = id; 29 | this.name = name; 30 | this.update = this.bindUpdate(options.update); 31 | this.history = new HistoryService(history ? history : {}, this); 32 | 33 | this.setPage(this.newNode(options.page || options.target, true)); 34 | } 35 | 36 | setOptions(options: Partial) { 37 | this.options = { 38 | ...this.options, 39 | ...options, 40 | }; 41 | this.update({ isKeepHistory: false }); 42 | } 43 | 44 | getIdx = () => { 45 | return this.idx; 46 | }; 47 | 48 | bindUpdate = (update: () => void): Update => { 49 | return ({ description, isKeepHistory = true }) => { 50 | if (isKeepHistory) { 51 | this.history.keep({ 52 | node: this.page.copy({ isCopyID: true }), 53 | description, 54 | }); 55 | } 56 | this.updateSign = !this.updateSign; 57 | update(); 58 | }; 59 | }; 60 | 61 | setCurrentNode = (nodes: NodeService[]) => { 62 | this.page.clearEffect(); 63 | 64 | this.currentNode = nodes.map(node => { 65 | node.isSelect = true; 66 | return node; 67 | }); 68 | 69 | this.update({ isKeepHistory: false }); 70 | }; 71 | 72 | setStyles = (styles: Style[]) => { 73 | this.currentNode.map(node => node.setStyles(styles)); 74 | this.update({ description: '更新样式' }); 75 | }; 76 | 77 | setContent = (content: string) => { 78 | this.currentNode.map(node => node.setContent(content)); 79 | this.update({ description: '添加内容' }); 80 | }; 81 | 82 | setComponent = (component: JSONComponent) => { 83 | this.currentNode.map(node => node.setComponent(component, this)); 84 | this.update({ description: '更新组件' }); 85 | }; 86 | 87 | setClassName = (className: string) => { 88 | this.currentNode.map(node => node.setClassName(className)); 89 | this.update({ description: '设置Class' }); 90 | }; 91 | 92 | setCodeConfig = (codeConfig: CodeConfig) => { 93 | this.currentNode.map(node => node.setCodeConfig(codeConfig)); 94 | this.update({ description: '设置组件配置' }); 95 | }; 96 | 97 | createView = () => { 98 | const view = this.page.createElement({ eventType: EventType.container }); 99 | return view; 100 | }; 101 | 102 | toString() { 103 | return this.page.toString(); 104 | } 105 | 106 | backOffHistory() { 107 | this.history.backOff(); 108 | const history = this.history.current(); 109 | if (history) { 110 | history.node.clearEffect(); 111 | this.page = history.node.copy({ isCopyID: true }); 112 | } else { 113 | // backOff first history 114 | this.page = this.newNode(this.target, true); 115 | } 116 | this.update({ isKeepHistory: false }); 117 | } 118 | 119 | forwardHistory() { 120 | this.history.forward(); 121 | this.setHistoryPage(); 122 | this.update({ isKeepHistory: false }); 123 | } 124 | 125 | returnHistory(_id: number) { 126 | this.history.return(_id); 127 | const history = this.history.current(); 128 | if (history) { 129 | history.node.clearEffect(); 130 | this.page = history.node.copy({ isCopyID: true }); 131 | } else { 132 | // backOff first history 133 | this.page = this.newNode(this.target, true); 134 | } 135 | this.update({ isKeepHistory: false }); 136 | } 137 | 138 | recoveryHistory(_id: number) { 139 | this.history.recovery(_id); 140 | this.setHistoryPage(); 141 | this.update({ isKeepHistory: false }); 142 | } 143 | 144 | setHistoryPage = () => { 145 | const history = this.history.current(); 146 | if (history) { 147 | history.node.clearEffect(); 148 | this.page = history.node.copy({ isCopyID: true }); 149 | } 150 | }; 151 | 152 | toObject = (): PageObject => { 153 | return { 154 | id: this.id, 155 | idx: this._idx, 156 | name: this.name, 157 | currentNode: [], 158 | page: this.page.toObject(), 159 | history: this.history.toObject(), 160 | target: this.ASTtoObject(this.target), 161 | }; 162 | }; 163 | 164 | ASTtoObject = (target: AST): ASTObject => { 165 | return { 166 | ...target, 167 | children: isString(target.children) 168 | ? target.children 169 | : target.children?.map(child => 170 | isString(child) ? child : this.ASTtoObject(child), 171 | ), 172 | }; 173 | }; 174 | 175 | newNode = (target: AST, isRoot?: boolean): NodeService => { 176 | return new NodeService( 177 | { 178 | ...target, 179 | isRoot, 180 | id: this.idx, 181 | children: isString(target.children) 182 | ? target.children 183 | : target.children?.map(node => 184 | isString(node) ? node : this.newNode(node), 185 | ), 186 | }, 187 | () => this, 188 | ); 189 | }; 190 | } 191 | -------------------------------------------------------------------------------- /src/pages/App/slider-components/Component/const.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { AST } from 'src/model'; 3 | 4 | export const Components: AST[] = [ 5 | { 6 | _name: 'Button', 7 | type: 'Component', 8 | styles: [], 9 | children: 'Button', 10 | props: { 11 | type: 'primary', 12 | }, 13 | }, 14 | { 15 | _name: 'Menu', 16 | type: 'Component', 17 | styles: [], 18 | children: [ 19 | { 20 | type: 'Component', 21 | _name: 'Menu.Item', 22 | children: 'Navigation One', 23 | }, 24 | { 25 | type: 'Component', 26 | _name: 'Menu.Item', 27 | children: 'Navigation Two', 28 | }, 29 | { 30 | type: 'Component', 31 | _name: 'Menu.Item', 32 | children: 'Navigation Three', 33 | }, 34 | ], 35 | props: {}, 36 | }, 37 | { 38 | _name: 'Pagination', 39 | type: 'Component', 40 | styles: [], 41 | children: [], 42 | props: { 43 | defaultCurrent: 1, 44 | total: 50, 45 | }, 46 | }, 47 | { 48 | _name: 'Steps', 49 | type: 'Component', 50 | styles: [], 51 | children: [ 52 | { 53 | _name: 'Steps.Step', 54 | type: 'Component', 55 | children: [], 56 | props: { 57 | title: 'Finished', 58 | description: 'This is a description.', 59 | }, 60 | }, 61 | { 62 | _name: 'Steps.Step', 63 | type: 'Component', 64 | children: [], 65 | props: { 66 | title: 'In Progress', 67 | description: 'This is a description.', 68 | }, 69 | }, 70 | ], 71 | props: { 72 | current: 1, 73 | }, 74 | }, 75 | { 76 | _name: 'Cascader', 77 | type: 'Component', 78 | styles: [], 79 | children: null, 80 | props: { 81 | options: [ 82 | { 83 | value: 'zhejiang', 84 | label: 'Zhejiang', 85 | children: [ 86 | { 87 | value: 'hangzhou', 88 | label: 'Hangzhou', 89 | children: [ 90 | { 91 | value: 'xihu', 92 | label: 'West Lake', 93 | }, 94 | ], 95 | }, 96 | ], 97 | }, 98 | { 99 | value: 'jiangsu', 100 | label: 'Jiangsu', 101 | children: [ 102 | { 103 | value: 'nanjing', 104 | label: 'Nanjing', 105 | children: [ 106 | { 107 | value: 'zhonghuamen', 108 | label: 'Zhong Hua Men', 109 | }, 110 | ], 111 | }, 112 | ], 113 | }, 114 | ], 115 | placeholder: 'Please select', 116 | }, 117 | }, 118 | { 119 | _name: 'Checkbox.Group', 120 | type: 'Component', 121 | styles: [], 122 | children: null, 123 | props: { 124 | options: ['Apple', 'Pear', 'Orange'], 125 | }, 126 | }, 127 | { 128 | _name: 'DatePicker', 129 | type: 'Component', 130 | styles: [], 131 | children: [], 132 | }, 133 | { 134 | _name: 'Input', 135 | type: 'Component', 136 | styles: [], 137 | children: null, 138 | props: { 139 | placeholder: 'Basic usage', 140 | }, 141 | }, 142 | { 143 | _name: 'InputNumber', 144 | type: 'Component', 145 | styles: [], 146 | children: null, 147 | props: { 148 | defaultValue: 10, 149 | }, 150 | }, 151 | { 152 | _name: 'Mentions', 153 | type: 'Component', 154 | styles: [], 155 | children: [ 156 | { 157 | _name: 'Mentions.Option', 158 | type: 'Component', 159 | children: [], 160 | props: { 161 | value: 'afc163', 162 | children: 'afc163', 163 | }, 164 | }, 165 | { 166 | _name: 'Mentions.Option', 167 | type: 'Component', 168 | children: [], 169 | props: { 170 | value: 'zombieJ', 171 | children: 'zombieJ', 172 | }, 173 | }, 174 | { 175 | _name: 'Mentions.Option', 176 | type: 'Component', 177 | children: [], 178 | props: { 179 | value: 'yesmeck', 180 | children: 'yesmeck', 181 | }, 182 | }, 183 | ], 184 | props: { 185 | placeholder: 'Mentions', 186 | }, 187 | }, 188 | { 189 | _name: 'Radio', 190 | type: 'Component', 191 | styles: [], 192 | children: 'Radio', 193 | }, 194 | { 195 | _name: 'Rate', 196 | type: 'Component', 197 | styles: [], 198 | children: [], 199 | }, 200 | { 201 | _name: 'Select', 202 | type: 'Component', 203 | styles: [], 204 | children: [ 205 | { 206 | _name: 'Select.Option', 207 | type: 'Component', 208 | children: [], 209 | props: { 210 | value: 'jack', 211 | children: 'jack', 212 | }, 213 | }, 214 | { 215 | _name: 'Select.Option', 216 | type: 'Component', 217 | children: [], 218 | props: { 219 | value: 'lucy', 220 | children: 'lucy', 221 | }, 222 | }, 223 | { 224 | _name: 'Select.Option', 225 | type: 'Component', 226 | children: [], 227 | props: { 228 | disabled: true, 229 | value: 'Disabled', 230 | children: 'Disabled', 231 | }, 232 | }, 233 | ], 234 | props: { 235 | defaultValue: 'lucy', 236 | }, 237 | }, 238 | { 239 | _name: 'Slider', 240 | type: 'Component', 241 | styles: [], 242 | children: [], 243 | props: { 244 | defaultValue: 30, 245 | }, 246 | }, 247 | { 248 | _name: 'Switch', 249 | type: 'Component', 250 | styles: [], 251 | children: [], 252 | props: { 253 | defaultChecked: true, 254 | }, 255 | }, 256 | { 257 | _name: 'TimePicker', 258 | type: 'Component', 259 | styles: [], 260 | children: [], 261 | }, 262 | { 263 | _name: 'Avatar', 264 | type: 'Component', 265 | styles: [], 266 | children: [], 267 | props: { 268 | size: 32, 269 | }, 270 | }, 271 | { 272 | _name: 'Badge', 273 | type: 'Component', 274 | styles: [], 275 | children: [ 276 | { 277 | _name: 'Avatar', 278 | type: 'Component', 279 | children: [], 280 | props: { 281 | size: 32, 282 | }, 283 | }, 284 | ], 285 | props: { 286 | count: 5, 287 | }, 288 | }, 289 | { 290 | _name: 'Avatar', 291 | type: 'Component', 292 | styles: [], 293 | children: [], 294 | props: { 295 | src: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', 296 | }, 297 | }, 298 | ]; 299 | 300 | const container: AST = { 301 | _name: 'div', 302 | type: 'Element', 303 | styles: [], 304 | children: [], 305 | }; 306 | 307 | export const ComponentsAST: AST[] = Components.map(component => { 308 | const Component = _.cloneDeep(container); 309 | Component.children = [component]; 310 | return Component; 311 | }); 312 | -------------------------------------------------------------------------------- /src/pages/App/components/Content/Header/component/code/util/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | AppService, 3 | NodeService, 4 | PageService, 5 | ProjectService, 6 | } from 'src/controller'; 7 | import { randomChart } from 'src/util'; 8 | import { isElement } from 'src/controller/util'; 9 | import { CodeConfig, getInitCodeConfig } from '..'; 10 | import { isString } from 'lodash'; 11 | import { html_beautify, css_beautify } from 'js-beautify'; 12 | import _ from 'lodash'; 13 | import { getTemplate } from './template'; 14 | import { saveAs } from 'file-saver'; 15 | import JSZip from 'jszip'; 16 | 17 | export enum templateType { 18 | classComponent = 'class-component', 19 | functionComponent = 'function-component', 20 | style = 'style', 21 | } 22 | 23 | export type Language = 'javascript' | 'css' | 'less' | 'scss' | 'json' | 'html'; 24 | export enum Type { 25 | module = 'module', 26 | more = 'more', 27 | none = 'none', 28 | } 29 | export interface Dep { 30 | [props: string]: { 31 | import: string[]; 32 | type: Type; 33 | }; 34 | } 35 | 36 | export interface FileContent { 37 | name: string; 38 | suffix: string; 39 | code: string; 40 | template: templateType; 41 | dep: Dep; 42 | language: Language; 43 | } 44 | 45 | export interface File { 46 | [props: string]: FileContent; 47 | } 48 | 49 | const getRenderer = (language: Language) => { 50 | if (['css', 'less', 'scss'].includes(language)) { 51 | return (code: string) => 52 | css_beautify(code, { 53 | end_with_newline: true, 54 | indent_size: 2, 55 | }); 56 | } else { 57 | return (code: string) => 58 | html_beautify(code, { 59 | end_with_newline: true, 60 | indent_size: 2, 61 | }); 62 | } 63 | }; 64 | 65 | const radomString = randomChart(7); 66 | const generateCodeFiles = ( 67 | page: PageService, 68 | codeConfig: CodeConfig, 69 | ): [string, FileContent][] => { 70 | const files: File = {}; 71 | const { cssModule, cssFileSuffix, fileSuffix, cssLocation, component } = 72 | codeConfig; 73 | 74 | const CSS_FILE_NAME = cssModule 75 | ? `index.module.${cssFileSuffix}` 76 | : `index.${cssFileSuffix}`; 77 | 78 | const generateCode = ({ 79 | node, 80 | isRoot = false, 81 | filePath, 82 | }: { 83 | node: NodeService; 84 | isRoot?: boolean; 85 | filePath: string; 86 | }): string => { 87 | const { _name, type, className, codeConfig, styles, props, children } = node; 88 | const { isComponent, componentName } = codeConfig; 89 | 90 | if (isComponent && !isRoot) { 91 | const name = _.capitalize(componentName || radomString()); 92 | 93 | files[filePath].dep[`./${name}.${fileSuffix}`] = { 94 | import: [name], 95 | type: Type.module, 96 | }; 97 | 98 | const childFilePath = `${name}.${fileSuffix}`; 99 | 100 | files[childFilePath] = { 101 | name: name, 102 | suffix: fileSuffix, 103 | dep: { 104 | [`./${CSS_FILE_NAME}`]: { 105 | import: ['styles'], 106 | type: cssModule ? Type.module : Type.none, 107 | }, 108 | }, 109 | language: 'javascript', 110 | template: component, 111 | code: ``, 112 | }; 113 | 114 | const code = generateCode({ node, isRoot: true, filePath: childFilePath }); 115 | 116 | files[childFilePath].code = code; 117 | 118 | return `<${name} />`; 119 | } 120 | 121 | const getClassName = (): string => { 122 | if (className) { 123 | const prefix = 'className'; 124 | const suffix = cssModule ? `styles.${className}` : `"${className}"`; 125 | return ` ${prefix}=${suffix}`; 126 | } 127 | return ''; 128 | }; 129 | 130 | const getStyle = () => { 131 | if (!styles.length) return ''; 132 | if (cssLocation === 'inner') { 133 | const reactStyle = styles 134 | .map(({ key, value }) => { 135 | return `${key}: "${value}"`; 136 | }) 137 | .join(','); 138 | return ` style={{${reactStyle}}}`; 139 | } else { 140 | const ClassName = (className || radomString()).toLowerCase(); 141 | files[CSS_FILE_NAME] = { 142 | ...files[CSS_FILE_NAME], 143 | code: `${files[CSS_FILE_NAME]?.code || ''} 144 | .${ClassName} { 145 | ${styles.map(({ key, value }) => `${key}: ${value};`).join('\n')} 146 | }`, 147 | }; 148 | return cssModule 149 | ? ` className={styles.${ClassName}}` 150 | : ` className="${ClassName}"`; 151 | } 152 | }; 153 | 154 | const getProps = () => { 155 | if (props) { 156 | const pro = Object.entries(props).reduce((props, [key, value]) => { 157 | props[key] = isElement(value) 158 | ? generateCode({ node: value as NodeService, filePath }) 159 | : value; 160 | return props; 161 | }, {} as { [props: string]: string | unknown }); 162 | 163 | const getValue = (value: any): any => { 164 | if (typeof value === 'string') { 165 | return `"${value}"`; 166 | } 167 | if (Array.isArray(value)) { 168 | return `[${value.map(_ => getValue(_)).join(',')}]`; 169 | } 170 | 171 | if (Object.prototype.toString.call(value) === '[object Object]') { 172 | return `{ ${Object.entries(value) 173 | .map(([key, _]) => { 174 | return `${key}: ${getValue(_)}`; 175 | }) 176 | .join(',')} }`; 177 | } 178 | 179 | return value; 180 | }; 181 | 182 | return ` ${Object.entries(pro) 183 | .map(([key, value]) => { 184 | return `${key}={${getValue(value)}}`; 185 | }) 186 | .join('\n')}`; 187 | } 188 | return ''; 189 | }; 190 | 191 | if (type === 'Component') { 192 | const importFrom = AppService.components.get(_name.split('.')[0])?.from; 193 | 194 | if (importFrom) { 195 | const dep = files[filePath].dep[importFrom]; 196 | if (dep) { 197 | dep.import.push(_name); 198 | } else { 199 | files[filePath].dep[importFrom] = { 200 | import: [_name], 201 | type: Type.more, 202 | }; 203 | } 204 | } else { 205 | console.log(`${_name} source not found`); 206 | } 207 | } 208 | 209 | return `<${_name}${getClassName()}${getStyle()}${getProps()}>${ 210 | isString(children) 211 | ? children 212 | : (children || []) 213 | .map(child => 214 | isString(child) ? child : generateCode({ node: child, filePath }), 215 | ) 216 | .join('') 217 | }`; 218 | }; 219 | 220 | if (cssLocation === 'link') { 221 | files[CSS_FILE_NAME] = { 222 | name: `index`, 223 | suffix: cssModule ? `module.${cssFileSuffix}` : cssFileSuffix, 224 | language: cssFileSuffix, 225 | dep: {}, 226 | template: templateType.style, 227 | code: ``, 228 | }; 229 | } 230 | 231 | files[`App.${fileSuffix}`] = { 232 | name: `App`, 233 | suffix: fileSuffix, 234 | dep: {}, 235 | language: 'javascript', 236 | template: component, 237 | code: ``, 238 | }; 239 | 240 | const code = generateCode({ node: page.page, filePath: `App.${fileSuffix}` }); 241 | 242 | files[`App.${fileSuffix}`].code = code; 243 | 244 | if (cssLocation === 'link') { 245 | files[`App.${fileSuffix}`].dep = { 246 | ...files[`App.${fileSuffix}`].dep, 247 | [`./${CSS_FILE_NAME}`]: { 248 | import: ['styles'], 249 | type: cssModule ? Type.module : Type.none, 250 | }, 251 | }; 252 | } 253 | 254 | return Object.entries(files).map(([key, values]) => { 255 | const { template, language } = values; 256 | return [ 257 | key, 258 | { 259 | ...values, 260 | code: getRenderer(language)(getTemplate(template)(values)), 261 | }, 262 | ]; 263 | }); 264 | }; 265 | 266 | const exportCode = ( 267 | project: ProjectService, 268 | codeConfig: CodeConfig = getInitCodeConfig(), 269 | ) => { 270 | const zip = new JSZip(); 271 | Object.values(project.getPages()).forEach(page => { 272 | const files = generateCodeFiles(page, codeConfig); 273 | files.forEach(([key, { code }]) => { 274 | zip.folder(page.name)?.file(key, code); 275 | }); 276 | }); 277 | 278 | zip.generateAsync({ type: 'blob' }).then(function (content) { 279 | saveAs(content, `${project.name || 'Project'}.zip`); 280 | }); 281 | }; 282 | 283 | export { getTemplate, generateCodeFiles, exportCode }; 284 | -------------------------------------------------------------------------------- /src/pages/App/components/Drawer/attribute/components/Display/flex.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ClearOutlined, 3 | InsertRowBelowOutlined, 4 | InsertRowLeftOutlined, 5 | VerticalAlignBottomOutlined, 6 | } from '@ant-design/icons'; 7 | import { CssProps } from '../../config'; 8 | import styles from './index.module.scss'; 9 | import FlexStart from './img/flex-start.png'; 10 | import FlexEnd from './img/flex-end.png'; 11 | import center from './img/item-center.png'; 12 | import SpaceBetween from './img/space-between.png'; 13 | import SpaceAround from './img/space-around.png'; 14 | import ItemStart from './img/item-start.png'; 15 | import ItemEnd from './img/item-end.png'; 16 | import baseline from './img/baseline.png'; 17 | import stretch from './img/stretch.png'; 18 | import wrap from './img/wrap.png'; 19 | import nowrap from './img/nowrap.png'; 20 | import { Tooltip } from 'antd'; 21 | 22 | const config = [ 23 | { 24 | title: '主轴方向 / Flex-Direction', 25 | key: 'flex-direction', 26 | getValue: ({ style }: CssProps) => 27 | style?.filter(css => css.key === 'flex-direction')[0]?.value, 28 | items: [ 29 | { 30 | value: 'column', 31 | icon: ( 32 | 33 | 34 | 35 | ), 36 | }, 37 | { 38 | value: 'row', 39 | icon: ( 40 | 41 | 42 | 43 | ), 44 | }, 45 | ], 46 | }, 47 | { 48 | title: '主轴对齐 / Justify-Content', 49 | key: 'justify-content', 50 | getValue: ({ style }: CssProps) => 51 | style?.filter(css => css.key === 'justify-content')[0]?.value, 52 | items: [ 53 | { 54 | value: 'flex-start', 55 | icon: ( 56 | 57 | 58 | 59 | ), 60 | }, 61 | { 62 | value: 'flex-end', 63 | icon: ( 64 | 65 | 66 | 67 | ), 68 | }, 69 | { 70 | value: 'center', 71 | icon: ( 72 | 73 | 74 | 75 | ), 76 | }, 77 | { 78 | value: 'space-between', 79 | icon: ( 80 | 81 | 82 | 83 | ), 84 | }, 85 | { 86 | value: 'space-around', 87 | icon: ( 88 | 89 | 90 | 91 | ), 92 | }, 93 | ], 94 | }, 95 | { 96 | title: '交叉轴对齐 / Align-Items:', 97 | key: 'align-items', 98 | getValue: ({ style }: CssProps) => 99 | style?.filter(css => css.key === 'align-items')[0]?.value, 100 | items: [ 101 | { 102 | value: 'flex-start', 103 | icon: ( 104 | 105 | 106 | 107 | ), 108 | }, 109 | { 110 | value: 'flex-end', 111 | icon: ( 112 | 113 | 114 | 115 | ), 116 | }, 117 | { 118 | value: 'center', 119 | icon: ( 120 | 121 | 122 | 123 | ), 124 | }, 125 | { 126 | value: 'baseline', 127 | icon: ( 128 | 129 | 130 | 131 | ), 132 | }, 133 | { 134 | value: 'stretch', 135 | icon: ( 136 | 137 | 138 | 139 | ), 140 | }, 141 | ], 142 | }, 143 | { 144 | title: '排列规则 / Flex-Wrap', 145 | key: 'flex-wrap', 146 | getValue: ({ style }: CssProps) => 147 | style?.filter(css => css.key === 'flex-wrap')[0]?.value, 148 | items: [ 149 | { 150 | value: 'wrap', 151 | icon: ( 152 | 153 | 154 | 155 | ), 156 | }, 157 | { 158 | value: 'nowrap', 159 | icon: ( 160 | 161 | 162 | 163 | ), 164 | }, 165 | { 166 | value: 'wrap-reverse', 167 | icon: ( 168 | 169 | 170 | 171 | ), 172 | }, 173 | ], 174 | }, 175 | { 176 | title: '多轴对齐 / align-content', 177 | key: 'align-content', 178 | getValue: ({ style }: CssProps) => 179 | style?.filter(css => css.key === 'align-content')[0]?.value, 180 | items: [ 181 | { 182 | value: 'flex-start', 183 | icon: ( 184 | 185 | 186 | 187 | ), 188 | }, 189 | { 190 | value: 'flex-end', 191 | icon: ( 192 | 193 | 194 | 195 | ), 196 | }, 197 | { 198 | value: 'center', 199 | icon: ( 200 | 201 | 202 | 203 | ), 204 | }, 205 | { 206 | value: 'space-between', 207 | icon: ( 208 | 209 | 210 | 211 | ), 212 | }, 213 | { 214 | value: 'space-around', 215 | icon: ( 216 | 217 | 218 | 219 | ), 220 | }, 221 | { 222 | value: 'stretch', 223 | icon: ( 224 | 225 | 226 | 227 | ), 228 | }, 229 | ], 230 | }, 231 | ]; 232 | 233 | const Flex: React.FC = ({ style = [], onChange }) => { 234 | return ( 235 |
236 |
237 |
238 | 239 | { 241 | onChange?.([ 242 | ...style.filter( 243 | css => !config.map(({ key }) => key).includes(css.key), 244 | ), 245 | ]); 246 | }} 247 | /> 248 | 249 |
250 |
251 | {config.map(({ title, key, getValue, items }) => ( 252 |
253 |
254 | {title}: {getValue({ style })} 255 |
256 |
257 | {items.map(({ value, icon }) => { 258 | const selectStyle = 259 | value === getValue({ style }) 260 | ? { 261 | border: `1px solid #1890ff`, 262 | } 263 | : {}; 264 | return ( 265 |
{ 269 | onChange?.([ 270 | { 271 | key, 272 | value, 273 | }, 274 | ...style.filter(css => css.key !== key), 275 | ]); 276 | }} 277 | > 278 | {icon} 279 |
280 | ); 281 | })} 282 |
283 |
284 | ))} 285 |
286 | ); 287 | }; 288 | 289 | export default Flex; 290 | -------------------------------------------------------------------------------- /src/pages/App/slider-components/Layout/const.ts: -------------------------------------------------------------------------------- 1 | import { AST } from 'src/model'; 2 | 3 | const flexRow: AST = { 4 | _name: 'div', 5 | type: 'Element', 6 | styles: [ 7 | { key: 'display', value: 'flex' }, 8 | { 9 | key: 'height', 10 | value: '100%', 11 | }, 12 | { 13 | key: 'width', 14 | value: '100%', 15 | }, 16 | ], 17 | children: new Array(3).fill(0).map(item => { 18 | return { 19 | _name: 'div', 20 | type: 'Element', 21 | styles: [ 22 | { 23 | key: 'display', 24 | value: 'flex', 25 | }, 26 | { 27 | key: 'flex', 28 | value: '1', 29 | }, 30 | ], 31 | children: [], 32 | extraStyles: [ 33 | { 34 | key: 'border', 35 | value: '1px dashed #000000', 36 | }, 37 | { 38 | key: 'padding', 39 | value: '5px', 40 | }, 41 | ], 42 | }; 43 | }), 44 | }; 45 | 46 | const flexCol: AST = { 47 | _name: 'div', 48 | type: 'Element', 49 | styles: [ 50 | { key: 'display', value: 'flex' }, 51 | { key: 'flex-direction', value: 'column' }, 52 | { 53 | key: 'height', 54 | value: '100%', 55 | }, 56 | { 57 | key: 'width', 58 | value: '100%', 59 | }, 60 | ], 61 | children: new Array(3).fill(0).map(item => { 62 | return { 63 | _name: 'div', 64 | type: 'Element', 65 | styles: [ 66 | { 67 | key: 'display', 68 | value: 'flex', 69 | }, 70 | { 71 | key: 'flex', 72 | value: '1', 73 | }, 74 | ], 75 | children: [], 76 | extraStyles: [ 77 | { 78 | key: 'border', 79 | value: '1px dashed #000000', 80 | }, 81 | { 82 | key: 'padding', 83 | value: '5px', 84 | }, 85 | ], 86 | }; 87 | }), 88 | }; 89 | 90 | const mobile: AST = { 91 | _name: 'div', 92 | type: 'Element', 93 | styles: [ 94 | { key: 'padding', value: '50px 0' }, 95 | { 96 | key: 'height', 97 | value: '100%', 98 | }, 99 | { 100 | key: 'width', 101 | value: '100%', 102 | }, 103 | { 104 | key: 'position', 105 | value: 'relative', 106 | }, 107 | { 108 | key: 'display', 109 | value: 'flex', 110 | }, 111 | ], 112 | children: [ 113 | { 114 | _name: 'div', 115 | type: 'Element', 116 | styles: [ 117 | { 118 | key: 'position', 119 | value: 'absolute', 120 | }, 121 | { 122 | key: 'height', 123 | value: '50px', 124 | }, 125 | { 126 | key: 'width', 127 | value: '100%', 128 | }, 129 | { 130 | key: 'top', 131 | value: '0', 132 | }, 133 | { 134 | key: 'left', 135 | value: '0', 136 | }, 137 | ], 138 | children: [], 139 | }, 140 | { 141 | _name: 'div', 142 | type: 'Element', 143 | styles: [ 144 | { 145 | key: 'flex', 146 | value: '1', 147 | }, 148 | { 149 | key: 'min-height', 150 | value: '60px', 151 | }, 152 | { 153 | key: 'overflow', 154 | value: 'auto', 155 | }, 156 | { 157 | key: 'margin', 158 | value: '10px 0', 159 | }, 160 | ], 161 | children: [], 162 | }, 163 | { 164 | _name: 'div', 165 | type: 'Element', 166 | styles: [ 167 | { 168 | key: 'position', 169 | value: 'absolute', 170 | }, 171 | { 172 | key: 'height', 173 | value: '50px', 174 | }, 175 | { 176 | key: 'width', 177 | value: '100%', 178 | }, 179 | { 180 | key: 'bottom', 181 | value: '0', 182 | }, 183 | { 184 | key: 'left', 185 | value: '0', 186 | }, 187 | ], 188 | children: [], 189 | }, 190 | ], 191 | }; 192 | 193 | const none: AST = { 194 | _name: 'div', 195 | type: 'Element', 196 | styles: [ 197 | { 198 | key: 'height', 199 | value: '100%', 200 | }, 201 | { 202 | key: 'width', 203 | value: '100%', 204 | }, 205 | { 206 | key: 'overflow', 207 | value: 'auto', 208 | }, 209 | ], 210 | children: [ 211 | { 212 | _name: 'div', 213 | type: 'Element', 214 | styles: [ 215 | { 216 | key: 'height', 217 | value: '100%', 218 | }, 219 | { 220 | key: 'width', 221 | value: '100%', 222 | }, 223 | { 224 | key: 'overflow', 225 | value: 'auto', 226 | }, 227 | ], 228 | children: [], 229 | }, 230 | ], 231 | }; 232 | 233 | const scroll: AST = { 234 | _name: 'div', 235 | type: 'Element', 236 | styles: [ 237 | { 238 | key: 'height', 239 | value: '100%', 240 | }, 241 | { 242 | key: 'width', 243 | value: '100%', 244 | }, 245 | { 246 | key: 'overflow', 247 | value: 'auto', 248 | }, 249 | ], 250 | children: [ 251 | { 252 | _name: 'div', 253 | type: 'Element', 254 | styles: [ 255 | { 256 | key: 'height', 257 | value: '300%', 258 | }, 259 | { 260 | key: 'width', 261 | value: '100%', 262 | }, 263 | { 264 | key: 'background', 265 | value: '#f5f5f5', 266 | }, 267 | ], 268 | children: [], 269 | }, 270 | ], 271 | }; 272 | 273 | const position: AST = { 274 | _name: 'div', 275 | type: 'Element', 276 | styles: [ 277 | { 278 | key: 'height', 279 | value: '100%', 280 | }, 281 | { 282 | key: 'width', 283 | value: '100%', 284 | }, 285 | { 286 | key: 'position', 287 | value: 'relative', 288 | }, 289 | ], 290 | children: [ 291 | { 292 | _name: 'div', 293 | type: 'Element', 294 | styles: [ 295 | { 296 | key: 'position', 297 | value: 'absolute', 298 | }, 299 | { 300 | key: 'width', 301 | value: '50px', 302 | }, 303 | { 304 | key: 'height', 305 | value: '50px', 306 | }, 307 | { 308 | key: 'top', 309 | value: '10px', 310 | }, 311 | { 312 | key: 'left', 313 | value: '10px', 314 | }, 315 | { 316 | key: 'background', 317 | value: '#f5f5f5', 318 | }, 319 | ], 320 | children: [], 321 | }, 322 | ], 323 | }; 324 | 325 | const componentLayout: AST = { 326 | _name: 'div', 327 | type: 'Element', 328 | styles: [], 329 | children: [ 330 | { 331 | _name: 'Layout', 332 | type: 'Component', 333 | styles: [], 334 | children: [ 335 | { 336 | _name: 'Layout.Header', 337 | type: 'Component', 338 | styles: [], 339 | hasCanChild: true, 340 | children: 'Header', 341 | }, 342 | { 343 | _name: 'Layout.Content', 344 | type: 'Component', 345 | styles: [], 346 | hasCanChild: true, 347 | children: 'Content', 348 | }, 349 | { 350 | _name: 'Layout.Footer', 351 | type: 'Component', 352 | styles: [], 353 | hasCanChild: true, 354 | children: 'Footer', 355 | }, 356 | ], 357 | props: { 358 | style: { height: '100%' }, 359 | }, 360 | }, 361 | ], 362 | }; 363 | 364 | const componentSliderLayout: AST = { 365 | _name: 'div', 366 | type: 'Element', 367 | styles: [], 368 | children: [ 369 | { 370 | _name: 'Layout', 371 | type: 'Component', 372 | styles: [], 373 | children: [ 374 | { 375 | _name: 'Layout.Sider', 376 | type: 'Component', 377 | styles: [], 378 | hasCanChild: true, 379 | children: 'Sider', 380 | }, 381 | { 382 | _name: 'Layout', 383 | type: 'Component', 384 | styles: [], 385 | hasCanChild: true, 386 | children: [ 387 | { 388 | _name: 'Layout.Header', 389 | type: 'Component', 390 | styles: [], 391 | hasCanChild: true, 392 | children: 'Header', 393 | }, 394 | { 395 | _name: 'Layout.Content', 396 | type: 'Component', 397 | styles: [], 398 | hasCanChild: true, 399 | children: 'Content', 400 | }, 401 | { 402 | _name: 'Layout.Footer', 403 | type: 'Component', 404 | styles: [], 405 | hasCanChild: true, 406 | children: 'Footer', 407 | }, 408 | ], 409 | }, 410 | ], 411 | props: { 412 | style: { height: '100%' }, 413 | }, 414 | }, 415 | ], 416 | }; 417 | 418 | const componentGrid: AST = { 419 | _name: 'div', 420 | type: 'Element', 421 | styles: [ 422 | { 423 | key: 'height', 424 | value: '100%', 425 | }, 426 | { 427 | key: 'width', 428 | value: '100%', 429 | }, 430 | ], 431 | children: [ 432 | { 433 | _name: 'Row', 434 | type: 'Component', 435 | hasCanChild: true, 436 | styles: [], 437 | props: { 438 | gutter: 6, 439 | }, 440 | children: [ 441 | { 442 | _name: 'Col', 443 | type: 'Component', 444 | styles: [], 445 | props: { 446 | span: 8, 447 | }, 448 | children: [{ _name: 'div', type: 'Element', styles: [], children: [] }], 449 | }, 450 | { 451 | _name: 'Col', 452 | type: 'Component', 453 | styles: [], 454 | props: { 455 | span: 8, 456 | }, 457 | children: [{ _name: 'div', type: 'Element', styles: [], children: [] }], 458 | }, 459 | { 460 | _name: 'Col', 461 | type: 'Component', 462 | styles: [], 463 | props: { 464 | span: 8, 465 | }, 466 | children: [{ _name: 'div', type: 'Element', styles: [], children: [] }], 467 | }, 468 | ], 469 | }, 470 | ], 471 | }; 472 | 473 | const Card: AST = { 474 | _name: 'div', 475 | type: 'Element', 476 | styles: [], 477 | children: [ 478 | { 479 | _name: 'Card', 480 | type: 'Component', 481 | props: { 482 | title: 'Title', 483 | }, 484 | hasCanChild: true, 485 | styles: [], 486 | children: [ 487 | { 488 | _name: 'div', 489 | type: 'Element', 490 | styles: [], 491 | children: 'children', 492 | }, 493 | ], 494 | }, 495 | ], 496 | }; 497 | 498 | const LayoutAST = [ 499 | { 500 | title: '组件', 501 | children: [ 502 | { 503 | title: 'Layout', 504 | layout: componentLayout, 505 | }, 506 | { 507 | title: 'Layout-Slider', 508 | layout: componentSliderLayout, 509 | }, 510 | { 511 | title: 'Card', 512 | layout: Card, 513 | }, 514 | { 515 | title: 'Grid', 516 | layout: componentGrid, 517 | }, 518 | ], 519 | }, 520 | { 521 | title: '基础', 522 | children: [ 523 | { 524 | title: 'DIV', 525 | layout: none, 526 | }, 527 | { 528 | title: 'Scroll', 529 | layout: scroll, 530 | }, 531 | 532 | { 533 | title: 'Position', 534 | layout: position, 535 | }, 536 | ], 537 | }, 538 | { 539 | title: 'Flex', 540 | children: [ 541 | { 542 | title: 'Flex Row', 543 | layout: flexRow, 544 | }, 545 | { 546 | title: 'Flex Col', 547 | layout: flexCol, 548 | }, 549 | ], 550 | }, 551 | { 552 | title: '移动端', 553 | children: [ 554 | { 555 | title: 'Mobile', 556 | layout: mobile, 557 | }, 558 | ], 559 | }, 560 | ]; 561 | 562 | export { LayoutAST }; 563 | --------------------------------------------------------------------------------