react 可视化编辑器1

可视化编辑器1

前言

前面我们学习低代码,例如百度的低代码平台 amis,也有相应的可视化编辑器,通过拖拽的方式生成配置文件。就像这样

react 可视化编辑器1

笔者自己也有类似需求:比如中台有个归档需求,通过选择一些配置让后端执行一些操作。目前只有A项目要归档,过些日子B项目也需要归档,后面还有 C项目归档。如果不想每次来都重新编码,最好做一个编辑器,配置好数据,归档功能根据编辑器生成的配置 json 自动生成岂不美哉!

本篇将开始自己实现一个可视化编辑器。

Tip:环境采用 spug 项目,一个开源的 react 后台系统。

编辑器页面框架搭建

需求:新建一个页面,有组件区、编辑区、属性区

效果如下图所示:
react 可视化编辑器1

核心代码如下:

// spugsrcpageslowcodeeditorindex.js  import React from 'react'; import { Layout } from 'antd'; import { observer } from 'mobx-react'; import styles from './style.module.less' const { Header, Footer, Sider, Content } = Layout;  export default observer(function () {     return (         <Layout className={styles.box}>             <Sider width='400' className={styles.componentBox}>组件区</Sider>             <Layout>                 <Header className={styles.editorMenuBox}>菜单区</Header>                 <Content className={styles.editorBox}>编辑区</Content>             </Layout>             <Sider width='400' className={styles.attributeBox}>属性区</Sider>         </Layout>     ) }) 

Tip: 直接用 antd 中的 layout 布局。

// spugsrcpageslowcodeeditorstyle.module.less .box{     min-width: 1500px;     // color: pink;     font-size: 2em;      // 组件盒子     .componentBox{         background-color: blue;     }     // 编辑器菜单     .editorMenuBox{         background-color: pink;     }     // 编辑器盒子     .editorBox{         background-color: red;     }     // 属性盒子     .attributeBox{         background-color: yellow;     } } 

根据配置文件渲染编辑区

需求:根据配置文件渲染编辑区

通常是从组件区拖拽组件到编辑区,编辑区就会显示该组件,这里先不管拖拽,直接通过定义数据,编辑区根据数据渲染。

这里采用绝对定位的玩法,你也可以选择其他的,比如拖拽到编辑器后释放,则渲染该组件,不支持拖动,就像 amis 编辑器。

思路:

  • 将编辑区提取出单独模块 Container.js,完成对应css效果
  • 新建数据配置文件 data.js,存放页面配置数据
  • Container 根据配置数据将组件渲染到编辑区

Tip:最初打算将配置文件改为 .json 文件,后来发现缺少对应 loader 无法导入import data from './data.json'

效果如下图所示

react 可视化编辑器1

react 可视化编辑器1

核心代码如下:

将容器抽取成单独组件:

// spugsrcpageslowcodeeditorContainer.js import React from 'react'; import { observer } from 'mobx-react'; import styles from './style.module.less' import data from './data.js' import store from './store'; import ComponentBlock from './ComponentBlock';  @observer class Container extends React.Component {   componentDidMount() {     // 初始化     store.json = data   }   render() {     const {container = {}, components} = store.json || {};     const {width, height} = container     return (       <div className={styles.containerBox}>           <div className={styles.container} style={{width, height}}>             <ComponentBlock/>           </div>       </div>     )   } }  export default Container 

容器对应的样式:

// spugsrcpageslowcodeeditorstyle.module.less .box{     ...     // 容器盒子     .containerBox{         height: 100%;         border: 1px solid red;         padding: 5px;         // 容器的宽度高度有可能很大         overflow: auto;     }     // 容器     .container{         // 容器中的组件需要相对容器定位         position: relative;         margin:0 auto;         background: rgb(202, 199, 199);         border: 2px solid orange;         // 容器中渲染的组件         .componentBlock{             position: absolute;         }     } } 

配置数据格式如下:

// spugsrcpageslowcodeeditordata.js  const data = {    container: {       width: "800px",       height: "600px"    },    components: [       {top: 100, left: 300, 'zIndex': 1, type: 'text'},       {top: 200, left: 200, 'zIndex': 1, type: 'input'},       {top: 300, left: 100, 'zIndex': 1, type: 'button'},    ] }  export default data 

组件块模块:

// spugsrcpageslowcodeeditorComponentBlocks.js  import React, {Fragment} from 'react'; import { observer } from 'mobx-react'; import styles from './style.module.less' import store from './store';  @observer class ComponentBlock extends React.Component {   componentDidMount() {     // 初始化   }   render() {     const { components} = store.json || {};     return (       <Fragment>             {              components?.map(item => <p className={styles.componentBlock} style={{left: item.left, top: item.top}}>我的类型是: {item.type}</p>)             }       </Fragment>     )   } }  export default ComponentBlock 

直接引入容器组件(Container):

// spugsrcpageslowcodeeditorindex.js ... import Container from './Container'  export default observer(function () {     return (         <Layout className={styles.box}>             <Layout>                 <Header className={styles.editorMenuBox}>菜单区</Header>                 <Content className={styles.editorBox}>                     <Container/>                 </Content>             </Layout>         </Layout>     ) })  

将配置数据放入 store:

// spugsrcpageslowcodeeditorstore.js  import { observable, computed } from 'mobx';  class Store {   // 配置数据   @observable json = null; }  export default new Store() 

物料区组件渲染

上面我们根据一些假数据,实现能根据位置渲染内容
编辑区现在渲染的三个组件都是文本,我们期望的是正确的textinputbutton组件。

需求:物料区渲染组件、编辑区渲染对应的三个组件

效果如下图所示:
react 可视化编辑器1

核心代码如下:

编辑区提取出单独的模块:

// spugsrcpageslowcodeeditorindex.js  import Material from './Material' ...  export default observer(function () {     return (         <Layout className={styles.box}>             <Sider width='400' className={styles.componentBox}>                 <Material/>             </Sider>         </Layout>     ) }) 

物料区(即组件区)注册三个组件:

// 物料区(即组件区)  // spugsrcpageslowcodeeditorMaterial.js  import React, { Fragment } from 'react'; import { observer } from 'mobx-react'; import { Input, Button, Tag } from 'antd'; import styles from './style.module.less' import store from './store';  @observer class Material extends React.Component {     componentDidMount() {         // 初始化         this.registerMaterial()     }      // 注册物料     registerMaterial = () => {         store.registerMaterial({             // 组件类型             key: 'text',             // 组件文本             label: '文本',             // 组件预览。函数以便传达参数进来             preview: () => '预览文本',             // 组件渲染             render: () => '渲染文本',         })          store.registerMaterial({             key: 'button',             label: '按钮',             preview: () => <Button type="primary">预览按钮</Button>,             render: () => <Button type="primary">渲染按钮</Button>,         })          store.registerMaterial({             key: 'input',             label: '输入框',             preview: () => <Input style={{ width: '50%', }} placeholder='预览输入框' />,             render: () => <Input placeholder='渲染输入框' />,         })     }     render() {         return (             <Fragment>                 {                     store.componentList.map((item, index) =>                         <section className={styles.combomentBlock} key={index}>                             {/* 文案 */}                             <Tag className={styles.combomentBlockLabel} color="cyan">{item.label}</Tag>                             {/* 组件渲染 */}                             <div className={styles.combomentBlockBox}>{item.preview()}</div>                         </section>)                 }             </Fragment>         )     } }  export default Material 

编辑区调整一下,根据 store.componentMap 渲染对应组件:

// spugsrcpageslowcodeeditorComponentBlocks.js  @observer class ComponentBlock extends React.Component {    render() {     const { components } = store.json || {};     return (       <Fragment>         {           components?.map(item =>             <div style={{ position: 'absolute', left: item.left, top: item.top }}>{store.componentMap[item.type].render()}</div>           )         }       </Fragment>     )   } } 

物料的数据都放入状态管理模块中:

// spugsrcpageslowcodeeditorstore.js  import { observable, computed } from 'mobx';  class Store {   // 配置数据   @observable json = null;    @observable componentList = []    @observable componentMap = {}    // 注册物料   registerMaterial = (item) => {     this.componentList.push(item)     this.componentMap[item.key] = item   } }  export default new Store() 

对应样式:

// spugsrcpageslowcodeeditorstyle.module.less .box{     min-width: 1500px;     // 组件盒子     .componentBox{         background-color: #fff;         // 组件块         .combomentBlock{             position: relative;             margin: 10px;             border: 1px solid #95de64;             .combomentBlockLabel{                 position: absolute;                 left:0;                 top:0;             }             .combomentBlockBox{                 display: flex;                 align-items: center;                 justify-content: center;                 height: 100px;             }         }         // 组件块上添加一个蒙版,防止用户点击组件         .combomentBlock::after{             content: '';             position: absolute;             left: 0;             right: 0;             top: 0;             bottom: 0;             background-color: rgba(0,0,0,.05);             // 增加移动效果             cursor: move;         }     }     ... 

物料组件拖拽到编辑区

需求:将物料区的组件拖拽到编辑区,并在鼠标释放的地方渲染该组件

效果如下图所示:
react 可视化编辑器1

物料区的组件需要增加 draggable 属性就可以拖动了。就像这样:

<section className={styles.combomentBlock} key={index}      // 元素可以拖拽     draggable >         </section>) 

效果如下:
react 可视化编辑器1

核心代码如下:

修改物料组件,让物料组件支持拖拽,并在拖拽前记录拖动的组件。

// 物料区(即组件区)  // spugsrcpageslowcodeeditorMaterial.js  @observer class Material extends React.Component {          // 记录拖动的元素     dragstartHander = (e, target) => {         // 标记正在拖拽         store.dragging = true;         // 记录拖拽的组件         store.currentDragedCompoent = target          // {"key":"text","label":"文本"}         // console.log(JSON.stringify(target))     }     render() {         return (             <Fragment>                 {                     store.componentList.map((item, index) =>                          <section className={styles.combomentBlock} key={index}                             // 元素可以拖拽                             draggable                             onDragStart={e => this.dragstartHander(e, item)}                         >                             {/* 文案 */}                             <Tag className={styles.combomentBlockLabel} color="cyan">{item.label}</Tag>                             {/* 组件预览 */}                             <div className={styles.combomentBlockBox}>{item.preview()}</div>                         </section>)                 }             </Fragment>         )     } }  export default Material  

容器组件中初始化配置文件,默认容器大小是800*600,并给容器增加 dragover 和 drop 事件,并在 drop 事件里新增组件到数据中。

// spugsrcpageslowcodeeditorContainer.js  import React from 'react'; import { observer } from 'mobx-react'; import styles from './style.module.less' import store from './store'; import ComponentBlocks from './ComponentBlocks';  @observer class Container extends React.Component {   componentDidMount() {     // 初始化     store.json = {       container: {         width: "800px",         height: "600px"       },       components: [         //  {top: 100, left: 300, 'zIndex': 1, type: 'text'},       ]     }   }    // 如果不阻止默认行为,则不会触发 drop,进入容器后也不会出现移动标识   dragOverHander = e => {     e.preventDefault()   }    // 新增组件到数据中   dropHander = e => {     store.dragging = false;     // e 中没有offsetX,到原始事件中找到 offsetX。     const { nativeEvent = {} } = e;     const component = {       top: nativeEvent.offsetY,       left: nativeEvent.offsetX,       zIndex: 1,       type: store.currentDragedCompoent.type     };     // 重置     store.currentDragedCompoent = null;     // 添加组件     store.json.components.push(component)    }   render() {     const { container = {}, components } = store.json || {};     const { width, height } = container     return (       <div className={styles.containerBox}>         <div className={styles.container} style={{ width, height, }}           onDragOver={this.dragOverHander}           onDrop={this.dropHander}         >           <ComponentBlocks />         </div>       </div>     )   } }  export default Container  

物料组件拖拽到编辑区,释放鼠标时需要在该位置渲染组件,而 offerX 会相对容器中的子元素定位,这里使用 pointer-events 解决:

// spugsrcpageslowcodeeditorComponentBlocks.js  @observer class ComponentBlocks extends React.Component {      render() {     const { components } = store.json || {};     return (       <Fragment>         {           components?.map((item, index) =>             // `pointer-events: none` 解决offsetX 穿透子元素的问题。             // pointer-events 兼容性高达98%以上             <div key={index} style={{ pointerEvents: store.dragging ? 'none' : 'auto', position: 'absolute', left: item.left, top: item.top }}>{store.componentMap[item.type]?.render()}</div>           )         }       </Fragment>     )   } } 

store 中新增数据方法如下:

// spugsrcpageslowcodeeditorstore.js  class Store {   @observable dragging = false;    // 记录当前拖动的组件,drop 时置空   @observable currentDragedCompoent = null    // 注册物料   registerMaterial = (item) => {     this.componentList.push(item)     this.componentMap[item.type] = item   }   ... }  export default new Store() 

相关知识点:

  • dragenter - 当拖动的元素或被选择的文本进入有效的放置目标时, dragenter 事件被触发。
  • dragover - 当元素或者选择的文本被拖拽到一个有效的放置目标上时,触发 dragover 事件(每几百毫秒触发一次)
  • dragleave - 事件在拖动的元素或选中的文本离开一个有效的放置目标时被触发。
  • drop - 事件在元素或选中的文本被放置在有效的放置目标上时被触发。
  • DataTransfer.dropEffect 属性控制在拖放操作中给用户的反馈(通常是视觉上的)。它会影响在拖拽过程中光标的手势。笔者设置了 e.dataTransfer.dropEffect = "move" 也没效果
  • offsetX - 规定了事件对象与目标节点的内填充边(padding edge)在 X 轴方向上的偏移量。会相对子元素,笔者采用 pointer-events: none 解决offsetX 穿透子元素的问题。

Tip:screenX、clientX、pageX, offsetX的区别如下图(来自网友 Demi)所示

react 可视化编辑器1

还有一个小问题:物料拖拽到编辑区释放时,新组件的左上角(left,top)正好是释放时鼠标的位置。

笔者期望鼠标释放的位置是新组件的正中心。

增加 translate(-50%, -50%) 即可。就像这样

// spugsrcpageslowcodeeditorComponentBlocks.js  <div key={index} style={{     ...     // 鼠标释放的位置是新组件的正中心     transform: `translate(-50%, -50%)` }}>...</div> 

不过这个方案在下面实现辅助线对齐时却遇到问题,需要替换方案。

编辑区的组件拖拽

这里分三步:首先是选中组件,然后拖拽选中的组件,最后添加对齐辅助线功能。

编辑区的组件选中

需求:编辑区的组件支持选中。比如:

  • 直接选中某组件就直接拖动(不释放鼠标)
  • 仅选中某组件
  • 选择多个组件,例如按住 shift

效果如下图所示:
react 可视化编辑器1

这里用 mousedown 事件。核心代码如下:

// spugsrcpageslowcodeeditorComponentBlocks.js  @observer class ComponentBlocks extends React.Component {   mouseDownHandler = (e, target) => {     // 例如防止点击 input 时,input 会触发 focus 聚焦。     e.preventDefault()      // 如果按下 shift 则只处理单个     if (e.shiftKey) {       target.focus = !target.focus     } else if (!target.focus) {       // 清除所有选中       store.json.components.forEach(item => item.focus = false)       target.focus = true     } else {       // 这里无需清除所有选中       // 注销这句话拖动效果更好。       // target.focus = false     }     console.log(target)   }   render() {     const { components } = store.json || {};     return (       <Fragment>         {           components?.map((item, index) =>              <div key={index}               className={styles.containerBlockBox}               style={{                 ...                 // 选中效果                 border: item.focus ? '1.5px dashed red' : 'none',               }}               onMouseDown={e => this.mouseDownHandler(e, item)}             >...</div>           )         }       </Fragment>     )   } }  export default ComponentBlocks 

添加样式:

// spugsrcpageslowcodeeditorstyle.module.less // 容器 .container{     ...     // 编辑区的组件上添加一个蒙版,防止用户点击组件     .containerBlockBox::after{         content: '';         position: absolute;         left: 0;         right: 0;         top: 0;         bottom: 0;     } } 

拖拽编辑区组件

需求:编辑区内的选中的组件支持拖拽

思路:

  • 点击容器,应取消所有选中组件
  • mousedown时记录点击的开始位置坐标,移动时计算偏移量并更新组件位置

核心代码如下:

// spugsrcpageslowcodeeditorContainer.js  @observer class Container extends React.Component {   ...   // 点击容器,取消所有选中   clickHander = e => {     e.target.className.split(/s+/).includes('container') &&      // 清除所有选中     store.json.components.forEach(item => item.focus = false)   }    mouseDownHandler = e => {     // 开始坐标     this.startCoordinate = {       x: e.pageX,       y: e.pageY     }     console.log('down。记录位置:', this.startCoordinate)   }    mouseMoveHander = e => {     if(!this.startCoordinate){       return     }     const {pageX, pageY} = e     const {x, y} = this.startCoordinate     // 移动的距离     const moveX = pageX - x     const moveY = pageY - y     console.log('move。更新位置。移动坐标:', {moveX, moveY})     store.focusComponents.forEach(item => {       item.top = item.top + moveY;       item.left = item.left + moveX;     })      // 更新开始位置。     this.startCoordinate = {       x: e.pageX,       y: e.pageY     }   }    mouseUpHander = e => {     console.log('up')     this.startCoordinate = null   }    render() {     const { container = {}, components } = store.json || {};     const { width, height } = container     return (       <div className={styles.containerBox}>         {/* 多个 className */}         <div className={`container ${styles.container}`} style={{ width, height, }}           ...           onClick={this.clickHander}           onMouseDown={e => this.mouseDownHandler(e)}           onMouseMove={e => this.mouseMoveHander(e)}           onMouseUp={e => this.mouseUpHander(e)}         >           <ComponentBlocks />         </div>       </div>     )   } }  export default Container 
// spugsrcpageslowcodeeditorstore.js  class Store {   // 配置数据   @observable json = null;   // 获取 json 中选中的项   @computed get focusComponents() {     return this.json.components.filter(item => item.focus)   }    // 获取 json 中未选中的项   @computed get unFocusComponents() {     return this.json.components.filter(item => !item.focus)   }  } 

辅助线对齐

需求:增加对齐辅助线。比如讲文本组件和按钮组件顶对齐、底对齐、居中对齐等等。靠近辅助线时也能自动贴上去。

效果如下图所示:
react 可视化编辑器1

react 可视化编辑器1

思路:

  • 记录未选中元素的辅助线的位置以及显现该辅助线的坐标
  • 当前选中元素的坐标如果和辅助线的坐标匹配则显示对应辅助线
  • 同时拖动多个元素,以最后选中的元素为准

每个未选中的元素有10种情况。请看下图:
react 可视化编辑器1

A 表示未选中元素,B表示选中拖动的元素,A 有三条辅助线,从上到下5种情况下显示:

  • A顶对B底
  • A顶对B顶
  • A中对B中
  • A底对B底
  • A底对B顶

这里是水平方向5中情况,垂直方向也有5种情况。

核心代码如下:

首先记录最后一个选中的元素。核心代码如下:

在 store.js 中增加如下相关变量:

  • startCoordinate - 作用:1. 记录开始坐标位置,用于计算移动偏移量 2. 松开鼠标后,辅助线消失
  • guide - 存放辅助线
  • adjacency - 拖拽的组件靠近辅助线时(2px内),辅助线出现
  • lastSelectedElement - 最后选中的元素
  • adjacencyGuides - 紧邻的辅助线,即要显示的辅助线
// spugsrcpageslowcodeeditorstore.js  import { observable, computed } from 'mobx';  class Store {   // 开始坐标。作用:1. 记录开始坐标位置,用于计算移动偏移量 2. 松开鼠标后,辅助线消失   @observable startCoordinate = null    // 辅助线。xArray 存储垂直方向的辅助线;yArray 存储水平方向的辅助线;   @observable guide = { xArray: [], yArray: [] }    // 拖拽的组件靠近辅助线时(2px内),辅助线出现   @observable adjacency = 2    // 最后选中的元素索引。用于辅助线   @observable lastSelectedIndex = -1    // 最后选中的元素   @computed get lastSelectedElement() {     return this.json.components[this.lastSelectedIndex]   }    // 紧邻的辅助线,即要显示的辅助线   @computed get adjacencyGuides() {     return this.lastSelectedElement && this.guide.yArray       // 相对元素坐标与靠近辅助线时,辅助线出现       ?.filter(item => Math.abs(item.y - this.lastSelectedElement.top) <= this.adjacency)   } }  export default new Store() 

ComponentBlocks.js 改动如下:

  • 将子组件的渲染抽离成一个单独组件ComponentBlock。主要用于获取组件的宽度和高度。组件得渲染后才知晓其尺寸
  • mousedown 时记录鼠标位置以及初始化辅助线
  • 辅助线有水平方向和垂直方向,分别存入 yArray 和 xArray 中。其数据结构为 {showTop: xx, y: xx}。笔者只实现了水平方向的辅助线。例如 A顶(未选中元素)对B底 的 y 的计算方式请看下图:

react 可视化编辑器1

  • 将组件的选中样式从 border 改为不占空间的 outline,防止取消选中时组件的抖动。
  • 鼠标释放的位置是新组件的正中心方案修改。第一次从物料区拖拽组件释放时修改坐标。
// spugsrcpageslowcodeeditorComponentBlocks.js ... @observer class ComponentBlocks extends React.Component {   mouseDownHandler = (e, target, index) => {     ...      // 记录开始位置     store.startCoordinate = {       x: e.pageX,       y: e.pageY     }      ...      // 初始化辅助线。选中就初始化辅助线,取消不管。     this.initGuide(target, index)   }    // 初始化辅助线。   // 注:仅完成水平辅助线,垂直辅助线请自行完成。   initGuide = (component, index) => {     // 记录最后一个选中元素的索引     // 问题:依次选中1个、2个、3个元素,然后取消第3个元素的选中,这时最后一个元素的索引依然指向第三个元素,这就不正确了。会导致辅助线中相对最后一个选中元素不正确。     // 解决办法:通过定义变量(store.startCoordinate)来解决此问题     store.lastSelectedIndex = component.focus ? index : -1      if (!component.focus) {       return     }      // console.log('初始化辅助线')     store.guide = { xArray: [], yArray: [] }     store.unFocusComponents.forEach(item => {        const { xArray: x, yArray: y } = store.guide        // 相对元素。即选中的最后一个元素       const { lastSelectedElement: relativeElement } = store       // A顶(未选中元素)对B底       // showTop 辅助线出现位置。y - 相对元素的 top 值为 y 时辅助线将显现       y.push({ showTop: item.top, y: item.top - relativeElement.height })       // A顶对B顶       y.push({ showTop: item.top, y: item.top })       // A中对B中       y.push({ showTop: item.top + item.height / 2, y: item.top + (item.height - relativeElement.height) / 2 })       // A底对B底       y.push({ showTop: item.top + item.height, y: item.top + item.height - relativeElement.height })       // A底对B顶       y.push({ showTop: item.top + item.height, y: item.top + item.height })     })   }    render() {     const { components } = store.json || {};     return (       <Fragment>         {           components?.map((item, index) =>             <ComponentBlock key={index} index={index} item={item} mouseDownHandler={this.mouseDownHandler} />           )         }       </Fragment>     )   } }  // 必须加上 @observer // 将子组件拆分用于设置组件的宽度和高度 @observer class ComponentBlock extends React.Component {   constructor(props) {     super(props)     this.box = React.createRef()   }   componentDidMount() {      // 初始化组件的宽度和高度     // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetWidth     const { offsetWidth, offsetHeight } = this.box.current     const component = store.json.components[this.props.index] ?? {}     component.width = offsetWidth     component.height = offsetHeight      // 组件第一次从物料区拖拽到编辑区,将组件中心位置设置为释放鼠标位置。     // transform: `translate(-50%, -50%)` 的替代方案     if (component.isFromMaterial) {       component.isFromMaterial = false       component.left = component.left - (component.width) / 2       component.top = component.top - (component.height) / 2     }   }   render() {     const { index, item, mouseDownHandler } = this.props;     return (       <div ref={this.box}         className={styles.containerBlockBox}         style={{           ...           // 选中效果           // border 改 outline(轮廓不占据空间)。否则取消元素选中会因border消失而抖动           outline: item.focus ? '1.5px dashed red' : 'none',           // 鼠标释放的位置是新组件的正中心           // transform: `translate(-50%, -50%)`,         }}         onMouseDown={e => mouseDownHandler(e, item, index)}       >{store.componentMap[item.type]?.render()}</div>     )   } } export default ComponentBlocks 

Container.js 主要改动:

  • isFromMaterial - 鼠标释放的位置是新组件的正中心的一个标记
  • adjacencyGuideOffset - 自动贴近辅助线的偏移量。体验有些问题,默认关闭
  • mouseUpHander - mouseup 后辅助线不在显示
  • render() - 增加辅助线的渲染
// spugsrcpageslowcodeeditorContainer.js  ... @observer class Container extends React.Component {   ...    dropHander = e => {     ...     const component = {       // 从物料拖拽到编辑器。在编辑器初次显示后则关闭。     + isFromMaterial: true,       top: nativeEvent.offsetY,       left: nativeEvent.offsetX,       zIndex: 1,       type: store.currentDragedCompoent.type     };     ...   }     // 自动贴近辅助线的偏移量   // 体验有些问题,默认关闭。即贴近后,移动得慢会导致元素挪不开,因为自动贴近总会执行   // 注:只实现了Y轴(水平辅助线)   adjacencyGuideOffset = (close = true) => {     const result = { offsetY: 0, offsetX: 0 }     if (close) {       return result     }     // 取得接近的辅助线     const adjacencyGuide = store.guide.yArray       // 拖拽的组件靠近辅助线时(2px内),辅助线出现       .find(item => Math.abs(item.y - store.lastSelectedElement.top) <= store.adjacency)      if (adjacencyGuide) {       // 体验不好:取消贴近辅助线功能       result.offsetY = adjacencyGuide.y - store.lastSelectedElement.top;     }     return result   }   mouseMoveHander = e => {     // 选中元素后,再移动才有效     if (!store.startCoordinate) {       return     }     // 上个位置     const { x, y } = store.startCoordinate     let { pageX: newX, pageY: newY } = e     // 自动贴近偏移量。默认关闭此功能。     const { offsetY: autoApproachOffsetY, offsetX: autoApproachOffsetX } = this.adjacencyGuideOffset()     // 移动的距离     const moveX = newX - x + autoApproachOffsetX     const moveY = newY - y + autoApproachOffsetY      // console.log('move。更新位置。移动坐标:', {moveX, moveY})     store.focusComponents.forEach(item => {       item.left += moveX       item.top += moveY     })      // 更新开始位置。     store.startCoordinate = {       x: newX,       y: newY     }   }    // mouseup 后辅助线不在显示   mouseUpHander = e => {     store.startCoordinate = null   }    render() {     ...     return (       <div className={styles.containerBox}>         <div>           <ComponentBlocks />           {/* 辅助线 */}           {             store.startCoordinate && store.adjacencyGuides               ?.map((item, index) => {                 return <i key={index} className={styles.guide} style={{ top: item.showTop }}></i>               })           }          </div>       </div>     )   } }  export default Container 

辅助线样式:

// spugsrcpageslowcodeeditorstyle.module.less  // 辅助线 .guide{     position: absolute;     width: 100%;     border-top: 1px dashed red; } 

发表评论

评论已关闭。

相关文章