├── src ├── components │ ├── PanelComponents │ │ ├── Color │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── FormEditor │ │ │ ├── index.less │ │ │ ├── types.ts │ │ │ └── index.tsx │ │ ├── FormItems │ │ │ ├── index.tsx │ │ │ ├── formItems.less │ │ │ ├── EditorModal.tsx │ │ │ └── FormItems.tsx │ │ ├── MutiText │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── Upload │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── CardPicker │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── Table │ │ │ └── index.less │ │ └── DataList │ │ │ ├── index.less │ │ │ ├── editorModal.tsx │ │ │ └── index.tsx │ ├── BasicShop │ │ ├── BasicComponents │ │ │ ├── Text │ │ │ │ ├── index.less │ │ │ │ ├── template.ts │ │ │ │ ├── index.tsx │ │ │ │ └── schema.ts │ │ │ ├── LongText │ │ │ │ ├── index.less │ │ │ │ ├── template.ts │ │ │ │ ├── index.tsx │ │ │ │ └── schema.ts │ │ │ ├── Tab │ │ │ │ ├── template.ts │ │ │ │ ├── index.less │ │ │ │ ├── index.tsx │ │ │ │ └── schema.ts │ │ │ ├── Footer │ │ │ │ ├── template.ts │ │ │ │ ├── index.tsx │ │ │ │ └── schema.ts │ │ │ ├── Form │ │ │ │ ├── template.ts │ │ │ │ ├── baseForm.less │ │ │ │ ├── index.less │ │ │ │ ├── index.tsx │ │ │ │ ├── schema.ts │ │ │ │ └── BaseForm.tsx │ │ │ ├── Header │ │ │ │ ├── template.ts │ │ │ │ ├── index.less │ │ │ │ ├── index.tsx │ │ │ │ └── schema.ts │ │ │ ├── Icon │ │ │ │ ├── template.ts │ │ │ │ ├── index.tsx │ │ │ │ ├── schema.ts │ │ │ │ └── icon.ts │ │ │ ├── Image │ │ │ │ ├── template.ts │ │ │ │ ├── index.tsx │ │ │ │ └── schema.ts │ │ │ ├── List │ │ │ │ ├── template.ts │ │ │ │ ├── index.less │ │ │ │ ├── index.tsx │ │ │ │ └── schema.ts │ │ │ ├── Notice │ │ │ │ ├── template.ts │ │ │ │ ├── index.tsx │ │ │ │ └── schema.ts │ │ │ ├── Carousel │ │ │ │ ├── template.ts │ │ │ │ ├── index.less │ │ │ │ ├── index.tsx │ │ │ │ └── schema.ts │ │ │ ├── Qrcode │ │ │ │ ├── template.ts │ │ │ │ ├── index.tsx │ │ │ │ └── schema.ts │ │ │ ├── schema.ts │ │ │ └── template.ts │ │ ├── VisualComponents │ │ │ ├── XProgress │ │ │ │ ├── index.less │ │ │ │ ├── template.ts │ │ │ │ ├── index.tsx │ │ │ │ └── schema.ts │ │ │ ├── Area │ │ │ │ ├── template.ts │ │ │ │ ├── index.less │ │ │ │ ├── schema.ts │ │ │ │ └── index.tsx │ │ │ ├── Chart │ │ │ │ ├── template.ts │ │ │ │ ├── index.less │ │ │ │ ├── index.tsx │ │ │ │ └── schema.ts │ │ │ ├── Line │ │ │ │ ├── template.ts │ │ │ │ ├── index.less │ │ │ │ ├── schema.ts │ │ │ │ └── index.tsx │ │ │ ├── Pie │ │ │ │ ├── template.ts │ │ │ │ ├── index.less │ │ │ │ ├── schema.ts │ │ │ │ └── index.tsx │ │ │ ├── schema.ts │ │ │ └── template.ts │ │ ├── MediaComponents │ │ │ ├── Video │ │ │ │ ├── template.ts │ │ │ │ ├── index.tsx │ │ │ │ └── schema.ts │ │ │ ├── schema.ts │ │ │ └── template.ts │ │ └── schema.ts │ ├── .DS_Store │ ├── Zan │ │ ├── index.less │ │ └── index.tsx │ ├── DynamicEngine │ │ ├── .DS_Store │ │ └── index.tsx │ ├── Calibration │ │ ├── index.less │ │ └── index.tsx │ ├── LoadingCp │ │ └── index.tsx │ ├── ErrorBundaries │ │ └── index.tsx │ └── BackTop │ │ └── index.tsx ├── .DS_Store ├── assets │ ├── pie.png │ ├── area.png │ ├── chart.png │ ├── code.png │ ├── icon.png │ ├── line.png │ ├── logo.png │ ├── login_bg.png │ └── chart copy.png ├── typings.d.ts ├── pages │ ├── editor │ │ ├── services │ │ │ └── editorService.js │ │ ├── index.js │ │ ├── preH5.js │ │ ├── components │ │ │ └── Header │ │ │ │ ├── index.less │ │ │ │ └── index.js │ │ ├── TargetBox.js │ │ ├── models │ │ │ └── editorModal.js │ │ ├── preview.js │ │ ├── index.less │ │ ├── SourceBox.js │ │ └── Container.js │ ├── mobileTip.tsx │ ├── __tests__ │ │ └── index.test.js │ ├── login │ │ ├── index.less │ │ └── index.tsx │ ├── document.ejs │ ├── ide │ │ ├── index.less │ │ └── index.tsx │ └── home │ │ ├── index.less │ │ └── index.tsx ├── layouts │ ├── __tests__ │ │ └── index.test.js │ └── index.tsx ├── app.tsx ├── global.css └── utils │ ├── req.ts │ └── tool.ts ├── .eslintrc ├── code.png ├── .DS_Store ├── .prettierignore ├── .prettierrc ├── webpack.config.js ├── .editorconfig ├── SECURITY.md ├── tsconfig.json ├── LICENSE ├── server.js ├── .umirc.ts ├── .gitignore ├── package.json └── readme.md /src/components/PanelComponents/Color/index.less: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "react-app" 3 | } 4 | -------------------------------------------------------------------------------- /src/components/PanelComponents/FormEditor/index.less: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/Text/index.less: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/LongText/index.less: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/BasicShop/VisualComponents/XProgress/index.less: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dynamic-form/h5-Dooring/master/code.png -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dynamic-form/h5-Dooring/master/.DS_Store -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dynamic-form/h5-Dooring/master/src/.DS_Store -------------------------------------------------------------------------------- /src/assets/pie.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dynamic-form/h5-Dooring/master/src/assets/pie.png -------------------------------------------------------------------------------- /src/assets/area.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dynamic-form/h5-Dooring/master/src/assets/area.png -------------------------------------------------------------------------------- /src/assets/chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dynamic-form/h5-Dooring/master/src/assets/chart.png -------------------------------------------------------------------------------- /src/assets/code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dynamic-form/h5-Dooring/master/src/assets/code.png -------------------------------------------------------------------------------- /src/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dynamic-form/h5-Dooring/master/src/assets/icon.png -------------------------------------------------------------------------------- /src/assets/line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dynamic-form/h5-Dooring/master/src/assets/line.png -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dynamic-form/h5-Dooring/master/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/login_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dynamic-form/h5-Dooring/master/src/assets/login_bg.png -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.md 2 | **/*.svg 3 | **/*.ejs 4 | **/*.html 5 | package.json 6 | .umi 7 | .umi-production 8 | -------------------------------------------------------------------------------- /src/assets/chart copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dynamic-form/h5-Dooring/master/src/assets/chart copy.png -------------------------------------------------------------------------------- /src/components/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dynamic-form/h5-Dooring/master/src/components/.DS_Store -------------------------------------------------------------------------------- /src/components/Zan/index.less: -------------------------------------------------------------------------------- 1 | .imgWrap { 2 | width: 160px; 3 | img { 4 | width: 100%; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/components/PanelComponents/FormItems/index.tsx: -------------------------------------------------------------------------------- 1 | import FormItems from './FormItems'; 2 | export default FormItems; 3 | -------------------------------------------------------------------------------- /src/components/DynamicEngine/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dynamic-form/h5-Dooring/master/src/components/DynamicEngine/.DS_Store -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/Tab/template.ts: -------------------------------------------------------------------------------- 1 | const template = { 2 | type: 'Tab', 3 | h: 130, 4 | }; 5 | export default template; 6 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/Footer/template.ts: -------------------------------------------------------------------------------- 1 | const template = { 2 | type: 'Footer', 3 | h: 24, 4 | }; 5 | export default template; 6 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/Form/template.ts: -------------------------------------------------------------------------------- 1 | const template = { 2 | type: 'Form', 3 | h: 172, 4 | }; 5 | export default template; 6 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/Header/template.ts: -------------------------------------------------------------------------------- 1 | const template = { 2 | type: 'Header', 3 | h: 28, 4 | }; 5 | export default template; 6 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/Icon/template.ts: -------------------------------------------------------------------------------- 1 | const template = { 2 | type: 'Icon', 3 | h: 23, 4 | }; 5 | export default template; 6 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/Image/template.ts: -------------------------------------------------------------------------------- 1 | const template = { 2 | type: 'Image', 3 | h: 188, 4 | }; 5 | export default template; 6 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/List/template.ts: -------------------------------------------------------------------------------- 1 | const template = { 2 | type: 'List', 3 | h: 110, 4 | }; 5 | export default template; 6 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/Notice/template.ts: -------------------------------------------------------------------------------- 1 | const template = { 2 | type: 'Notice', 3 | h: 20, 4 | }; 5 | export default template; 6 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/Text/template.ts: -------------------------------------------------------------------------------- 1 | const template = { 2 | type: 'Text', 3 | h: 20, 4 | }; 5 | export default template; 6 | -------------------------------------------------------------------------------- /src/components/BasicShop/MediaComponents/Video/template.ts: -------------------------------------------------------------------------------- 1 | const template = { 2 | type: 'Video', 3 | h: 107, 4 | }; 5 | export default template; 6 | -------------------------------------------------------------------------------- /src/components/BasicShop/VisualComponents/Area/template.ts: -------------------------------------------------------------------------------- 1 | const template = { 2 | type: 'Area', 3 | h: 108, 4 | }; 5 | export default template; 6 | -------------------------------------------------------------------------------- /src/components/BasicShop/VisualComponents/Chart/template.ts: -------------------------------------------------------------------------------- 1 | const template = { 2 | type: 'Chart', 3 | h: 102, 4 | }; 5 | export default template; 6 | -------------------------------------------------------------------------------- /src/components/BasicShop/VisualComponents/Line/template.ts: -------------------------------------------------------------------------------- 1 | const template = { 2 | type: 'Line', 3 | h: 104, 4 | }; 5 | export default template; 6 | -------------------------------------------------------------------------------- /src/components/BasicShop/VisualComponents/Pie/template.ts: -------------------------------------------------------------------------------- 1 | const template = { 2 | type: 'Pie', 3 | h: 106, 4 | }; 5 | export default template; 6 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/Carousel/template.ts: -------------------------------------------------------------------------------- 1 | const template = { 2 | type: 'Carousel', 3 | h: 82, 4 | }; 5 | export default template; 6 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/LongText/template.ts: -------------------------------------------------------------------------------- 1 | const template = { 2 | type: 'LongText', 3 | h: 36, 4 | }; 5 | export default template; 6 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/Qrcode/template.ts: -------------------------------------------------------------------------------- 1 | const template = { 2 | type: 'Qrcode', 3 | h: 150, 4 | }; 5 | export default template; 6 | -------------------------------------------------------------------------------- /src/components/BasicShop/VisualComponents/XProgress/template.ts: -------------------------------------------------------------------------------- 1 | const template = { 2 | type: 'XProgress', 3 | h: 102, 4 | }; 5 | export default template; 6 | -------------------------------------------------------------------------------- /src/components/BasicShop/MediaComponents/schema.ts: -------------------------------------------------------------------------------- 1 | import Video from './Video/schema'; 2 | 3 | const mediaSchema = { 4 | Video, 5 | }; 6 | export default mediaSchema; 7 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css'; 2 | declare module '*.png'; 3 | declare module '*.less'; 4 | interface Window { 5 | currentCates: null | Array; 6 | } 7 | -------------------------------------------------------------------------------- /src/pages/editor/services/editorService.js: -------------------------------------------------------------------------------- 1 | import req from '@/utils/req' 2 | 3 | export function getTemplate(data) { 4 | return req('/test', { method: 'GET', params: data }) 5 | } -------------------------------------------------------------------------------- /src/components/Calibration/index.less: -------------------------------------------------------------------------------- 1 | .calibration{ 2 | width: calc(200% - 50px); 3 | height: 200%; 4 | position: relative; 5 | white-space: nowrap; 6 | :global(.calibrationNumber){ 7 | font-size: 12px; 8 | } 9 | } -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 100, 5 | "overrides": [ 6 | { 7 | "files": ".prettierrc", 8 | "options": { "parser": "json" } 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/Form/baseForm.less: -------------------------------------------------------------------------------- 1 | .radioWrap { 2 | margin-bottom: 10px; 3 | .radioTitle { 4 | padding: 6px 16px; 5 | font-size: 15px; 6 | } 7 | .radioItem { 8 | margin-top: 10px; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/Carousel/index.less: -------------------------------------------------------------------------------- 1 | .carousel__item__pic { 2 | display: inline-block; 3 | width: 100%; 4 | max-height: 220px; 5 | overflow: hidden; 6 | vertical-align: top; 7 | img { 8 | width: 100%; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/components/PanelComponents/MutiText/index.less: -------------------------------------------------------------------------------- 1 | .mutiText { 2 | .iptWrap { 3 | margin-bottom: 12px; 4 | display: flex; 5 | .delBtn { 6 | font-size: 18px; 7 | margin-left: 12px; 8 | cursor: pointer; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/components/PanelComponents/Upload/index.less: -------------------------------------------------------------------------------- 1 | :global(.ant-upload-select-picture-card i) { 2 | color: #999; 3 | font-size: 32px; 4 | } 5 | 6 | :global(.ant-upload-select-picture-card .ant-upload-text) { 7 | margin-top: 8px; 8 | color: #666; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/BasicShop/VisualComponents/Area/index.less: -------------------------------------------------------------------------------- 1 | .chartWrap { 2 | position: relative; 3 | width: 100%; 4 | .chartTitle { 5 | text-align: center; 6 | } 7 | img { 8 | width: 100%; 9 | } 10 | canvas { 11 | width: 100%; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/components/BasicShop/VisualComponents/Chart/index.less: -------------------------------------------------------------------------------- 1 | .chartWrap { 2 | position: relative; 3 | width: 100%; 4 | .chartTitle { 5 | text-align: center; 6 | } 7 | img { 8 | width: 100%; 9 | } 10 | canvas { 11 | width: 100%; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/components/BasicShop/VisualComponents/Line/index.less: -------------------------------------------------------------------------------- 1 | .chartWrap { 2 | position: relative; 3 | width: 100%; 4 | .chartTitle { 5 | text-align: center; 6 | } 7 | img { 8 | width: 100%; 9 | } 10 | canvas { 11 | width: 100%; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/components/BasicShop/VisualComponents/Pie/index.less: -------------------------------------------------------------------------------- 1 | .chartWrap { 2 | position: relative; 3 | width: 100%; 4 | .chartTitle { 5 | text-align: center; 6 | } 7 | img { 8 | width: 100%; 9 | } 10 | canvas { 11 | width: 100%; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/components/BasicShop/MediaComponents/template.ts: -------------------------------------------------------------------------------- 1 | import Video from './Video/template'; 2 | 3 | const mediaTemplate = [Video]; 4 | 5 | const MediaTemplate = mediaTemplate.map(v => { 6 | return { ...v, category: 'media' }; 7 | }); 8 | 9 | export default MediaTemplate; 10 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 不是真实的 webpack 配置,仅为兼容 webstorm 和 intellij idea 代码跳转 3 | * ref: https://github.com/umijs/umi/issues/1109#issuecomment-423380125 4 | */ 5 | 6 | module.exports = { 7 | resolve: { 8 | alias: { 9 | '@': require('path').resolve(__dirname, 'src'), 10 | }, 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /src/components/BasicShop/schema.ts: -------------------------------------------------------------------------------- 1 | import BasicSchema from './BasicComponents/schema'; 2 | import MediaSchema from './MediaComponents/schema'; 3 | import VisualSchema from './VisualComponents/schema'; 4 | 5 | const schema = { 6 | ...BasicSchema, 7 | ...MediaSchema, 8 | ...VisualSchema, 9 | }; 10 | 11 | export default schema; 12 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/Form/index.less: -------------------------------------------------------------------------------- 1 | .formWrap { 2 | margin: 10px; 3 | padding: 20px 16px; 4 | border-radius: 6px; 5 | background-color: #fff; 6 | box-shadow: 0 2px 6px #f0f0f0; 7 | .title { 8 | padding-bottom: 20px; 9 | text-align: center; 10 | font-size: 18px; 11 | } 12 | .formContent { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/pages/mobileTip.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Result } from 'antd'; 3 | 4 | function MobileTip() { 5 | return ( 6 |
7 | 12 |
13 | ); 14 | } 15 | 16 | export default MobileTip; 17 | -------------------------------------------------------------------------------- /src/components/PanelComponents/CardPicker/index.less: -------------------------------------------------------------------------------- 1 | .pickerWrap { 2 | display: flex; 3 | flex-wrap: wrap; 4 | .picker { 5 | display: inline-block; 6 | padding: 10px; 7 | border: 2px solid transparent; 8 | cursor: pointer; 9 | &:hover { 10 | border-color: #4091f7; 11 | } 12 | &.selected { 13 | border-color: #4091f7; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/components/BasicShop/VisualComponents/schema.ts: -------------------------------------------------------------------------------- 1 | import Chart from './Chart/schema'; 2 | import Line from './Line/schema'; 3 | import Pie from './Pie/schema'; 4 | import Area from './Area/schema'; 5 | import XProgress from './XProgress/schema'; 6 | 7 | const visualSchema = { 8 | Chart, 9 | Line, 10 | Pie, 11 | Area, 12 | XProgress, 13 | }; 14 | 15 | export default visualSchema; 16 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/List/index.less: -------------------------------------------------------------------------------- 1 | .list { 2 | margin: 20px auto; 3 | width: 94%; 4 | .sourceList { 5 | .sourceItem { 6 | display: flex; 7 | align-items: center; 8 | margin-bottom: 16px; 9 | .imgWrap { 10 | } 11 | .content { 12 | margin-left: 12px; 13 | .tit { 14 | line-height: 2; 15 | } 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/components/BasicShop/VisualComponents/template.ts: -------------------------------------------------------------------------------- 1 | import Chart from './Chart/template'; 2 | import Line from './Line/template'; 3 | import Pie from './Pie/template'; 4 | import Area from './Area/template'; 5 | import XProgress from './XProgress/template'; 6 | 7 | const visualTemplate = [Chart, Line, Pie, Area, XProgress]; 8 | 9 | const VisualTemplate = visualTemplate.map(v => { 10 | return { ...v, category: 'visual' }; 11 | }); 12 | export default VisualTemplate; 13 | -------------------------------------------------------------------------------- /src/pages/__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | import Index from '..'; 2 | import renderer from 'react-test-renderer'; 3 | 4 | 5 | describe('Page: index', () => { 6 | it('Render correctly', () => { 7 | const wrapper = renderer.create(); 8 | expect(wrapper.root.children.length).toBe(1); 9 | const outerLayer = wrapper.root.children[0]; 10 | expect(outerLayer.type).toBe('div'); 11 | expect(outerLayer.children.length).toBe(2); 12 | 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/Text/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import styles from './index.less'; 3 | import { ITextConfig } from './schema'; 4 | 5 | const Text = memo((props: ITextConfig) => { 6 | const { align, text, fontSize, color, lineHeight } = props; 7 | return ( 8 |
9 | {text} 10 |
11 | ); 12 | }); 13 | export default Text; 14 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/Image/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import { IImageConfig } from './schema'; 3 | const Image = memo((props: IImageConfig) => { 4 | const { imgUrl, round = 0 } = props; 5 | return ( 6 |
7 | 8 |
9 | ); 10 | }); 11 | 12 | export default Image; 13 | -------------------------------------------------------------------------------- /src/pages/editor/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { HTML5Backend } from 'react-dnd-html5-backend'; 4 | import { DndProvider } from 'react-dnd'; 5 | 6 | import Container from './Container' 7 | 8 | import styles from './index.less'; 9 | 10 | function BasicLayout(props) { 11 | return ( 12 |
13 | 14 | 15 | 16 |
17 | ); 18 | } 19 | 20 | export default BasicLayout; 21 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/Header/index.less: -------------------------------------------------------------------------------- 1 | .header { 2 | box-sizing: content-box; 3 | padding: 3px 12px; 4 | display: flex; 5 | align-items: center; 6 | height: 50px; 7 | background-color: #000; 8 | .logo { 9 | margin-right: 10px; 10 | max-width: 160px; 11 | max-height: 46px; 12 | height: 46px; 13 | overflow: hidden; 14 | img { 15 | height: 100%; 16 | object-fit: contain; 17 | } 18 | } 19 | .title { 20 | font-size: 20px; 21 | color: #fff; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/Notice/index.tsx: -------------------------------------------------------------------------------- 1 | import { NoticeBar } from 'zarm'; 2 | import React, { memo } from 'react'; 3 | import { INoticeConfig } from './schema'; 4 | const Notice = memo((props: INoticeConfig) => { 5 | const { text, speed, theme, isClose = false } = props; 6 | return ( 7 | 8 | {text} 9 | 10 | ); 11 | }); 12 | 13 | export default Notice; 14 | -------------------------------------------------------------------------------- /src/components/LoadingCp/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default () => ( 4 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | ); 21 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/LongText/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import styles from './index.less'; 3 | import { ILongTextConfig } from './schema'; 4 | const LongText = memo((props: ILongTextConfig) => { 5 | const { text, fontSize, color, indent, lineHeight, textAlign } = props; 6 | return ( 7 |
11 | {text} 12 |
13 | ); 14 | }); 15 | export default LongText; 16 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/Qrcode/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import { IQrcodeConfig } from './schema'; 3 | 4 | const Qrcode = memo((props: IQrcodeConfig) => { 5 | const { qrcode, text, color, fontSize = 14 } = props; 6 | return ( 7 |
8 | {text} 9 |
{text}
10 |
11 | ); 12 | }); 13 | 14 | export default Qrcode; 15 | -------------------------------------------------------------------------------- /src/layouts/__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | import BasicLayout from '..'; 2 | import renderer from 'react-test-renderer'; 3 | 4 | describe('Layout: BasicLayout', () => { 5 | it('Render correctly', () => { 6 | const wrapper = renderer.create(); 7 | expect(wrapper.root.children.length).toBe(1); 8 | const outerLayer = wrapper.root.children[0]; 9 | expect(outerLayer.type).toBe('div'); 10 | const title = outerLayer.children[0]; 11 | expect(title.type).toBe('h1'); 12 | expect(title.children[0]).toBe('Yay! Welcome to umi!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/Footer/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import { IFooterConfig } from './schema'; 3 | const Footer = memo((props: IFooterConfig) => { 4 | const { bgColor, text, color, align, fontSize, height } = props; 5 | return ( 6 |
16 | {text} 17 |
18 | ); 19 | }); 20 | 21 | export default Footer; 22 | -------------------------------------------------------------------------------- /src/components/PanelComponents/Table/index.less: -------------------------------------------------------------------------------- 1 | :global(.editable-cell) { 2 | position: relative; 3 | } 4 | 5 | :global(.editable-cell-value-wrap) { 6 | padding: 5px 12px; 7 | cursor: pointer; 8 | } 9 | 10 | :global(.editable-row) { 11 | &:hover :global(.editable-cell-value-wrap) { 12 | border: 1px solid #d9d9d9; 13 | border-radius: 4px; 14 | padding: 4px 11px; 15 | } 16 | } 17 | 18 | :global([data-theme='dark']) { 19 | :global(.editable-row) { 20 | &:hover { 21 | :global(.editable-cell-value-wrap) { 22 | border: 1px solid #434343; 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/components/BasicShop/MediaComponents/Video/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import { Player, BigPlayButton } from 'video-react'; 3 | import './index.css'; 4 | import { IVideoConfig } from './schema'; 5 | 6 | const VideoPlayer = memo((props: IVideoConfig) => { 7 | const { poster, url } = props; 8 | return ( 9 |
10 | 15 | 16 | 17 |
18 | ); 19 | }); 20 | 21 | export default VideoPlayer; 22 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/Tab/index.less: -------------------------------------------------------------------------------- 1 | .tabWrap { 2 | padding-top: 16px; 3 | padding-bottom: 16px; 4 | .content { 5 | display: flex; 6 | flex-wrap: wrap; 7 | .item { 8 | padding: 20px 20px 0; 9 | width: 50%; 10 | text-align: center; 11 | justify-content: center; 12 | .imgWrap { 13 | display: inline-block; 14 | width: 80%; 15 | img { 16 | border-radius: 6px; 17 | width: 120px; 18 | height: 120px; 19 | object-fit: cover; 20 | } 21 | .title { 22 | line-height: 2.4; 23 | } 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/components/BasicShop/VisualComponents/XProgress/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import { Progress } from 'zarm'; 3 | import styles from './index.less'; 4 | import { IXProgressConfig } from './schema'; 5 | 6 | const XProgress = memo((props: IXProgressConfig) => { 7 | const { theme, size, shape, percent, strokeWidth } = props; 8 | return ( 9 |
10 | 17 |
18 | ); 19 | }); 20 | 21 | export default XProgress; 22 | -------------------------------------------------------------------------------- /src/components/Zan/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import { Button, Popover } from 'antd'; 3 | import styles from './index.less'; 4 | 5 | ///这组件写的有问题 popover会重定位 6 | const content = ( 7 |
8 | sponsorship 9 |
10 | ); 11 | 12 | export default memo(function ZanPao() { 13 | return ( 14 |
15 | 16 | 19 | 20 |
21 | ); 22 | }); 23 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 5.1.x | :white_check_mark: | 11 | | 5.0.x | :x: | 12 | | 4.0.x | :white_check_mark: | 13 | | < 4.0 | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Use this section to tell people how to report a vulnerability. 18 | 19 | Tell them where to go, how often they can expect to get an update on a 20 | reported vulnerability, what to expect if the vulnerability is accepted or 21 | declined, etc. 22 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import styles from './index.less'; 3 | import React from 'react'; 4 | import { IHeaderConfig } from './schema'; 5 | 6 | const Header = memo((props: IHeaderConfig) => { 7 | const { bgColor, logo, logoText, fontSize, color } = props; 8 | return ( 9 |
10 |
11 | {logoText} 12 |
13 |
14 | {logoText} 15 |
16 |
17 | ); 18 | }); 19 | 20 | export default Header; 21 | -------------------------------------------------------------------------------- /src/pages/editor/preH5.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | Header, Text, Notice, Qrcode, Footer, Image, List 4 | } from '@/components/DynamicEngine/components' 5 | import Carousel from '@/components/Carousel' 6 | import Tab from '@/components/Tab' 7 | 8 | export default function PrevH5(props) { 9 | return
10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "ESNext", 5 | "moduleResolution": "node", 6 | "jsx": "react", 7 | "baseUrl": ".", 8 | "strict": true, 9 | "importHelpers": true, 10 | "sourceMap": true, 11 | "lib": ["dom", "dom.iterable", "esnext"], 12 | "paths": { 13 | "@/*": ["src/*"], 14 | "@@/*": ["src/.umi/*"], 15 | "components/*": ["src/components/*"], 16 | "utils/*": ["src/utils/*"], 17 | "assets/*": ["src/assets/*"] 18 | }, 19 | "allowSyntheticDefaultImports": true, 20 | "forceConsistentCasingInFileNames": true, 21 | "isolatedModules": true, 22 | "noEmit": true, 23 | "skipLibCheck": true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/schema.ts: -------------------------------------------------------------------------------- 1 | import Carousel from './Carousel/schema'; 2 | import Footer from './Footer/schema'; 3 | import Form from './Form/schema'; 4 | import Header from './Header/schema'; 5 | import Icon from './Icon/schema'; 6 | import Image from './Image/schema'; 7 | import List from './List/schema'; 8 | import LongText from './LongText/schema'; 9 | import Notice from './Notice/schema'; 10 | import Qrcode from './Qrcode/schema'; 11 | import Tab from './Tab/schema'; 12 | import Text from './Text/schema'; 13 | 14 | const basicSchema = { 15 | Carousel, 16 | Footer, 17 | Form, 18 | Header, 19 | Icon, 20 | Image, 21 | List, 22 | LongText, 23 | Notice, 24 | Qrcode, 25 | Tab, 26 | Text, 27 | }; 28 | export default basicSchema; 29 | -------------------------------------------------------------------------------- /src/pages/login/index.less: -------------------------------------------------------------------------------- 1 | .loginWrap { 2 | width: 100vw; 3 | height: 100vh; 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | flex-direction: column; 8 | background: #f0f0f0 url(../../assets/login_bg.png) center center; 9 | background-size: cover; 10 | .tit { 11 | font-size: 35px; 12 | text-align: center; 13 | padding-bottom: 20px; 14 | margin-bottom: 36px; 15 | border-bottom: 1px solid #f0f0f0; 16 | } 17 | .formWrap { 18 | margin-left: auto; 19 | margin-right: auto; 20 | width: 520px; 21 | padding: 22px 0 20px; 22 | background-color: #fff; 23 | box-shadow: 0 0 20px rgba(0,0,0,.1); 24 | border-radius: 6px; 25 | } 26 | } -------------------------------------------------------------------------------- /src/app.tsx: -------------------------------------------------------------------------------- 1 | import { createLogger } from 'redux-logger'; 2 | import { message } from 'antd'; 3 | import undoable, { StateWithHistory } from 'redux-undo'; 4 | import { Reducer, AnyAction } from 'redux'; 5 | import { isDev } from './utils/tool'; 6 | 7 | export const dva = { 8 | config: { 9 | onAction: isDev ? createLogger() : undefined, 10 | onError(e: Error) { 11 | message.error(e.message, 3); 12 | }, 13 | onReducer: (reducer: Reducer) => { 14 | let undoReducer = undoable(reducer); 15 | return function(state: StateWithHistory, action: AnyAction) { 16 | let newState = undoReducer(state, action); 17 | let router = newState.present.router ? newState.present.router : newState.present.routing; 18 | return { ...newState, router: router }; 19 | }; 20 | }, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /src/pages/document.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | H5编辑器之神-Dooring 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | -------------------------------------------------------------------------------- /src/pages/editor/components/Header/index.less: -------------------------------------------------------------------------------- 1 | .header { 2 | position: relative; 3 | z-index: 10; 4 | padding-left: 30px; 5 | padding-right: 30px; 6 | display: flex; 7 | align-items: center; 8 | height: 56px; 9 | background: #fff; 10 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 11 | .logoArea { 12 | width: 300px; 13 | .backBtn { 14 | display: inline-block; 15 | padding: 12px 10px; 16 | margin-right: 22px; 17 | cursor: pointer; 18 | } 19 | .logo { 20 | display: inline-block; 21 | width: 100px; 22 | overflow: hidden; 23 | border-radius: 3px; 24 | vertical-align: middle; 25 | font-size: 20px; 26 | font-weight: bold; 27 | } 28 | } 29 | .controlArea { 30 | flex: 1; 31 | text-align: center; 32 | } 33 | .btnArea { 34 | width: 320px; 35 | text-align: right; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/template.ts: -------------------------------------------------------------------------------- 1 | import Carousel from './Carousel/template'; 2 | import Footer from './Footer/template'; 3 | import Form from './Form/template'; 4 | import Header from './Header/template'; 5 | import Icon from './Icon/template'; 6 | import Image from './Image/template'; 7 | import List from './List/template'; 8 | import LongText from './LongText/template'; 9 | import Notice from './Notice/template'; 10 | import Qrcode from './Qrcode/template'; 11 | import Tab from './Tab/template'; 12 | import Text from './Text/template'; 13 | 14 | const basicTemplate = [ 15 | Carousel, 16 | Footer, 17 | Form, 18 | Header, 19 | Icon, 20 | Image, 21 | List, 22 | LongText, 23 | Notice, 24 | Qrcode, 25 | Tab, 26 | Text, 27 | ]; 28 | const BasicTemplate = basicTemplate.map(v => { 29 | return { ...v, category: 'base' }; 30 | }); 31 | 32 | export default BasicTemplate; 33 | -------------------------------------------------------------------------------- /src/components/ErrorBundaries/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { ErrorInfo, PropsWithChildren } from 'react'; 2 | 3 | interface ErrorBoundaryState { 4 | hasError: boolean; 5 | } 6 | 7 | class ErrorBoundary extends React.Component, ErrorBoundaryState> { 8 | constructor(props: PropsWithChildren<{}>) { 9 | super(props); 10 | this.state = { hasError: false }; 11 | } 12 | 13 | componentDidCatch(_error: Error, _info: ErrorInfo) { 14 | // Display fallback UI 15 | this.setState({ hasError: true }); 16 | // You can also log the error to an error reporting service 17 | //logErrorToMyService(error, info); 18 | } 19 | 20 | render() { 21 | if (this.state.hasError) { 22 | // You can render any custom fallback UI 23 | return

Something went wrong.

; 24 | } 25 | return this.props.children; 26 | } 27 | } 28 | 29 | export default ErrorBoundary; 30 | -------------------------------------------------------------------------------- /src/components/PanelComponents/DataList/index.less: -------------------------------------------------------------------------------- 1 | .dataList { 2 | padding: 6px 10px; 3 | border: 1px solid #f0f0f0; 4 | } 5 | .listItem { 6 | position: relative; 7 | padding-bottom: 6px; 8 | margin-bottom: 6px; 9 | border-bottom: 1px solid #f0f0f0; 10 | &:hover { 11 | .actionBar { 12 | display: block; 13 | } 14 | } 15 | &:last-child { 16 | border-bottom: none; 17 | margin-bottom: 0; 18 | } 19 | .tit { 20 | font-weight: bold; 21 | } 22 | .desc { 23 | font-size: 12px; 24 | color: #ccc; 25 | } 26 | .actionBar { 27 | position: absolute; 28 | right: 0; 29 | top: 50%; 30 | transform: translateY(-50%); 31 | display: none; 32 | background: #fff; 33 | box-shadow: -20px 0 10px 10px #fff; 34 | .action { 35 | margin-right: 18px; 36 | cursor: pointer; 37 | &:last-child { 38 | cursor: move; 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/pages/editor/TargetBox.js: -------------------------------------------------------------------------------- 1 | import React, { useMemo, memo } from 'react'; 2 | import { useDrag } from 'react-dnd'; 3 | import schema from 'components/BasicShop/schema'; 4 | import styles from './index.less'; 5 | 6 | const TargetBox = memo(props => { 7 | const { item } = props; 8 | const [{ isDragging }, drag] = useDrag({ 9 | item: { 10 | type: item.type, 11 | config: schema[item.type].config, 12 | h: item.h, 13 | editableEl: schema[item.type].editData, 14 | category: item.category, 15 | }, 16 | collect: monitor => ({ 17 | isDragging: monitor.isDragging(), 18 | }), 19 | }); 20 | 21 | const containerStyle = useMemo( 22 | () => ({ 23 | opacity: isDragging ? 0.4 : 1, 24 | cursor: 'move', 25 | }), 26 | [isDragging], 27 | ); 28 | 29 | return ( 30 |
31 | {props.children} 32 |
33 | ); 34 | }); 35 | 36 | export default TargetBox; 37 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/Icon/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import * as Icon from '@ant-design/icons'; 3 | import IconImg from 'assets/icon.png'; 4 | import { AntdIconProps } from '@ant-design/icons/lib/components/AntdIcon'; 5 | import { AntdIconType } from './icon'; 6 | import { IIconConfig } from './schema'; 7 | 8 | interface IconType extends IIconConfig { 9 | isTpl?: boolean; 10 | } 11 | const XIcon = memo((props: IconType) => { 12 | const { color, size, type, spin, isTpl } = props; 13 | 14 | const MyIcon: React.ForwardRefExoticComponent & 15 | React.RefAttributes> = Icon[type]; 16 | 17 | return isTpl ? ( 18 |
19 | {type} 20 | 图标 21 |
22 | ) : ( 23 | 24 | ); 25 | }); 26 | 27 | export default XIcon; 28 | -------------------------------------------------------------------------------- /src/global.css: -------------------------------------------------------------------------------- 1 | html, body, #root { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | margin: 0; 7 | } 8 | 9 | :root { 10 | --sk-size: 40px; 11 | --sk-color: #06c; 12 | } 13 | 14 | .ant-btn-primary { 15 | color: #fff; 16 | background: #2F54EB !important; 17 | border-color: #2F54EB !important; 18 | } 19 | 20 | .ant-btn.ant-btn-link { 21 | color: #2F54EB !important; 22 | } 23 | 24 | .ant-btn-background-ghost.ant-btn-primary { 25 | color: #2F54EB !important; 26 | border-color: #2F54EB !important; 27 | } 28 | 29 | .ant-btn-link[disabled], .ant-btn-link[disabled]:active, .ant-btn-link[disabled]:focus, .ant-btn-link[disabled]:hover { 30 | color: rgba(0,0,0,.25) !important; 31 | background: transparent !important; 32 | border-color: transparent !important; 33 | } 34 | 35 | @import '~react-grid-layout/css/styles.css'; 36 | @import '~react-resizable/css/styles.css'; 37 | @import '~zarm/dist/zarm.min.css'; 38 | 39 | @import '~codemirror/lib/codemirror.css'; 40 | @import '~codemirror/theme/material.css'; 41 | -------------------------------------------------------------------------------- /src/components/BasicShop/MediaComponents/Video/schema.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ITextConfigType, 3 | IUploadConfigType, 4 | TTextDefaultType, 5 | TUploadDefaultType, 6 | } from '@/components/PanelComponents/FormEditor/types'; 7 | 8 | export type TVideoEditData = Array; 9 | export interface IVideoConfig { 10 | poster: TUploadDefaultType; 11 | url: TTextDefaultType; 12 | } 13 | 14 | export interface IVideoSchema { 15 | editData: TVideoEditData; 16 | config: IVideoConfig; 17 | } 18 | 19 | const Video: IVideoSchema = { 20 | editData: [ 21 | { 22 | key: 'poster', 23 | name: '视频封面', 24 | type: 'Upload', 25 | }, 26 | { 27 | key: 'url', 28 | name: '视频链接', 29 | type: 'Text', 30 | }, 31 | ], 32 | config: { 33 | poster: [ 34 | { 35 | uid: '001', 36 | name: 'image.png', 37 | status: 'done', 38 | url: 'http://io.nainor.com/uploads/1_1740c6fbcd9.png', 39 | }, 40 | ], 41 | url: '', 42 | }, 43 | }; 44 | 45 | export default Video; 46 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/Image/schema.ts: -------------------------------------------------------------------------------- 1 | import { 2 | INumberConfigType, 3 | IUploadConfigType, 4 | TNumberDefaultType, 5 | TUploadDefaultType, 6 | } from '@/components/PanelComponents/FormEditor/types'; 7 | 8 | export type TImageEditData = Array; 9 | export interface IImageConfig { 10 | imgUrl: TUploadDefaultType; 11 | round: TNumberDefaultType; 12 | } 13 | 14 | export interface IImageSchema { 15 | editData: TImageEditData; 16 | config: IImageConfig; 17 | } 18 | 19 | const Image: IImageSchema = { 20 | editData: [ 21 | { 22 | key: 'imgUrl', 23 | name: '上传', 24 | type: 'Upload', 25 | isCrop: false, 26 | }, 27 | { 28 | key: 'round', 29 | name: '圆角', 30 | type: 'Number', 31 | }, 32 | ], 33 | config: { 34 | imgUrl: [ 35 | { 36 | uid: '001', 37 | name: 'image.png', 38 | status: 'done', 39 | url: 'http://io.nainor.com/uploads/4_1740bf4535c.png', 40 | }, 41 | ], 42 | round: 0, 43 | }, 44 | }; 45 | 46 | export default Image; 47 | -------------------------------------------------------------------------------- /src/components/BackTop/index.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import { BackToTop, Icon } from 'zarm'; 3 | import React from 'react'; 4 | const themeObj = { 5 | simple: { bgColor: '#fff', color: '#999' }, 6 | black: { bgColor: '#000', color: '#fff' }, 7 | danger: { bgColor: '#ff5050', color: '#fff' }, 8 | primary: { bgColor: '#00bc71', color: '#fff' }, 9 | blue: { bgColor: '#06c', color: '#fff' }, 10 | }; 11 | const BackTop = memo((props: { theme: keyof typeof themeObj }) => { 12 | const { theme = 'simple' } = props; 13 | 14 | return ( 15 | 16 |
30 | 31 |
32 |
33 | ); 34 | }); 35 | 36 | export default BackTop; 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 MrXujiang 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 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa'); 2 | const { resolve } = require('path'); 3 | const staticServer = require('koa-static'); 4 | const koaBody = require('koa-body'); 5 | const cors = require('koa2-cors'); 6 | const logger = require('koa-logger'); 7 | 8 | const app = new Koa(); 9 | 10 | app.use(staticServer(resolve(__dirname, './static'))); 11 | app.use(koaBody()); 12 | app.use(logger()); 13 | 14 | // 设置跨域 15 | app.use( 16 | cors({ 17 | origin: function(ctx) { 18 | if (ctx.url.indexOf('/dooring') > -1) { 19 | return '*'; // 允许来自所有域名请求 20 | } 21 | return ''; 22 | }, 23 | exposeHeaders: ['WWW-Authenticate', 'Server-Authorization', 'x-test-code'], 24 | maxAge: 5, // 该字段可选,用来指定本次预检请求的有效期,单位为秒 25 | credentials: true, 26 | allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], 27 | allowHeaders: [ 28 | 'Content-Type', 29 | 'Authorization', 30 | 'Accept', 31 | 'x-requested-with', 32 | 'Content-Encoding', 33 | ], 34 | }), 35 | ); 36 | 37 | let htmlStr = ''; 38 | 39 | app.use(async (ctx, next) => { 40 | console.log(ctx.url); 41 | if (ctx.url === '/dooring/render') { 42 | htmlStr = ctx.request.body; 43 | ctx.body = 'success'; 44 | } else if (ctx.url.indexOf('/html') === 0) { 45 | ctx.type = 'html'; 46 | ctx.body = htmlStr; 47 | } 48 | }); 49 | 50 | app.listen(3000); 51 | -------------------------------------------------------------------------------- /src/pages/ide/index.less: -------------------------------------------------------------------------------- 1 | .wrap { 2 | display: flex; 3 | flex-direction: column; 4 | .header { 5 | position: relative; 6 | z-index: 10; 7 | padding-left: 30px; 8 | padding-right: 30px; 9 | display: flex; 10 | align-items: center; 11 | height: 42px; 12 | background: #fff; 13 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 14 | .logoArea { 15 | width: 300px; 16 | font-size: 18px; 17 | color: #2f54eb; 18 | .backBtn { 19 | display: inline-block; 20 | padding: 12px 10px; 21 | margin-right: 22px; 22 | cursor: pointer; 23 | } 24 | .logo { 25 | display: inline-block; 26 | width: 38px; 27 | overflow: hidden; 28 | border-radius: 3px; 29 | vertical-align: middle; 30 | img { 31 | width: 100%; 32 | } 33 | } 34 | } 35 | .operationBar { 36 | margin-left: auto; 37 | } 38 | } 39 | } 40 | .contentWrap { 41 | display: flex; 42 | } 43 | .codeWrap { 44 | width: calc(100vw - 440px); 45 | min-height: 560px; 46 | height: 100%; 47 | } 48 | :global(.cm-s-material.CodeMirror) { 49 | height: 100%; 50 | } 51 | .previewWrap { 52 | margin: 0 30px; 53 | margin-top: 30px; 54 | width: 375px; 55 | min-width: 375px; 56 | overflow: auto; 57 | border: 12px solid #000; 58 | border-radius: 20px; 59 | } 60 | -------------------------------------------------------------------------------- /src/utils/req.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { message } from 'antd'; 3 | 4 | const isDev = process.env.NODE_ENV === 'development'; 5 | 6 | const instance = axios.create({ 7 | // 服务器地址需要自己配置和开发 8 | baseURL: isDev ? 'http://localhost:3000/xxx' : 'http://xxxxx', 9 | timeout: 10000, 10 | withCredentials: true, 11 | }); 12 | 13 | // 添加请求拦截器 14 | instance.interceptors.request.use( 15 | function(config) { 16 | // 在发送请求之前做些什么 17 | config.headers = { 18 | 'x-requested-with': '', 19 | authorization: '', 20 | }; 21 | return config; 22 | }, 23 | function(error) { 24 | // 对请求错误做些什么 25 | return Promise.reject(error); 26 | }, 27 | ); 28 | 29 | // 添加响应拦截器 30 | instance.interceptors.response.use( 31 | function(response) { 32 | // 对响应数据做点什么 33 | // 你的业务数据 34 | return response; 35 | }, 36 | function(error) { 37 | // 对响应错误做点什么 38 | const { response } = error; 39 | if (response) { 40 | if (response.status === 404) { 41 | message.error('请求资源未发现'); 42 | } else if (response.status === 403) { 43 | message.error(response.data.msg, () => { 44 | window.location.href = '/admin/login'; 45 | }); 46 | } else { 47 | message.error(response.data.msg); 48 | } 49 | } 50 | 51 | return Promise.reject(error); 52 | }, 53 | ); 54 | 55 | export default instance; 56 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/Carousel/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, PropsWithChildren } from 'react'; 2 | import { Carousel } from 'zarm'; 3 | import styles from './index.less'; 4 | import { ICarouselConfig } from './schema'; 5 | 6 | interface CarouselTypes extends ICarouselConfig { 7 | isTpl: boolean; 8 | } 9 | 10 | const XCarousel = memo((props: PropsWithChildren) => { 11 | const { direction, swipeable, autoPlay, isTpl, imgList, tplImg } = props; 12 | const contentRender = () => { 13 | return imgList.map((item, i) => { 14 | return ( 15 |
16 | 0 ? item.imgUrl[0].url : ''} alt="" /> 17 |
18 | ); 19 | }); 20 | }; 21 | return ( 22 |
23 | {isTpl ? ( 24 |
25 | 26 |
27 | ) : ( 28 | { 30 | // console.log(`onChange: ${index}`); 31 | }} 32 | direction={direction} 33 | swipeable={swipeable} 34 | autoPlay={autoPlay} 35 | loop 36 | > 37 | {contentRender()} 38 | 39 | )} 40 |
41 | ); 42 | }); 43 | 44 | export default XCarousel; 45 | -------------------------------------------------------------------------------- /src/components/PanelComponents/FormItems/formItems.less: -------------------------------------------------------------------------------- 1 | .formItemWrap { 2 | .editForm { 3 | .formItem { 4 | position: relative; 5 | &:hover { 6 | .operationWrap { 7 | display: inline-block; 8 | } 9 | } 10 | .operationWrap { 11 | position: absolute; 12 | display: none; 13 | right: 0; 14 | top: 16px; 15 | box-shadow: 0 0 20px #fff; 16 | background-color: #fff; 17 | .operationBtn { 18 | margin-right: 15px; 19 | display: inline-block; 20 | cursor: pointer; 21 | } 22 | } 23 | } 24 | } 25 | .formTpl { 26 | margin-top: 12px; 27 | border-top: 1px dashed #ccc; 28 | padding-top: 16px; 29 | .formItem { 30 | position: relative; 31 | border: 1px solid #ccc; 32 | margin-bottom: 2px; 33 | .disClick { 34 | pointer-events: none; 35 | } 36 | &:hover { 37 | border-color: #2f54eb; 38 | .addBtn { 39 | display: inline-block; 40 | } 41 | } 42 | .addBtn { 43 | position: absolute; 44 | right: 0; 45 | top: 50%; 46 | transform: translateY(-50%); 47 | display: none; 48 | padding: 3px 6px; 49 | color: #fff; 50 | border-radius: 3px; 51 | background-color: #2f54eb; 52 | cursor: pointer; 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/components/PanelComponents/CardPicker/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, memo } from 'react'; 2 | import classnames from 'classnames'; 3 | import Icon from '../../BasicShop/BasicComponents/Icon'; 4 | import styles from './index.less'; 5 | import React from 'react'; 6 | import { IconTypes } from '@/components/BasicShop/BasicComponents/Icon/schema'; 7 | import { ICardPickerConfigType } from '../FormEditor/types'; 8 | 9 | interface CardPickerType extends Omit, 'type' | 'key' | 'name'> { 10 | onChange?: (v: string) => void; 11 | type: IconTypes; 12 | } 13 | 14 | export default memo((props: CardPickerType) => { 15 | const { type, icons, onChange } = props; 16 | const [selected, setSelected] = useState(type); 17 | 18 | const handlePicker = (v: IconTypes) => { 19 | if (onChange) { 20 | onChange(v); 21 | return; 22 | } 23 | setSelected(v); 24 | }; 25 | 26 | useEffect(() => { 27 | setSelected(type); 28 | }, [type]); 29 | 30 | return ( 31 |
32 | {icons.map((item, i) => { 33 | return ( 34 | handlePicker(item)} 37 | key={i} 38 | > 39 | 40 | 41 | ); 42 | })} 43 |
44 | ); 45 | }); 46 | -------------------------------------------------------------------------------- /src/utils/tool.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { RGBColor } from 'react-color'; 3 | 4 | // 生成uuid 5 | function uuid(len: number, radix: number) { 6 | let chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split(''); 7 | let uuid = [], 8 | i; 9 | radix = radix || chars.length; 10 | 11 | if (len) { 12 | for (i = 0; i < len; i++) uuid[i] = chars[0 | (Math.random() * radix)]; 13 | } else { 14 | let r; 15 | uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-'; 16 | uuid[14] = '4'; 17 | 18 | for (i = 0; i < 36; i++) { 19 | if (!uuid[i]) { 20 | r = 0 | (Math.random() * 16); 21 | uuid[i] = chars[i === 19 ? (r & 0x3) | 0x8 : r]; 22 | } 23 | } 24 | } 25 | 26 | return uuid.join(''); 27 | } 28 | 29 | // 将rgba字符串对象转化为rgba对象 30 | function rgba2Obj(rgba = '') { 31 | let reg = /rgba\((\d+),(\d+),(\d+),(\d+)\)/g; 32 | let rgbaObj: RGBColor = { r: 0, g: 0, b: 0, a: 0 }; 33 | 34 | rgba.replace(reg, (_m, r, g, b, a) => { 35 | rgbaObj = { r, g, b, a }; 36 | return rgba; 37 | }); 38 | return rgbaObj; 39 | } 40 | 41 | export { uuid, rgba2Obj }; 42 | 43 | export const isDev = process.env.NODE_ENV === 'development'; 44 | 45 | export function useGetRect() { 46 | const [rect, setRect] = useState({ width: 0, height: 0 }); 47 | useEffect(() => { 48 | setRect({ 49 | width: window.innerWidth, 50 | height: window.innerHeight, 51 | }); 52 | }, []); 53 | return rect; 54 | } 55 | -------------------------------------------------------------------------------- /src/pages/home/index.less: -------------------------------------------------------------------------------- 1 | .homeWrap { 2 | display: flex; 3 | height: 100%; 4 | .leftArea { 5 | padding: 10px 0 10px 20px; 6 | width: 320px; 7 | border-right: 1px solid #f0f0f0; 8 | } 9 | .contentArea { 10 | flex: 1; 11 | .logoTip { 12 | padding-top: 100px; 13 | text-align: center; 14 | .logo { 15 | margin-bottom: 20px; 16 | width: 100%; 17 | font-size: 30px; 18 | .logoText { 19 | font-weight: bold; 20 | color: #2f54eb; 21 | } 22 | } 23 | } 24 | .operation { 25 | margin-top: 30px; 26 | text-align: center; 27 | .card { 28 | display: inline-flex; 29 | align-items: center; 30 | flex-direction: column; 31 | justify-content: center; 32 | width: 220px; 33 | height: 220px; 34 | border-radius: 6px; 35 | box-shadow: 0 0 20px rgba(0, 0, 0, 0); 36 | font-size: 56px; 37 | border: 1px solid #f0f0f0; 38 | margin-right: 30px; 39 | cursor: pointer; 40 | &:hover { 41 | box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); 42 | } 43 | div { 44 | margin-top: 10px; 45 | font-size: 20px; 46 | } 47 | &:last-child { 48 | margin-right: 0; 49 | } 50 | } 51 | } 52 | .footer { 53 | margin-top: 50px; 54 | text-align: center; 55 | font-size: 50px; 56 | p { 57 | font-size: 16px; 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/components/BasicShop/VisualComponents/Chart/index.tsx: -------------------------------------------------------------------------------- 1 | import { Chart } from '@antv/f2'; 2 | import React, { memo, useEffect, useRef } from 'react'; 3 | // import { uuid } from 'utils/tool'; 4 | import ChartImg from '@/assets/chart.png'; 5 | 6 | import styles from './index.less'; 7 | import { IChartConfig } from './schema'; 8 | 9 | interface XChartProps extends IChartConfig { 10 | isTpl: boolean; 11 | } 12 | 13 | const XChart = (props: XChartProps) => { 14 | const { isTpl, data, color, size, paddingTop, title } = props; 15 | const chartRef = useRef(null); 16 | useEffect(() => { 17 | if (!isTpl) { 18 | const chart = new Chart({ 19 | el: chartRef.current || undefined, 20 | pixelRatio: window.devicePixelRatio, // 指定分辨率 21 | }); 22 | 23 | // step 2: 处理数据 24 | const dataX = data.map(item => ({ ...item, value: Number(item.value) })); 25 | 26 | // Step 2: 载入数据源 27 | chart.source(dataX); 28 | 29 | // Step 3:创建图形语法,绘制柱状图,由 genre 和 sold 两个属性决定图形位置,genre 映射至 x 轴,sold 映射至 y 轴 30 | chart 31 | .interval() 32 | .position('name*value') 33 | .color('name'); 34 | 35 | // Step 4: 渲染图表 36 | chart.render(); 37 | } 38 | }, [data, isTpl]); 39 | return ( 40 |
41 |
42 | {title} 43 |
44 | {isTpl ? dooring chart : } 45 |
46 | ); 47 | }; 48 | 49 | export default memo(XChart); 50 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/Qrcode/schema.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IColorConfigType, 3 | INumberConfigType, 4 | ITextConfigType, 5 | IUploadConfigType, 6 | TColorDefaultType, 7 | TNumberDefaultType, 8 | TTextDefaultType, 9 | TUploadDefaultType, 10 | } from '@/components/PanelComponents/FormEditor/types'; 11 | 12 | export type TQrcodeEditData = Array< 13 | IUploadConfigType | ITextConfigType | IColorConfigType | INumberConfigType 14 | >; 15 | export interface IQrcodeConfig { 16 | qrcode: TUploadDefaultType; 17 | text: TTextDefaultType; 18 | color: TColorDefaultType; 19 | fontSize: TNumberDefaultType; 20 | } 21 | 22 | export interface IQrcodeSchema { 23 | editData: TQrcodeEditData; 24 | config: IQrcodeConfig; 25 | } 26 | 27 | const Qrcode: IQrcodeSchema = { 28 | editData: [ 29 | { 30 | key: 'qrcode', 31 | name: '二维码', 32 | type: 'Upload', 33 | isCrop: true, 34 | cropRate: 1, 35 | }, 36 | { 37 | key: 'text', 38 | name: '文字', 39 | type: 'Text', 40 | }, 41 | { 42 | key: 'color', 43 | name: '文字颜色', 44 | type: 'Color', 45 | }, 46 | { 47 | key: 'fontSize', 48 | name: '文字大小', 49 | type: 'Number', 50 | }, 51 | ], 52 | config: { 53 | qrcode: [ 54 | { 55 | uid: '001', 56 | name: 'image.png', 57 | status: 'done', 58 | url: 'http://io.nainor.com/uploads/code_173e1705e0c.png', 59 | }, 60 | ], 61 | text: '二维码', 62 | color: 'rgba(153,153,153,1)', 63 | fontSize: 14, 64 | }, 65 | }; 66 | 67 | export default Qrcode; 68 | -------------------------------------------------------------------------------- /.umirc.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { defineConfig } from 'umi'; 3 | 4 | export default defineConfig({ 5 | dynamicImport: { 6 | loading: '@/components/LoadingCp', 7 | }, 8 | dva: { 9 | immer: true, 10 | }, 11 | devtool: 'source-map', 12 | antd: {}, 13 | title: '趣谈前端-h5-dooring', 14 | exportStatic: {}, 15 | base: 'h5_plus', 16 | publicPath: '/h5_plus/', 17 | outputPath: '../server/static/h5_plus', 18 | 19 | routes: [ 20 | { 21 | exact: false, 22 | path: '/', 23 | component: '@/layouts/index', 24 | routes: [ 25 | { 26 | path: '/', 27 | component: '../pages/home', 28 | }, 29 | { 30 | path: '/editor', 31 | component: '../pages/editor', 32 | }, 33 | { 34 | path: '/ide', 35 | component: '../pages/ide', 36 | }, 37 | { 38 | path: '/login', 39 | component: '../pages/login', 40 | }, 41 | { 42 | path: '/mobileTip', 43 | component: '../pages/mobileTip', 44 | }, 45 | { 46 | path: '/preview', 47 | component: '../pages/editor/preview', 48 | }, 49 | ], 50 | }, 51 | ], 52 | theme: { 53 | 'primary-color': '#2F54EB', 54 | // "btn-primary-bg": "#2F54EB" 55 | }, 56 | extraBabelPlugins: [['import', { libraryName: 'zarm', style: true }]], 57 | // sass: {}, 58 | alias: { 59 | components: path.resolve(__dirname, 'src/components/'), 60 | utils: path.resolve(__dirname, 'src/utils/'), 61 | assets: path.resolve(__dirname, 'src/assets/'), 62 | }, 63 | }); 64 | -------------------------------------------------------------------------------- /src/components/BasicShop/VisualComponents/Chart/schema.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IColorConfigType, 3 | INumberConfigType, 4 | ITableConfigType, 5 | ITextConfigType, 6 | TColorDefaultType, 7 | TNumberDefaultType, 8 | TTableDefaultType, 9 | TTextDefaultType, 10 | } from '@/components/PanelComponents/FormEditor/types'; 11 | 12 | export type TChartEditData = Array< 13 | ITextConfigType | INumberConfigType | IColorConfigType | ITableConfigType 14 | >; 15 | export interface IChartConfig { 16 | title: TTextDefaultType; 17 | size: TNumberDefaultType; 18 | color: TColorDefaultType; 19 | paddingTop: TNumberDefaultType; 20 | data: TTableDefaultType; 21 | } 22 | 23 | export interface IChartSchema { 24 | editData: TChartEditData; 25 | config: IChartConfig; 26 | } 27 | 28 | const Chart: IChartSchema = { 29 | editData: [ 30 | { 31 | key: 'title', 32 | name: '标题', 33 | type: 'Text', 34 | }, 35 | { 36 | key: 'size', 37 | name: '标题大小', 38 | type: 'Number', 39 | }, 40 | { 41 | key: 'color', 42 | name: '标题颜色', 43 | type: 'Color', 44 | }, 45 | { 46 | key: 'paddingTop', 47 | name: '上边距', 48 | type: 'Number', 49 | }, 50 | { 51 | key: 'data', 52 | name: '数据源', 53 | type: 'Table', 54 | }, 55 | ], 56 | config: { 57 | title: '柱状图', 58 | size: 14, 59 | color: 'rgba(0,0,0,1)', 60 | paddingTop: 10, 61 | data: [ 62 | { 63 | name: 'A', 64 | value: 20, 65 | }, 66 | { 67 | name: 'B', 68 | value: 60, 69 | }, 70 | { 71 | name: 'C', 72 | value: 20, 73 | }, 74 | ], 75 | }, 76 | }; 77 | 78 | export default Chart; 79 | -------------------------------------------------------------------------------- /src/pages/editor/models/editorModal.js: -------------------------------------------------------------------------------- 1 | const pointData = localStorage.getItem('userData') || '[]'; 2 | 3 | function overSave(name, data) { 4 | localStorage.setItem(name, JSON.stringify(data)); 5 | } 6 | 7 | export default { 8 | namespace: 'editorModal', 9 | state: { 10 | pointData: JSON.parse(pointData), 11 | curPoint: null, 12 | }, 13 | reducers: { 14 | addPointData(state, { payload }) { 15 | let pointData = [...state.pointData, payload]; 16 | overSave('userData', pointData); 17 | return { 18 | ...state, 19 | pointData, 20 | curPoint: payload, 21 | }; 22 | }, 23 | modPointData(state, { payload }) { 24 | const { id } = payload; 25 | const pointData = state.pointData.map(item => { 26 | if (item.id === id) { 27 | return payload; 28 | } 29 | return { ...item }; 30 | }); 31 | overSave('userData', pointData); 32 | return { 33 | ...state, 34 | pointData, 35 | curPoint: payload, 36 | }; 37 | }, 38 | delPointData(state, { payload }) { 39 | const { id } = payload; 40 | const pointData = state.pointData.filter(item => item.id !== id); 41 | overSave('userData', pointData); 42 | return { 43 | ...state, 44 | pointData, 45 | curPoint: null, 46 | }; 47 | }, 48 | clearAll(state) { 49 | overSave('userData', []); 50 | return { 51 | ...state, 52 | pointData: [], 53 | curPoint: null, 54 | }; 55 | }, 56 | }, 57 | effects: {}, 58 | subscriptions: { 59 | setup({ dispatch, history }) { 60 | return history.listen(({ pathname, query }) => {}); 61 | }, 62 | }, 63 | }; 64 | -------------------------------------------------------------------------------- /src/components/BasicShop/VisualComponents/Pie/schema.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IColorConfigType, 3 | INumberConfigType, 4 | ITableConfigType, 5 | ITextConfigType, 6 | TColorDefaultType, 7 | TNumberDefaultType, 8 | TTableDefaultType, 9 | TTextDefaultType, 10 | } from '@/components/PanelComponents/FormEditor/types'; 11 | 12 | export type TChartEditData = Array< 13 | ITextConfigType | INumberConfigType | IColorConfigType | ITableConfigType 14 | >; 15 | export interface IChartConfig { 16 | title: TTextDefaultType; 17 | size: TNumberDefaultType; 18 | color: TColorDefaultType; 19 | paddingTop: TNumberDefaultType; 20 | data: TTableDefaultType; 21 | } 22 | 23 | export interface IChartSchema { 24 | editData: TChartEditData; 25 | config: IChartConfig; 26 | } 27 | 28 | const Chart: IChartSchema = { 29 | editData: [ 30 | { 31 | key: 'title', 32 | name: '标题', 33 | type: 'Text', 34 | }, 35 | { 36 | key: 'size', 37 | name: '标题大小', 38 | type: 'Number', 39 | }, 40 | { 41 | key: 'color', 42 | name: '标题颜色', 43 | type: 'Color', 44 | }, 45 | { 46 | key: 'paddingTop', 47 | name: '上边距', 48 | type: 'Number', 49 | }, 50 | { 51 | key: 'data', 52 | name: '数据源', 53 | type: 'Table', 54 | }, 55 | ], 56 | config: { 57 | title: '饼图', 58 | size: 14, 59 | color: 'rgba(0,0,0,1)', 60 | paddingTop: 10, 61 | data: [ 62 | { 63 | name: 'A', 64 | value: 20, 65 | }, 66 | { 67 | name: 'B', 68 | value: 60, 69 | }, 70 | { 71 | name: 'C', 72 | value: 20, 73 | }, 74 | { 75 | name: 'D', 76 | value: 80, 77 | }, 78 | ], 79 | }, 80 | }; 81 | 82 | export default Chart; 83 | -------------------------------------------------------------------------------- /src/components/BasicShop/VisualComponents/Area/schema.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IColorConfigType, 3 | INumberConfigType, 4 | ITableConfigType, 5 | ITextConfigType, 6 | TColorDefaultType, 7 | TNumberDefaultType, 8 | TTableDefaultType, 9 | TTextDefaultType, 10 | } from '@/components/PanelComponents/FormEditor/types'; 11 | 12 | export type TChartEditData = Array< 13 | ITextConfigType | INumberConfigType | IColorConfigType | ITableConfigType 14 | >; 15 | export interface IChartConfig { 16 | title: TTextDefaultType; 17 | size: TNumberDefaultType; 18 | color: TColorDefaultType; 19 | paddingTop: TNumberDefaultType; 20 | data: TTableDefaultType; 21 | } 22 | 23 | export interface IChartSchema { 24 | editData: TChartEditData; 25 | config: IChartConfig; 26 | } 27 | 28 | const Chart: IChartSchema = { 29 | editData: [ 30 | { 31 | key: 'title', 32 | name: '标题', 33 | type: 'Text', 34 | }, 35 | { 36 | key: 'size', 37 | name: '标题大小', 38 | type: 'Number', 39 | }, 40 | { 41 | key: 'color', 42 | name: '标题颜色', 43 | type: 'Color', 44 | }, 45 | { 46 | key: 'paddingTop', 47 | name: '上边距', 48 | type: 'Number', 49 | }, 50 | { 51 | key: 'data', 52 | name: '数据源', 53 | type: 'Table', 54 | }, 55 | ], 56 | config: { 57 | title: '面积图', 58 | size: 14, 59 | color: 'rgba(0,0,0,1)', 60 | paddingTop: 10, 61 | data: [ 62 | { 63 | name: 'A', 64 | value: 20, 65 | }, 66 | { 67 | name: 'B', 68 | value: 60, 69 | }, 70 | { 71 | name: 'C', 72 | value: 20, 73 | }, 74 | { 75 | name: 'D', 76 | value: 80, 77 | }, 78 | ], 79 | }, 80 | }; 81 | 82 | export default Chart; 83 | -------------------------------------------------------------------------------- /src/components/BasicShop/VisualComponents/Line/schema.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IColorConfigType, 3 | INumberConfigType, 4 | ITableConfigType, 5 | ITextConfigType, 6 | TColorDefaultType, 7 | TNumberDefaultType, 8 | TTableDefaultType, 9 | TTextDefaultType, 10 | } from '@/components/PanelComponents/FormEditor/types'; 11 | 12 | export type TChartEditData = Array< 13 | ITextConfigType | INumberConfigType | IColorConfigType | ITableConfigType 14 | >; 15 | export interface IChartConfig { 16 | title: TTextDefaultType; 17 | size: TNumberDefaultType; 18 | color: TColorDefaultType; 19 | paddingTop: TNumberDefaultType; 20 | data: TTableDefaultType; 21 | } 22 | 23 | export interface IChartSchema { 24 | editData: TChartEditData; 25 | config: IChartConfig; 26 | } 27 | 28 | const Chart: IChartSchema = { 29 | editData: [ 30 | { 31 | key: 'title', 32 | name: '标题', 33 | type: 'Text', 34 | }, 35 | { 36 | key: 'size', 37 | name: '标题大小', 38 | type: 'Number', 39 | }, 40 | { 41 | key: 'color', 42 | name: '标题颜色', 43 | type: 'Color', 44 | }, 45 | { 46 | key: 'paddingTop', 47 | name: '上边距', 48 | type: 'Number', 49 | }, 50 | { 51 | key: 'data', 52 | name: '数据源', 53 | type: 'Table', 54 | }, 55 | ], 56 | config: { 57 | title: '折线图', 58 | size: 14, 59 | color: 'rgba(0,0,0,1)', 60 | paddingTop: 10, 61 | data: [ 62 | { 63 | name: 'A', 64 | value: 20, 65 | }, 66 | { 67 | name: 'B', 68 | value: 60, 69 | }, 70 | { 71 | name: 'C', 72 | value: 20, 73 | }, 74 | { 75 | name: 'D', 76 | value: 80, 77 | }, 78 | ], 79 | }, 80 | }; 81 | 82 | export default Chart; 83 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/List/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from 'react'; 2 | import styles from './index.less'; 3 | import { IListConfig } from './schema'; 4 | const List = memo((props: IListConfig) => { 5 | const { round, sourceData, imgSize, fontSize, color } = props; 6 | return ( 7 |
8 |
9 | {sourceData.map((item, i) => { 10 | return ( 11 |
12 |
13 | {item.desc} 27 |
28 | 43 |
44 | ); 45 | })} 46 |
47 |
48 | ); 49 | }); 50 | 51 | export default List; 52 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/Text/schema.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IColorConfigType, 3 | INumberConfigType, 4 | ISelectConfigType, 5 | ITextConfigType, 6 | TColorDefaultType, 7 | TNumberDefaultType, 8 | TSelectDefaultType, 9 | TTextDefaultType, 10 | } from '@/components/PanelComponents/FormEditor/types'; 11 | 12 | export type TTextSelectKeyType = 'left' | 'right' | 'center'; 13 | export type TTextEditData = Array< 14 | ITextConfigType | IColorConfigType | INumberConfigType | ISelectConfigType 15 | >; 16 | export interface ITextConfig { 17 | text: TTextDefaultType; 18 | color: TColorDefaultType; 19 | fontSize: TNumberDefaultType; 20 | align: TSelectDefaultType; 21 | lineHeight: TNumberDefaultType; 22 | } 23 | 24 | export interface ITextSchema { 25 | editData: TTextEditData; 26 | config: ITextConfig; 27 | } 28 | const Text: ITextSchema = { 29 | editData: [ 30 | { 31 | key: 'text', 32 | name: '文字', 33 | type: 'Text', 34 | }, 35 | { 36 | key: 'color', 37 | name: '标题颜色', 38 | type: 'Color', 39 | }, 40 | { 41 | key: 'fontSize', 42 | name: '字体大小', 43 | type: 'Number', 44 | }, 45 | { 46 | key: 'align', 47 | name: '对齐方式', 48 | type: 'Select', 49 | range: [ 50 | { 51 | key: 'left', 52 | text: '左对齐', 53 | }, 54 | { 55 | key: 'center', 56 | text: '居中对齐', 57 | }, 58 | { 59 | key: 'right', 60 | text: '右对齐', 61 | }, 62 | ], 63 | }, 64 | { 65 | key: 'lineHeight', 66 | name: '行高', 67 | type: 'Number', 68 | }, 69 | ], 70 | config: { 71 | text: '我是文本', 72 | color: 'rgba(60,60,60,1)', 73 | fontSize: 18, 74 | align: 'center', 75 | lineHeight: 2, 76 | }, 77 | }; 78 | export default Text; 79 | -------------------------------------------------------------------------------- /src/components/DynamicEngine/index.tsx: -------------------------------------------------------------------------------- 1 | import { dynamic } from 'umi'; 2 | import Loading from '../LoadingCp'; 3 | import { useMemo, memo, FC } from 'react'; 4 | import React from 'react'; 5 | // import { AllTemplateType } from './schema'; 6 | 7 | export type componentsType = 'media' | 'base' | 'visible'; 8 | 9 | const DynamicFunc = (type: any, componentsType: componentsType) => 10 | dynamic({ 11 | loader: async function() { 12 | let Component: FC<{ isTpl: boolean }>; 13 | if (componentsType === 'base') { 14 | const { default: Graph } = await import(`@/components/BasicShop/BasicComponents/${type}`); 15 | Component = Graph; 16 | } else if (componentsType === 'media') { 17 | const { default: Graph } = await import(`@/components/BasicShop/MediaComponents/${type}`); 18 | Component = Graph; 19 | } else { 20 | const { default: Graph } = await import(`@/components/BasicShop/VisualComponents/${type}`); 21 | Component = Graph; 22 | } 23 | return (props: DynamicType) => { 24 | const { config, isTpl } = props; 25 | return ; 26 | }; 27 | }, 28 | loading: () => ( 29 |
30 | 31 |
32 | ), 33 | }); 34 | 35 | type DynamicType = { 36 | isTpl: boolean; 37 | config: { [key: string]: any }; 38 | type: any; 39 | componentsType: componentsType; 40 | category: componentsType; 41 | }; 42 | const DynamicEngine = memo((props: DynamicType) => { 43 | const { type, config, category } = props; 44 | const Dynamic = useMemo(() => { 45 | return (DynamicFunc(type, category) as unknown) as FC; 46 | // eslint-disable-next-line react-hooks/exhaustive-deps 47 | }, [type, config]); 48 | 49 | return ; 50 | }); 51 | 52 | export default DynamicEngine; 53 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/Header/schema.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IColorConfigType, 3 | INumberConfigType, 4 | ITextConfigType, 5 | IUploadConfigType, 6 | TColorDefaultType, 7 | TNumberDefaultType, 8 | TTextDefaultType, 9 | TUploadDefaultType, 10 | } from '@/components/PanelComponents/FormEditor/types'; 11 | 12 | export type THeaderEditData = Array< 13 | IColorConfigType | INumberConfigType | IUploadConfigType | ITextConfigType 14 | >; 15 | export interface IHeaderConfig { 16 | bgColor: TColorDefaultType; 17 | logo: TUploadDefaultType; 18 | logoText: TTextDefaultType; 19 | fontSize: TNumberDefaultType; 20 | color: TColorDefaultType; 21 | height: TNumberDefaultType; 22 | } 23 | 24 | export interface IHeaderSchema { 25 | editData: THeaderEditData; 26 | config: IHeaderConfig; 27 | } 28 | 29 | const Header: IHeaderSchema = { 30 | editData: [ 31 | { 32 | key: 'bgColor', 33 | name: '背景色', 34 | type: 'Color', 35 | }, 36 | { 37 | key: 'height', 38 | name: '高度', 39 | type: 'Number', 40 | }, 41 | { 42 | key: 'logo', 43 | name: 'logo', 44 | type: 'Upload', 45 | isCrop: true, 46 | cropRate: 1000 / 618, 47 | }, 48 | { 49 | key: 'logoText', 50 | name: 'logo文字', 51 | type: 'Text', 52 | }, 53 | { 54 | key: 'color', 55 | name: '文字颜色', 56 | type: 'Color', 57 | }, 58 | { 59 | key: 'fontSize', 60 | name: '文字大小', 61 | type: 'Number', 62 | }, 63 | ], 64 | config: { 65 | bgColor: 'rgba(0,0,0,1)', 66 | logo: [ 67 | { 68 | uid: '001', 69 | name: 'image.png', 70 | status: 'done', 71 | url: 'http://io.nainor.com/uploads/3_1740be8a482.png', 72 | }, 73 | ], 74 | logoText: '页头Header', 75 | fontSize: 20, 76 | color: 'rgba(255,255,255,1)', 77 | height: 50, 78 | }, 79 | }; 80 | 81 | export default Header; 82 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/Notice/schema.ts: -------------------------------------------------------------------------------- 1 | import { 2 | INumberConfigType, 3 | ISelectConfigType, 4 | ISwitchConfigType, 5 | ITextConfigType, 6 | TNumberDefaultType, 7 | TSelectDefaultType, 8 | TSwitchDefaultType, 9 | TTextDefaultType, 10 | } from '@/components/PanelComponents/FormEditor/types'; 11 | 12 | export type TNoticeSelectKeyType = 'default' | 'warning' | 'primary' | 'success' | 'danger'; 13 | export type TNoticeEditData = Array< 14 | ITextConfigType | INumberConfigType | ISelectConfigType | ISwitchConfigType 15 | >; 16 | export interface INoticeConfig { 17 | text: TTextDefaultType; 18 | speed: TNumberDefaultType; 19 | theme: TSelectDefaultType; 20 | isClose: TSwitchDefaultType; 21 | } 22 | 23 | export interface INoticeSchema { 24 | editData: TNoticeEditData; 25 | config: INoticeConfig; 26 | } 27 | 28 | const Notice: INoticeSchema = { 29 | editData: [ 30 | { 31 | key: 'text', 32 | name: '文本', 33 | type: 'Text', 34 | }, 35 | { 36 | key: 'speed', 37 | name: '滚动速度', 38 | type: 'Number', 39 | }, 40 | { 41 | key: 'theme', 42 | name: '主题', 43 | type: 'Select', 44 | range: [ 45 | { 46 | key: 'default', 47 | text: '默认', 48 | }, 49 | { 50 | key: 'warning', 51 | text: '警告', 52 | }, 53 | { 54 | key: 'primary', 55 | text: '主要', 56 | }, 57 | { 58 | key: 'success', 59 | text: '成功', 60 | }, 61 | { 62 | key: 'danger', 63 | text: '危险', 64 | }, 65 | ], 66 | }, 67 | { 68 | key: 'isClose', 69 | name: '是否可关闭', 70 | type: 'Switch', 71 | }, 72 | ], 73 | config: { 74 | text: '通知栏: 趣谈前端上线啦', 75 | speed: 50, 76 | theme: 'warning', 77 | isClose: false, 78 | }, 79 | }; 80 | 81 | export default Notice; 82 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/Form/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useCallback } from 'react'; 2 | import { Button } from 'zarm'; 3 | import BaseForm from './BaseForm'; 4 | import styles from './index.less'; 5 | import { IFormConfig } from './schema'; 6 | 7 | const FormComponent = (props: IFormConfig) => { 8 | const { title, bgColor, fontSize, titColor, btnColor, btnTextColor, api, formControls } = props; 9 | const formData: Record = {}; 10 | const handleChange = useCallback( 11 | (item, v) => { 12 | if (item.options) { 13 | formData[item.label] = v[0].label; 14 | return; 15 | } 16 | formData[item.label] = v; 17 | }, 18 | [formData], 19 | ); 20 | const handleSubmit = () => { 21 | if (api) { 22 | fetch(api, { 23 | body: JSON.stringify(formData), 24 | cache: 'no-cache', 25 | headers: { 26 | 'content-type': 'application/json', 27 | }, 28 | method: 'POST', 29 | mode: 'cors', 30 | }); 31 | } 32 | }; 33 | return ( 34 |
35 | {title && ( 36 |
37 | {title} 38 |
39 | )} 40 |
41 | {formControls.map(item => { 42 | const FormItem = BaseForm[item.type]; 43 | return ; 44 | })} 45 |
46 | 55 |
56 |
57 |
58 | ); 59 | }; 60 | 61 | export default memo(FormComponent); 62 | -------------------------------------------------------------------------------- /src/layouts/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from 'react'; 2 | import { library, generateRespones, RenderList, useRegister } from 'chatbot-antd'; 3 | import { IRouteComponentProps } from 'umi'; 4 | import { Button } from 'antd'; 5 | import { CustomerServiceOutlined } from '@ant-design/icons'; 6 | 7 | library.push( 8 | //语料库,push进去,也可以不用 9 | { 10 | text: '我是机器人', 11 | reg: '你是谁', 12 | }, 13 | { 14 | text: ( 15 |
16 | @徐小夕 17 | @yehuozhili 18 |
19 | ), 20 | useReg: /(.*?)作者是谁(.*?)/, 21 | }, 22 | ); 23 | 24 | export default function Layout({ children }: IRouteComponentProps) { 25 | const [modalOpen, setModalOpen] = useState(false); 26 | const callb = useCallback((v: RenderList) => { 27 | setTimeout(() => { 28 | //使用settimeout 更像机器人回话 29 | let returnValue = generateRespones(v); 30 | if (returnValue) { 31 | //排除null 32 | setList(prev => [...prev, { isUser: false, text: returnValue }]); 33 | } 34 | }, 500); 35 | // eslint-disable-next-line react-hooks/exhaustive-deps 36 | }, []); 37 | // 注册 38 | const [render, setList] = useRegister( 39 | modalOpen, 40 | callb, 41 | { 42 | onOk: () => setModalOpen(false), 43 | onCancel: () => setModalOpen(false), 44 | title: 'h5-Dooring机器人客服', 45 | }, 46 | {}, 47 |
welcome!欢迎使用h5-Dooring,你有任何问题,都可以咨询我哦~
, 48 | ); 49 | return ( 50 |
51 |
60 | 63 |
64 | {render} 65 | {children} 66 |
67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/Footer/schema.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IColorConfigType, 3 | INumberConfigType, 4 | ISelectConfigType, 5 | ITextConfigType, 6 | TColorDefaultType, 7 | TNumberDefaultType, 8 | TSelectDefaultType, 9 | TTextDefaultType, 10 | } from '@/components/PanelComponents/FormEditor/types'; 11 | 12 | export type TfooterSelectKeyType = 'left' | 'center' | 'right'; 13 | 14 | export type TFooterEditData = Array< 15 | IColorConfigType | INumberConfigType | ITextConfigType | ISelectConfigType 16 | >; 17 | export interface IFooterConfig { 18 | bgColor: TColorDefaultType; 19 | text: TTextDefaultType; 20 | color: TColorDefaultType; 21 | align: TSelectDefaultType; 22 | fontSize: TNumberDefaultType; 23 | height: TNumberDefaultType; 24 | } 25 | 26 | export interface IFooterSchema { 27 | editData: TFooterEditData; 28 | config: IFooterConfig; 29 | } 30 | 31 | const Footer: IFooterSchema = { 32 | editData: [ 33 | { 34 | key: 'bgColor', 35 | name: '背景色', 36 | type: 'Color', 37 | }, 38 | { 39 | key: 'height', 40 | name: '高度', 41 | type: 'Number', 42 | }, 43 | { 44 | key: 'text', 45 | name: '文字', 46 | type: 'Text', 47 | }, 48 | { 49 | key: 'fontSize', 50 | name: '字体大小', 51 | type: 'Number', 52 | }, 53 | { 54 | key: 'color', 55 | name: '文字颜色', 56 | type: 'Color', 57 | }, 58 | { 59 | key: 'align', 60 | name: '对齐方式', 61 | type: 'Select', 62 | range: [ 63 | { 64 | key: 'left', 65 | text: '左对齐', 66 | }, 67 | { 68 | key: 'center', 69 | text: '居中对齐', 70 | }, 71 | { 72 | key: 'right', 73 | text: '右对齐', 74 | }, 75 | ], 76 | }, 77 | ], 78 | config: { 79 | bgColor: 'rgba(0,0,0,1)', 80 | text: '页脚Footer', 81 | color: 'rgba(255,255,255,1)', 82 | align: 'center', 83 | fontSize: 16, 84 | height: 48, 85 | }, 86 | }; 87 | export default Footer; 88 | -------------------------------------------------------------------------------- /src/components/BasicShop/VisualComponents/Line/index.tsx: -------------------------------------------------------------------------------- 1 | import { Chart } from '@antv/f2'; 2 | import React, { memo, useEffect, useRef } from 'react'; 3 | // import { uuid } from 'utils/tool'; 4 | import LineImg from '@/assets/line.png'; 5 | 6 | import styles from './index.less'; 7 | import { IChartConfig } from './schema'; 8 | 9 | interface XChartProps extends IChartConfig { 10 | isTpl: boolean; 11 | } 12 | 13 | const XLine = (props: XChartProps) => { 14 | const { isTpl, data, color, size, paddingTop, title } = props; 15 | const chartRef = useRef(null); 16 | useEffect(() => { 17 | if (!isTpl) { 18 | const chart = new Chart({ 19 | el: chartRef.current || undefined, 20 | pixelRatio: window.devicePixelRatio, // 指定分辨率 21 | }); 22 | 23 | // step 2: 处理数据 24 | const dataX = data.map(item => ({ ...item, value: Number(item.value) })); 25 | 26 | // Step 2: 载入数据源 27 | chart.source(dataX, { 28 | value: { 29 | tickCount: 5, 30 | min: 0, 31 | }, 32 | }); 33 | 34 | chart.tooltip({ 35 | showCrosshairs: true, 36 | showItemMarker: false, 37 | }); 38 | 39 | chart.axis('name', { 40 | label: function label(text, index, total) { 41 | const textCfg: any = {}; 42 | if (index === 0) { 43 | textCfg.textAlign = 'left'; 44 | } else if (index === total - 1) { 45 | textCfg.textAlign = 'right'; 46 | } 47 | return textCfg; 48 | }, 49 | }); 50 | 51 | chart.line().position('name*value'); 52 | chart 53 | .point() 54 | .position('name*value') 55 | .style({ 56 | stroke: '#fff', 57 | lineWidth: 1, 58 | }); 59 | 60 | chart.render(); 61 | } 62 | }, [data, isTpl]); 63 | return ( 64 |
65 |
66 | {title} 67 |
68 | {isTpl ? dooring chart : } 69 |
70 | ); 71 | }; 72 | 73 | export default memo(XLine); 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | .umi/ 107 | .umi-production/ -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/Tab/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | import { Tabs } from 'zarm'; 3 | import styles from './index.less'; 4 | import { ITabConfig } from './schema'; 5 | 6 | const { Panel } = Tabs; 7 | 8 | const XTab = (props: ITabConfig) => { 9 | const { tabs = ['分类一', '分类二'], activeColor, color, fontSize, sourceData } = props; 10 | 11 | const tabWrapRef = useRef(null); 12 | 13 | useEffect(() => { 14 | if (tabWrapRef.current) { 15 | let res = tabWrapRef.current.querySelector('.za-tabs__line') as HTMLElement; 16 | if (res) { 17 | res.style.backgroundColor = activeColor; 18 | } 19 | } 20 | }, [activeColor]); 21 | 22 | return ( 23 |
24 | { 27 | console.log(i); 28 | }} 29 | > 30 | {tabs.map((item, i) => { 31 | return ( 32 | 33 |
34 | {sourceData 35 | .filter(item => item.type === i) 36 | .map((item, i) => { 37 | return ( 38 | 53 | ); 54 | })} 55 |
56 |
57 | ); 58 | })} 59 |
60 |
61 | ); 62 | }; 63 | 64 | export default XTab; 65 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/LongText/schema.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IColorConfigType, 3 | INumberConfigType, 4 | ISelectConfigType, 5 | ITextAreaConfigType, 6 | TColorDefaultType, 7 | TNumberDefaultType, 8 | TSelectDefaultType, 9 | TTextAreaDefaultType, 10 | } from '@/components/PanelComponents/FormEditor/types'; 11 | export type TLongTextSelectKeyType = 'left' | 'center' | 'right'; 12 | 13 | export type TLongTextEditData = Array< 14 | | ITextAreaConfigType 15 | | IColorConfigType 16 | | INumberConfigType 17 | | ISelectConfigType 18 | >; 19 | export interface ILongTextConfig { 20 | text: TTextAreaDefaultType; 21 | color: TColorDefaultType; 22 | fontSize: TNumberDefaultType; 23 | indent: TNumberDefaultType; 24 | lineHeight: TNumberDefaultType; 25 | textAlign: TSelectDefaultType; 26 | } 27 | 28 | export interface ILongTextSchema { 29 | editData: TLongTextEditData; 30 | config: ILongTextConfig; 31 | } 32 | 33 | const LongText: ILongTextSchema = { 34 | editData: [ 35 | { 36 | key: 'text', 37 | name: '文字', 38 | type: 'TextArea', 39 | }, 40 | { 41 | key: 'color', 42 | name: '标题颜色', 43 | type: 'Color', 44 | }, 45 | { 46 | key: 'fontSize', 47 | name: '字体大小', 48 | type: 'Number', 49 | }, 50 | { 51 | key: 'indent', 52 | name: '首行缩进', 53 | type: 'Number', 54 | range: [0, 100], 55 | }, 56 | { 57 | key: 'textAlign', 58 | name: '对齐方式', 59 | type: 'Select', 60 | range: [ 61 | { 62 | key: 'left', 63 | text: '左对齐', 64 | }, 65 | { 66 | key: 'center', 67 | text: '居中对齐', 68 | }, 69 | { 70 | key: 'right', 71 | text: '右对齐', 72 | }, 73 | ], 74 | }, 75 | { 76 | key: 'lineHeight', 77 | name: '行高', 78 | type: 'Number', 79 | step: 0.1, 80 | }, 81 | ], 82 | config: { 83 | text: '我是长文本有一段故事,dooring可视化编辑器无限可能,赶快来体验吧,骚年们,奥利给~', 84 | color: 'rgba(60,60,60,1)', 85 | fontSize: 14, 86 | indent: 20, 87 | lineHeight: 1.8, 88 | textAlign: 'left', 89 | }, 90 | }; 91 | 92 | export default LongText; 93 | -------------------------------------------------------------------------------- /src/pages/editor/preview.js: -------------------------------------------------------------------------------- 1 | import React, { memo, useEffect, useState } from 'react'; 2 | import GridLayout from 'react-grid-layout'; 3 | import DynamicEngine from 'components/DynamicEngine'; 4 | import req from '@/utils/req'; 5 | import styles from './index.less'; 6 | 7 | const isMac = navigator.platform.indexOf('Mac') === 0; 8 | 9 | const pcStyle = { 10 | width: isMac ? 395 : 412, 11 | margin: '20px auto', 12 | border: '10px solid #000', 13 | borderRadius: '20px', 14 | height: '684px', 15 | overflow: 'auto', 16 | }; 17 | 18 | const PreviewPage = memo(props => { 19 | const [pointData, setPointData] = useState(() => { 20 | let pointDataStr = localStorage.getItem('pointData'); 21 | let points; 22 | 23 | try { 24 | points = JSON.parse(pointDataStr) || []; 25 | } catch (err) { 26 | points = []; 27 | } 28 | return points.map(item => ({ 29 | ...item, 30 | point: { ...item.point, isDraggable: false, isResizable: false }, 31 | })); 32 | }); 33 | 34 | const vw = window.innerWidth; 35 | 36 | useEffect(() => { 37 | const { tid } = props.location.query; 38 | let timer = null; 39 | req 40 | .get('/visible/preview/get', { params: { tid } }) 41 | .then(res => { 42 | setPointData( 43 | res.map(item => ({ 44 | ...item, 45 | point: { ...item.point, isDraggable: false, isResizable: false }, 46 | })), 47 | ); 48 | }) 49 | .catch(err => { 50 | timer = setTimeout(() => { 51 | window.close(); 52 | }, 3000); 53 | }); 54 | return () => { 55 | window.clearTimeout(timer); 56 | }; 57 | }, []); 58 | 59 | return ( 60 |
800 ? pcStyle : {}}> 61 | 800 ? 375 : vw} 66 | margin={[0, 0]} 67 | id="xx" 68 | > 69 | {pointData.map(value => ( 70 |
71 | 72 |
73 | ))} 74 |
75 |
76 | ); 77 | }); 78 | 79 | export default PreviewPage; 80 | -------------------------------------------------------------------------------- /src/components/BasicShop/VisualComponents/Area/index.tsx: -------------------------------------------------------------------------------- 1 | import { Chart } from '@antv/f2'; 2 | import React, { memo, useEffect, useRef } from 'react'; 3 | // import { uuid } from 'utils/tool'; 4 | import AreaImg from '@/assets/area.png'; 5 | 6 | import styles from './index.less'; 7 | import { IChartConfig } from './schema'; 8 | 9 | interface XChartProps extends IChartConfig { 10 | isTpl: boolean; 11 | } 12 | 13 | const XLine = (props: XChartProps) => { 14 | const { isTpl, data, color, size, paddingTop, title } = props; 15 | const chartRef = useRef(null); 16 | useEffect(() => { 17 | if (!isTpl) { 18 | const chart = new Chart({ 19 | el: chartRef.current || undefined, 20 | pixelRatio: window.devicePixelRatio, // 指定分辨率 21 | }); 22 | 23 | // step 2: 处理数据 24 | const dataX = data.map(item => ({ ...item, value: Number(item.value), a: '1' })); 25 | 26 | // Step 2: 载入数据源 27 | chart.source(dataX, { 28 | percent: { 29 | formatter: function formatter(val) { 30 | return val * 100 + '%'; 31 | }, 32 | }, 33 | }); 34 | 35 | chart.tooltip({ 36 | showCrosshairs: true, 37 | }); 38 | 39 | chart.scale({ 40 | name: { 41 | range: [0, 1], 42 | }, 43 | value: { 44 | tickCount: 5, 45 | min: 0, 46 | }, 47 | }); 48 | 49 | chart.axis('name', { 50 | label: function label(text, index, total) { 51 | const textCfg: any = {}; 52 | if (index === 0) { 53 | textCfg.textAlign = 'left'; 54 | } else if (index === total - 1) { 55 | textCfg.textAlign = 'right'; 56 | } 57 | return textCfg; 58 | }, 59 | }); 60 | 61 | chart.area().position('name*value'); 62 | chart.line().position('name*value'); 63 | chart.render(); 64 | } 65 | }, [data, isTpl]); 66 | return ( 67 |
68 |
69 | {title} 70 |
71 | {isTpl ? dooring chart : } 72 |
73 | ); 74 | }; 75 | 76 | export default memo(XLine); 77 | -------------------------------------------------------------------------------- /src/components/BasicShop/VisualComponents/XProgress/schema.ts: -------------------------------------------------------------------------------- 1 | import { 2 | INumberConfigType, 3 | IRadioConfigType, 4 | ISelectConfigType, 5 | TNumberDefaultType, 6 | TRadioDefaultType, 7 | TSelectDefaultType, 8 | } from '@/components/PanelComponents/FormEditor/types'; 9 | export type TXProgressSelectKeyType = 'success' | 'warning' | 'danger'; 10 | export type TXProgressRadiotKeyType = 'circle' | 'line' | 'semi-circle'; 11 | export type TXProgressEditData = Array< 12 | | ISelectConfigType 13 | | IRadioConfigType 14 | | INumberConfigType 15 | >; 16 | export interface IXProgressConfig { 17 | theme: TSelectDefaultType; 18 | shape: TRadioDefaultType; 19 | size: TNumberDefaultType; 20 | percent: TNumberDefaultType; 21 | strokeWidth: TNumberDefaultType; 22 | } 23 | 24 | export interface IXProgressSchema { 25 | editData: TXProgressEditData; 26 | config: IXProgressConfig; 27 | } 28 | 29 | const XProgress: IXProgressSchema = { 30 | editData: [ 31 | { 32 | key: 'theme', 33 | name: '主题', 34 | type: 'Select', 35 | range: [ 36 | { 37 | key: 'success', 38 | text: '成功', 39 | }, 40 | { 41 | key: 'warning', 42 | text: '警告', 43 | }, 44 | { 45 | key: 'danger', 46 | text: '危险', 47 | }, 48 | ], 49 | }, 50 | { 51 | key: 'shape', 52 | name: '形状', 53 | type: 'Radio', 54 | range: [ 55 | { 56 | key: 'circle', 57 | text: '圆形', 58 | }, 59 | { 60 | key: 'line', 61 | text: '线形', 62 | }, 63 | { 64 | key: 'semi-circle', 65 | text: '半圆形', 66 | }, 67 | ], 68 | }, 69 | { 70 | key: 'size', 71 | name: '大小', 72 | type: 'Number', 73 | }, 74 | { 75 | key: 'percent', 76 | name: '进度值', 77 | type: 'Number', 78 | range: [0, 100], 79 | }, 80 | { 81 | key: 'strokeWidth', 82 | name: '线条粗细', 83 | type: 'Number', 84 | }, 85 | ], 86 | config: { 87 | theme: 'success', 88 | shape: 'circle', 89 | size: 200, 90 | percent: 30, 91 | strokeWidth: 10, 92 | }, 93 | }; 94 | 95 | export default XProgress; 96 | -------------------------------------------------------------------------------- /src/pages/login/index.tsx: -------------------------------------------------------------------------------- 1 | import { Form, Input, Button } from 'antd'; 2 | import http from '@/utils/req'; 3 | import { history } from 'umi'; 4 | import styles from './index.less'; 5 | import React from 'react'; 6 | import { ValidateErrorEntity, Store } from 'rc-field-form/lib/interface'; 7 | import { RouteComponentProps } from 'react-router-dom'; 8 | const layout = { 9 | labelCol: { span: 6 }, 10 | wrapperCol: { span: 16 }, 11 | }; 12 | const tailLayout = { 13 | wrapperCol: { offset: 6, span: 16 }, 14 | }; 15 | 16 | const Login = (props: RouteComponentProps) => { 17 | const onFinish = (values: Store) => { 18 | http 19 | .post('/login', { ...values }) 20 | .then(res => { 21 | localStorage.setItem('token', res.token); 22 | localStorage.setItem('user', values.username); 23 | history.push('/'); 24 | }); 25 | }; 26 | 27 | const onFinishFailed = (errorInfo: ValidateErrorEntity) => { 28 | console.log('Failed:', errorInfo); 29 | }; 30 | 31 | return ( 32 |
33 |
41 |
42 | Doring开放平台 43 | 登录 44 |
45 | 50 | 51 | 52 | 53 | 58 | 59 | 60 | 61 | 62 | 65 | 66 | 67 | 70 | 71 |
72 |
73 | ); 74 | }; 75 | 76 | export default Login; 77 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/Carousel/schema.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IDataListConfigType, 3 | IRadioConfigType, 4 | ISwitchConfigType, 5 | TDataListDefaultType, 6 | TRadioDefaultType, 7 | TSwitchDefaultType, 8 | } from '@/components/PanelComponents/FormEditor/types'; 9 | 10 | export type CarouselDirectionKeyType = 'down' | 'left'; 11 | 12 | export type TCarouselEditData = Array< 13 | IRadioConfigType | ISwitchConfigType | IDataListConfigType 14 | >; 15 | export interface ICarouselConfig { 16 | direction: TRadioDefaultType; 17 | swipeable: TSwitchDefaultType; 18 | autoPlay: TSwitchDefaultType; 19 | imgList: TDataListDefaultType; 20 | tplImg: string; 21 | } 22 | 23 | export interface ICarouselSchema { 24 | editData: TCarouselEditData; 25 | config: ICarouselConfig; 26 | } 27 | 28 | const Carousel: ICarouselSchema = { 29 | editData: [ 30 | { 31 | key: 'direction', 32 | name: '方向', 33 | type: 'Radio', 34 | range: [ 35 | { 36 | key: 'down', 37 | text: '从上到下', 38 | }, 39 | { 40 | key: 'left', 41 | text: '从左到右', 42 | }, 43 | ], 44 | }, 45 | { 46 | key: 'swipeable', 47 | name: '是否可拖拽', 48 | type: 'Switch', 49 | }, 50 | { 51 | key: 'autoPlay', 52 | name: '是否自动播放', 53 | type: 'Switch', 54 | }, 55 | { 56 | key: 'imgList', 57 | name: '图片列表', 58 | type: 'DataList', 59 | }, 60 | ], 61 | config: { 62 | direction: 'left', 63 | swipeable: false, 64 | autoPlay: false, 65 | imgList: [ 66 | { 67 | id: '1', 68 | title: '趣谈小课1', 69 | desc: '致力于打造优质小课程', 70 | link: 'xxxxx', 71 | imgUrl: [ 72 | { 73 | uid: '001', 74 | name: 'image.png', 75 | status: 'done', 76 | url: 'http://io.nainor.com/uploads/1_1740bd7c3dc.png', 77 | }, 78 | ], 79 | }, 80 | { 81 | id: '2', 82 | title: '趣谈小课1', 83 | desc: '致力于打造优质小课程', 84 | link: 'xxxxx', 85 | imgUrl: [ 86 | { 87 | uid: '001', 88 | name: 'image.png', 89 | status: 'done', 90 | url: 'http://io.nainor.com/uploads/2_1740bd8d525.png', 91 | }, 92 | ], 93 | }, 94 | ], 95 | tplImg: 'http://io.nainor.com/uploads/carousal_17442e1420f.png', 96 | }, 97 | }; 98 | export default Carousel; 99 | -------------------------------------------------------------------------------- /src/components/PanelComponents/MutiText/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useState, useEffect } from 'react'; 2 | import { Input, Button, Popconfirm } from 'antd'; 3 | import { MinusCircleOutlined } from '@ant-design/icons'; 4 | import styles from './index.less'; 5 | import { TMutiTextDefaultType } from '../FormEditor/types'; 6 | 7 | type MultiTextProps = { 8 | onChange?: (v: TMutiTextDefaultType) => void; 9 | value?: TMutiTextDefaultType; 10 | }; 11 | 12 | export default memo(function MutiText(props: MultiTextProps) { 13 | const { value, onChange } = props; 14 | const [valueList, setValueList] = useState(value || []); 15 | const handleAdd = () => { 16 | setValueList(prev => { 17 | return [...prev, '新增项']; 18 | }); 19 | }; 20 | 21 | const handleDel = (index: number) => { 22 | setValueList(prev => { 23 | let newList = prev.filter((_item, i) => i !== index); 24 | onChange && onChange(newList); 25 | return newList; 26 | }); 27 | }; 28 | 29 | const handleChange = (index: number, e: React.ChangeEvent) => { 30 | const { value } = e.target; 31 | setValueList(prev => { 32 | let newList = prev.map((item, i) => (i === index ? value : item)); 33 | onChange && onChange(newList); 34 | return newList; 35 | }); 36 | }; 37 | 38 | useEffect(() => { 39 | window['currentCates'] = valueList; 40 | return () => { 41 | window['currentCates'] = null; 42 | }; 43 | }, [valueList]); 44 | 45 | return ( 46 |
47 | {valueList.length ? ( 48 | valueList.map((item, i) => { 49 | return ( 50 |
51 | handleChange(i, e)} /> 52 | handleDel(i)} 55 | placement="leftTop" 56 | okText="确定" 57 | cancelText="取消" 58 | > 59 | 60 | 61 | 62 | 63 |
64 | ); 65 | }) 66 | ) : ( 67 |
68 | 69 |
70 | )} 71 | {valueList.length < 3 && ( 72 |
73 | 76 |
77 | )} 78 |
79 | ); 80 | }); 81 | -------------------------------------------------------------------------------- /src/components/PanelComponents/Color/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SketchPicker, ColorResult } from 'react-color'; 3 | import { rgba2Obj } from '@/utils/tool'; 4 | // import styles from './index.less' 5 | 6 | export type ColorConfigType = string; 7 | 8 | //value 初始值传来,onchange item给的回调 9 | interface ColorProps { 10 | value?: ColorConfigType; 11 | onChange?: (v: ColorConfigType) => void; 12 | } 13 | 14 | class colorPicker extends React.Component { 15 | state = { 16 | displayColorPicker: false, 17 | color: rgba2Obj(this.props.value), 18 | }; 19 | 20 | handleClick = () => { 21 | this.setState({ displayColorPicker: !this.state.displayColorPicker }); 22 | }; 23 | 24 | handleClose = () => { 25 | this.setState({ displayColorPicker: false }); 26 | }; 27 | 28 | handleChange = (color: ColorResult) => { 29 | this.setState({ color: color.rgb }); 30 | this.props.onChange && 31 | this.props.onChange(`rgba(${color.rgb.r},${color.rgb.g},${color.rgb.b},${color.rgb.a})`); 32 | }; 33 | 34 | render() { 35 | return ( 36 |
37 |
48 |
56 |
57 | {this.state.displayColorPicker ? ( 58 | 59 |
65 | 66 |
67 |
78 | 79 | ) : null} 80 |
81 | ); 82 | } 83 | } 84 | 85 | export default colorPicker; 86 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/Form/schema.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IColorConfigType, 3 | IFormItemsConfigType, 4 | INumberConfigType, 5 | ITextConfigType, 6 | TColorDefaultType, 7 | TFormItemsDefaultType, 8 | TNumberDefaultType, 9 | TTextDefaultType, 10 | } from '@/components/PanelComponents/FormEditor/types'; 11 | 12 | export type TFormEditData = Array< 13 | ITextConfigType | INumberConfigType | IColorConfigType | ITextConfigType | IFormItemsConfigType 14 | >; 15 | export interface IFormConfig { 16 | title: TTextDefaultType; 17 | fontSize: TNumberDefaultType; 18 | titColor: TColorDefaultType; 19 | bgColor: TColorDefaultType; 20 | btnColor: TColorDefaultType; 21 | btnTextColor: TColorDefaultType; 22 | api: TTextDefaultType; 23 | formControls: TFormItemsDefaultType; 24 | } 25 | 26 | export interface IFormSchema { 27 | editData: TFormEditData; 28 | config: IFormConfig; 29 | } 30 | 31 | const Form: IFormSchema = { 32 | editData: [ 33 | { 34 | key: 'title', 35 | name: '标题', 36 | type: 'Text', 37 | }, 38 | { 39 | key: 'fontSize', 40 | name: '标题大小', 41 | type: 'Number', 42 | }, 43 | { 44 | key: 'titColor', 45 | name: '标题颜色', 46 | type: 'Color', 47 | }, 48 | { 49 | key: 'bgColor', 50 | name: '背景色', 51 | type: 'Color', 52 | }, 53 | { 54 | key: 'btnColor', 55 | name: '按钮颜色', 56 | type: 'Color', 57 | }, 58 | { 59 | key: 'btnTextColor', 60 | name: '按钮文本颜色', 61 | type: 'Color', 62 | }, 63 | { 64 | key: 'api', 65 | name: '表单Api地址', 66 | type: 'Text', 67 | }, 68 | { 69 | key: 'formControls', 70 | name: '表单控件', 71 | type: 'FormItems', 72 | }, 73 | ], 74 | config: { 75 | title: '表单定制组件', 76 | fontSize: 18, 77 | titColor: 'rgba(60,60,60,1)', 78 | bgColor: 'rgba(255,255,255,1)', 79 | btnColor: 'rgba(129,173,173,1)', 80 | btnTextColor: 'rgba(255,255,255,1)', 81 | api: '', 82 | formControls: [ 83 | { 84 | id: '1', 85 | type: 'Text', 86 | label: '姓名', 87 | placeholder: '请输入姓名', 88 | }, 89 | { 90 | id: '2', 91 | type: 'Number', 92 | label: '年龄', 93 | placeholder: ' 请输入年龄', 94 | }, 95 | { 96 | id: '4', 97 | type: 'MySelect', 98 | label: '爱好', 99 | options: [ 100 | { label: '选项一', value: '1' }, 101 | { label: '选项二', value: '2' }, 102 | { label: '选项三', value: '3' }, 103 | ], 104 | }, 105 | ], 106 | }, 107 | }; 108 | export default Form; 109 | -------------------------------------------------------------------------------- /src/pages/home/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Tabs, message } from 'antd'; 3 | import { history } from 'umi'; 4 | import { 5 | MobileOutlined, 6 | ConsoleSqlOutlined, 7 | GithubOutlined, 8 | CodeOutlined, 9 | } from '@ant-design/icons'; 10 | import styles from './index.less'; 11 | 12 | const { TabPane } = Tabs; 13 | 14 | const Home = () => { 15 | const handleGo = (type: string) => { 16 | if (type === 'H5') { 17 | history.push('/editor?tid=123456'); 18 | } else if (type === 'PC') { 19 | message.error('该功能暂未开放, 敬请关注...'); 20 | } else { 21 | history.push('/ide'); 22 | } 23 | }; 24 | return ( 25 |
26 |
27 | 28 | 31 | 32 | 我的H5 33 | 34 | } 35 | key="1" 36 | > 37 | 正在开发... 38 | 39 | 42 | 43 | 我的大屏 44 | 45 | } 46 | key="2" 47 | > 48 | 正在开发... 49 | 50 | 51 |
52 |
53 |
54 |
55 | 56 | H5-Dooring 57 | 58 | 可视化编辑器 59 |
60 |

61 | H5-Dooring是一款功能强大,开源免费的H5可视化页面配置解决方案, 62 | 致力于提供一套简单方便、专业可靠、无限可能的H5落地页最佳实践。 技术栈以react为主, 63 | 后台采用nodejs开发。 64 |

65 |
66 |
67 |
handleGo('H5')}> 68 | 69 |
制作H5页面
70 |
71 |
handleGo('online')}> 72 | 73 |
在线编程
74 |
75 |
handleGo('PC')}> 76 | 77 |
制作可视化大屏
78 |
79 |
80 |
81 |
82 | 83 | 84 | 85 |

Welcome to H5-Dooring 👋

86 |
87 |
88 |
89 |
90 | ); 91 | }; 92 | 93 | export default Home; 94 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/List/schema.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IColorConfigType, 3 | IDataListConfigType, 4 | INumberConfigType, 5 | ISelectConfigType, 6 | TColorDefaultType, 7 | TDataListDefaultType, 8 | TNumberDefaultType, 9 | TSelectDefaultType, 10 | } from '@/components/PanelComponents/FormEditor/types'; 11 | export type TListSelectKeyType = '60' | '80' | '100' | '120' | '150'; 12 | export type TListEditData = Array< 13 | IColorConfigType | IDataListConfigType | INumberConfigType | ISelectConfigType 14 | >; 15 | export interface IListConfig { 16 | sourceData: TDataListDefaultType; 17 | round: TNumberDefaultType; 18 | imgSize: TSelectDefaultType; 19 | fontSize: TNumberDefaultType; 20 | color: TColorDefaultType; 21 | } 22 | 23 | export interface IListSchema { 24 | editData: TListEditData; 25 | config: IListConfig; 26 | } 27 | 28 | const List: IListSchema = { 29 | editData: [ 30 | { 31 | key: 'sourceData', 32 | name: '数据源', 33 | type: 'DataList', 34 | }, 35 | { 36 | key: 'round', 37 | name: '圆角', 38 | type: 'Number', 39 | }, 40 | { 41 | key: 'imgSize', 42 | name: '图片大小', 43 | type: 'Select', 44 | range: [ 45 | { 46 | key: '60', 47 | text: '60 x 60', 48 | }, 49 | { 50 | key: '80', 51 | text: '80 x 80', 52 | }, 53 | { 54 | key: '100', 55 | text: '100 x 100', 56 | }, 57 | { 58 | key: '120', 59 | text: '120 x 120', 60 | }, 61 | { 62 | key: '150', 63 | text: '150 x 150', 64 | }, 65 | ], 66 | }, 67 | { 68 | key: 'fontSize', 69 | name: '文字大小', 70 | type: 'Number', 71 | }, 72 | { 73 | key: 'color', 74 | name: '文字颜色', 75 | type: 'Color', 76 | }, 77 | ], 78 | config: { 79 | sourceData: [ 80 | { 81 | id: '1', 82 | title: '趣谈小课', 83 | desc: '致力于打造优质小课程', 84 | link: 'xxxxx', 85 | imgUrl: [ 86 | { 87 | uid: '001', 88 | name: 'image.png', 89 | status: 'done', 90 | url: 'http://io.nainor.com/uploads/1_1740c6fbcd9.png', 91 | }, 92 | ], 93 | }, 94 | { 95 | id: '2', 96 | title: '趣谈小课', 97 | desc: '致力于打造优质小课程', 98 | link: 'xxxxx', 99 | imgUrl: [ 100 | { 101 | uid: '002', 102 | name: 'image.png', 103 | status: 'done', 104 | url: 'http://io.nainor.com/uploads/1_1740c6fbcd9.png', 105 | }, 106 | ], 107 | }, 108 | ], 109 | round: 0, 110 | imgSize: '80', 111 | fontSize: 16, 112 | color: 'rgba(153,153,153,1)', 113 | }, 114 | }; 115 | 116 | export default List; 117 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/Tab/schema.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IColorConfigType, 3 | IDataListConfigType, 4 | IMutiTextConfigType, 5 | INumberConfigType, 6 | TColorDefaultType, 7 | TDataListDefaultType, 8 | TMutiTextDefaultType, 9 | TNumberDefaultType, 10 | } from '@/components/PanelComponents/FormEditor/types'; 11 | 12 | export type TTabEditData = Array< 13 | IMutiTextConfigType | IColorConfigType | INumberConfigType | IDataListConfigType 14 | >; 15 | export interface ITabConfig { 16 | tabs: TMutiTextDefaultType; 17 | color: TColorDefaultType; 18 | activeColor: TColorDefaultType; 19 | fontSize: TNumberDefaultType; 20 | imgSize: TNumberDefaultType; 21 | sourceData: TDataListDefaultType; 22 | } 23 | 24 | export interface ITabSchema { 25 | editData: TTabEditData; 26 | config: ITabConfig; 27 | } 28 | 29 | const Tab: ITabSchema = { 30 | editData: [ 31 | { 32 | key: 'tabs', 33 | name: '项目类别', 34 | type: 'MutiText', 35 | }, 36 | { 37 | key: 'activeColor', 38 | name: '激活颜色', 39 | type: 'Color', 40 | }, 41 | { 42 | key: 'color', 43 | name: '文字颜色', 44 | type: 'Color', 45 | }, 46 | { 47 | key: 'fontSize', 48 | name: '文字大小', 49 | type: 'Number', 50 | }, 51 | { 52 | key: 'imgSize', 53 | name: '图片大小', 54 | type: 'Number', 55 | }, 56 | { 57 | key: 'sourceData', 58 | name: '数据源', 59 | type: 'DataList', 60 | }, 61 | ], 62 | config: { 63 | tabs: ['类别一', '类别二'], 64 | color: 'rgba(153,153,153,1)', 65 | activeColor: 'rgba(0,102,204,1)', 66 | fontSize: 16, 67 | imgSize: 100, 68 | sourceData: [ 69 | { 70 | id: '1', 71 | title: '趣谈小课1', 72 | desc: '致力于打造优质小课程', 73 | link: 'xxxxx', 74 | type: 0, 75 | imgUrl: [ 76 | { 77 | uid: '001', 78 | name: 'image.png', 79 | status: 'done', 80 | url: 'http://io.nainor.com/uploads/1_1740c6fbcd9.png', 81 | }, 82 | ], 83 | }, 84 | { 85 | id: '2', 86 | title: '趣谈小课2', 87 | desc: '致力于打造优质小课程', 88 | link: 'xxxxx', 89 | type: 0, 90 | imgUrl: [ 91 | { 92 | uid: '001', 93 | name: 'image.png', 94 | status: 'done', 95 | url: 'http://io.nainor.com/uploads/2_1740c7033a9.png', 96 | }, 97 | ], 98 | }, 99 | { 100 | id: '3', 101 | title: '趣谈小课3', 102 | desc: '致力于打造优质小课程', 103 | link: 'xxxxx', 104 | type: 1, 105 | imgUrl: [ 106 | { 107 | uid: '001', 108 | name: 'image.png', 109 | status: 'done', 110 | url: 'http://io.nainor.com/uploads/1_1740c6fbcd9.png', 111 | }, 112 | ], 113 | }, 114 | ], 115 | }, 116 | }; 117 | 118 | export default Tab; 119 | -------------------------------------------------------------------------------- /src/components/BasicShop/VisualComponents/Pie/index.tsx: -------------------------------------------------------------------------------- 1 | import { Chart } from '@antv/f2'; 2 | import React, { memo, useEffect, useRef } from 'react'; 3 | // import { uuid } from 'utils/tool'; 4 | import PieImg from '@/assets/pie.png'; 5 | 6 | import styles from './index.less'; 7 | import { IChartConfig } from './schema'; 8 | 9 | interface XChartProps extends IChartConfig { 10 | isTpl: boolean; 11 | } 12 | 13 | interface DataMap { 14 | [name: string]: number | string; 15 | } 16 | 17 | const XLine = (props: XChartProps) => { 18 | const { isTpl, data, color, size, paddingTop, title } = props; 19 | const chartRef = useRef(null); 20 | useEffect(() => { 21 | if (!isTpl) { 22 | const chart = new Chart({ 23 | el: chartRef.current || undefined, 24 | pixelRatio: window.devicePixelRatio, // 指定分辨率 25 | }); 26 | 27 | // step 2: 处理数据 28 | const dataX = data.map(item => ({ ...item, value: Number(item.value), a: '1' })); 29 | 30 | // Step 2: 载入数据源 31 | chart.source(dataX, { 32 | percent: { 33 | formatter: function formatter(val) { 34 | return val * 100 + '%'; 35 | }, 36 | }, 37 | }); 38 | 39 | // 获取数据的map类型,用以展示图例说明 40 | const dataMap: DataMap = dataX.reduce((prev: any, cur) => { 41 | return prev.name 42 | ? { [prev.name]: prev.value, ...{ [cur.name]: cur.value } } 43 | : { ...prev, ...{ [cur.name]: cur.value } }; 44 | }); 45 | 46 | chart.legend({ 47 | position: 'right', 48 | itemFormatter: function itemFormatter(val) { 49 | return val + ' ' + dataMap[val] + '%'; 50 | }, 51 | }); 52 | chart.tooltip(false); 53 | chart.coord('polar', { 54 | transposed: true, 55 | radius: 0.85, 56 | }); 57 | chart.axis(false); 58 | chart 59 | .interval() 60 | .position('a*value') 61 | .color('name', [ 62 | '#1890FF', 63 | '#13C2C2', 64 | '#2FC25B', 65 | '#FACC14', 66 | '#00CC99', 67 | '#CC3366', 68 | '#CC6600', 69 | '#CC66CC', 70 | '#FF3366', 71 | '#0066CC', 72 | ]) 73 | .adjust('stack') 74 | .style({ 75 | lineWidth: 1, 76 | stroke: '#fff', 77 | lineJoin: 'round', 78 | lineCap: 'round', 79 | }) 80 | .animate({ 81 | appear: { 82 | duration: 1200, 83 | easing: 'bounceOut', 84 | }, 85 | }); 86 | 87 | chart.render(); 88 | } 89 | }, [data, isTpl]); 90 | return ( 91 |
92 |
93 | {title} 94 |
95 | {isTpl ? dooring chart : } 96 |
97 | ); 98 | }; 99 | 100 | export default memo(XLine); 101 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/Form/BaseForm.tsx: -------------------------------------------------------------------------------- 1 | import { Input, Cell, DateSelect, Radio, Select } from 'zarm'; 2 | import styles from './baseForm.less'; 3 | import React from 'react'; 4 | import { 5 | baseFormDateTpl, 6 | baseFormMyRadioTpl, 7 | baseFormMySelectTpl, 8 | baseFormNumberTpl, 9 | baseFormTextAreaTpl, 10 | baseFormTextTpl, 11 | baseFormUnionType, 12 | } from '@/components/PanelComponents/FormEditor/types'; 13 | // 维护表单控件, 提高form渲染性能 14 | 15 | type TBaseForm = { 16 | [key in baseFormUnionType]: any; 17 | }; 18 | 19 | const BaseForm: TBaseForm = { 20 | Text: (props: baseFormTextTpl & { onChange: (v: string | undefined) => void }) => { 21 | const { label, placeholder, onChange } = props; 22 | return ( 23 | 24 | 25 | 26 | ); 27 | }, 28 | Textarea: (props: baseFormTextAreaTpl & { onChange: (v: string | undefined) => void }) => { 29 | const { label, placeholder, onChange } = props; 30 | return ( 31 | 32 | 40 | 41 | ); 42 | }, 43 | Number: (props: baseFormNumberTpl & { onChange: (v: string | undefined | number) => void }) => { 44 | const { label, placeholder, onChange } = props; 45 | return ( 46 | 47 | 48 | 49 | ); 50 | }, 51 | MyRadio: (props: baseFormMyRadioTpl & { onChange: (v: string | undefined | number) => void }) => { 52 | const { label, options, onChange } = props; 53 | return ( 54 |
55 |
{label}
56 | 57 | 58 | {options.map((item, i) => { 59 | return ( 60 | 61 | {item.label} 62 | 63 | ); 64 | })} 65 | 66 | 67 |
68 | ); 69 | }, 70 | Date: (props: baseFormDateTpl & { onChange: (v: Date) => void }) => { 71 | const { label, placeholder, onChange } = props; 72 | return ( 73 | 74 | 81 | 82 | ); 83 | }, 84 | MySelect: ( 85 | props: baseFormMySelectTpl & { onChange: ((v: Record) => void) | undefined }, 86 | ) => { 87 | const { label, options, onChange } = props; 88 | return ( 89 | 90 | 68 | 69 | } 70 | {!!item.label && ( 71 | 76 | 77 | 78 | )} 79 | {!!item.placeholder && ( 80 | 81 | 82 | 83 | )} 84 | {!!item.options && ( 85 | 90 | 105 | 106 | )} 107 | 108 | 109 | )} 110 | 111 | ); 112 | }; 113 | 114 | export default memo(EditorModal); 115 | -------------------------------------------------------------------------------- /src/components/PanelComponents/DataList/editorModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useEffect, FC } from 'react'; 2 | import { Form, Select, Input, Modal } from 'antd'; 3 | import Upload from '../Upload'; 4 | import { Store } from 'antd/lib/form/interface'; 5 | import { TDataListDefaultTypeItem } from '../FormEditor/types'; 6 | // import styles from './index.less'; 7 | const normFile = (e: any) => { 8 | if (Array.isArray(e)) { 9 | return e; 10 | } 11 | return e && e.fileList; 12 | }; 13 | 14 | const { Option } = Select; 15 | 16 | const formItemLayout = { 17 | labelCol: { span: 6 }, 18 | wrapperCol: { span: 14 }, 19 | }; 20 | 21 | export type EditorModalProps = { 22 | visible: boolean; 23 | onCancel: ((e: React.MouseEvent) => void) | undefined; 24 | item?: TDataListDefaultTypeItem; 25 | onSave: Function; 26 | }; 27 | 28 | const EditorModal: FC = props => { 29 | const { item, onSave, visible, onCancel } = props; 30 | const onFinish = (values: Store) => { 31 | onSave && onSave(values); 32 | }; 33 | const handleOk = () => { 34 | form 35 | .validateFields() 36 | .then(values => { 37 | console.log(values); 38 | if (item) { 39 | values.id = item.id; 40 | onSave && onSave(values); 41 | } 42 | }) 43 | .catch(err => { 44 | console.log(err); 45 | }); 46 | }; 47 | 48 | const [form] = Form.useForm(); 49 | 50 | useEffect(() => { 51 | return () => { 52 | form.resetFields(); 53 | }; 54 | }, [item]); 55 | 56 | return ( 57 | <> 58 | {!!item && ( 59 | 67 |
74 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | {!!window['currentCates'] && ( 88 | 93 | 102 | 103 | )} 104 | 105 | 111 | 112 | 113 |
114 |
115 | )} 116 | 117 | ); 118 | }; 119 | 120 | export default memo(EditorModal); 121 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "h5-dooring", 3 | "version": "1.3.0", 4 | "description": "H5-Dooring是一款功能强大,开源免费的H5可视化页面配置解决方案,致力于提供一套简单方便、专业可靠、无限可能的H5落地页最佳实践。技术栈以react为主, 后台采用nodejs开发。", 5 | "private": false, 6 | "author": { 7 | "name": "徐小夕", 8 | "email": "xujiang156@qq.com", 9 | "url": "http://io.nainor.com/h5_visible" 10 | }, 11 | "keywords": [ 12 | "h5 editor", 13 | "h5", 14 | "react", 15 | "antd", 16 | "react-dnd", 17 | "web visible" 18 | ], 19 | "contributors": [ 20 | "徐小夕 (https://github.com/MrXujiang))", 21 | "yehuozhili (https://github.com/yehuozhili))" 22 | ], 23 | "scripts": { 24 | "start": "umi dev", 25 | "build": "umi build", 26 | "server": "node server.js", 27 | "postinstall": "umi generate tmp", 28 | "prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'", 29 | "test": "umi-test", 30 | "test:coverage": "umi-test --coverage" 31 | }, 32 | "gitHooks": { 33 | "pre-commit": "lint-staged" 34 | }, 35 | "lint-staged": { 36 | "*.{js,jsx,less,md,json}": [ 37 | "prettier --write" 38 | ], 39 | "*.ts?(x)": [ 40 | "prettier --parser=typescript --write" 41 | ] 42 | }, 43 | "homepage": "http://io.nainor.com/h5_visible", 44 | "repository": { 45 | "type": "git", 46 | "url": "git+https://github.com/MrXujiang/h5-Dooring.git" 47 | }, 48 | "bugs": { 49 | "url": "https://github.com/MrXujiang/h5-Dooring/issues" 50 | }, 51 | "dependencies": { 52 | "@ant-design/icons": "^4.2.1", 53 | "@antv/f2": "^3.7.7", 54 | "@types/node": "^14.6.2", 55 | "@umijs/plugin-sass": "^1.1.1", 56 | "@umijs/preset-react": "1.x", 57 | "@umijs/test": "^3.2.19", 58 | "antd": "^4.2.3", 59 | "antd-img-crop": "^3.10.0", 60 | "axios": "^0.19.2", 61 | "chatbot-antd": "^0.6.0", 62 | "codemirror": "^5.57.0", 63 | "file-saver": "^2.0.2", 64 | "qrcode.react": "^1.0.0", 65 | "react": "^16.12.0", 66 | "react-codemirror2": "^7.2.1", 67 | "react-color": "^2.18.1", 68 | "react-dnd": "^11.1.3", 69 | "react-dnd-html5-backend": "^11.1.3", 70 | "react-dom": "^16.12.0", 71 | "react-draggable": "^4.4.3", 72 | "react-grid-layout": "^1.0.0", 73 | "react-hotkeys-hook": "^2.3.1", 74 | "react-text-loop": "^2.3.0", 75 | "redux-undo": "^1.0.1", 76 | "socket.io-client": "^2.3.0", 77 | "umi": "^3.2.19", 78 | "video-react": "^0.14.1", 79 | "xlsx": "^0.16.7", 80 | "yorkie": "^2.0.0", 81 | "zarm": "^2.5.1" 82 | }, 83 | "license": "MIT", 84 | "devDependencies": { 85 | "@types/classnames": "^2.2.10", 86 | "@types/codemirror": "^0.0.98", 87 | "@types/events": "^3.0.0", 88 | "@types/file-saver": "^2.0.1", 89 | "@types/node": "^14.6.2", 90 | "@types/react-color": "^3.0.4", 91 | "@types/redux-logger": "^3.0.8", 92 | "@typescript-eslint/eslint-plugin": "4.1.1", 93 | "@typescript-eslint/parser": "4.1.1", 94 | "babel-eslint": "10.x", 95 | "babel-plugin-import": "^1.13.0", 96 | "eslint": "6.x", 97 | "eslint-config-react-app": "^5.2.1", 98 | "eslint-plugin-flowtype": "4.x", 99 | "eslint-plugin-import": "2.x", 100 | "eslint-plugin-jsx-a11y": "6.x", 101 | "eslint-plugin-react": "7.x", 102 | "eslint-plugin-react-hooks": "2.x", 103 | "koa": "^2.13.0", 104 | "koa-body": "^4.2.0", 105 | "koa-logger": "^3.2.1", 106 | "koa-static": "^5.0.0", 107 | "koa2-cors": "^2.0.6", 108 | "lint-staged": "^10.0.7", 109 | "prettier": "^1.19.1", 110 | "redux-logger": "^3.0.6", 111 | "sass-loader": "^9.0.3", 112 | "typescript": "^4.0.2" 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/components/BasicShop/BasicComponents/Icon/schema.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ICardPickerConfigType, 3 | IColorConfigType, 4 | INumberConfigType, 5 | ISwitchConfigType, 6 | TCardPickerDefaultType, 7 | TColorDefaultType, 8 | TNumberDefaultType, 9 | TSwitchDefaultType, 10 | } from '@/components/PanelComponents/FormEditor/types'; 11 | 12 | export type TIconEditData = Array< 13 | IColorConfigType | INumberConfigType | ISwitchConfigType | ICardPickerConfigType 14 | >; 15 | export interface IIconConfig { 16 | color: TColorDefaultType; 17 | size: TNumberDefaultType; 18 | spin: TSwitchDefaultType; 19 | type: TCardPickerDefaultType; 20 | } 21 | 22 | export interface IIconSchema { 23 | editData: TIconEditData; 24 | config: IIconConfig; 25 | } 26 | 27 | export type IconTypes = 28 | | 'AccountBookTwoTone' 29 | | 'AlertTwoTone' 30 | | 'ApiTwoTone' 31 | | 'AppstoreTwoTone' 32 | | 'AudioTwoTone' 33 | | 'BankTwoTone' 34 | | 'BellTwoTone' 35 | | 'BookTwoTone' 36 | | 'BugTwoTone' 37 | | 'BuildTwoTone' 38 | | 'BulbTwoTone' 39 | | 'CalculatorTwoTone' 40 | | 'CalendarTwoTone' 41 | | 'CameraTwoTone' 42 | | 'CarTwoTone' 43 | | 'CarryOutTwoTone' 44 | | 'CiCircleTwoTone' 45 | | 'CloudTwoTone' 46 | | 'CodeTwoTone' 47 | | 'CrownTwoTone' 48 | | 'CustomerServiceTwoTone' 49 | | 'DollarCircleTwoTone' 50 | | 'EnvironmentTwoTone' 51 | | 'ExperimentTwoTone' 52 | | 'FireTwoTone' 53 | | 'GiftTwoTone' 54 | | 'InsuranceTwoTone' 55 | | 'LikeTwoTone' 56 | | 'LockTwoTone' 57 | | 'MailTwoTone' 58 | | 'MessageTwoTone' 59 | | 'PhoneTwoTone' 60 | | 'PictureTwoTone' 61 | | 'PlaySquareTwoTone' 62 | | 'RedEnvelopeTwoTone' 63 | | 'ShopTwoTone' 64 | | 'TrademarkCircleTwoTone' 65 | | 'StarTwoTone' 66 | | 'SafetyCertificateTwoTone' 67 | | 'SettingTwoTone' 68 | | 'RocketTwoTone'; 69 | 70 | const Icon: IIconSchema = { 71 | editData: [ 72 | { 73 | key: 'color', 74 | name: '颜色', 75 | type: 'Color', 76 | }, 77 | { 78 | key: 'size', 79 | name: '大小', 80 | type: 'Number', 81 | }, 82 | { 83 | key: 'spin', 84 | name: '旋转动画', 85 | type: 'Switch', 86 | }, 87 | { 88 | key: 'type', 89 | name: '图标类型', 90 | type: 'CardPicker', 91 | icons: [ 92 | 'AccountBookTwoTone', 93 | 'AlertTwoTone', 94 | 'ApiTwoTone', 95 | 'AppstoreTwoTone', 96 | 'AudioTwoTone', 97 | 'BankTwoTone', 98 | 'BellTwoTone', 99 | 'BookTwoTone', 100 | 'BugTwoTone', 101 | 'BuildTwoTone', 102 | 'BulbTwoTone', 103 | 'CalculatorTwoTone', 104 | 'CalendarTwoTone', 105 | 'CameraTwoTone', 106 | 'CarTwoTone', 107 | 'CarryOutTwoTone', 108 | 'CiCircleTwoTone', 109 | 'CloudTwoTone', 110 | 'CodeTwoTone', 111 | 'CrownTwoTone', 112 | 'CustomerServiceTwoTone', 113 | 'DollarCircleTwoTone', 114 | 'EnvironmentTwoTone', 115 | 'ExperimentTwoTone', 116 | 'FireTwoTone', 117 | 'GiftTwoTone', 118 | 'InsuranceTwoTone', 119 | 'LikeTwoTone', 120 | 'LockTwoTone', 121 | 'MailTwoTone', 122 | 'MessageTwoTone', 123 | 'PhoneTwoTone', 124 | 'PictureTwoTone', 125 | 'PlaySquareTwoTone', 126 | 'RedEnvelopeTwoTone', 127 | 'ShopTwoTone', 128 | 'TrademarkCircleTwoTone', 129 | 'StarTwoTone', 130 | 'SafetyCertificateTwoTone', 131 | 'SettingTwoTone', 132 | 'RocketTwoTone', 133 | ], 134 | }, 135 | ], 136 | config: { 137 | color: 'rgba(74,144,226,1)', 138 | size: 36, 139 | spin: false, 140 | type: 'CarTwoTone', 141 | }, 142 | }; 143 | 144 | export default Icon; 145 | -------------------------------------------------------------------------------- /src/components/PanelComponents/Upload/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Upload, Modal, message } from 'antd'; 3 | import { PlusOutlined } from '@ant-design/icons'; 4 | import ImgCrop from 'antd-img-crop'; 5 | import styles from './index.less'; 6 | import { UploadFile, UploadChangeParam, RcFile } from 'antd/lib/upload/interface'; 7 | import { isDev } from '@/utils/tool'; 8 | function getBase64(file: File | Blob) { 9 | return new Promise((resolve, reject) => { 10 | const reader = new FileReader(); 11 | reader.readAsDataURL(file); 12 | reader.onload = () => resolve(reader.result as string); 13 | reader.onerror = error => reject(error); 14 | }); 15 | } 16 | interface PicturesWallType { 17 | fileList?: UploadFile[]; 18 | action?: string; 19 | headers?: any; 20 | withCredentials?: boolean; 21 | maxLen?: number; 22 | onChange?: (v: any) => void; 23 | cropRate?: boolean; 24 | isCrop?: boolean; 25 | } 26 | 27 | class PicturesWall extends React.Component { 28 | state = { 29 | previewVisible: false, 30 | previewImage: '', 31 | previewTitle: '', 32 | fileList: this.props.fileList || [], 33 | }; 34 | 35 | handleCancel = () => this.setState({ previewVisible: false }); 36 | 37 | handlePreview = async (file: UploadFile) => { 38 | if (!file.url && !file.preview) { 39 | file.preview = await getBase64(file.originFileObj!); 40 | } 41 | 42 | this.setState({ 43 | previewImage: file.url || file.preview, 44 | previewVisible: true, 45 | previewTitle: file.name || file.url!.substring(file.url!.lastIndexOf('/') + 1), 46 | }); 47 | }; 48 | 49 | handleChange = ({ file, fileList }: UploadChangeParam>) => { 50 | this.setState({ fileList }); 51 | if (file.status === 'done') { 52 | const files = fileList.map(item => { 53 | const { uid, name, status } = item; 54 | const url = item.url || item.response.result.url; 55 | return { uid, name, status, url }; 56 | }); 57 | this.props.onChange && this.props.onChange(files); 58 | } 59 | }; 60 | 61 | handleBeforeUpload = (file: RcFile) => { 62 | const isJpgOrPng = 63 | file.type === 'image/jpeg' || 64 | file.type === 'image/png' || 65 | file.type === 'image/jpg' || 66 | file.type === 'image/gif'; 67 | if (!isJpgOrPng) { 68 | message.error('只能上传格式为jpeg/png/gif的图片'); 69 | } 70 | const isLt2M = file.size / 1024 / 1024 < 2; 71 | if (!isLt2M) { 72 | message.error('图片必须小于2MB!'); 73 | } 74 | return isJpgOrPng && isLt2M; 75 | }; 76 | 77 | render() { 78 | const { previewVisible, previewImage, fileList, previewTitle } = this.state; 79 | const { 80 | // 配置自己的服务器地址 81 | action = isDev ? 'http://192.168.1.6:3000/api/xxx' : 'http://xxxx', 82 | headers, 83 | withCredentials = true, 84 | maxLen = 1, 85 | } = this.props; 86 | 87 | const uploadButton = ( 88 |
89 | 90 |
上传
91 |
92 | ); 93 | 94 | return ( 95 | 102 | 118 | {fileList.length >= maxLen ? null : uploadButton} 119 | 120 | 126 | example 127 | 128 | 129 | ); 130 | } 131 | } 132 | 133 | export default PicturesWall; 134 | -------------------------------------------------------------------------------- /src/components/Calibration/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef, useCallback } from 'react'; 2 | 3 | import styles from './index.less'; 4 | 5 | export interface calibrationTypes { 6 | width: number; 7 | height: number; 8 | } 9 | export type CalibrationTypes = { 10 | direction: 'up' | 'left'; 11 | multiple: number; 12 | }; 13 | 14 | export default function Calibration(props: CalibrationTypes) { 15 | const { direction, multiple } = props; 16 | const [calibrationLength, setCalibration] = useState({ width: 0, height: 0 }); 17 | const calibrationRef = useRef(null); 18 | 19 | const generateElement = useCallback( 20 | (item?: boolean, num?: number) => { 21 | if (calibrationRef.current) { 22 | let createSpan = document.createElement('div'); 23 | createSpan.className = 'calibrationLine'; 24 | createSpan.style.backgroundColor = '#ccc'; 25 | calibrationRef.current.style.display = 'flex'; 26 | calibrationRef.current.style.justifyContent = 'space-between'; 27 | if (direction === 'up') { 28 | calibrationRef.current.style.marginLeft = '50px'; 29 | createSpan.style.width = '1px'; 30 | createSpan.style.height = '6px'; 31 | createSpan.style.display = 'inline-block'; 32 | } else { 33 | calibrationRef.current.style.flexDirection = 'column'; 34 | createSpan.style.height = '1px'; 35 | createSpan.style.width = '6px'; 36 | } 37 | if (item) { 38 | let createSpanContent = document.createElement('span'); 39 | if (direction === 'up') { 40 | createSpan.style.height = '12px'; 41 | createSpanContent.style.transform = 'translate3d(-4px, 20px, 0px)'; 42 | createSpan.style.transform = 'translateY(0px)'; 43 | } else { 44 | createSpan.style.width = '12px'; 45 | createSpanContent.style.paddingLeft = '20px'; 46 | } 47 | createSpanContent.style.display = 'block'; 48 | createSpanContent.className = 'calibrationNumber'; 49 | createSpanContent.innerHTML = num! * 5 + ''; 50 | createSpan.appendChild(createSpanContent); 51 | } 52 | calibrationRef.current.appendChild(createSpan); 53 | } 54 | }, 55 | [direction], 56 | ); 57 | 58 | useEffect(() => { 59 | if (calibrationRef.current) { 60 | let calibration = calibrationRef.current.getBoundingClientRect(); 61 | setCalibration({ width: calibration.width, height: calibration.height }); 62 | let length = direction === 'up' ? calibration.width : calibration.height; 63 | for (let i = 0; i < length / 5; i++) { 64 | if (i % 10 === 0) { 65 | generateElement(true, i); 66 | } else { 67 | generateElement(); 68 | } 69 | } 70 | } 71 | }, [direction, generateElement]); 72 | 73 | useEffect(() => { 74 | if (calibrationRef.current) { 75 | let width = calibrationLength.width 76 | ? calibrationLength.width 77 | : calibrationRef.current.getBoundingClientRect().width; 78 | let height = calibrationLength.height 79 | ? calibrationLength.height 80 | : calibrationRef.current.getBoundingClientRect().height; 81 | let arr = [...calibrationRef.current.querySelectorAll('.calibrationLine')]; 82 | if (arr.length) { 83 | if (direction === 'up') { 84 | calibrationRef.current.style.width = parseFloat(multiple.toFixed(1)) * width + 'px'; 85 | arr.forEach(el => { 86 | let dom = [...el.querySelectorAll('.calibrationNumber')][0] as HTMLElement; 87 | if (dom) { 88 | dom.style.transform = `translate3d(-4px, 16px, 0px) scale(${(multiple + 0.1).toFixed( 89 | 1, 90 | )})`; 91 | } 92 | }); 93 | } else { 94 | calibrationRef.current.style.height = parseFloat(multiple.toFixed(1)) * height + 'px'; 95 | arr.forEach(el => { 96 | let dom = [...el.querySelectorAll('.calibrationNumber')][0] as HTMLElement; 97 | if (dom) { 98 | dom.style.transform = `translate3d(-4px, -8px, 0px) scale(${(multiple + 0.1).toFixed( 99 | 1, 100 | )})`; 101 | } 102 | }); 103 | } 104 | } 105 | } 106 | }, [calibrationLength.height, calibrationLength.width, direction, multiple]); 107 | 108 | return
; 109 | } 110 | -------------------------------------------------------------------------------- /src/components/PanelComponents/FormItems/FormItems.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useState } from 'react'; 2 | import BaseForm from '../../BasicShop/BasicComponents/Form/BaseForm'; 3 | import EditorModal from './EditorModal'; 4 | import { EditOutlined, MinusCircleOutlined } from '@ant-design/icons'; 5 | import styles from './formItems.less'; 6 | import { baseFormUnion, TFormItemsDefaultType } from '../FormEditor/types'; 7 | import { uuid } from '@/utils/tool'; 8 | 9 | // import { Popconfirm } from 'antd'; 10 | 11 | const formTpl: TFormItemsDefaultType = [ 12 | { 13 | id: '1', 14 | type: 'Text', 15 | label: '文本', 16 | placeholder: '请输入文本', 17 | }, 18 | { 19 | id: '2', 20 | type: 'Textarea', 21 | label: '长文本', 22 | placeholder: '请输入长文本请输入长文本', 23 | }, 24 | { 25 | id: '3', 26 | type: 'Number', 27 | label: '数值', 28 | placeholder: ' 请输入数值', 29 | }, 30 | { 31 | id: '4', 32 | type: 'MyRadio', 33 | label: '单选框', 34 | options: [ 35 | { label: '选项一', value: '1' }, 36 | { label: '选项二', value: '2' }, 37 | ], 38 | }, 39 | { 40 | id: '5', 41 | type: 'MySelect', 42 | label: '下拉选择框', 43 | options: [ 44 | { label: '选项一', value: '1' }, 45 | { label: '选项二', value: '2' }, 46 | { label: '选项三', value: '3' }, 47 | ], 48 | }, 49 | { 50 | id: '6', 51 | type: 'Date', 52 | label: '日期框', 53 | placeholder: '', 54 | }, 55 | ]; 56 | 57 | interface FormItemsProps { 58 | formList?: TFormItemsDefaultType; 59 | onChange?: (v: TFormItemsDefaultType) => void; 60 | data: any; 61 | } 62 | 63 | const FormItems = (props: FormItemsProps) => { 64 | const { formList, onChange } = props; 65 | const [formData, setFormData] = useState(formList || []); 66 | const [visible, setVisible] = useState(false); 67 | const [curItem, setCurItem] = useState(); 68 | 69 | const handleAddItem = (item: baseFormUnion) => { 70 | let tpl = formTpl.find(v => v.type === item.type); 71 | let newData = [...formData, { ...tpl!, id: uuid(6, 10) }]; 72 | setFormData(newData); 73 | onChange && onChange(newData); 74 | }; 75 | 76 | const handleEditItem = (item: baseFormUnion) => { 77 | setVisible(true); 78 | setCurItem(item); 79 | }; 80 | 81 | const handleDelItem = (item: baseFormUnion) => { 82 | let newData = formData.filter(v => v.type !== item.type); 83 | setFormData(newData); 84 | onChange && onChange(newData); 85 | }; 86 | 87 | const handleCloseModal = () => { 88 | setVisible(false); 89 | }; 90 | 91 | const handleSaveItem = (data: baseFormUnion) => { 92 | let newData = formData.map(v => (v.type === data.type ? data : v)); 93 | setFormData(newData); 94 | onChange && onChange(newData); 95 | setVisible(false); 96 | }; 97 | return ( 98 |
99 |
100 | {formData.map((item: baseFormUnion, i: number) => { 101 | let FormItem = BaseForm[item.type]; 102 | return ( 103 |
104 |
105 | 106 |
107 |
108 | 109 | 110 | 111 | 112 | 113 | 114 |
115 |
116 | ); 117 | })} 118 |
119 |
120 |

表单模版

121 | {formTpl.map((item, i) => { 122 | let FormItem = BaseForm[item.type]; 123 | return ( 124 |
125 |
126 | 127 |
128 | 129 | 添加 130 | 131 |
132 | ); 133 | })} 134 |
135 | 141 |
142 | ); 143 | }; 144 | 145 | export default memo(FormItems); 146 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |

Welcome to H5-Dooring 👋

2 |

3 | Version 4 | 5 | Documentation 6 | 7 | 8 | License: MIT 9 | 10 |

11 | 12 | > H5-Dooring是一款功能强大,开源免费的H5可视化页面配置解决方案,致力于提供一套简单方便、专业可靠、无限可能的H5落地页最佳实践。技术栈以react为主, 后台采用nodejs开发。 13 | 14 | ### 🏠 [Homepage](http://io.nainor.com/h5_visible) 15 | 16 | ### ✨ [Demo](http://io.nainor.com/h5_plus/editor?tid=123456) 17 | 18 | H5可视化编辑器 19 | 20 | ## Author 21 | 22 | 👤 **徐小夕** 23 | 24 | * Website: http://io.nainor.com/h5_visible 25 | * Github: [@MrXujiang](https://github.com/MrXujiang) 26 | 27 | ## 🤝 Contributing 28 | 29 | Contributions, issues and feature requests are welcome!
Feel free to check [issues page](https://github.com/MrXujiang/h5-Dooring/issues). 30 | 31 | ## Show your support 32 | 33 | Give a ⭐️ if this project helped you! 34 | 35 | ## 技术栈 36 | * **React** 前端主流框架(react,vue,angular)之一,更适合开发灵活度高且复杂的应用 37 | * **dva** 主流的react应用状态管理工具,基于redux 38 | * **less** css预编译语言,轻松编写结构化分明的css 39 | * **umi** 基于react的前端集成解决方案 40 | * **antd** 地球人都知道的react组件库 41 | * **axios** 强大的前端请求库 42 | * **react-dnd** 基于react的拖拽组件解决方案,具有优秀的设计哲学 43 | * **qrcode.react** 基于react的二维码生成插件 44 | * **zarm** 基于react的移动端ui库,轻松实现美观的H5应用 45 | * **koa** 基于nodejs的上一代开发框架,轻松实现基于nodejs的后端开发 46 | * **@koa/router** 基于koa2的服务端路由中间件 47 | * **ramda** 优秀的函数式js工具库 48 | 49 | ### 预览功能 50 | 预览功能这块比较简单, 我们只需要将用户生成的json数据丢进H5渲染器中即可, 这里我们需要做一个渲染页面单独用来预览组件. 先来看看几个预览效果: 51 | 52 | h5-editor 53 |
54 | h5-editor 55 | 56 | 前面的渲染器原理已经介绍了, 这里就不一一介绍了,感兴趣的可以交流讨论. 57 | 58 | ### 实现在线下载功能 59 | 在线下载这块我们需要用到一个开源库: **file-saver**, 专门解决前端下载文件困难的窘境. 具体使用举例: 60 | ``` js 61 | var FileSaver = require('file-saver'); 62 | var blob = new Blob(["Hello, world!"], {type: "text/plain;charset=utf-8"}); 63 | FileSaver.saveAs(blob, "hello world.txt"); 64 | ``` 65 | 以上代码可以实现将传入的数据下载为txt文件, 如果是Blob, 是不是还能在线下载图片, html呢? 答案是肯定的, 所以我们的下载任务采用该方案来实现. 66 | 67 | ### 后端部分 68 | 后端部分由于涉及的知识点比较多, 不是本文考虑的重点, 所以这里大致提几个点, 大家可以用完全不同的技术来实现后台服务, 比如说**PHP**, **Java**, **Python**或者**Egg**. 笔者这里采用的是**koa**. 主要实现功能如下: 69 | * 保存模板 70 | * 真机原理的数据源存储 71 | * 用户相关功能 72 | * H5图床和静态文件托管 73 | 74 | 具体代码可以参考笔者的另一篇全栈开发文章 75 | 76 | [基于nodeJS从0到1实现一个CMS全栈项目](https://juejin.im/post/6844903952761225230) 77 | 78 | 模式基本一致. 79 | 80 | ## wiki(参考文档) 81 | * [H5可视化编辑器(H5 Dooring)介绍](https://github.com/MrXujiang/h5-Dooring/wiki/H5%E5%8F%AF%E8%A7%86%E5%8C%96%E7%BC%96%E8%BE%91%E5%99%A8(H5-Dooring)%E4%BB%8B%E7%BB%8D) 82 | * [Form Editor(动态表单设计器)](https://github.com/MrXujiang/h5-Dooring/wiki/Form-Editor(%E5%8A%A8%E6%80%81%E8%A1%A8%E5%8D%95%E8%AE%BE%E8%AE%A1%E5%99%A8)) 83 | * [基于f2实现移动端可视化编辑器(dooring升级版)](https://github.com/MrXujiang/h5-Dooring/wiki/%E5%9F%BA%E4%BA%8Ef2%E5%AE%9E%E7%8E%B0%E7%A7%BB%E5%8A%A8%E7%AB%AF%E5%8F%AF%E8%A7%86%E5%8C%96%E7%BC%96%E8%BE%91%E5%99%A8(dooring%E5%8D%87%E7%BA%A7%E7%89%88)) 84 | 85 | ## 已完成功能 86 | 1. 组件库拖拽和显示 87 | 2. 组件库动态编辑 88 | 3. H5页面预览功能 89 | 4. 保存H5页面配置文件 90 | 5. 保存为模版 91 | 6. 移动端跨端适配 92 | 7. 媒体组件 93 | 8. 在线下载网站代码功能 94 | 9. 添加typescript支持 95 | 10. 表单设计器/自定义表单组件 96 | 11. 可视化组件Chart实现 97 | 12. 在线编程模块(Mini Web IDE) 98 | 13. 新增图表组件 面积图,折线图, 饼图 99 | 100 | ## 正在完成功能 101 | * 升级模版库 102 | * 丰富组件库组件,添加可视化组件如折线图, 饼图, 面积图等 103 | * 添加配置交互功能 104 | * 组件细分和代码优化 105 | * 单元测试 106 | 107 | ## Install(安装) 108 | 1. 下载代码 109 | ```sh 110 | git clone https://github.com/MrXujiang/h5-Dooring.git 111 | ``` 112 | 2. 进入项目目录 113 | ```sh 114 | cd ./h5-Dooring 115 | ``` 116 | 117 | 3. 安装依赖包 118 | ```sh 119 | yarn install 120 | ``` 121 | 122 | ## Usage 123 | 启动应用 124 | ```sh 125 | yarn run start 126 | ``` 127 | 128 | ## 更新日志 129 | 1. 添加在线编程模块(在执行代码前先启动node服务 npm run server) 130 | 2. 添加客服机器人模块[chatbot-antd](https://www.npmjs.com/package/chatbot-antd) 131 | 132 | 133 | ## 持续升级 134 | 正在升级1.3版本,敬请期待... 135 | 136 | ## 赞助 137 | 开源不易, 有了您的赞助, 我们会做的更好~ 138 | 139 | ## 技术反馈和交流 140 | 微信:beautifulFront 141 | 142 | 143 | 144 | 145 | ## 技术交流群 146 | 147 | -------------------------------------------------------------------------------- /src/components/PanelComponents/FormEditor/types.ts: -------------------------------------------------------------------------------- 1 | //////////////////// 2 | export interface IUploadConfigType { 3 | key: string; 4 | name: string; 5 | type: 'Upload'; 6 | isCrop?: boolean; 7 | cropRate?: number; 8 | } 9 | 10 | export type TUploadDefaultType = Array<{ 11 | uid: string; 12 | name: string; 13 | status: string; 14 | url: string; 15 | }>; 16 | ///////////////// 17 | export interface ITextConfigType { 18 | key: string; 19 | name: string; 20 | type: 'Text'; 21 | } 22 | export type TTextDefaultType = string; 23 | //////////////////////// 24 | export interface ITextAreaConfigType { 25 | key: string; 26 | name: string; 27 | type: 'TextArea'; 28 | } 29 | export type TTextAreaDefaultType = string; 30 | //////////////////////////// 31 | export interface INumberConfigType { 32 | key: string; 33 | name: string; 34 | type: 'Number'; 35 | range?: [number, number]; 36 | step?: number; 37 | } 38 | 39 | export type TNumberDefaultType = number; 40 | 41 | /////////////////// 42 | export interface IDataListConfigType { 43 | key: string; 44 | name: string; 45 | type: 'DataList'; 46 | } 47 | 48 | export type TDataListDefaultTypeItem = { 49 | id: string; 50 | title: string; 51 | desc: string; 52 | link: string; 53 | type?: number; 54 | imgUrl: TUploadDefaultType; 55 | }; 56 | 57 | export type TDataListDefaultType = Array; 58 | 59 | //////////////////// 60 | export interface IColorConfigType { 61 | key: string; 62 | name: string; 63 | type: 'Color'; 64 | } 65 | 66 | export type TColorDefaultType = string; 67 | 68 | export interface IMutiTextConfigType { 69 | key: string; 70 | name: string; 71 | type: 'MutiText'; 72 | } 73 | 74 | export type TMutiTextDefaultType = Array; 75 | 76 | ///////////////////////////////// 77 | export interface ISelectConfigType { 78 | key: string; 79 | name: string; 80 | type: 'Select'; 81 | range: Array<{ 82 | key: KeyType; 83 | text: string; 84 | }>; 85 | } 86 | export type TSelectDefaultType = KeyType; 87 | 88 | ///////////////////////// 89 | export interface IRadioConfigType { 90 | key: string; 91 | name: string; 92 | type: 'Radio'; 93 | range: Array<{ 94 | key: KeyType; 95 | text: string; 96 | }>; 97 | } 98 | export type TRadioDefaultType = KeyType; 99 | 100 | /////////////// 101 | 102 | export interface ISwitchConfigType { 103 | key: string; 104 | name: string; 105 | type: 'Switch'; 106 | } 107 | export type TSwitchDefaultType = boolean; 108 | 109 | ///////////////////////////// 110 | export interface ICardPickerConfigType { 111 | key: string; 112 | name: string; 113 | type: 'CardPicker'; 114 | icons: Array; 115 | } 116 | export type TCardPickerDefaultType = T; 117 | 118 | ///////////// 119 | 120 | export interface ITableConfigType { 121 | key: string; 122 | name: string; 123 | type: 'Table'; 124 | } 125 | export type TTableDefaultType = Array<{ 126 | name: string; 127 | value: number; 128 | }>; 129 | 130 | ////////////////// 131 | export interface IFormItemsConfigType { 132 | key: string; 133 | name: string; 134 | type: 'FormItems'; 135 | } 136 | 137 | //0---------baseform 138 | export type baseFormOptionsType = { 139 | label: string; 140 | value: string; 141 | }; 142 | 143 | export type baseFormTextTpl = { 144 | id: string; 145 | type: 'Text'; 146 | label: string; 147 | placeholder: string; 148 | }; 149 | export type baseFormNumberTpl = { 150 | id: string; 151 | type: 'Number'; 152 | label: string; 153 | placeholder: string; 154 | }; 155 | 156 | export type baseFormTextAreaTpl = { 157 | id: string; 158 | type: 'Textarea'; 159 | label: string; 160 | placeholder: string; 161 | }; 162 | 163 | export type baseFormMyRadioTpl = { 164 | id: string; 165 | type: 'MyRadio'; 166 | label: string; 167 | options: baseFormOptionsType[]; 168 | }; 169 | 170 | export type baseFormMySelectTpl = { 171 | id: string; 172 | type: 'MySelect'; 173 | label: string; 174 | options: baseFormOptionsType[]; 175 | }; 176 | 177 | export type baseFormDateTpl = { 178 | id: string; 179 | type: 'Date'; 180 | label: string; 181 | placeholder: string; 182 | }; 183 | 184 | export type baseFormUnion = 185 | | baseFormTextTpl 186 | | baseFormNumberTpl 187 | | baseFormTextAreaTpl 188 | | baseFormMyRadioTpl 189 | | baseFormMySelectTpl 190 | | baseFormDateTpl; 191 | export type baseFormUnionType = 192 | | baseFormTextTpl['type'] 193 | | baseFormNumberTpl['type'] 194 | | baseFormTextAreaTpl['type'] 195 | | baseFormMyRadioTpl['type'] 196 | | baseFormMySelectTpl['type'] 197 | | baseFormDateTpl['type']; 198 | 199 | export type TFormItemsDefaultType = Array; 200 | -------------------------------------------------------------------------------- /src/pages/editor/index.less: -------------------------------------------------------------------------------- 1 | .layout { 2 | .editorWrap { 3 | background-color: rgba(245, 245, 245, 1); 4 | } 5 | .container { 6 | width: 100vw; 7 | // height: 100vh; 8 | display: flex; 9 | .list { 10 | width: 350px; 11 | height: 100vh; 12 | padding: 10px 16px 16px; 13 | box-shadow: 2px 0px 10px rgba(0, 0, 0, 0.2); 14 | display: inline-block; 15 | background-color: #fff; 16 | overflow: scroll; 17 | .searchBar { 18 | margin-bottom: 20px; 19 | } 20 | .module { 21 | position: relative; 22 | margin-bottom: 12px; 23 | width: 100%; 24 | overflow: hidden; 25 | user-select: none; 26 | border: 2px solid transparent; 27 | transition: border 0.3s; 28 | &:hover { 29 | border: 2px solid #06c; 30 | } 31 | &::after { 32 | content: ''; 33 | position: absolute; 34 | z-index: 99; 35 | width: 100%; 36 | height: 100%; 37 | top: 0; 38 | left: 0; 39 | } 40 | } 41 | } 42 | .tickMark { 43 | width: calc(100% - 750px); 44 | overflow: hidden; 45 | height: 100vh; 46 | position: relative; 47 | .tickMarkTop { 48 | width: 100%; 49 | height: 50px; 50 | position: absolute; 51 | top: 0; 52 | left: 0; 53 | } 54 | .tickMarkLeft { 55 | width: 50px; 56 | height: calc(100% - 50px); 57 | position: absolute; 58 | bottom: 0; 59 | left: 0; 60 | } 61 | .canvasBox { 62 | width: 375px; 63 | min-height: 664px; 64 | height: 664px; 65 | position: relative; 66 | left: 50%; 67 | margin-left: -200px; 68 | top: 80px; 69 | .canvas { 70 | position: relative; 71 | width: 375px; 72 | min-height: 664px; 73 | background-color: #fff; 74 | box-shadow: 2px 0px 10px rgba(0, 0, 0, 0.2); 75 | position: absolute; 76 | left: 0; 77 | top: 0; 78 | transition: transform 300ms ease-out; 79 | .dragItem { 80 | display: inline-block; 81 | position: absolute; 82 | z-index: 2; 83 | border: 2px solid transparent; 84 | cursor: move; 85 | &:hover { 86 | border: 2px solid #06c; 87 | } 88 | :global(a) { 89 | display: block; 90 | pointer-events: none; 91 | } 92 | } 93 | } 94 | } 95 | .sliderWrap { 96 | position: absolute; 97 | width: 30px; 98 | right: 10px; 99 | bottom: 130px; 100 | .slider { 101 | height: 120px; 102 | display: inline-block; 103 | vertical-align: middle; 104 | } 105 | .sliderBtn { 106 | cursor: pointer; 107 | user-select: none; 108 | } 109 | span { 110 | margin: 0 12px; 111 | display: inline-block; 112 | font-size: 18px; 113 | } 114 | } 115 | .backSize { 116 | position: absolute; 117 | right: 18px; 118 | bottom: 95px; 119 | cursor: pointer; 120 | } 121 | } 122 | .attrSetting { 123 | width: 400px; 124 | padding: 20px 0 20px 20px; 125 | background: #fff; 126 | height: 100vh; 127 | box-shadow: -2px 0px 4px 0px rgba(0, 0, 0, 0.1); 128 | overflow: scroll; 129 | .tit { 130 | margin-bottom: 16px; 131 | font-size: 18px; 132 | color: #000; 133 | } 134 | .posWrap { 135 | padding: 20px; 136 | .posItem { 137 | margin-bottom: 12px; 138 | font-size: 14px; 139 | color: rgba(0, 0, 0, 0.5); 140 | .posTit { 141 | margin-right: 50px; 142 | } 143 | .pos { 144 | margin-right: 40px; 145 | color: #000; 146 | &::before { 147 | content: attr(data-flag); 148 | color: rgba(0, 0, 0, 0.4); 149 | margin-right: 8px; 150 | } 151 | } 152 | } 153 | } 154 | .graphWrap { 155 | .graphTable { 156 | .iptControl { 157 | margin-bottom: 18px; 158 | display: flex; 159 | align-items: flex-start; 160 | .iptLabel { 161 | width: 108px; 162 | flex-shrink: 0; 163 | color: rgba(0, 0, 0, 0.5); 164 | } 165 | .cpSelector { 166 | img { 167 | width: 100%; 168 | } 169 | } 170 | .colorSelector { 171 | span { 172 | display: inline-block; 173 | margin-right: 10px; 174 | width: 42px; 175 | height: 16px; 176 | } 177 | } 178 | } 179 | } 180 | } 181 | } 182 | } 183 | } 184 | 185 | .saveForm { 186 | padding-top: 10px; 187 | .formIpt { 188 | span { 189 | display: block; 190 | line-height: 2.4em; 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/pages/editor/SourceBox.js: -------------------------------------------------------------------------------- 1 | import React, { memo, useEffect, useState } from 'react'; 2 | import { useDrop } from 'react-dnd'; 3 | import Draggable from 'react-draggable'; 4 | import GridLayout from 'react-grid-layout'; 5 | import { Tooltip } from 'antd'; 6 | import { connect } from 'dva'; 7 | import DynamicEngine from 'components/DynamicEngine'; 8 | import styles from './index.less'; 9 | import { uuid } from '@/utils/tool'; 10 | 11 | const SourceBox = memo(props => { 12 | const { pstate, scaleNum, canvasId, allType, dispatch } = props; 13 | 14 | const pointData = pstate ? pstate.pointData : {}; 15 | const [canvasRect, setCanvasRect] = useState([]); 16 | const [isShowTip, setIsShowTip] = useState(true); 17 | const [{ isOver }, drop] = useDrop({ 18 | accept: allType, 19 | drop: (item, monitor) => { 20 | let parentDiv = document.getElementById(canvasId), 21 | pointRect = parentDiv.getBoundingClientRect(), 22 | top = pointRect.top, 23 | pointEnd = monitor.getSourceClientOffset(), 24 | y = pointEnd.y < top ? 0 : pointEnd.y - top, 25 | col = 24, // 网格列数 26 | cellHeight = 2, 27 | w = item.type === 'Icon' ? 3 : col; 28 | // 转换成网格规则的坐标和大小 29 | let gridY = Math.ceil(y / cellHeight); 30 | dispatch({ 31 | type: 'editorModal/addPointData', 32 | payload: { 33 | id: uuid(6, 10), 34 | item, 35 | point: { i: `x-${pointData.length}`, x: 0, y: gridY, w, h: item.h, isBounded: true }, 36 | }, 37 | }); 38 | }, 39 | collect: monitor => ({ 40 | isOver: monitor.isOver(), 41 | canDrop: monitor.canDrop(), 42 | item: monitor.getItem(), 43 | }), 44 | }); 45 | 46 | const dragStop = (layout, oldItem, newItem, placeholder, e, element) => { 47 | const curPointData = pointData.filter(item => item.id === newItem.i)[0]; 48 | dispatch({ 49 | type: 'editorModal/modPointData', 50 | payload: { ...curPointData, point: newItem }, 51 | }); 52 | }; 53 | 54 | const onDragStart = (layout, oldItem, newItem, placeholder, e, element) => { 55 | const curPointData = pointData.filter(item => item.id === newItem.i)[0]; 56 | dispatch({ 57 | type: 'editorModal/modPointData', 58 | payload: { ...curPointData }, 59 | }); 60 | }; 61 | 62 | const onResizeStop = (layout, oldItem, newItem, placeholder, e, element) => { 63 | const curPointData = pointData.filter(item => item.id === newItem.i)[0]; 64 | dispatch({ 65 | type: 'editorModal/modPointData', 66 | payload: { ...curPointData, point: newItem }, 67 | }); 68 | }; 69 | 70 | useEffect(() => { 71 | let { width, height } = document.getElementById(canvasId).getBoundingClientRect(); 72 | setCanvasRect([width, height]); 73 | }, [canvasId]); 74 | 75 | useEffect(() => { 76 | let timer = window.setTimeout(() => { 77 | setIsShowTip(false); 78 | }, 3000); 79 | return () => { 80 | window.clearTimeout(timer); 81 | }; 82 | }, []); 83 | const opacity = isOver ? 0.7 : 1; 84 | const backgroundColor = isOver ? 1 : 0.7; 85 | return ( 86 | 87 |
88 |
96 |
105 | 106 |
120 | 121 | 122 | {pointData.length > 0 ? ( 123 | 133 | {pointData.map(value => ( 134 |
135 | 136 |
137 | ))} 138 |
139 | ) : null} 140 |
141 |
142 |
143 | 144 | ); 145 | }); 146 | 147 | export default connect(state => ({ pstate: state.present.editorModal }))(SourceBox); 148 | -------------------------------------------------------------------------------- /src/components/PanelComponents/FormEditor/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo, useEffect } from 'react'; 2 | import { Form, Select, InputNumber, Input, Switch, Radio, Button } from 'antd'; 3 | import Upload from '../Upload'; 4 | import DataList from '../DataList'; 5 | import MutiText from '../MutiText'; 6 | import Color from '../Color'; 7 | import CardPicker from '../CardPicker'; 8 | import Table from '../Table'; 9 | import { Store } from 'antd/lib/form/interface'; 10 | import FormItems from '../FormItems'; 11 | // import styles from './index.less'; 12 | const normFile = (e: any) => { 13 | console.log('Upload event:', e); 14 | if (Array.isArray(e)) { 15 | //待修改 16 | return e; 17 | } 18 | return e && e.fileList; 19 | }; 20 | 21 | const { Option } = Select; 22 | const { TextArea } = Input; 23 | 24 | const formItemLayout = { 25 | labelCol: { span: 6 }, 26 | wrapperCol: { span: 16 }, 27 | }; 28 | 29 | interface FormEditorProps { 30 | uid: string; 31 | onSave: Function; 32 | onDel: Function; 33 | defaultValue: { [key: string]: any }; 34 | config: Array; 35 | } 36 | 37 | const FormEditor = (props: FormEditorProps) => { 38 | const { config, defaultValue, onSave, onDel, uid } = props; 39 | console.log(defaultValue, config); 40 | const onFinish = (values: Store) => { 41 | onSave && onSave(values); 42 | }; 43 | 44 | const handleDel = () => { 45 | onDel && onDel(uid); 46 | }; 47 | 48 | const [form] = Form.useForm(); 49 | 50 | useEffect(() => { 51 | return () => { 52 | form.resetFields(); 53 | }; 54 | }, [defaultValue, form]); 55 | return ( 56 |
63 | {config.map((item, i) => { 64 | return ( 65 | 66 | {item.type === 'Number' && ( 67 | 68 | 69 | 70 | )} 71 | {item.type === 'Text' && ( 72 | 73 | 74 | 75 | )} 76 | {item.type === 'TextArea' && ( 77 | 78 |