├── webpack-assets.json ├── src ├── pages │ ├── coms │ │ ├── common │ │ │ └── style.config.js │ │ ├── RJImgUploader.js │ │ ├── Layout.Header.js │ │ ├── Badge.js │ │ ├── Layout.Content.js │ │ ├── Calendar.js │ │ ├── Form.Item.Submit.js │ │ ├── RJPagination.js │ │ ├── Switch.js │ │ ├── DatePicker.js │ │ ├── Form.Item.Inline.js │ │ ├── InputNumber.js │ │ ├── Layout.Sider.js │ │ ├── Form.Item.js │ │ ├── RJFormItem.js │ │ ├── Table.Column.js │ │ ├── Tag.js │ │ ├── Modal.js │ │ ├── Input.js │ │ ├── RJBreadcrumb.js │ │ ├── Breadcrumb.js │ │ ├── div.js │ │ ├── Form.js │ │ ├── Radio.Group.js │ │ ├── Button.js │ │ ├── RJSelect.js │ │ ├── Row2.js │ │ ├── Layout.js │ │ ├── RJMenu.js │ │ ├── Row4.js │ │ ├── Table_Data.js │ │ ├── Row8.js │ │ └── Table.js │ ├── page.json │ ├── preview.jsx │ ├── ant-coms.js │ └── editor.jsx ├── components │ ├── index.jsx │ ├── RJPagination.jsx │ ├── RJSelect.jsx │ ├── es5 │ │ └── image.js │ ├── RJMenu.jsx │ └── RJImgUploader.jsx ├── App.css ├── index.jsx ├── index.css └── logo.svg ├── .babelrc ├── .gitignore ├── public └── template.html ├── docs └── index.html ├── SECURITY.md ├── package.json ├── webpack.loaders.js ├── webpack.production.config.js ├── webpack.config.js └── README.md /webpack-assets.json: -------------------------------------------------------------------------------- 1 | {"index":{"js":"/index_2969feb91e0758ea6cb3.js"}} -------------------------------------------------------------------------------- /src/pages/coms/common/style.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | background:{ 3 | text:'背景色', 4 | type:'color' 5 | }, 6 | } -------------------------------------------------------------------------------- /src/pages/coms/RJImgUploader.js: -------------------------------------------------------------------------------- 1 | export default { 2 | type:'RJImgUploader', 3 | title:'图片上传', 4 | props:{ 5 | action:"#" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/pages/coms/Layout.Header.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "type":"Layout.Header", 3 | "title":"头部", 4 | props:{ 5 | style:{ 6 | minHeight:20, 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/pages/coms/Badge.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "type":"Badge", 3 | "title":"圆点", 4 | "props":{ 5 | "count":10 6 | }, 7 | config:{ 8 | count:{ 9 | text:"数值" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/pages/coms/Layout.Content.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "type":"Layout.Content", 3 | "title":"内容区域", 4 | can_place:true, 5 | "props":{ 6 | style:{ 7 | padding:20, 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/pages/coms/Calendar.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "type":"Calendar", 3 | "title":"日历", 4 | "props":{ 5 | "fullscreen":false, 6 | "style":{ 7 | "width":300, 8 | "height":300 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/pages/coms/Form.Item.Submit.js: -------------------------------------------------------------------------------- 1 | export default { 2 | type:"Form.Item", 3 | "title":"不带描述的表单项(例如提交按钮用的)", 4 | can_place:true, 5 | wrap_inner:true, 6 | props:{ 7 | wrapperCol: { span: 20 ,offset:4}, 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/pages/coms/RJPagination.js: -------------------------------------------------------------------------------- 1 | export default { 2 | type:"RJPagination", 3 | "title":"分页", 4 | props:{ 5 | pageData:{ 6 | currentIndex:1, 7 | pageSize:20, 8 | totalNumber:200 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react","stage-3"], 3 | "plugins": [ 4 | "transform-runtime", 5 | "transform-decorators-legacy", 6 | "transform-class-properties", 7 | "react-hot-loader/babel" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /src/pages/coms/Switch.js: -------------------------------------------------------------------------------- 1 | export default { 2 | type:'Switch', 3 | title:'开关切换', 4 | props:{ 5 | size:'default' 6 | }, 7 | config:{ 8 | size:{ 9 | text:'大小', 10 | enum:[ 11 | 'default', 12 | 'small' 13 | ] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/pages/coms/DatePicker.js: -------------------------------------------------------------------------------- 1 | export default { 2 | type:"DatePicker", 3 | title:"日期选择器", 4 | wrap:true, 5 | props:{ 6 | style:{ 7 | width:'200px' 8 | } 9 | }, 10 | config:{ 11 | style:{ 12 | width:{ 13 | text:"宽度" 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/pages/coms/Form.Item.Inline.js: -------------------------------------------------------------------------------- 1 | export default { 2 | type:"Form.Item", 3 | "title":"内联的表单项", 4 | can_place:true, 5 | wrap_inner:true, 6 | props:{ 7 | label:"表单项描述", 8 | style:{ 9 | } 10 | }, 11 | config:{ 12 | "label":{ 13 | text:"表单项描述" 14 | }, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/pages/coms/InputNumber.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "type":"InputNumber", 3 | "title":"数字输入框", 4 | "props":{ 5 | defaultValue:10, 6 | style:{ 7 | // width:200 8 | } 9 | }, 10 | config:{ 11 | style:{ 12 | width:{ 13 | text:"宽度" 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/pages/coms/Layout.Sider.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "type":"Layout.Sider", 3 | "title":"侧边栏", 4 | can_place:true, 5 | props:{ 6 | style:{ 7 | width:"300px", 8 | border:"1px solid #ff6600" 9 | } 10 | }, 11 | config:{ 12 | style:{ 13 | width:"宽度" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | .idea 20 | -------------------------------------------------------------------------------- /src/pages/coms/Form.Item.js: -------------------------------------------------------------------------------- 1 | export default { 2 | type:"Form.Item", 3 | "title":"表单项", 4 | can_place:true, 5 | wrap_inner:true, 6 | props:{ 7 | label:"表单项描述", 8 | labelCol: { span: 4 }, 9 | wrapperCol: { span: 20 }, 10 | style:{ 11 | } 12 | }, 13 | config:{ 14 | "label":{ 15 | text:"表单项描述" 16 | }, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/pages/coms/RJFormItem.js: -------------------------------------------------------------------------------- 1 | export default { 2 | type:"Form.Item", 3 | "title":"表单项", 4 | can_place:true, 5 | wrap_inner:true, 6 | props:{ 7 | label:"表单项描述", 8 | labelCol: { span: 4 }, 9 | wrapperCol: { span: 20 }, 10 | style:{ 11 | } 12 | }, 13 | config:{ 14 | "label":{ 15 | text:"表单项描述" 16 | }, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/pages/coms/Table.Column.js: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | type:"Table.Column", 4 | title:"表格列", 5 | props:{ 6 | title:"名称", 7 | dataIndex:"1", 8 | key:"1" 9 | }, 10 | config:{ 11 | title:{ 12 | text:"列名", 13 | }, 14 | dataIndex:{ 15 | text:"唯一key" 16 | }, 17 | key:{ 18 | text:"唯一key" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /public/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= htmlWebpackPlugin.options.title %> 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/components/index.jsx: -------------------------------------------------------------------------------- 1 | import RJImgUploader from './RJImgUploader'; 2 | import RJPagination from './RJPagination'; 3 | import RJSelect from './RJSelect'; 4 | import RJMenu from './RJMenu'; 5 | 6 | 7 | var antd = require('antd'); 8 | 9 | Object.assign(antd, { 10 | RJImgUploader, 11 | RJPagination, 12 | RJSelect, 13 | RJMenu 14 | }); 15 | module.exports = antd; 16 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Antd Editor 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/pages/coms/Tag.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "type":"Tag", 3 | "title":"标签", 4 | 5 | props:{ 6 | closable:false, 7 | "content":"标签", 8 | }, 9 | config:{ 10 | closable:{ 11 | text:"是否可关闭", 12 | enum:["true","false"] 13 | }, 14 | color:{ 15 | text:"颜色", 16 | enum:[ 17 | 'red', 18 | 'orange', 19 | 'green', 20 | 'blue' 21 | ] 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/pages/coms/Modal.js: -------------------------------------------------------------------------------- 1 | export default { 2 | type:"Modal", 3 | title:"模态窗口", 4 | can_place:true, 5 | wrap:true, 6 | props:{ 7 | visible:true, 8 | title:"标题", 9 | width:500, 10 | event_onOk:'', 11 | event_onCancel:'' 12 | }, 13 | config:{ 14 | visible:{ 15 | text:"是否可见", 16 | type:"Boolean" 17 | }, 18 | title:{ 19 | text:"标题" 20 | }, 21 | width:{ 22 | text:"宽度" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | @import '~antd/dist/antd.css'; 2 | .App { 3 | text-align: center; 4 | } 5 | 6 | .App-logo { 7 | animation: App-logo-spin infinite 20s linear; 8 | height: 80px; 9 | } 10 | 11 | .App-header { 12 | background-color: #222; 13 | height: 150px; 14 | padding: 20px; 15 | color: white; 16 | } 17 | 18 | .App-intro { 19 | font-size: large; 20 | } 21 | 22 | @keyframes App-logo-spin { 23 | from { transform: rotate(0deg); } 24 | to { transform: rotate(360deg); } 25 | } 26 | -------------------------------------------------------------------------------- /src/pages/coms/Input.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "type":"Input", 3 | "title":"输入框", 4 | "props":{ 5 | "placeholder":"这是一个输入框", 6 | type:"text", 7 | style:{ 8 | // width:200 9 | } 10 | }, 11 | config:{ 12 | "placeholder":{ 13 | text:"默认提醒" 14 | }, 15 | type:{ 16 | text:"是否多行文本", 17 | enum:[ 18 | 'textarea', 19 | 'text' 20 | ] 21 | }, 22 | style:{ 23 | width:{ 24 | text:"宽度" 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/pages/page.json: -------------------------------------------------------------------------------- 1 | [{ 2 | type:'Layout', 3 | childrens:[ 4 | { 5 | type:'Layout.Header', 6 | props:{ 7 | 8 | } 9 | }, 10 | ] 11 | },{ 12 | type:'Layout', 13 | childrens:[ 14 | { 15 | type:'Layout.Sider', 16 | col:4, 17 | props:{ 18 | 19 | } 20 | }, 21 | { 22 | type:'Layout.Content', 23 | col:8, 24 | props:{ 25 | 26 | } 27 | } 28 | ] 29 | },{ 30 | type:'Layout', 31 | },{ 32 | type:'Layout', 33 | }, 34 | ] 35 | -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import './index.css'; 5 | import './App.css'; 6 | 7 | import Editor from './pages/editor.jsx'; 8 | import preview from './pages/preview.jsx'; 9 | import { Router, Route, hashHistory,IndexRoute } from 'react-router' 10 | ReactDOM.render( 11 | ( 12 | 13 | 14 | 15 | 16 | ), 17 | document.getElementById('root') 18 | ); 19 | -------------------------------------------------------------------------------- /src/components/RJPagination.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Pagination } from 'antd'; 3 | 4 | class RJPagination extends React.Component { 5 | constructor() { 6 | super(); 7 | } 8 | 9 | render() { 10 | let pageData = this.props.pageData; 11 | 12 | return ( 13 | 19 | ); 20 | } 21 | } 22 | 23 | export default RJPagination; 24 | -------------------------------------------------------------------------------- /src/pages/coms/RJBreadcrumb.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "type":"RJBreadcrumb", 3 | "title":"面包屑", 4 | props:{ 5 | data:[ 6 | { 7 | title:"一级目录", 8 | href:"#", 9 | key:1 10 | }, 11 | { 12 | title:"二级目录", 13 | href:"#", 14 | key:2 15 | } 16 | ] 17 | }, 18 | config:{ 19 | data:{ 20 | text:"项目配置", 21 | enumobject:[{ 22 | key:1, 23 | dataIndex:"title", 24 | title:"显示文本", 25 | type:'String', 26 | },{ 27 | key:2, 28 | dataIndex:"href", 29 | title:"链接", 30 | type:'String', 31 | }] 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /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/pages/coms/Breadcrumb.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "type":"Breadcrumb", 3 | "title":"面包屑", 4 | props:{ 5 | routes:[ 6 | { 7 | breadcrumbName:"一级目录", 8 | path:"#", 9 | key:1 10 | }, 11 | { 12 | breadcrumbName:"二级目录", 13 | path:"#", 14 | key:2 15 | } 16 | ] 17 | }, 18 | config:{ 19 | routes:{ 20 | text:"项目配置", 21 | enumobject:[{ 22 | key:1, 23 | dataIndex:"breadcrumbName", 24 | title:"显示文本", 25 | type:'String', 26 | },{ 27 | key:2, 28 | dataIndex:"path", 29 | title:"链接", 30 | type:'String', 31 | }] 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/pages/coms/div.js: -------------------------------------------------------------------------------- 1 | export default { 2 | type:"div", 3 | title:"通用布局块", 4 | is_native:true, 5 | can_place:true, 6 | props:{ 7 | style:{ 8 | minHeight:20, 9 | padding:"20px", 10 | } 11 | }, 12 | config:{ 13 | style:{ 14 | padding:{ 15 | text:"内间距", 16 | type:"4-value" 17 | }, 18 | margin:{ 19 | text:"外边距", 20 | type:"4-value" 21 | }, 22 | backgroundColor:{ 23 | text:"背景色", 24 | type:"color" 25 | }, 26 | } 27 | 28 | } 29 | } -------------------------------------------------------------------------------- /src/pages/coms/Form.js: -------------------------------------------------------------------------------- 1 | import Common_Style from './common/style.config'; 2 | export default { 3 | type:"Form", 4 | "title":"表单容器", 5 | can_place:true, 6 | props:{ 7 | layout:'horizontal', 8 | style:{ 9 | padding:"20px", 10 | margin:"0px" 11 | } 12 | }, 13 | config:{ 14 | layout:{ 15 | text:"布局方式", 16 | enum:[ 17 | 'inline', 18 | 'horizontal', 19 | 'vertical' 20 | ] 21 | }, 22 | style:{ 23 | width:{ 24 | text:"宽度" 25 | }, 26 | padding:{ 27 | text:"内边距", 28 | type:"4-value" 29 | }, 30 | margin:{ 31 | text:"外边距", 32 | type:"4-value" 33 | }, 34 | ...Common_Style 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/pages/coms/Radio.Group.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "type":"Radio.Group", 3 | "title":"单选框", 4 | "props":{ 5 | options:[ 6 | { label: 'Apple', value: 'Apple',key:1 }, 7 | { label: 'Pear', value: 'Pear',key:2 }, 8 | { label: 'Orange', value: 'Orange', disabled: true,key:3 }, 9 | ] 10 | }, 11 | config:{ 12 | options:{ 13 | text:"项目配置", 14 | enumobject:[{ 15 | key:1, 16 | dataIndex:"label", 17 | title:"显示文本", 18 | type:'String', 19 | },{ 20 | key:2, 21 | dataIndex:"value", 22 | title:"真实值", 23 | type:'String', 24 | },{ 25 | key:3, 26 | dataIndex:"disabled", 27 | title:"是否禁用", 28 | type:'Boolean', 29 | render: (v) => { 30 | return v?'是':'否'; 31 | } 32 | }] 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/pages/coms/Button.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "type": "Button", 3 | "title": "按钮", 4 | "props": { 5 | type: 'primary', 6 | content: '按钮一只', 7 | style: { 8 | margin: "0px 10px 0px 0px" 9 | } 10 | }, 11 | config: { 12 | type: { 13 | text: "主题", 14 | enum: [ 15 | 'primary', 16 | 'default', 17 | 'dashed', 18 | 'danger' 19 | ] 20 | }, 21 | icon: { 22 | text: "图标", 23 | }, 24 | content: { 25 | text: '文案', 26 | }, 27 | style: { 28 | width: { 29 | text: "宽度", 30 | }, 31 | margin: { 32 | text: "外边距", 33 | type: "4-value" 34 | } 35 | } 36 | }, 37 | } 38 | -------------------------------------------------------------------------------- /src/pages/coms/RJSelect.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "type":"RJSelect", 3 | "title":"下拉列表", 4 | props:{ 5 | selectData:[{ 6 | key:'1', 7 | value:'第一个', 8 | },{ 9 | key:'2', 10 | value:'第二个' 11 | }], 12 | mode: "", 13 | placeholder:'请选择', 14 | style:{} 15 | }, 16 | config:{ 17 | style:{ 18 | width:{ 19 | text:"宽度" 20 | } 21 | }, 22 | value:{ 23 | text:"默认值" 24 | }, 25 | mode:{ 26 | text:"类型", 27 | enum:[ 28 | '','multiple', 'tags', 'combobox' 29 | ] 30 | }, 31 | selectData:{ 32 | text:"数据源", 33 | enumobject:[ 34 | { 35 | title: '文本', 36 | dataIndex: 'value', 37 | type:"String" 38 | }, 39 | { 40 | title: '真实值', 41 | dataIndex: 'key', 42 | type:"String" 43 | } 44 | ] 45 | } 46 | 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/pages/coms/Row2.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "type":"Row", 3 | "title":"2列栅格", 4 | childrens:[ 5 | { 6 | type:'Col', 7 | title:'栅格单元', 8 | can_place:true, 9 | props:{ 10 | span:4, 11 | style:{ 12 | minHeight:30, 13 | }, 14 | }, 15 | config:{ 16 | span:{ 17 | text:"栅格", 18 | enum:[ 19 | 2,4,6,8,10,12,14,16,18,20,22,24 20 | ] 21 | } 22 | } 23 | }, 24 | { 25 | type:'Col', 26 | title:'栅格单元', 27 | can_place:true, 28 | props:{ 29 | span:20, 30 | style:{ 31 | minHeight:30, 32 | }, 33 | }, 34 | config:{ 35 | span:{ 36 | text:"栅格", 37 | enum:[ 38 | 2,4,6,8,10,12,14,16,18,20,22,24 39 | ] 40 | } 41 | } 42 | } 43 | ], 44 | props:{ 45 | 46 | }, 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | overflow: visible !important; 6 | } 7 | 8 | .editor .ant-tag{ 9 | margin-bottom:8px; 10 | } 11 | 12 | .editor .edit_layer,.ant-modal .edit_layer{ 13 | opacity: 0.5; 14 | background: #eee; 15 | border:1px dashed #999; 16 | pointer-events:none; 17 | position:absolute; 18 | z-index: 10000000; 19 | } 20 | .editor div[class^="ant-col-"],.editor .ant-layout{ 21 | border:1px dashed #ccc; 22 | } 23 | .editor .isdroping,.ant-modal .isdroping{ 24 | border:1px solid #ff0000 !important; 25 | } 26 | .ant-modal-wrap { 27 | position: relative !important; 28 | overflow: visible !important; 29 | margin-right:500px !important; 30 | } 31 | .ant-modal{ 32 | top:20px !important; 33 | } 34 | .ant-modal-mask{ 35 | display: none !important; 36 | } 37 | 38 | .editor .draggable { 39 | border:1px dashed #f7bebe; 40 | } 41 | -------------------------------------------------------------------------------- /src/pages/coms/Layout.js: -------------------------------------------------------------------------------- 1 | import Common_Style from './common/style.config'; 2 | console.log({ 3 | "type":"Layout", 4 | "title":"外层布局块", 5 | can_place:true, 6 | props:{ 7 | style:{ 8 | minHeight:20, 9 | padding:"20px", 10 | backgroundColor:"#fff" 11 | } 12 | }, 13 | config:{ 14 | style:{ 15 | padding:{ 16 | text:"内间距", 17 | type:"4-value" 18 | }, 19 | margin:{ 20 | text:"外边距", 21 | type:"4-value" 22 | }, 23 | ...Common_Style 24 | } 25 | } 26 | }) 27 | export default { 28 | "type":"Layout", 29 | "title":"外层布局块", 30 | can_place:true, 31 | props:{ 32 | style:{ 33 | minHeight:20, 34 | padding:"20px", 35 | backgroundColor:"#fff" 36 | } 37 | }, 38 | config:{ 39 | style:{ 40 | padding:{ 41 | text:"内间距", 42 | type:"4-value" 43 | }, 44 | margin:{ 45 | text:"外边距", 46 | type:"4-value" 47 | }, 48 | ...Common_Style 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/pages/preview.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | var antd =require('../components/index.jsx'); 3 | import Editor from './editor'; 4 | 5 | class Preview extends Editor { 6 | constructor() { 7 | super(); 8 | this.state = { 9 | dependComponents:[], 10 | comNowIndex:0, 11 | indent_space:'', 12 | data:[{ 13 | type:'Layout', 14 | title:'布局块', 15 | can_place:true, 16 | props:{ 17 | style:{ 18 | paddingBottom:'100px', 19 | background:"#fff" 20 | } 21 | }, 22 | }] 23 | } 24 | var previewData = localStorage.getItem('preview_data') 25 | this.state.data = JSON.parse(previewData) 26 | setTimeout(()=>{ 27 | this.forceUpdate(); 28 | },100) 29 | } 30 | render() { 31 | return (
32 |
33 | { 34 | this.renderJSON(this.state.data) 35 | } 36 |
37 |
38 | 39 |
40 |
) 41 | } 42 | 43 | } 44 | 45 | export default Preview; 46 | -------------------------------------------------------------------------------- /src/components/RJSelect.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Select } from 'antd'; 3 | 4 | const Option = Select.Option; 5 | 6 | class RJSelect extends React.Component { 7 | constructor() { 8 | super(); 9 | } 10 | 11 | /** 12 | * 获取 Option Jsx 组件数组 13 | * @return [type] [description] 14 | */ 15 | _getOptions(data) { 16 | if (Array.isArray(data) && data.length > 0) { 17 | return data.map((item, index) => { 18 | if (typeof item !== 'object') { 19 | return ( 20 | 23 | ); 24 | } 25 | 26 | return ( 27 | 30 | ); 31 | }); 32 | } else { 33 | console.warn('Warnnig: selectData must be Array'); 34 | return null; 35 | } 36 | } 37 | 38 | render() { 39 | let selectData = this.props.selectData; 40 | let options = this._getOptions(selectData); 41 | 42 | return ; 43 | } 44 | } 45 | 46 | export default RJSelect; 47 | -------------------------------------------------------------------------------- /src/pages/coms/RJMenu.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "type":"RJMenu", 3 | "title":"导航菜单", 4 | props:{ 5 | mode:"horizontal", 6 | theme:"light", 7 | items:[{ 8 | key:'1', 9 | icon:'mail', 10 | title:'第一级菜单', 11 | disabled:false, 12 | link:'#', 13 | 14 | },{ 15 | key:'2', 16 | icon:'plus', 17 | title:'第1级菜单', 18 | disabled:false, 19 | link:'#', 20 | },{ 21 | key:'3', 22 | title:'第3级菜单', 23 | disabled:true, 24 | link:'#', 25 | }] 26 | }, 27 | config:{ 28 | mode:{ 29 | text:'模式', 30 | enum:[ 31 | 'inline', 32 | 'horizontal', 33 | 'vertical' 34 | ] 35 | }, 36 | theme:{ 37 | text:'主题色', 38 | enum:[ 39 | 'light', 40 | 'dark' 41 | ] 42 | }, 43 | items:{ 44 | text:"数据源", 45 | enumobject:[ 46 | { 47 | title: '菜单文本', 48 | dataIndex: 'title', 49 | type:"String" 50 | }, 51 | { 52 | title: '链接', 53 | dataIndex: 'link', 54 | type:"String" 55 | }, 56 | { 57 | title: '图标', 58 | dataIndex: 'icon', 59 | type:"String" 60 | }, 61 | { 62 | title: '是否禁用', 63 | dataIndex: 'disabled', 64 | type:"Boolean" 65 | } 66 | ] 67 | } 68 | 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/pages/coms/Row4.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "type":"Row", 3 | "title":"4列栅格", 4 | childrens:[ 5 | { 6 | type:'Col', 7 | title:'栅格单元', 8 | can_place:true, 9 | props:{ 10 | span:6, 11 | style:{ 12 | minHeight:30, 13 | }, 14 | }, 15 | config:{ 16 | span:{ 17 | text:"栅格", 18 | enum:[ 19 | 2,4,6,8,10,12,14,16,18,20,22,24 20 | ] 21 | } 22 | } 23 | }, 24 | { 25 | type:'Col', 26 | title:'栅格单元', 27 | can_place:true, 28 | props:{ 29 | span:6, 30 | style:{ 31 | minHeight:30, 32 | }, 33 | }, 34 | config:{ 35 | span:{ 36 | text:"栅格", 37 | enum:[ 38 | 2,4,6,8,10,12,14,16,18,20,22,24 39 | ] 40 | } 41 | } 42 | }, 43 | { 44 | type:'Col', 45 | title:'栅格单元', 46 | can_place:true, 47 | props:{ 48 | span:6, 49 | style:{ 50 | minHeight:30, 51 | }, 52 | }, 53 | config:{ 54 | span:{ 55 | text:"栅格", 56 | enum:[ 57 | 2,4,6,8,10,12,14,16,18,20,22,24 58 | ] 59 | } 60 | } 61 | }, 62 | { 63 | type:'Col', 64 | title:'栅格单元', 65 | can_place:true, 66 | props:{ 67 | span:6, 68 | style:{ 69 | minHeight:30, 70 | }, 71 | }, 72 | config:{ 73 | span:{ 74 | text:"栅格", 75 | enum:[ 76 | 2,4,6,8,10,12,14,16,18,20,22,24 77 | ] 78 | } 79 | } 80 | } 81 | ], 82 | props:{ 83 | 84 | }, 85 | 86 | } 87 | -------------------------------------------------------------------------------- /src/components/es5/image.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _antd = require('antd'); 8 | 9 | var IMAGE_PREFIX = 'http://img.souche.com/'; 10 | 11 | /** 12 | * 从 antd 的 Upload 获取图片地址 13 | */ 14 | function getImagesUrl(files) { 15 | var UPLOAD_ERRORS = []; 16 | 17 | // 过滤掉没有返回 response 的 file 18 | files = files.filter(function (file) { 19 | return !!file.response; 20 | }); 21 | 22 | var images = files.map(function (file) { 23 | if (!file.response.success) { 24 | var failMessage = '[' + file.name + ']: ' + file.response.msg; 25 | UPLOAD_ERRORS.push(failMessage); 26 | 27 | return new Error(failMessage); 28 | } 29 | 30 | // 后台返回了完整的 url 31 | if (file.response.data && file.response.data.fullFilePath) { 32 | return file.response.data.fullFilePath; 33 | } 34 | 35 | // 后台只返回了 path,需要自己拼域名 36 | if (file.response.data && file.response.data.relativeFilepath) { 37 | return IMAGE_PREFIX + file.response.data.relativeFilepath; 38 | } 39 | }); 40 | 41 | // 错误提示 42 | if (UPLOAD_ERRORS.length > 0) { 43 | _antd.Modal.error({ 44 | title: 'Error', 45 | content: UPLOAD_ERRORS.join('\n\n') 46 | }); 47 | } 48 | 49 | // 过滤掉错误的文件 50 | images = images.filter(function (image) { 51 | return image instanceof Error === false; 52 | }); 53 | 54 | // 只有一张图片的话,直接返回 url;空数组的话返回 '' 55 | if (images.length < 2) { 56 | return images[0] || ''; 57 | } 58 | 59 | return images; 60 | } 61 | 62 | exports.default = { 63 | getImagesUrl: getImagesUrl 64 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "antd-visual-editor", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "antd": "^2.10.2", 7 | "assets-webpack-plugin": "^3.5.1", 8 | "babel-plugin-transform-object-assign": "^6.22.0", 9 | "html-webpack-plugin": "^2.28.0", 10 | "lodash": "^4.17.4", 11 | "rc-color-picker": "^1.1.5", 12 | "react": "^15.5.4", 13 | "react-dom": "^15.5.4", 14 | "react-drag-and-drop": "^2.4.0", 15 | "react-get-element": "^0.9.0", 16 | "react-native-listener": "^1.0.2", 17 | "react-router": "^2.8.1" 18 | }, 19 | "devDependencies": { 20 | "babel-core": "^6.23.1", 21 | "babel-loader": "^6.3.2", 22 | "babel-plugin-import": "^1.1.1", 23 | "babel-plugin-transform-class-properties": "^6.22.0", 24 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 25 | "babel-plugin-transform-runtime": "^6.22.0", 26 | "babel-preset-es2015": "6.22.0", 27 | "babel-preset-react": "^6.23.0", 28 | "babel-preset-stage-3": "^6.24.1", 29 | "babel-runtime": "^6.22.0", 30 | "css-loader": "0.26.1", 31 | "eslint": "^3.17.1", 32 | "extract-text-webpack-plugin": "^v2.0.0-rc.1", 33 | "file-loader": "^0.10.0", 34 | "glob": "^7.1.1", 35 | "html-webpack-plugin": "^2.26.0", 36 | "less": "^2.7.2", 37 | "less-loader": "^2.2.3", 38 | "react-hot-loader": "^3.0.0-beta.6", 39 | "style-loader": "0.13.1", 40 | "url-loader": "0.5.7", 41 | "webpack": "^2.2.1", 42 | "webpack-cleanup-plugin": "^0.4.2", 43 | "webpack-dashboard": "^0.3.0", 44 | "webpack-dev-server": "^2.4.1" 45 | }, 46 | "scripts": { 47 | "i": "npm install", 48 | "build": "webpack --config webpack.production.config.js --progress --profile --colors", 49 | "start": "webpack-dev-server --progress --profile --colors" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /webpack.loaders.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | test: /\.jsx$/, 4 | loader: "babel-loader", 5 | query: { 6 | presets: [ 7 | 'es2015', 8 | 'react' 9 | ].map(dep => require.resolve(`babel-preset-${dep}`)), 10 | plugins: [ 11 | 'transform-runtime', 12 | 'transform-class-properties', 13 | ].map(dep => require.resolve(`babel-plugin-${dep}`)) 14 | } 15 | }, 16 | { 17 | test: /\.js$/, 18 | exclude: /(node_modules|bower_components|public\/)/, 19 | loader: "babel-loader" 20 | }, 21 | { 22 | test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, 23 | exclude: /(node_modules|bower_components)/, 24 | loader: "file-loader" 25 | }, 26 | { 27 | test: /\.(woff|woff2)$/, 28 | exclude: /(node_modules|bower_components)/, 29 | loader: "url-loader?prefix=font/&limit=5000" 30 | }, 31 | { 32 | test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, 33 | exclude: /(node_modules|bower_components)/, 34 | loader: "url-loader?limit=10000&mimetype=application/octet-stream" 35 | }, 36 | { 37 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, 38 | exclude: /(node_modules|bower_components)/, 39 | loader: "url-loader?limit=10000&mimetype=image/svg+xml" 40 | }, 41 | { 42 | test: /\.gif/, 43 | exclude: /(node_modules|bower_components)/, 44 | loader: "url-loader?limit=10000&mimetype=image/gif" 45 | }, 46 | { 47 | test: /\.jpg/, 48 | exclude: /(node_modules|bower_components)/, 49 | loader: "url-loader?limit=10000&mimetype=image/jpg" 50 | }, 51 | { 52 | test: /\.png/, 53 | exclude: /(node_modules|bower_components)/, 54 | loader: "url-loader?limit=10000&mimetype=image/png" 55 | } 56 | ]; 57 | -------------------------------------------------------------------------------- /webpack.production.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const loaders = require('./webpack.loaders'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const WebpackCleanupPlugin = require('webpack-cleanup-plugin'); 6 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 7 | const glob = require('glob'); 8 | loaders.push({ 9 | test: /\.css$/, 10 | loaders: ['style-loader', 'css-loader?importLoaders=1'], 11 | exclude: ['node_modules'] 12 | }); 13 | 14 | loaders.push({ 15 | test: /\.less$/, 16 | loaders: ['style-loader', 'css-loader?importLoaders=1', 'less-loader'], 17 | exclude: ['node_modules'] 18 | }); 19 | 20 | const srcFiles = glob.sync('./src/index.jsx'); 21 | const entries = {}; 22 | srcFiles.forEach(entry => { 23 | let name = entry.replace('./src/', '').replace('.jsx', '').replace('.js', ''); 24 | 25 | entries[name] = entry; 26 | }); 27 | module.exports = { 28 | entry: entries, 29 | output: { 30 | publicPath: './', 31 | path: path.join(__dirname, './docs'), 32 | filename: '[name]_[hash].js' 33 | }, 34 | resolve: { 35 | extensions: ['.js', '.jsx'] 36 | }, 37 | module: { 38 | loaders 39 | }, 40 | plugins: [ 41 | new webpack.DefinePlugin({ 42 | 'process.env': { 43 | NODE_ENV: '"production"' 44 | } 45 | }), 46 | new webpack.optimize.UglifyJsPlugin({ 47 | compress: { 48 | warnings: false, 49 | screw_ie8: true, 50 | drop_console: false, 51 | drop_debugger: false 52 | } 53 | }), 54 | new webpack.optimize.OccurrenceOrderPlugin(), 55 | new ExtractTextPlugin({ 56 | filename: 'style.css', 57 | allChunks: true 58 | }), 59 | new HtmlWebpackPlugin({ 60 | title:"Antd Editor", 61 | template:"public/template.html" 62 | }) 63 | ] 64 | }; 65 | -------------------------------------------------------------------------------- /src/pages/coms/Table_Data.js: -------------------------------------------------------------------------------- 1 | 2 | var _ = require('lodash'); 3 | import Common_Style from './common/style.config'; 4 | 5 | var com = { 6 | "type":"Table", 7 | "title":"纯数据表格", 8 | "props":{ 9 | "columns":[ 10 | { 11 | title: '姓名', 12 | dataIndex: 'name', 13 | key: 'name', 14 | }, { 15 | title: '年龄', 16 | dataIndex: 'age', 17 | key: 'age', 18 | }, { 19 | title: '住址', 20 | dataIndex: 'address', 21 | key: 'address', 22 | } 23 | ], 24 | "dataSource":[{ 25 | name: '胡彦斌', 26 | age: 32, 27 | address: '西湖区湖底公园1号' 28 | },{ 29 | name: '胡彦斌', 30 | age: 32, 31 | address: '西湖区湖底公园1号' 32 | }], 33 | bordered:true, 34 | size:"small", 35 | style:{ 36 | margin:"0px", 37 | padding:"0px" 38 | }, 39 | showRowSelection:false, 40 | pagination:false, 41 | rowSelection:{ 42 | type:'relative', 43 | target:'showRowSelection', 44 | true:{ 45 | selectedRowKeys:[], 46 | onChange:(selectedRowKeys)=>{ 47 | 48 | } 49 | }, 50 | false:null 51 | } 52 | }, 53 | config:{ 54 | columns:{ 55 | text:"列管理", 56 | enumobject:[ 57 | { 58 | title: '列文本', 59 | dataIndex: 'title', 60 | type:"String" 61 | }, 62 | { 63 | title: '列key', 64 | dataIndex: 'dataIndex', 65 | type:"String" 66 | } 67 | ] 68 | }, 69 | dataSource:{ 70 | text:"值管理", 71 | enumobject:{ 72 | type:'relative_props_object', 73 | target:'columns' 74 | } 75 | }, 76 | showRowSelection:{ 77 | text:"是否显示选择框", 78 | type:'Boolean' 79 | }, 80 | pagination:{ 81 | text:"是否显示分页", 82 | type:'Boolean' 83 | }, 84 | style:{ 85 | padding:{ 86 | text:"内间距", 87 | type:"4-value" 88 | }, 89 | margin:{ 90 | text:"外边距", 91 | type:"4-value" 92 | }, 93 | ...Common_Style 94 | }, 95 | } 96 | 97 | }; 98 | export default com; 99 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const webpack = require('webpack'); 3 | const path = require('path'); 4 | const loaders = require('./webpack.loaders.js'); 5 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | const DashboardPlugin = require('webpack-dashboard/plugin'); 7 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 8 | const glob = require('glob'); 9 | 10 | const HOST = process.env.HOST || "127.0.0.1"; 11 | const PORT = process.env.PORT || "3000"; 12 | 13 | loaders.push({ 14 | test: /\.css$/, 15 | loaders: ['style-loader', 'css-loader?importLoaders=1'], 16 | exclude: ['node_modules'] 17 | }); 18 | 19 | loaders.push({ 20 | test: /\.less$/, 21 | loaders: ['style-loader', 'css-loader?importLoaders=1', 'less-loader'], 22 | exclude: ['node_modules'] 23 | }); 24 | 25 | 26 | const srcFiles = glob.sync('./src/**/*.+(jsx|js)'); 27 | const entries = {}; 28 | srcFiles.forEach(entry => { 29 | let name = entry.replace('./src/', '').replace('.jsx', '').replace('.js', ''); 30 | 31 | entries[name] = entry; 32 | }); 33 | // const srcFiles2 = glob.sync('./node_modules/@souche-f2e/**/*.jsx'); 34 | // srcFiles2.forEach(entry => { 35 | // let name = entry.replace('./node_modules/', '').replace('.jsx', ''); 36 | // 37 | // entries[name] = entry; 38 | // }); 39 | 40 | // const htmls = srcFiles.map(entry => { 41 | // let name = entry.replace('./pages/', '').replace('.jsx', ''); 42 | // 43 | // return new HtmlWebpackPlugin({ 44 | // filename: `${name}.html`, 45 | // template: './template.html', 46 | // chunks: [ name ] 47 | // }) 48 | // }); 49 | console.log(entries) 50 | module.exports = { 51 | entry: entries, 52 | devtool: process.env.WEBPACK_DEVTOOL || 'eval-source-map', 53 | output: { 54 | publicPath: '/', 55 | path: path.join(__dirname, 'public'), 56 | filename: '[name].js' 57 | }, 58 | resolve: { 59 | extensions: ['.js', '.jsx'] 60 | }, 61 | module: { 62 | loaders 63 | }, 64 | devServer: { 65 | contentBase: "./public", 66 | // do not print bundle build stats 67 | noInfo: true, 68 | // enable HMR 69 | hot: true, 70 | // embed the webpack-dev-server runtime into the bundle 71 | inline: true, 72 | // serve index.html in place of 404 responses to allow HTML5 history 73 | historyApiFallback: true, 74 | port: PORT, 75 | host: HOST, 76 | 77 | }, 78 | 79 | plugins: [ 80 | new webpack.NoEmitOnErrorsPlugin(), 81 | new webpack.HotModuleReplacementPlugin(), 82 | new ExtractTextPlugin({ 83 | filename: 'style.css', 84 | allChunks: true 85 | }), 86 | new HtmlWebpackPlugin({ 87 | title:"Antd Editor", 88 | template:"public/template.html" 89 | }), 90 | new DashboardPlugin() 91 | ] 92 | }; 93 | -------------------------------------------------------------------------------- /src/pages/coms/Row8.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "type":"Row", 3 | "title":"8列栅格", 4 | childrens:[ 5 | { 6 | type:'Col', 7 | title:'栅格单元', 8 | can_place:true, 9 | props:{ 10 | span:3, 11 | style:{ 12 | minHeight:30, 13 | }, 14 | }, 15 | config:{ 16 | span:{ 17 | text:"栅格", 18 | enum:[ 19 | 2,4,6,8,10,12,14,16,18,20,22,24 20 | ] 21 | } 22 | } 23 | }, 24 | { 25 | type:'Col', 26 | title:'栅格单元', 27 | can_place:true, 28 | props:{ 29 | span:3, 30 | style:{ 31 | minHeight:30, 32 | }, 33 | }, 34 | config:{ 35 | span:{ 36 | text:"栅格", 37 | enum:[ 38 | 2,4,6,8,10,12,14,16,18,20,22,24 39 | ] 40 | } 41 | } 42 | }, 43 | { 44 | type:'Col', 45 | title:'栅格单元', 46 | can_place:true, 47 | props:{ 48 | span:3, 49 | style:{ 50 | minHeight:30, 51 | }, 52 | }, 53 | config:{ 54 | span:{ 55 | text:"栅格", 56 | enum:[ 57 | 2,4,6,8,10,12,14,16,18,20,22,24 58 | ] 59 | } 60 | } 61 | }, 62 | { 63 | type:'Col', 64 | title:'栅格单元', 65 | can_place:true, 66 | props:{ 67 | span:3, 68 | style:{ 69 | minHeight:30, 70 | }, 71 | }, 72 | config:{ 73 | span:{ 74 | text:"栅格", 75 | enum:[ 76 | 2,4,6,8,10,12,14,16,18,20,22,24 77 | ] 78 | } 79 | } 80 | }, 81 | { 82 | type:'Col', 83 | title:'栅格单元', 84 | can_place:true, 85 | props:{ 86 | span:3, 87 | style:{ 88 | minHeight:30, 89 | }, 90 | }, 91 | config:{ 92 | span:{ 93 | text:"栅格", 94 | enum:[ 95 | 2,4,6,8,10,12,14,16,18,20,22,24 96 | ] 97 | } 98 | } 99 | }, 100 | { 101 | type:'Col', 102 | title:'栅格单元', 103 | can_place:true, 104 | props:{ 105 | span:3, 106 | style:{ 107 | minHeight:30, 108 | }, 109 | }, 110 | config:{ 111 | span:{ 112 | text:"栅格", 113 | enum:[ 114 | 2,4,6,8,10,12,14,16,18,20,22,24 115 | ] 116 | } 117 | } 118 | }, 119 | { 120 | type:'Col', 121 | title:'栅格单元', 122 | can_place:true, 123 | props:{ 124 | span:3, 125 | style:{ 126 | minHeight:30, 127 | }, 128 | }, 129 | config:{ 130 | span:{ 131 | text:"栅格", 132 | enum:[ 133 | 2,4,6,8,10,12,14,16,18,20,22,24 134 | ] 135 | } 136 | } 137 | }, 138 | { 139 | type:'Col', 140 | title:'栅格单元', 141 | can_place:true, 142 | props:{ 143 | span:3, 144 | style:{ 145 | minHeight:30, 146 | }, 147 | }, 148 | config:{ 149 | span:{ 150 | text:"栅格", 151 | enum:[ 152 | 2,4,6,8,10,12,14,16,18,20,22,24 153 | ] 154 | } 155 | } 156 | } 157 | ], 158 | props:{ 159 | 160 | }, 161 | 162 | } 163 | -------------------------------------------------------------------------------- /src/pages/coms/Table.js: -------------------------------------------------------------------------------- 1 | 2 | var _ = require('lodash'); 3 | import Common_Style from './common/style.config'; 4 | 5 | var com = { 6 | "type":"Table", 7 | "title":"表格", 8 | "sub_type":"table_container", 9 | "props":{ 10 | "columns":[{ 11 | title: '姓名', 12 | dataIndex: 'name', 13 | key: 'name', 14 | childrens:[{ 15 | type:"div", 16 | title:"通用布局块", 17 | is_native:true, 18 | can_place:true, 19 | props:{ 20 | style:{ 21 | minHeight:20, 22 | padding:"0px", 23 | } 24 | }, 25 | config:{ 26 | padding:{ 27 | text:"内间距", 28 | type:"4-value" 29 | }, 30 | margin:{ 31 | text:"外边距", 32 | type:"4-value" 33 | }, 34 | ...Common_Style 35 | } 36 | }] 37 | 38 | }, { 39 | title: '年龄', 40 | dataIndex: 'age', 41 | key: 'age', 42 | childrens:[{ 43 | type:"div", 44 | title:"通用布局块", 45 | is_native:true, 46 | can_place:true, 47 | props:{ 48 | style:{ 49 | minHeight:20, 50 | padding:"0px", 51 | } 52 | }, 53 | config:{ 54 | padding:{ 55 | text:"内间距", 56 | type:"4-value" 57 | }, 58 | margin:{ 59 | text:"外边距", 60 | type:"4-value" 61 | }, 62 | ...Common_Style 63 | } 64 | }] 65 | }, { 66 | title: '住址', 67 | dataIndex: 'address', 68 | key: 'address', 69 | childrens:[{ 70 | type:"div", 71 | title:"通用布局块", 72 | is_native:true, 73 | can_place:true, 74 | props:{ 75 | style:{ 76 | minHeight:20, 77 | padding:"0px", 78 | } 79 | }, 80 | config:{ 81 | padding:{ 82 | text:"内间距", 83 | type:"4-value" 84 | }, 85 | margin:{ 86 | text:"外边距", 87 | type:"4-value" 88 | }, 89 | ...Common_Style 90 | } 91 | }] 92 | }], 93 | "dataSource":[{ 94 | key: '1', 95 | name: '胡彦斌', 96 | age: 32, 97 | address: '西湖区湖底公园1号' 98 | }], 99 | bordered:true, 100 | size:"small", 101 | style:{ 102 | margin:"0px", 103 | padding:"0px" 104 | }, 105 | showRowSelection:false, 106 | pagination:false, 107 | rowSelection:{ 108 | type:'relative', 109 | target:'showRowSelection', 110 | true:{ 111 | selectedRowKeys:[], 112 | onChange:(selectedRowKeys)=>{ 113 | 114 | } 115 | }, 116 | false:null 117 | } 118 | }, 119 | config:{ 120 | columns:{ 121 | text:"列管理", 122 | enumobject:[ 123 | { 124 | title: '列文本', 125 | dataIndex: 'title', 126 | type:"String" 127 | }, 128 | { 129 | title: '列key', 130 | dataIndex: 'dataIndex', 131 | type:"String" 132 | } 133 | ] 134 | }, 135 | 136 | showRowSelection:{ 137 | text:"是否显示选择框", 138 | type:'Boolean' 139 | }, 140 | pagination:{ 141 | text:"是否显示分页", 142 | type:'Boolean' 143 | }, 144 | style:{ 145 | padding:{ 146 | text:"内间距", 147 | type:"4-value" 148 | }, 149 | margin:{ 150 | text:"外边距", 151 | type:"4-value" 152 | }, 153 | ...Common_Style 154 | }, 155 | } 156 | 157 | }; 158 | export default com; 159 | -------------------------------------------------------------------------------- /src/components/RJMenu.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Menu, Icon } from 'antd'; 3 | 4 | const PropTypes = React.PropTypes; 5 | 6 | class RJMenu extends React.Component { 7 | constructor() { 8 | super(); 9 | /** 10 | this.state = { 11 | data:{ 12 | mode:"horizontal", 13 | items:[{ 14 | key:'1', 15 | icon:'mail', 16 | title:'第一级菜单', 17 | disabled:false, 18 | link:'#', 19 | subs:[ 20 | { 21 | key:'', 22 | icon:'mail', 23 | title:'第一级菜单', 24 | type:"menu" 25 | }, 26 | { 27 | type:'group', 28 | title:'分组1', 29 | subs:[ 30 | { 31 | key:'', 32 | icon:'mail', 33 | title:'第一级菜单' 34 | }, 35 | { 36 | key:'', 37 | icon:'mail', 38 | title:'第一级菜单' 39 | } 40 | ] 41 | }, 42 | { 43 | type:'group', 44 | title:'分组2', 45 | subs:[ 46 | { 47 | key:'', 48 | icon:'mail', 49 | title:'第一级菜单' 50 | }, 51 | { 52 | key:'', 53 | icon:'mail', 54 | title:'第一级菜单' 55 | } 56 | ] 57 | } 58 | ] 59 | },{ 60 | key:'2', 61 | icon:'plus', 62 | title:'第1级菜单', 63 | disabled:false, 64 | link:'#', 65 | },{ 66 | key:'3', 67 | title:'第1级菜单', 68 | disabled:true, 69 | link:'#', 70 | }] 71 | } 72 | } 73 | */ 74 | } 75 | 76 | render() { 77 | var data = this.props; 78 | return ( 79 | 80 | {data.items.map(v => { 81 | return v.subs ? ( 82 | 85 | {v.icon ? : null} 86 | {v.title} 87 | 88 | } 89 | > 90 | {v.subs.map(s => { 91 | return s.type === 'group' ? ( 92 | 93 | {s.subs.map(ss => { 94 | return ( 95 | 96 | {ss.icon ? : null} 97 | {ss.title} 98 | 99 | ); 100 | })} 101 | 102 | ) : ( 103 | 104 | {s.icon ? : null} 105 | {s.title} 106 | 107 | ); 108 | })} 109 | 110 | ) : ( 111 | 112 | {v.icon ? : null} 113 | {v.title} 114 | 115 | ); 116 | })} 117 | 118 | ); 119 | } 120 | } 121 | 122 | export default RJMenu; 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 一个 可视化实时渲染的 ant-design 页面搭建工具 2 | 3 | > 此项目是上古项目(2017年),代码基本很难维护,现在发布出来仅供参考思路,感兴趣的可以根据原理重构一版,实现一个更完备的可视化编辑 4 | 5 | 6 | ## 线上实例 7 | 8 | https://yu-tou.github.io/antd-visual-editor/ 9 | 10 | 托管在 github,第一次加载会比较慢 11 | 12 | 截图: 13 | 14 | ![](https://img.souche.com/0a3a88462435f20952346980e3ee4df5.png) 15 | 16 | ## 运行 17 | 18 | ```bash 19 | npm run build; 20 | npm run start; 21 | # (已修复)因为我不太懂 webpack ,不太会配置,这个项目修改代码后实时生效还有问题。。求 pr 22 | ``` 23 | 24 | ## 特性 25 | 26 | * 可视化编辑,同时实时生成结果代码,还可以单独预览 27 | * 丰富的数据编辑能力,可以编辑组件的二维属性 28 | * 组件可嵌套 29 | * 自适应布局 30 | * 除了 antd 的组件,还有一些原生 html 元素可使用 31 | 32 | ## 原理解析 33 | 34 | ### 1.如何实现实时编辑 35 | 36 | 第一步,抽象整个可视化工作台的数据表达,无非是放了一个什么组件在什么位置,这个组件的父组件是谁,这个组件的属性是什么 37 | 38 | 如下图: 39 | 40 | ![](https://img.souche.com/c1c1605094e3a8bcaa1118ce00f8fcc3.png) 41 | 42 | 一个组件的基础定义: 43 | 44 | ``` 45 | title 组件名 46 | type 组件类型(组件真实类名) 47 | can_place 组件是否可以包含子组件 48 | children 组件的子组件,数组类型 49 | is_native 组件是否是原生 html 元素 50 | config 组件可用的配置信息 51 | props 组件配置信息的值,包含样式和属性等 52 | ``` 53 | 54 | 根据这些值,我们就可以渲染和编辑组件了,编辑组件后, 55 | 会有一个大的表示画布当前状态的数据结构存储到 state 中, 56 | 另外,有一个方法可以根据这个数据结构渲染出整个画布, 57 | 所以每次有任何编辑动作之后,我们会触发 forceUpdate,重新绘制画布 58 | 也就是说,添加组件,编辑属性,和画布的显示是分离的,中间由一个大的数据结构连接(就是图片里这个) 59 | 60 | ### 2.如何反向生成 react 代码 61 | 62 | 根据上图中的数据结构,反向遍历,可轻易的生成 React 代码 63 | 64 | ### 3.如何定义组件可用的配置 65 | 66 | 在 pages/coms/xxx 里面定义一个组件的可用配置,然后即可在主界面中选择组件后在右侧"属性编辑区"中编辑属性。 67 | 68 | 来看看我们可以定义哪些属性吧 69 | 70 | 以一个按钮为例 71 | 72 | ```javascript 73 | export default { 74 | "type": "Button", 75 | "title": "按钮", 76 | "props": { 77 | type: 'primary', // 定义可以配置的 props 78 | content: '按钮一只', // 定义可以配置的 props 79 | style: { // 定义可以配置的样式 80 | margin: "0px 10px 0px 0px" 81 | } 82 | }, 83 | config: { // 可用的配置项 84 | type: { // type 这个配置的描述 85 | text: "主题", // 配置的标题 86 | enum: [ // 可用的枚举,配置时会显示成下拉框 87 | 'primary', 88 | 'default', 89 | 'dashed', 90 | 'danger' 91 | ] 92 | }, 93 | icon: { 94 | text: "图标", 95 | }, 96 | content: { 97 | text: '文案', 98 | }, 99 | style: { // 可用的样式配置 100 | width: { 101 | text: "宽度", 102 | }, 103 | margin: { 104 | text: "外边距", 105 | type: "4-value" // 一种定制类型,会渲染成 4 个输入框 106 | } 107 | } 108 | }, 109 | } 110 | ``` 111 | 112 | 这是最基本的配置项,只能适用于最基本的组件,但是遇到像 table 或者 Breadcrumb 这种组件就不行了 113 | 114 | ### 4.高级配置(二维数据) 115 | 116 | 以 Breadcrumb 为例,他有一个数据源的属性,数据源是一个数组+对象的混合表达,这种组件不少,应该如何定义呢 117 | 118 | ```javascript 119 | export default { 120 | "type":"Breadcrumb", 121 | "title":"面包屑", 122 | props:{ 123 | routes:[ // 这里是数据源的属性,和默认值 124 | { 125 | breadcrumbName:"一级目录", 126 | path:"#", 127 | key:1 128 | }, 129 | { 130 | breadcrumbName:"二级目录", 131 | path:"#", 132 | key:2 133 | } 134 | ] 135 | }, 136 | config:{ 137 | routes:{ // 如何表达这个属性应该如何配置 138 | text:"项目配置", 139 | enumobject:[{ // 一种新的类型,enumobject,对象枚举 140 | key:1, 141 | dataIndex:"breadcrumbName", // 枚举的对象的第一个 key 是什么 142 | title:"显示文本", // 枚举的对象的第一个 key 的文本描述 143 | type:'String', // 枚举的对象的第一个 key 的类型 144 | },{ 145 | key:2, 146 | dataIndex:"path", // 枚举的对象的第二个 key 是什么 147 | title:"链接", // 枚举的对象的第二个 key 的文本描述 148 | type:'String', //枚举的对象的第二个 key 的类型 149 | }] 150 | } 151 | } 152 | } 153 | ``` 154 | 155 | 最终的属性编辑区: 156 | 157 | ![](https://img.souche.com/397b944593c4506a2085955fffee09d7.png) 158 | 159 | 即可边界对象枚举属性 160 | 161 | ### 5.更复杂的组件 162 | 163 | 大家会发现,table 这种组件和上述的组件都不太一样,首先看纯数据表格 164 | 165 | ![](https://img.souche.com/6c17078da4321e4e2738941718fe17d8.png) 166 | 167 | 其实这里还好,只是 table 有两个属性,一个表达列的数据,一个表达行的数据,我们只需要两个对象枚举即可 168 | 169 | ```javascript 170 | { 171 | config:{ 172 | columns:{ 173 | text:"列管理", 174 | enumobject:[ 175 | { 176 | title: '列文本', 177 | dataIndex: 'title', 178 | type:"String" 179 | }, 180 | { 181 | title: '列key', 182 | dataIndex: 'dataIndex', 183 | type:"String" 184 | } 185 | ] 186 | }, 187 | dataSource:{ 188 | text:"值管理", 189 | enumobject:{ 190 | type:'relative_props_object', 191 | target:'columns' 192 | } 193 | } 194 | } 195 | } 196 | ``` 197 | 198 | 这里实现了一个 关联,可以把 dataSource 的配置和 columns 关联起来 (relative_props_object) 199 | 200 | ### 6.更更复杂的表格 201 | 202 | 如果只是数据,还好, 203 | 但是 table 里可以还可以嵌套其他组件,每行每列,想想是不是头疼。。如下图 204 | 205 | ![](https://img.souche.com/d201501fd416b798678a6d9f4ca44b6e.png) 206 | 207 | table 的每个 column 其实可以定义内部显示的元素,我们在默认值里就给他塞一个空的 layout 进去, 208 | 这样之后这里就会变成一个可以放置其他子元素的坑,具体不展开了,这里的逻辑比较复杂。 209 | 210 | 211 | -------------------------------------------------------------------------------- /src/components/RJImgUploader.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Upload, Icon, Modal } from 'antd'; 3 | 4 | import imageUtil from './es5/image.js'; 5 | 6 | class RJImgUploader extends React.Component { 7 | state = { 8 | fileList: [], 9 | previewVisible: false, 10 | previewImage: '', 11 | updatedDefaultValue: null, 12 | }; 13 | 14 | static propTypes = { 15 | onChange: React.PropTypes.func.isRequired, 16 | num: React.PropTypes.number, 17 | action: React.PropTypes.string.isRequired, 18 | defaultValue: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.array]), 19 | onRemove: React.PropTypes.func, 20 | }; 21 | 22 | componentDidMount() { 23 | let fileList = this.getDefauleFilelist(this.props.defaultValue); 24 | this.setState({ 25 | fileList: fileList, 26 | }); 27 | } 28 | 29 | onChange = ({ fileList }) => { 30 | // 当为done的时候,如果success 为false,不应该加入fileList中 31 | let files = fileList.filter( 32 | file => (file.status === 'done' && file.response && file.response.success) || file.status === 'uploading' 33 | ); 34 | this.setState({ fileList: files }); 35 | 36 | this.props.onChange(imageUtil.getImagesUrl(fileList)); 37 | }; 38 | 39 | handleCancel = () => 40 | this.setState({ 41 | previewVisible: false, 42 | }); 43 | 44 | handlePreview = file => { 45 | this.setState({ 46 | previewImage: file.url || file.thumbUrl, 47 | previewVisible: true, 48 | }); 49 | }; 50 | 51 | getDefauleFilelist = defaultValue => { 52 | if (!defaultValue) { 53 | return []; 54 | } 55 | 56 | if (Array.isArray(defaultValue)) { 57 | // 数组情况 ['http://img.souche.com/a.png', 'http://img.souche.com/b.png'] 58 | return defaultValue.map((value, index) => { 59 | return { 60 | /** 61 | * uid 不可为0 62 | * 否则在删除 uid 为0的图片时,会清空整个 fileList 63 | */ 64 | uid: index + 1, 65 | status: 'done', 66 | url: value, 67 | response: { 68 | success: true, 69 | }, 70 | }; 71 | }); 72 | } else { 73 | // 字符串情况 'http://img.souche.com/a.png,http://img.souche.com/b.png' 74 | if (defaultValue.indexOf(',') !== -1) { 75 | let t = defaultValue.split(','); 76 | 77 | return t.map((value, index) => { 78 | return { 79 | uid: index + 1, 80 | status: 'done', 81 | url: value, 82 | response: { 83 | success: true, 84 | }, 85 | }; 86 | }); 87 | } else { 88 | // 'http://img.souche.com/a.png' 89 | return [ 90 | { 91 | uid: -1, 92 | status: 'done', 93 | url: defaultValue, 94 | response: { 95 | success: true, 96 | }, 97 | }, 98 | ]; 99 | } 100 | } 101 | }; 102 | 103 | updateDefaultValue = (defaultValue, url) => { 104 | if (!defaultValue) { 105 | return null; 106 | } 107 | 108 | if (Array.isArray(defaultValue)) { 109 | // 数组情况 ['http://img.souche.com/a.png', 'http://img.souche.com/b.png'] 110 | return defaultValue.filter(defalutURL => { 111 | return defalutURL !== url; 112 | }); 113 | } else { 114 | // 字符串情况 'http://img.souche.com/a.png,http://img.souche.com/b.png' 115 | if (defaultValue.indexOf(',') !== -1) { 116 | let t = defaultValue.split(','); 117 | return t 118 | .filter(defalutURL => { 119 | return defalutURL !== url; 120 | }) 121 | .toString(); 122 | } else { 123 | // 'http://img.souche.com/a.png' 124 | if (defaultValue === url) { 125 | return null; 126 | } else { 127 | return defaultValue; 128 | } 129 | } 130 | } 131 | }; 132 | 133 | handleRemove = file => { 134 | this.props.onRemove({ file, updatedDefaultValues: this.updateDefaultValue(this.props.defaultValue, file.url) }); 135 | }; 136 | 137 | render() { 138 | const { previewVisible, previewImage, fileList } = this.state; 139 | 140 | const UploadButton = ( 141 |
142 | 143 |
上传图片
144 |
145 | ); 146 | 147 | let action = ''; 148 | if (this.props.action.indexOf('http') === 0) { 149 | action = this.props.action; 150 | } else { 151 | action = window.SERVER_URL + this.props.action; 152 | } 153 | 154 | let num = this.props.num; 155 | let accept = this.props.accept || 'image/*'; 156 | 157 | return ( 158 |
159 | 170 | {fileList.length >= num ? null : UploadButton} 171 | 172 | 173 | 174 | 175 |
176 | ); 177 | } 178 | } 179 | 180 | export default RJImgUploader; 181 | -------------------------------------------------------------------------------- /src/pages/ant-coms.js: -------------------------------------------------------------------------------- 1 | import Layout from './coms/Layout'; 2 | import Layout_Header from './coms/Layout.Header'; 3 | import Layout_Sider from './coms/Layout.Sider'; 4 | import Layout_Content from './coms/Layout.Content'; 5 | import Breadcrumb from './coms/Breadcrumb'; 6 | import Form from './coms/Form'; 7 | import Form_Item from './coms/Form.Item'; 8 | import Form_Item_Inline from './coms/Form.Item.Inline'; 9 | import Form_Item_Submit from './coms/Form.Item.Submit'; 10 | import Button from './coms/Button'; 11 | import Input from './coms/Input'; 12 | import DatePicker from './coms/DatePicker'; 13 | import Radio_Group from './coms/Radio.Group'; 14 | import Badge from './coms/Badge'; 15 | import Calendar from './coms/Calendar'; 16 | import Tag from './coms/Tag'; 17 | import Table from './coms/Table'; 18 | import Table_Data from './coms/Table_Data'; 19 | import InputNumber from './coms/InputNumber'; 20 | import Switch from './coms/Switch'; 21 | import Row2 from './coms/Row2'; 22 | import Row4 from './coms/Row4'; 23 | import Row8 from './coms/Row8'; 24 | import Table_Column from './coms/Table.Column'; 25 | import Modal from './coms/Modal'; 26 | import div from './coms/div'; 27 | 28 | import RJImageUploader from './coms/RJImgUploader'; 29 | import RJMenu from './coms/RJMenu'; 30 | import RJPagination from './coms/RJPagination'; 31 | import RJSelect from './coms/RJSelect'; 32 | 33 | var _ = require('lodash'); 34 | 35 | export default [ 36 | { 37 | "group_title": "栅格组件(横向百分比)", 38 | "coms": [ 39 | Row2, 40 | Row4, 41 | Row8 42 | ] 43 | }, 44 | { 45 | "group_title": "布局容器组件", 46 | "coms": [ 47 | div, 48 | Layout, 49 | Layout_Header, 50 | Layout_Sider, 51 | Layout_Content, 52 | { 53 | type: "div", 54 | title: "分割线", 55 | is_native: true, 56 | props: { 57 | style: { 58 | backgroundColor: "#bbb", 59 | height: '1px' 60 | } 61 | }, 62 | config: { 63 | backgroundColor: { 64 | text: "背景色", 65 | }, 66 | margin: { 67 | text: "外边距", 68 | type: "4-value" 69 | }, 70 | } 71 | }, 72 | 73 | ] 74 | }, 75 | { 76 | "group_title": "导引组件", 77 | "coms": [ 78 | Breadcrumb, 79 | RJMenu, 80 | 81 | { 82 | type: "Tabs", 83 | title: "Tabs", 84 | can_place: true, 85 | props: {}, 86 | childrens: [ 87 | { 88 | type: "Tabs.TabPane", 89 | title: "tab项", 90 | props: { 91 | tab: '测试', 92 | key: 1 93 | } 94 | } 95 | ] 96 | }, 97 | { 98 | type: "Tabs.TabPane", 99 | title: "tab项", 100 | props: { 101 | tab: '测试' 102 | } 103 | } 104 | ] 105 | }, 106 | { 107 | "group_title": "表单容器和表单项", 108 | "coms": [ 109 | Form, 110 | Form_Item, 111 | Form_Item_Submit, 112 | (() => { 113 | var inlineForm = _.cloneDeep(Form); 114 | inlineForm.title = '内联的表单容器' 115 | inlineForm.props.layout = 'inline' 116 | return inlineForm 117 | })(), 118 | Form_Item_Inline, 119 | Button, 120 | Input, 121 | InputNumber, 122 | RJSelect, 123 | Radio_Group, 124 | DatePicker, 125 | { 126 | type: "DatePicker.MonthPicker", 127 | title: "月份选择器", 128 | }, 129 | { 130 | type: "DatePicker.RangePicker", 131 | title: "日期范围选择器", 132 | }, 133 | { 134 | type: "TimePicker", 135 | title: "时间选择器", 136 | }, 137 | Switch, 138 | RJImageUploader 139 | ] 140 | }, 141 | { 142 | "group_title": "数据组件/图片/标签", 143 | "coms": [ 144 | Table, 145 | Table_Data, 146 | Badge, 147 | Calendar, 148 | Tag, 149 | RJPagination, 150 | { 151 | type: "img", 152 | title: "图片", 153 | is_native: true, 154 | props: { 155 | src: "http://dummyimage.com/200x100/894FC4/FFF.png&text=!", 156 | style: { 157 | width: 50, 158 | height: 50 159 | } 160 | }, 161 | config: { 162 | style: { 163 | width: { 164 | text: '宽度' 165 | }, 166 | height: { 167 | text: '高度' 168 | }, 169 | margin: { 170 | text: "外边距", 171 | type: "4-value" 172 | } 173 | } 174 | } 175 | } 176 | ] 177 | }, 178 | { 179 | group_title: "弹窗", 180 | coms: [ 181 | Modal 182 | ] 183 | }, { 184 | group_title: "文本组件/标题", 185 | coms: [ 186 | { 187 | type: "a", 188 | title: "链接", 189 | is_native: true, 190 | props: { 191 | content: "链接", 192 | style: { 193 | marginLeft: "10px" 194 | } 195 | }, 196 | config: { 197 | content: { 198 | text: "文本内容" 199 | }, 200 | style: { 201 | marginLeft: { 202 | text: "左边距" 203 | }, 204 | lineHeight: { 205 | text: "行高" 206 | }, 207 | fontSize: { 208 | text: "字体大小" 209 | }, 210 | color: { 211 | text: '字体颜色', 212 | type: 'color' 213 | }, 214 | } 215 | } 216 | }, 217 | { 218 | type: "h1", 219 | title: "标题H1", 220 | is_native: true, 221 | props: { 222 | content: "标题", 223 | style: {} 224 | }, 225 | config: { 226 | content: { 227 | text: "文本内容" 228 | }, 229 | style: { 230 | color: { 231 | text: '字体颜色', 232 | type: 'color' 233 | }, 234 | } 235 | } 236 | }, 237 | { 238 | type: "h2", 239 | title: "标题H2", 240 | is_native: true, 241 | props: { 242 | content: "标题", 243 | style: {} 244 | }, 245 | config: { 246 | content: { 247 | text: "文本内容" 248 | }, 249 | style: { 250 | color: { 251 | text: '字体颜色', 252 | type: 'color' 253 | }, 254 | } 255 | } 256 | }, 257 | { 258 | type: "h3", 259 | title: "标题H3", 260 | is_native: true, 261 | props: { 262 | content: "标题", 263 | style: {} 264 | }, 265 | config: { 266 | content: { 267 | text: "文本内容" 268 | }, 269 | style: { 270 | color: { 271 | text: '字体颜色', 272 | type: 'color' 273 | }, 274 | } 275 | } 276 | }, 277 | { 278 | type: "h4", 279 | title: "标题H4", 280 | is_native: true, 281 | props: { 282 | content: "标题", 283 | style: {} 284 | }, 285 | config: { 286 | content: { 287 | text: "文本内容" 288 | }, 289 | style: { 290 | color: { 291 | text: '字体颜色', 292 | type: 'color' 293 | }, 294 | } 295 | } 296 | }, 297 | { 298 | type: "h5", 299 | title: "标题H5", 300 | is_native: true, 301 | props: { 302 | content: "标题", 303 | style: {} 304 | }, 305 | config: { 306 | content: { 307 | text: "文本内容" 308 | }, 309 | style: { 310 | color: { 311 | text: '字体颜色', 312 | type: 'color' 313 | }, 314 | } 315 | } 316 | }, 317 | { 318 | type: "span", 319 | title: "内联文本", 320 | is_native: true, 321 | props: { 322 | content: "文本", 323 | style: {} 324 | }, 325 | config: { 326 | content: { 327 | text: "文本内容" 328 | }, 329 | style: { 330 | lineHeight: { 331 | text: "行高" 332 | }, 333 | fontSize: { 334 | text: "字体大小" 335 | }, 336 | color: { 337 | text: '字体颜色', 338 | type: 'color' 339 | }, 340 | } 341 | } 342 | }, 343 | { 344 | type: "div", 345 | title: "块状文本", 346 | is_native: true, 347 | props: { 348 | content: "文本", 349 | style: {} 350 | }, 351 | config: { 352 | content: { 353 | text: "文本内容" 354 | }, 355 | style: { 356 | fontSize: { 357 | text: "字体大小" 358 | }, 359 | lineHeight: { 360 | text: "行高" 361 | }, 362 | background: { 363 | text: '背景色', 364 | type: 'color' 365 | }, 366 | color: { 367 | text: '字体颜色', 368 | type: 'color' 369 | }, 370 | margin: { 371 | text: "外边距", 372 | type: '4-value' 373 | }, 374 | padding: { 375 | text: "内边距", 376 | type: '4-value' 377 | } 378 | } 379 | } 380 | } 381 | ] 382 | } 383 | ] 384 | -------------------------------------------------------------------------------- /src/pages/editor.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | var antd =require('./../components/index.jsx'); 3 | import antComponents from './ant-coms.js'; 4 | import { Collapse,Modal,Form,Table,Button } from 'antd'; 5 | const Panel = Collapse.Panel; 6 | import { Tag,Input,InputNumber,Alert,Select,Checkbox } from 'antd'; 7 | import _ from 'lodash'; 8 | import NativeListener from 'react-native-listener'; 9 | import ColorPicker from 'rc-color-picker'; 10 | 11 | import com_div from './coms/div'; 12 | 13 | import 'rc-color-picker/assets/index.css' 14 | class Editor extends Component { 15 | constructor() { 16 | super(); 17 | this.state = { 18 | dependComponents:[], 19 | draggingData:null, 20 | modal_visible:true, 21 | resultJSX:'', 22 | editCom:{}, 23 | value4EditResult:{}, // 属性编辑器中4值编辑的临时存储 24 | comNowIndex:1, // 用来给每个组件唯一编号 25 | activeCom:{}, 26 | editTarget:null, 27 | showAddList:false, 28 | hasBeginEdit:false, 29 | isDragLayer:false, 30 | addListData:{ 31 | 32 | }, 33 | mouse_x:0, 34 | mouse_y:0, 35 | dragdiv_x:0, 36 | dragdiv_y:0, 37 | layer_x:0, 38 | layer_y:0, 39 | layer_show:false, 40 | layer_w:0, 41 | layer_h:0, 42 | layer_isTop:true, 43 | indent_space:'', 44 | data:[ 45 | (function(){ 46 | var div = _.cloneDeep(com_div); 47 | div.props.style.height = 200; 48 | return div; 49 | })() 50 | ] 51 | }; 52 | var v = localStorage.getItem('cache_data'); 53 | if(v){ 54 | this.state.data = JSON.parse(v); 55 | } 56 | 57 | setTimeout(()=>{ 58 | this.setState({ 59 | hasBeginEdit:true 60 | }) 61 | },3000) 62 | } 63 | 64 | _getComponent(types) { 65 | if(types.length==1){ 66 | return antd[types[0]] 67 | }else{ 68 | var lastT = types.pop(); 69 | var com = this._getComponent(types)[lastT]; 70 | return com; 71 | } 72 | } 73 | findCanDropTarget(target){ 74 | if(target.className.indexOf('draggable')!=-1){ 75 | return target; 76 | }else{ 77 | return this.findCanDropTarget(target.parentNode); 78 | } 79 | } 80 | renderJSON(json){ 81 | return ( 82 | json.map((d,i)=>{ 83 | d.id = this.state.comNowIndex++; 84 | if(d.hasDelete) return; 85 | 86 | var component; 87 | if(d.is_native){ 88 | component = d.type; 89 | }else{ 90 | component = this._getComponent(d.type.split('.')); 91 | this.state.dependComponents.push(d.type.split('.')[0]); 92 | } 93 | 94 | var props = {}; 95 | d.props = d.props||{}; 96 | 97 | if(d.can_place){ 98 | 99 | props.className = 'draggable'; 100 | props.onDragOver = (e)=>{ 101 | e.preventDefault(); 102 | } 103 | props.onDrop = (e)=>{ 104 | e.preventDefault(); 105 | e.stopPropagation(); 106 | this.findCanDropTarget(e.target).className = this.findCanDropTarget(e.target).className.replace('isdroping','') 107 | var com = this.state.draggingData ; 108 | d.childrens = d.childrens?d.childrens:[]; 109 | d.childrens.push(_.cloneDeep(com)); 110 | this.forceUpdate(); 111 | } 112 | props.onDragOver = (e)=>{ 113 | e.preventDefault(); 114 | if(this.findCanDropTarget(e.target).className.indexOf('isdroping')==-1){ 115 | this.findCanDropTarget(e.target).className += (' isdroping') 116 | } 117 | 118 | } 119 | props.onDragLeave = (e)=>{ 120 | e.preventDefault(); 121 | this.findCanDropTarget(e.target).className = this.findCanDropTarget(e.target).className.replace('isdroping','') 122 | } 123 | } 124 | var outerProps = {}; 125 | outerProps.onMouseOver = (e)=>{ 126 | e.stopPropagation(); 127 | e.preventDefault() 128 | if(!this.state.isDragLayer){ 129 | this.showLayer(e.target,d); 130 | } 131 | } 132 | outerProps.onMouseLeave = (e)=>{ 133 | e.stopPropagation(); 134 | 135 | if(!this.state.isDragLayer){ 136 | this.hideLayer(); 137 | } 138 | } 139 | outerProps.onClick = (e)=>{ 140 | e.stopPropagation(); 141 | this.setState({ 142 | editCom:d 143 | }) 144 | this.forceUpdate(); 145 | } 146 | var realProps = Object.assign({},d.props); 147 | for(var i in realProps){ 148 | if(typeof(realProps[i]) == 'object' && realProps[i].type == 'relative'){ 149 | realProps[i] = realProps[realProps[i].target]?realProps[i].true:realProps[i].false; 150 | } 151 | } 152 | if(d.sub_type == 'table_container') { 153 | realProps.columns.forEach((c)=>{ 154 | if(c.childrens){ 155 | c.render = ()=>{ 156 | return this.renderJSON(c.childrens) 157 | } 158 | }else if(c.type == '图文') { 159 | c.render = ()=>{ 160 | return
161 | 162 | {c.title} 163 |
164 | } 165 | }else if(c.type == '图片'){ 166 | c.render = ()=>{ 167 | return 168 | } 169 | }else if(c.type == '链接'){ 170 | c.render = ()=>{ 171 | return 动作 172 | } 173 | } 174 | }) 175 | } 176 | if(d.wrap_inner){ 177 | return 178 | {React.createElement( 179 | component, 180 | realProps, 181 | [
{ 182 | d.props.content?[d.props.content]:(d.childrens?this.renderJSON(d.childrens):null) 183 | }
] 184 | )} 185 |
186 | }else if(d.wrap){ 187 | return 188 |
189 | {React.createElement( 190 | component, 191 | realProps, 192 | d.props.content?[d.props.content]:(d.childrens?this.renderJSON(d.childrens):null) 193 | )} 194 |
195 |
196 | }else{ 197 | Object.assign(realProps,props); 198 | return 199 | {React.createElement( 200 | component, 201 | realProps, 202 | d.props.content?[d.props.content]:(d.childrens?this.renderJSON(d.childrens):null) 203 | )} 204 | 205 | } 206 | }) 207 | ) 208 | } 209 | handleOk(){ 210 | this.setState({ 211 | modal_visible:false 212 | }) 213 | } 214 | handleCancel(){ 215 | this.setState({ 216 | modal_visible:false 217 | }) 218 | } 219 | 220 | showLayer(target,d) { 221 | var pos = this.getDOMPOS(target); 222 | this.setState({ 223 | activeCom:d, 224 | layer_x:pos.x, 225 | layer_y:pos.y, 226 | layer_w:target.offsetWidth, 227 | layer_h:target.offsetHeight, 228 | layer_show:true, 229 | layer_isTop:true, 230 | dragdiv_x: target.offsetWidth+pos.x, 231 | dragdiv_y:target.offsetHeight+pos.y 232 | }) 233 | 234 | document.getElementById("dragdiv").style.left = (target.offsetWidth+pos.x - 10) + 'px' 235 | document.getElementById("dragdiv").style.top = (target.offsetHeight+pos.y - 10) + 'px' 236 | 237 | } 238 | 239 | hideLayer(){ 240 | this.setState({ 241 | layer_show:false 242 | }) 243 | } 244 | 245 | getDOMPOS(target) { 246 |     var actualLeft = target.offsetLeft; 247 |     var current = target.offsetParent; 248 |     while (current !== null){ 249 |       actualLeft += current.offsetLeft; 250 |       current = current.offsetParent; 251 |     } 252 |     var actualTop = target.offsetTop; 253 |     var current = target.offsetParent; 254 |     while (current !== null){ 255 |       actualTop += current.offsetTop; 256 |       current = current.offsetParent; 257 |     } 258 | return { 259 | x:actualLeft, 260 | y:actualTop 261 | } 262 | } 263 | 264 | renderEnumObject(editcom,key){ 265 | var props = editcom.props[key]; 266 | var config = editcom.config[key].enumobject; 267 | if(config.type == 'relative_props_object') { 268 | config = editcom.props[config.target]; 269 | } 270 | return
271 |
{editcom.config[key].text}
272 | { 281 | return { 282 | props.splice(index,1) 283 | editcom.props[key] = _.cloneDeep(props); 284 | this.forceUpdate(); 285 | }}>Delete 286 | } 287 | }])} 288 | dataSource={props} 289 | /> 290 | 296 | { 297 | this.state.showAddList? 298 | { 299 | config.map((c)=>{ 300 | 301 | return 302 | { 303 | (()=>{ 304 | if(c.type == 'String') 305 | return { 306 | this.state.addListData[c.dataIndex] = v.target.value+""; 307 | }}> 308 | if(c.type == 'Number') 309 | return { 310 | this.state.addListData[c.dataIndex] = v; 311 | }}> 312 | if(c.type == 'Boolean') 313 | return { 314 | this.state.addListData[c.dataIndex] = v; 315 | }}> 316 | else 317 | return { 318 | this.state.addListData[c.dataIndex] = v.target.value+""; 319 | }}> 320 | })() 321 | } 322 | 323 | }) 324 | } 325 | 326 | 365 | 366 | :null 367 | } 368 | 369 | 370 | } 371 | 372 | findIdFromComs(id,coms,parent){ 373 | var result = null; 374 | for(var i=0;i{ 402 | if(this.state.isDragLayer){ 403 | e.stopPropagation(); 404 | this.state.mouse_x = e.clientX; 405 | this.state.mouse_y = e.clientY; 406 | 407 | this.state.dragdiv_x = this.state.mouse_x 408 | this.state.dragdiv_y = this.state.mouse_y 409 | document.getElementById("dragdiv").style.left = this.state.mouse_x-5 +'px' 410 | document.getElementById("dragdiv").style.top = this.state.mouse_y-5 +'px' 411 | if(!this.state.activeCom.props.style) { 412 | this.state.activeCom.props.style = {} 413 | } 414 | this.state.layer_w = this.state.activeCom.props.style.width = this.state.mouse_x + 5 - this.state.layer_x; 415 | this.state.layer_h = this.state.activeCom.props.style.height = this.state.mouse_y + 5 - this.state.layer_y; 416 | this.forceUpdate() 417 | } 418 | }} onMouseUp={()=>{ 419 | this.state.isDragLayer = false; 420 | }}> 421 |
422 |
{this.state.activeCom.title}
423 | 424 | 425 |
426 | { 427 | e.stopPropagation(); 428 | this.state.isDragLayer = true; 429 | }}> 430 |
431 |
432 |
433 | { 434 | (!this.state.hasBeginEdit)?
435 | 设计板,拖拽元素到此,点击元素可以编辑属性,红色虚线区域可以放置子组件 436 |
:null 437 | } 438 | { 439 | this.renderJSON(this.state.data) 440 | } 441 |
442 |
443 | {}}> 444 | 445 |
446 | 447 | { 448 | this.state.editCom.type?:null 452 | } 453 | { 454 | this.state.editCom.type?:null 458 | } 459 | 463 | { 464 | (this.state.editCom&&this.state.editCom.config)?Object.keys(this.state.editCom.config).map((key)=>{ 465 | if(key == 'style') { 466 | var style = this.state.editCom.config.style; 467 | 468 | return Object.keys(this.state.editCom.config.style).map((s)=>{ 469 | if(style[s].type=='color'){ 470 | console.log(this.state.editCom.props.style[s]) 471 | return 472 | { 475 | this.state.editCom.props.style[s] = c.color; 476 | this.forceUpdate(); 477 | }} 478 | placement="topRight" 479 | /> 480 | 481 | }else if(style[s].type=='4-value'){ 482 | var defaultValue = this.state.editCom.props.style[s] || "0"; 483 | if(defaultValue.toString().indexOf(' ')==-1){ 484 | this.state.value4EditResult[s] = [defaultValue,defaultValue,defaultValue,defaultValue]; 485 | } else { 486 | this.state.value4EditResult[s] = defaultValue.split(' ') 487 | } 488 | return 489 | 上:{ 490 | this.state.value4EditResult[s][0] = v.target.value; 491 | this.state.editCom.props.style[s] = this.state.value4EditResult[s].join(" ") 492 | this.forceUpdate() 493 | }} style={{width:50,marginRight:5}}> 494 | 右:{ 495 | this.state.value4EditResult[s][1] = v.target.value; 496 | this.state.editCom.props.style[s] = this.state.value4EditResult[s].join(" ") 497 | this.forceUpdate() 498 | }} style={{width:50,marginRight:5}}> 499 | 下:{ 500 | this.state.value4EditResult[s][2] = v.target.value; 501 | this.state.editCom.props.style[s] = this.state.value4EditResult[s].join(" ") 502 | this.forceUpdate() 503 | }} style={{width:50,marginRight:5}}> 504 | 左:{ 505 | this.state.value4EditResult[s][3] = v.target.value; 506 | this.state.editCom.props.style[s] = this.state.value4EditResult[s].join(" ") 507 | this.forceUpdate() 508 | }} style={{width:50}}> 509 | 510 | }else{ 511 | return 512 | { 513 | this.state.editCom.props[key][s] = v.target.value; 514 | this.forceUpdate() 515 | }}> 516 | 517 | } 518 | }) 519 | }else if(this.state.editCom.config[key].enumobject){ 520 | return this.renderEnumObject(this.state.editCom,key); 521 | }else{ 522 | return 523 | { 524 | (()=>{ 525 | if(this.state.editCom.config[key].enum){ 526 | return 539 | }else if(this.state.editCom.config[key].type=="Boolean"){ 540 | return { 543 | this.state.editCom.props[key] = v.target.checked; 544 | this.forceUpdate(); 545 | }}/> 546 | 547 | }else if(key=='content'){ 548 | return { 549 | this.state.editCom.props[key] = v.target.value; 550 | this.forceUpdate() 551 | }}> 552 | }else{ 553 | return { 554 | this.state.editCom.props[key] = v.target.value; 555 | this.forceUpdate() 556 | }}> 557 | } 558 | })() 559 | } 560 | 561 | 562 | } 563 | }):( 564 | 565 | ) 566 | } 567 |
568 |
569 | 570 | { 571 | antComponents.map((group,i)=>{ 572 | return 573 | { 574 | group.coms.map((com,i2)=>{ 575 | return { 576 | this.setState({ 577 | hasBeginEdit:true 578 | }) 579 | this.state.draggingData = com; 580 | }} draggable={true} key={i+''+i2}>{com.type} {com.title} 581 | }) 582 | } 583 | 584 | }) 585 | } 586 | 587 | 591 | 592 | 593 |
594 | 595 |
596 | 597 | 598 | ); 599 | } 600 | 601 | collectCom(data){ 602 | 603 | } 604 | 605 | renderJSONtoJSX(){ 606 | this.state.indent_space = '' 607 | 608 | return `import React, { Component } from 'react'; 609 | 610 | /* 611 | * 这里声明要引入的组件 612 | */ 613 | import { ${_.uniq(this.state.dependComponents).join(', ')} } from '../components/index.jsx'; 614 | 615 | /** 616 | * Index 一般是当前页面名称 index.html 617 | */ 618 | class Index extends Component { 619 | constructor () { 620 | super(); 621 | } 622 | render(){ 623 | return ${this.renderElementtoJSX(this.state.data).replace(/\n /,'')} 624 | } 625 | } 626 | export default Index; 627 | 628 | ` 629 | } 630 | 631 | renderElementtoJSX(data){ 632 | var result = ''; 633 | this.state.indent_space += " "; 634 | data.forEach((d)=>{ 635 | if(d.hasDelete) return; 636 | console.log(d) 637 | console.log(d.props) 638 | result += ` 639 | ${this.state.indent_space}<${d.type}${this.renderProps(d.props,d)}>${d.props.content?[d.props.content]:(d.childrens?this.renderElementtoJSX(d.childrens):'')} 640 | ` 641 | }) 642 | this.state.indent_space = this.state.indent_space.replace(' ',''); 643 | result += `${this.state.indent_space}`; 644 | return result; 645 | } 646 | 647 | renderProps(props,d){ 648 | var result = ''; 649 | var props = _.cloneDeep(props); 650 | for(var i in props){ 651 | if(!(/^on[A-Z]/.test(i)||/draggable/.test(i)||/content/.test(i))){ 652 | if(/^event_(.*?)/.test(i)){ 653 | result += ` ${i.replace('event_','')}={()=>{ }}` 654 | }else if(typeof(props[i])=='object' && props[i].type == 'relative'){ 655 | result += ` ${i}={${JSON.stringify(props[props[i].target]?props[i].true:props[i].false)}}` 656 | }else if(d.sub_type=="table_container" && i == 'columns'){ 657 | var renderCache = { 658 | 659 | } 660 | props[i].forEach((p,pi)=>{ 661 | if(p.childrens){ 662 | var indentCache = this.state.indent_space; 663 | this.state.indent_space = ''; 664 | 665 | renderCache['$$'+pi+'$$'] = `()=>{ return ${this.renderElementtoJSX(p.childrens).replace(/\n /,'')}}` 666 | this.state.indent_space = indentCache 667 | p.render = '$$'+pi+'$$' 668 | delete p.childrens; 669 | } 670 | }) 671 | var noindent = JSON.stringify(props[i]) 672 | var indent = JSON.stringify(props[i],null,2); 673 | var r = noindent.length>100?indent:noindent; 674 | for(var m in renderCache){ 675 | r = r.replace(`"${m}"`,renderCache[m]) 676 | } 677 | result += ` ${i}={${r}}` 678 | }else{ 679 | var noindent = JSON.stringify(props[i]) 680 | var indent = JSON.stringify(props[i],null,2); 681 | result += ` ${i}={${noindent.length>100?indent:noindent}}` 682 | } 683 | 684 | } 685 | } 686 | return result; 687 | } 688 | } 689 | export default Editor; 690 | --------------------------------------------------------------------------------