├── 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 | 
8 |
9 | 二. 「 基本页面 」
10 |
11 | 1. 通过布局定义结构
12 | 2. 通过点击或节点树选中节点
13 | 3. 操作节点相关属性
14 |
15 | 
16 |
17 | 三. 「 生成代码 」
18 |
19 | 
20 |
21 | 四. 「 下载代码本地运行代码 」
22 |
23 | 1. 导出代码
24 | 2. 导入到项目目录
25 | 3. 引入相关依赖
26 | 4. 相关位置引入项目代码
27 | 5. 运行项目
28 |
29 | 
30 |
31 | 五. 「 运行效果 」
32 |
33 | 
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 |
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 |
68 |
69 |
70 |
{}} style={{ height: '100%' }}>
71 | {tabPanes}
72 |
73 |
74 | );
75 | };
76 |
77 | export default CodeEdit;
78 |
--------------------------------------------------------------------------------
/src/pages/App/components/Header/components/Project/index.tsx:
--------------------------------------------------------------------------------
1 | import { ArrowDownOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons';
2 | import { Popconfirm } from 'antd';
3 | import { AppService } from 'src/controller';
4 | import { ProjectObject } from 'src/model';
5 | import { exportCode } from '../../../Content/Header/component/code/util';
6 | import styles from './index.module.scss';
7 |
8 | const Project: React.FC<{
9 | project: ProjectObject;
10 | appService: AppService;
11 | setVisible: (visible: boolean) => void;
12 | }> = ({ project, appService, setVisible }) => {
13 | const operation = [
14 | {
15 | key: 'EditOutlined',
16 | icon: (
17 | {
20 | appService.set(project.id);
21 | setVisible(false);
22 | }}
23 | >
24 |
25 |
26 | ),
27 | },
28 | {
29 | key: 'ArrowDownOutlined',
30 | icon: (
31 | {
34 | exportCode(appService.project);
35 | }}
36 | >
37 |
38 |
39 | ),
40 | },
41 | {
42 | key: 'DeleteOutlined',
43 | icon: (
44 | appService.delete(project.id)}
47 | onCancel={() => {}}
48 | okText="是"
49 | cancelText="否"
50 | >
51 |
52 |
53 |
54 |
55 | ),
56 | },
57 | ];
58 |
59 | const isSelect = appService.project.id === project.id;
60 |
61 | return (
62 |
63 |
64 |
65 | {operation.map(({ key, icon }) => (
66 |
{icon}
67 | ))}
68 |
69 |
70 |
71 | 项目名:
72 | {project.name ? project.name : '--'}
73 |
74 |
75 | 项目描述:
76 | {project.description ? project.description : '--'}
77 |
78 |
79 |
80 | );
81 | };
82 |
83 | export default Project;
84 |
--------------------------------------------------------------------------------
/src/controller/service/project.ts:
--------------------------------------------------------------------------------
1 | import { PageObject, Project, ProjectObject } from 'src/model';
2 | import PageService from './page';
3 | import Keyboard from '../keyboard-event';
4 | import { Options, ProjectOptions } from 'src/controller';
5 |
6 | export default class ProjectService extends Project {
7 | constructor(options?: ProjectObject & ProjectOptions) {
8 | super(options);
9 | new Keyboard(this);
10 | }
11 |
12 | getPages() {
13 | return this.pages;
14 | }
15 |
16 | setPages(id: string) {
17 | this.currentId = id;
18 | this.update();
19 | }
20 |
21 | ceratePage(options: Options) {
22 | const id = `Page${this.idx}`;
23 | Reflect.set(
24 | this.pages,
25 | id,
26 | new PageService({
27 | id,
28 | update: this.update,
29 | name: options.name || 'Page',
30 | selectStyle: options.selectStyle || [],
31 | target: options.target,
32 | previewStyle:
33 | options.previewStyle.map(option => ({
34 | ...option,
35 | isCanUse: option?.isCanUse ?? true,
36 | })) || [],
37 | }),
38 | );
39 | this.currentId = id;
40 | this.update();
41 | }
42 |
43 | delete(id: string) {
44 | // delete page === current page
45 | if (id === this.currentId) {
46 | const pagesKey = Object.keys(this.pages);
47 | const currentPageIndex = pagesKey.findIndex(key => key === id);
48 | if (currentPageIndex !== -1) {
49 | this.currentId =
50 | pagesKey[
51 | currentPageIndex > 0 ? currentPageIndex - 1 : currentPageIndex + 1
52 | ];
53 | }
54 | }
55 | Reflect.deleteProperty(this.pages, id);
56 | this.update();
57 | }
58 |
59 | updateName = (name: string) => {
60 | this.name = name;
61 | };
62 |
63 | updateDescription = (description: string) => {
64 | this.description = description;
65 | };
66 |
67 | getCurrentPage = (): PageService => {
68 | return Object.values(this.pages).filter(({ id }) => id === this.currentId)[0];
69 | };
70 |
71 | toObject = (): ProjectObject => {
72 | return {
73 | idx: this._idx,
74 | currentId: this.currentId,
75 | id: this.id,
76 | name: this.name,
77 | description: this.description,
78 | pages: Object.entries(this.pages).reduce((projects, [key, value]) => {
79 | projects[key] = value.toObject();
80 | return projects;
81 | }, {} as { [props: string]: PageObject }),
82 | };
83 | };
84 | }
85 |
--------------------------------------------------------------------------------
/src/pages/App/components/Content/Header/component/size.tsx:
--------------------------------------------------------------------------------
1 | import { CloseOutlined } from '@ant-design/icons';
2 | import { InputNumber, Select } from 'antd';
3 | import { Canvas } from '../new-build';
4 | import styles from '../index.module.scss';
5 | import { models } from 'src/const/container';
6 |
7 | const { Option } = Select;
8 |
9 | interface SizeProps {
10 | value?: Canvas;
11 | onChange?: (value: Canvas) => void;
12 | }
13 |
14 | const Size: React.FC = ({ value, onChange }) => {
15 | return (
16 | <>
17 |
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 | }
54 | onClick={() => exportCode(project, codeConfig)}
55 | >
56 | 导出
57 |
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 |
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 |
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 | 
19 |
20 | ## 项目分层
21 |
22 | 
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/author-TCYong-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 | }${_name}>`;
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 |
--------------------------------------------------------------------------------