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

笔者自己也有类似需求:比如中台有个归档需求,通过选择一些配置让后端执行一些操作。目前只有A项目要归档,过些日子B项目也需要归档,后面还有 C项目归档。如果不想每次来都重新编码,最好做一个编辑器,配置好数据,归档功能根据编辑器生成的配置 json 自动生成岂不美哉!
本篇将开始自己实现一个可视化编辑器。
Tip:环境采用 spug 项目,一个开源的 react 后台系统。
编辑器页面框架搭建
需求:新建一个页面,有组件区、编辑区、属性区
效果如下图所示:

核心代码如下:
// 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'。
效果如下图所示


核心代码如下:
将容器抽取成单独组件:
// 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()
物料区组件渲染
上面我们根据一些假数据,实现能根据位置渲染内容
编辑区现在渲染的三个组件都是文本,我们期望的是正确的text、input和button组件。
需求:物料区渲染组件、编辑区渲染对应的三个组件
效果如下图所示:

核心代码如下:
编辑区提取出单独的模块:
// 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; } } ...
物料组件拖拽到编辑区
需求:将物料区的组件拖拽到编辑区,并在鼠标释放的地方渲染该组件
效果如下图所示:

物料区的组件需要增加 draggable 属性就可以拖动了。就像这样:
<section className={styles.combomentBlock} key={index} // 元素可以拖拽 draggable > </section>)
效果如下:

核心代码如下:
修改物料组件,让物料组件支持拖拽,并在拖拽前记录拖动的组件。
// 物料区(即组件区) // 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)所示

还有一个小问题:物料拖拽到编辑区释放时,新组件的左上角(left,top)正好是释放时鼠标的位置。
笔者期望鼠标释放的位置是新组件的正中心。
增加 translate(-50%, -50%) 即可。就像这样
// spugsrcpageslowcodeeditorComponentBlocks.js <div key={index} style={{ ... // 鼠标释放的位置是新组件的正中心 transform: `translate(-50%, -50%)` }}>...</div>
不过这个方案在下面实现辅助线对齐时却遇到问题,需要替换方案。
编辑区的组件拖拽
这里分三步:首先是选中组件,然后拖拽选中的组件,最后添加对齐辅助线功能。
编辑区的组件选中
需求:编辑区的组件支持选中。比如:
- 直接选中某组件就直接拖动(不释放鼠标)
- 仅选中某组件
- 选择多个组件,例如按住 shift
效果如下图所示:

这里用 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) } }
辅助线对齐
需求:增加对齐辅助线。比如讲文本组件和按钮组件顶对齐、底对齐、居中对齐等等。靠近辅助线时也能自动贴上去。
效果如下图所示:


思路:
- 记录未选中元素的辅助线的位置以及显现该辅助线的坐标
- 当前选中元素的坐标如果和辅助线的坐标匹配则显示对应辅助线
- 同时拖动多个元素,以最后选中的元素为准
每个未选中的元素有10种情况。请看下图:

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 的计算方式请看下图:

- 将组件的选中样式从 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; }