一、前言
项目概述:实现一个支持采用图形化拖拽的方式,配置参数来生成JSON,通过生成的JSON来渲染出可JSONSchema描述的页面。
Github 地址:github.com/Zilong-417/…
接下来将对以下技术要点进行核心代码分析:
- 编辑器
- 拖拽
- 标线
- 撤销与还原
- 删除选中与清空画布
- 放大与缩小
- 预览与导出
- 组件属性设置
- 组件自定义事件
二、核心代码介绍
1、编辑器
(1)整体布局
编辑器页面效果图如下:
编辑器页面采用的是三栏布局,左右固定宽度,中间自适应;
(2)编辑区域实现
中间的画布是编辑区域。它的作用是:从左边物料区拖拽出一个组件放到画布中时,画布要把这个组件渲染出来。
实现思路:
- 创建一个
data.json文件用于存放容器和组件json数据 - 拖拽到画布中时,使用
Es6解构赋值方法将新的组件数据添加到blocks数组对象中 - 编辑器使用
map方法遍历blocks数组对象,将每个组件逐个渲染到画布中
核心代码:
(1)容器与组件初始json数据:
{
"container": {
"width": 900,
"height": 650
},
"blocks": []
}
(2)单组件构成:
registerConfig.register({
lable: '按钮',
preview: () => <ElButton>预览按钮</ElButton>,
resize: {
width: true,
height: true
},
render: ({ props, size }) => <ElButton style={{ height: size.height + 'px', width: size.width + 'px', fontSize: props.fontsize + 'px' }} type={props.type} size={props.size}>{props.text || '按钮'}</ElButton>,
key: 'button',
props: {
text: createInputProp('按钮内容'),
fontsize: createInputProp('字体大小/px'),
type: createSelectProp('按钮类型', [
{ label: '基础', value: 'primary' },
{ label: '成功', value: 'success' },
{ label: '警告', value: 'warning' },
{ label: '危险', value: 'danger' },
{ label: '文本', value: 'text' },
]),
size: createSelectProp('按钮尺寸', [
{ label: '默认', value: 'default' },
{ label: '大', value: 'large' },
{ label: '小', value: 'small' }
])
}
})
(3)添加组件(解构赋值方法):
const drop = (e) => {
let blocks = data.value.blocks;
data.value = {
...data.value, blocks: [
...blocks, {
top: e.offsetY,
left: e.offsetX,
zIndex: 1,
key: currentComponent.key,
alignCenter: true,
props: {},
model: {},
events: {}
}
]
}
}
(4)渲染组件数据:(map方法)
(data.value.blocks.map((block, index) => (
<editorBlock
class={
[block.focus ? 'editor-block-focus' : '',
previewRef.value ? 'editor-block-preview' : '']}
block={block}
onMousedown={(e) => blockMousedown(e, block, index)}
formData={props.formData}>
}
</editorBlock>
)))
2、拖拽
(1)首先考虑四个状态
- dragenter进入组件中添加一个移动的标志
- dragover在目标组件经过要阻止默认行为 否则不能触发drop
- dragleave离开组件的时候 需要增加一个禁用标识
- drop松手时根据拖拽的组件,添加一个组件
核心代码:
const dragenter = (e) => {
e.dataTransfer.dropEffect = 'move'
}
const dragover = (e) => {
e.preventDefault()
}
const dragleave = (e) => {
e.dataTransfer.dropEffect = 'none'
}
const drop = (e) => {
let blocks = data.value.blocks;
data.value = {
...data.value, blocks: [
...blocks, {
top: e.offsetY,
left: e.offsetX,
zIndex: 1,
key: currentComponent.key,
alignCenter: true,
props: {},
model: {},
events: {}
}
]
}
}
(2)其次组件到画布过程
要想实现组件拖拽就得用h5属性,给它添加一个 draggable 属性。另外,在将组件列表中的组件拖拽到画布中,还有两个事件是起到关键作用的:
dragstart事件,事件内绑定目标元素的拖拽事件(通过addEventListener)dragend事件,在拖拽结束时触发。主要用于移除行为(通过removeEventListener)。
左侧物料区组件列表的代码:
这里使用的map方法遍历数组componentList,渲染出组件列表
{config.componentList.map(component => (
<div class="editor-left-item"
draggable
onDragstart={e => dragstart(e, component)}
onDragend={dragend} >
<span>{component.lable}</span>
<div >{component.preview()}</div>
</div>
))}
下面是dragstart和dragend事件的代码:
const dragstart = (e, component) => {
//dragenter进入元素中 添加一个移动的标志
//dragover 在目标元素经过 必须要阻止默认行为 否则不能触发drop
//dragleave 离开元素的时候 需要增加一个禁用标识
//drop 松手时 根据拖拽的组件 ,添加一个组件
containerRef.value.addEventListener('dragenter', dragenter)
containerRef.value.addEventListener('dragover', dragover)
containerRef.value.addEventListener('dragleave', dragleave)
containerRef.value.addEventListener('drop', drop)
currentComponent = component
}
const dragend = (e) => {
containerRef.value.removeEventListener('dragenter', dragenter)
containerRef.value.removeEventListener('dragover', dragover)
containerRef.value.removeEventListener('dragleave', dragleave)
containerRef.value.removeEventListener('drop', drop)
}
(3)最后实现组件的移动
要想实现组件移动,在样式方面首先需要将画布设为相对定位 position: relative,然后将每个组件设为绝对定位 position: absolute。除此之外,还要通过监听三个事件来进行移动:
mousedown事件,在组件上按下鼠标时,记录组件当前的位置,即 xy 坐标mousemove事件,每次鼠标移动时,都用当前最新的 xy 坐标减去最开始的 xy 坐标,从而计算出移动距离,再改变组件位置。mouseup事件,鼠标抬起时结束移动。
关键代码:
mousedown 事件:
// 按下鼠标,获取初始值和选中的位置
const mousedown = (e) => {
const { width: BWidth, height: BHeight } = lastSelectBlock.value//最后选中的组件
dragState = {
startX: e.clientX,
startY: e.clientY, // 记录每一个选中的位置
startLeft: lastSelectBlock.value.left, // 拖拽前的位置 left(即x坐标)
startTop: lastSelectBlock.value.top,// 拖拽前的位置 top(即y坐标)
dragging: false,//不是正在拖拽
startPos: focusData.value.focus.map(({ top, left }) => ({ top, left })),
document.addEventListener('mousemove', mousemove)
document.addEventListener('mouseup', mouseup)
}
mousemove事件:
const mousemove = (e) => {
let { clientX: moveX, clientY: moveY } = e
// 鼠标移动后 - 鼠标移动前 + left、
let durX = moveX - dragState.startX
let durY = moveY - dragState.startY
focusData.value.focus.forEach((block, idx) => {
block.top = dragState.startPos[idx].top + durY
block.left = dragState.startPos[idx].left + durX
})
}
mouseup 事件:
const mouseup = (e) => {
document.removeEventListener('mousemove', mousemove)
document.removeEventListener('mouseup', mouseup)
if (dragState.dragging) { // 如果只是点击就不会触发
events.emit('end');
}
}
(4)PS:多组件移动
需求:按住shift键+鼠标左键可实现多选组件,从而实现多组件移动
实现思路:
blockMousedown事件:点击某组件时:若按了shift健,同时选中某组件;若没有,则取消其他选中,并选中某组件clearBlockFocus事件:点击画布时,清空所有选中状态
const selectIndex = ref(-1)//表示没有任何一个被选中
//最后选择的那一个组件
const lastSelectBlock = computed(() => data.value.blocks[selectIndex.value])
const focusData = computed(() => {
let focus = []
let unfocused = []
data.value.blocks.forEach(block => (block.focus ? focus : unfocused).push(block))
return { focus, unfocused }
})
const clearBlockFocus = () => {
data.value.blocks.forEach(block => block.focus = false)
}
//实现拖拽多个元素
const containerMousedown = () => {
if (previewRef.value) return;
clearBlockFocus()
selectIndex.value = -1
}
const blockMousedown = (e, block, index) => {
if (previewRef.value) return;
e.preventDefault()
e.stopPropagation()
//block上我们规划一个属性focus获取焦点后就将focus变成true
if (e.shiftKey) {
if (focusData.value.focus.length <= 1) {
block.focus = true//当前只有一个节点被选中时 摁住shift键也不会focus状态
} else {
block.focus = !block.focus
}
} else {
if (!block.focus) {
clearBlockFocus()
block.focus = true//要清空其他的focus属性
} //当自己已经被选中了,再次点击时还是选中状态
}
selectIndex.value = index
}
3、标线
标线的用处在于确定两组件之间的对齐方式,两组件间对齐方式有10种
- 上下方向的两个组件底对顶、顶对顶、中间对中间、底对底、顶对顶会出现横线
- 左右方向的两个组件右对左、左对左、中间对中间、右对右、左对右对齐时会出现竖线
实现思路:
(1)获取按下鼠标的初始值和选中的位置
const mousedown = (e) => {
const { width: BWidth, height: BHeight } = lastSelectBlock.value
dragState = {
startX: e.clientX,
startY: e.clientY, // 记录每一个选中的位置
startLeft: lastSelectBlock.value.left, // b点拖拽前的位置 (x轴)
startTop: lastSelectBlock.value.top, // b点拖拽前的位置 (y轴)
dragging: false,//不是在拖拽
startPos: focusData.value.focus.map(({ top, left }) => ({ top, left })),
lines: (() => {
const { unfocused } = focusData.value // 获取其他没有选中的以及它们的位置坐辅助线
let lines = { // 计算横线的位置用y来存放 x存放的是纵向
x: [],
y: []
};
[...unfocused,
{
top: 0,
left: 0,
width: data.value.container.width,
height: data.value.container.height
}].forEach((block) => {
const { top: ATop, left: ALeft, width: AWidth, height: AHeight } = block;
// 当此元素拖拽到和A元素top一致的时候,要显示这根辅助线,辅助线的位置就是ATop
lines.y.push({ showTop: ATop, top: ATop })
lines.y.push({ showTop: ATop, top: ATop - BHeight }) // 顶对底
lines.y.push({ showTop: ATop + AHeight / 2, top: ATop + AHeight / 2 - BHeight / 2 }) // 中对中
lines.y.push({ showTop: ATop + AHeight, top: ATop + AHeight }) // 底对顶
lines.y.push({ showTop: ATop + AHeight, top: ATop + AHeight - BHeight }); // 底对底
lines.x.push({ showLeft: ALeft, left: ALeft }) // 左对左边
lines.x.push({ showLeft: ALeft + AWidth, left: ALeft + AWidth })// 右边对左边
lines.x.push({ showLeft: ALeft + AWidth / 2, left: ALeft + AWidth / 2 - BWidth / 2 })
lines.x.push({ showLeft: ALeft + AWidth, left: ALeft + AWidth - BWidth })
lines.x.push({ showLeft: ALeft, left: ALeft - BWidth }) // 左对右
});
return lines
})()
}
document.addEventListener('mousemove', mousemove)
document.addEventListener('mouseup', mouseup)
}
(2)计算当前元素与标线数组元素位置
当拖拽B元素到和A元素top(y轴)一致的时候,要显示这根标线,标线的位置就是A Top(y轴),同样的思路,当拖拽B元素到和A元素left(x轴)一致的时候,要显示这根标线,标线的位置就是A left(x轴)
const mousemove = (e) => {
let { clientX: moveX, clientY: moveY } = e
if (!dragState.dragging) {
dragState.dragging = true,
events.emit('start'); // 触发事件就会记住拖拽前的位置
}
// 计算当前元素最新的left和top去线里面找,找到显示线
// 鼠标移动后 - 鼠标移动前 + left
let left = moveX - dragState.startX + dragState.startLeft
let top = moveY - dragState.startY + dragState.startTop
// 先计算横线 距离参照物元素还有5px的时候,显示
let y = null
let x = null
for (let i = 0; i < dragState.lines.y.length; i++) {
const { top: t, showTop: s } = dragState.lines.y[i]
if (Math.abs(t - top) < 5) { // 小于5说明接近了
y = s // 线要实现的位置
moveY = dragState.startY - dragState.startTop + t // 容器距离顶部的距离 + 目标的高度 就是最新的moveY
// 实现快速和这个元素贴在一起
break // 找到一根线后就跳出循环
}
}
for (let i = 0; i < dragState.lines.x.length; i++) {
const { left: l, showLeft: s } = dragState.lines.x[i]
if (Math.abs(l - left) < 5) { // 小于5说明接近了
x = s // 线要实现的位置
moveX = dragState.startX - dragState.startLeft + l // 容器距离顶部的距离 + 目标的高度 就是最新的moveY
// 实现快速和这个元素贴在一起
break // 找到一根线后就跳出循环
}
}
markLine.x = x // markLine 是一个响应式数据 x, y更新会导致视图更新
markLine.y = y
let durX = moveX - dragState.startX
let durY = moveY - dragState.startY
focusData.value.focus.forEach((block, idx) => {
block.top = dragState.startPos[idx].top + durY
block.left = dragState.startPos[idx].left + durX
})
}
(3)页面渲染出标线
{markLine.x !== null && <div class="line-x" style={{ left: markLine.x + 'px' }}></div>}
{markLine.y !== null && <div class="line-y" style={{ top: markLine.y + 'px' }}></div>}
4、撤销与还原
(1)设置保存命令数组与索引
current为索引值,当撤销时索引值减1,当索引为-1时即为有撤销值
queue数组存放操作命令(删除、清空画布。。。。)
commands对象制作命令和执行功能一个映射表
commandArray数组存放所有的命令
destroyArray数组销毁命令
const state = {
current: -1, // 前进后退的索引值
queue: [], // 存放所有的操作命令
commands: {}, // 制作命令和执行功能一个映射表
commandArray: [], // 存放所有的命令
destroyArray: [],//销毁
}
(2)撤销与还原操作
撤销:假设现在 queue 保存了操作为 [a, b, c, d],当前画布数据是 d,进行撤销后, current索引 -1,现在画布的数据是 c。
还原:还原则相反, current索引加 1,然后将对应的数据赋值给画布
const registry = (command) => {
state.commandArray.push(command);
state.commands[command.name] = (...args) => { // 命令名字对应执行函数
const { redo, undo } = command.execute(...args);
redo();
if (!command.pushQueue) { // 不需要放到队列中直接跳过即可
return
}
let { queue, current } = state;
if (queue.length > 0) {
queue = queue.slice(0, current + 1); // 可能在放置的过程中有撤销操作,所以根据当前最新的current值来计算新的对了
state.queue = queue;
}
queue.push({ redo, undo }); // 保存指令的前进后退
state.current = current + 1;
console.log(queue);
}
}
registry({//还原操作
name: 'redo',
keyboard: 'ctrl+y',
execute() {
return {
redo() {
if (state.current == -1 || state.current == state.queue.length - 1) {
ElMessage({
showClose: true,
message: '没有可以还原的内容了!',
type: 'warning',
})
return
}; // 没有可以还原的了
let item = state.queue[state.current + 1] // 找到当前的下一步还原操作
if (item) {
item.redo && item.redo()
state.current++
}
}
}
}
})
registry({//撤销操作
name: 'undo',
keyboard: 'ctrl+z',
execute() {
return {
redo() {
if (state.current == -1) {
ElMessage({
showClose: true,
message: '没有可以撤销的内容了!',
type: 'warning',
})
return
}; // 没有可以撤销的了
let item = state.queue[state.current] // 找到上一步还原
if (item) {
item.undo && item.undo()// 这里没有操作队列
state.current--
}
}
}
}
})
registry({ // 如果希望将操作放到队列中可以增加一个属性 标识等会操作要放到队列中
name: 'drag',
pushQueue: true,
init() { // 初始化操作 默认就会执行
this.before = null
// 监控拖拽开始事件,保存状态
const start = () => {
this.before = deepcopy(data.value.blocks)
}
// 拖拽之后需要触发对应的指令
const end = () => {
state.commands.drag()
}
events.on('start', start)
events.on('end', end)
return () => {
events.off('start', start);
events.off('end', end)
}
},
execute() { // state.commands.drag()
let before = this.before;
let after = data.value.blocks // 之后的状态
return {
redo() { // 默认一松手 就直接把当前事情做了
data.value = { ...data.value, blocks: after }
},
undo() { // 前一步的
data.value = { ...data.value, blocks: before }
}
}
}
})
5、删除选中与清空画布
删除选中与清空画布的思路在于设置两个变量:
before:当前画布中的组件
after: 画布中未选中的组件(删除选中下after为unfocused,清空画布下after为[])
registry({// 删除操作
name: 'delete', // 删除
pushQueue: true,
execute() {
let state = {
before: deepcopy(data.value.blocks), // 当前的值
after: focusData.value.unfocused// 选中的都删除了 留下的都是没选中的
}
return {
redo: () => {
if (focusData.value.focus.length == 0 && focusData.value.unfocused.length == 0) {
ElMessage({
showClose: true,
message: '没有可以删除的内容了!',
type: 'warning',
})
return
} else if (focusData.value.focus.length == 0) {
ElMessage({
showClose: true,
message: '还没选中需要删除的内容呢!',
type: 'warning',
})
return
}
else {
data.value = { ...data.value, blocks: state.after }
}
},
undo: () => {
data.value = { ...data.value, blocks: state.before }
}
}
}
})
registry({// 清空画布操作
name: 'deleteAll', // 删除
pushQueue: true,
execute() {
let state = {
before: deepcopy(data.value.blocks), // 当前的值
after: []
}
return {
redo: () => {
if (focusData.value.focus.length == 0 && focusData.value.unfocused.length == 0) {
ElMessage({
showClose: true,
message: '没有可以清空的内容了!',
type: 'warning',
})
return
}
else {
data.value = { ...data.value, blocks: state.after }
}
},
undo: () => {
data.value = { ...data.value, blocks: state.before }
}
}
}
})
6、放大与缩小
当点击画布组件时,组件上会出现 8 个小圆点。点击圆点拖拽可放大与缩小 实现思路
(1)向组件属性添加 对象resize
width: true表示更改组件的横向大小 ,height: true表示更改组件的竖向大小, render函数中形参size接收实时组件大小
registerConfig.register({
lable: '输入框',
resize: {
width: true, // 更改输入框的横向大小
height: true
},
preview: () => <ElInput placeholder="预览输入框" ></ElInput>,
render: ({ model, props, size }) => <ElInput placeholder={props.text == null ? '请输入内容' : props.text} {...model.default} style={{ width: size.width + 'px' }}></ElInput>,
key: 'input',
model: {
default: '绑定字段'
},
props: {
text: createInputProp('提示信息'),
}
})
(2)绘制圆点与设置鼠标按下事件
horizontal: 'start'表示标记圆点的位置,start表示组件左边的圆点,end表示右边的圆点,center表示中间的圆点
vertical: 'center' 表示圆点居中
{width && <>
<div class="block-resize block-resize-left"
onMousedown={e => onmousedown(e, { horizontal: 'start', vertical: 'center' })}></div>
<div class="block-resize block-resize-right"
onMousedown={e => onmousedown(e, { horizontal: 'end', vertical: 'center' })}></div>
</>}
{height && <>
<div class="block-resize block-resize-top"
onMousedown={e => onmousedown(e, { horizontal: 'center', vertical: 'start' })}></div>
<div class="block-resize block-resize-bottom"
onMousedown={e => onmousedown(e, { horizontal: 'center', vertical: 'end' })}></div>
</>}
{width && height && <>
<div class="block-resize block-resize-top-left"
onMousedown={e => onmousedown(e, { horizontal: 'start', vertical: 'start' })}></div>
<div class="block-resize block-resize-top-right"
onMousedown={e => onmousedown(e, { horizontal: 'end', vertical: 'start' })}></div>
<div class="block-resize block-resize-bottom-left"
onMousedown={e => onmousedown(e, { horizontal: 'start', vertical: 'end' })}></div>
<div class="block-resize block-resize-bottom-right"
onMousedown={e => onmousedown(e, { horizontal: 'end', vertical: 'end' })}
></div>
</>}
(3)组件放大与缩小
实现思路
- 记录点击的坐标 xy。
- 如果拖拽的是 中间的点 X轴是不变的,如果是对角点 需要取反,拿到正确的组件top和left
- 用新的 坐标减去原来的坐标,就可以知道在横轴或者纵轴方向的移动距离是多少。
- 最后再将移动距离加上原来组件的高度,就可以得出新的组件高度。
获取点击组件坐标及宽高数据代码:
const onmousedown = (e, direction) => {
e.stopPropagation();
data = {
startX: e.clientX,
startY: e.clientY,
startWidth: props.block.width,
startHeight: props.block.height,
startLeft: props.block.left,
startTop: props.block.top,
direction
}
document.body.addEventListener('mousemove', onmousemove)
document.body.addEventListener('mouseup', onmouseup)
}
组件放大与缩小逻辑代码:
const { width, height } = props.component.resize || {}
let data = {}
const onmousemove = (e) => {
let { clientX, clientY } = e;
let { startX, startY, startWidth, startHeight, startLeft, startTop, direction } = data;
if (direction.horizontal == 'center') { // 如果拖拽的是 中间的点 X轴是不变的
clientX = startX
}
if (direction.vertical == 'center') { // 只能改横向 纵向是不发生变化的
clientY = startY
}
let durX = clientX - startX;
let durY = clientY - startY;
if (direction.vertical == 'start') { // 针对反向拖拽的点 需要取反,拿到正确的组件top和left
durY = -durY
props.block.top = startTop - durY
}
if (direction.horizontal == 'start') {
durX = -durX
props.block.left = startLeft - durX
}
const width = startWidth + durX;
const height = startHeight + durY;
props.block.width = width;
props.block.height = height; // 拖拽的时候 改变了宽高
props.block.hasResize = true
7、预览与导出
(1)预览
实现预览是在编辑功能上实现的,当点击预览时,点击组件不能够获取到焦点
(1)首先设置初始态为编辑态const previewRef = ref(false);
(2)当点击预览按钮时,previewRef.value取非并清除全部焦点 clearBlockFocus();
{
label: '预览', handler: () => {
previewRef.value = !previewRef.value;
clearBlockFocus();
}
},
(3)根据焦点状态渲染页面样式
class={
[block.focus ? 'editor-block-focus' : '',
previewRef.value ? 'editor-block-preview' : ''
]}
(2)导出
导出代码的方式为json,通过jsonSchema描述页面 只需要接收到用户给组件的属性,再通过双向绑定的方法呈现在页面即可
const state = reactive({
option: props.option, // 用户给组件的属性
})
return () => {
return <ElDialog v-model={state.isShow} title={state.option.title}>
{{
default: () => <ElInput
type="textarea"
v-model={state.option.content}
rows={10}
></ElInput>,
footer: () => state.option.footer && <div>
<ElButton onClick={onCancel}>取消</ElButton>
<ElButton type="primary" onClick={onConfirm}>确定</ElButton>
</div>
}}
</ElDialog>
}
{
label: '导出', handler: () => {
$dialog({
title: '导出json使用',
content: JSON.stringify(data.value),
})
}
},
8、组件属性设置
(1)配置属性
我们通过工厂方法,提取函数的公共部分,实现逻辑复用; 在props对象中配置属性,并通过render函数将所需属性配置进标签中。
export let registerConfig = createEditorConfig();
const createInputProp = (label) => ({ type: 'input', label })//工厂方法,复用
const createColorProp = (label) => ({ type: 'color', label })
const createSelectProp = (label, options) => ({ type: 'select', label, options })
const createAddressProp = (label) => ({ type: 'link', label })
const createPictureProp = (label) => ({ type: 'picture', label })
const createFileProp = (label) => ({ type: "file", accept: "video/*", label })
registerConfig.register({
lable: '按钮',
preview: () => <ElButton>预览按钮</ElButton>,
resize: {
width: true,
height: true
},
render: ({ props, size }) => <ElButton style={{ height: size.height + 'px', width: size.width + 'px', fontSize: props.fontsize + 'px' }} type={props.type} size={props.size}>{props.text || '按钮'}</ElButton>,
key: 'button',
props: {
text: createInputProp('按钮内容'),
fontsize: createInputProp('字体大小/px'),
type: createSelectProp('按钮类型', [
{ label: '基础', value: 'primary' },
{ label: '成功', value: 'success' },
{ label: '警告', value: 'warning' },
{ label: '危险', value: 'danger' },
{ label: '文本', value: 'text' },
]),
size: createSelectProp('按钮尺寸', [
{ label: '默认', value: 'default' },
{ label: '大', value: 'large' },
{ label: '小', value: 'small' }
])
}
})
(2)渲染页面属性
先判断鼠标是否点击组件,如果点击画布,则显示容器宽高,否则根据组件配置props遍历所含属性。
if (!props.block) {
content.push(<>
<ElFormItem label="容器宽度">
<ElInputNumber controls-position="right" style="width:400px" v-model={state.editData.width}></ElInputNumber>
</ElFormItem>
<ElFormItem label="容器高度">
<ElInputNumber controls-position="right" style="width:400px" v-model={state.editData.height}></ElInputNumber>
</ElFormItem>
</>)
} else {
let component = config.componentMap[props.block.key];
if (component && component.props) {
content.push(Object.entries(component.props).map(([propName, propConfig]) => {
return <ElFormItem label={propConfig.label}>
{{
input: () => <ElInput style="width:400px" v-model={state.editData.props[propName]}></ElInput>,
color: () => <ElColorPicker v-model={state.editData.props[propName]}></ElColorPicker>,
select: () => <ElSelect style="width:400px" v-model={state.editData.props[propName]}>
{propConfig.options.map(opt => {
return <ElOption label={opt.label} value={opt.value}></ElOption>
})}
</ElSelect>,
link: () =>
<div id="prepend">
<div>Https://</div>
<ElInput v-model={state.editData.props[propName]} style="width:180px"></ElInput>
</div>,
picture: () =>
<ElUpload
action="/dev-api/admin/product/fileUpload"
show-file-list={false}
on-success={handleAvatarSuccess}
before-upload={beforeAvatarUpload}
v-model={state.editData.props[propName]}
limit={1}
>
<div class="uploader-box">
<ElImage v-show={state.editData.props['picture']} src={state.editData.props['picture']} class="avatar"></ElImage>
<ElButton type="success" >{!state.editData.props['picture'] ? '点击上传' : '点击替换'}</ElButton>
</div>
</ElUpload>,
file: () =>
<div>
<input class="file" type="file" accept="video/*" onchange={updateFace} ></input>
{/* <video class="video" src={AUDIO} controls></video> */}
{/* <button onClick={console.log(AUDIO,'onclick')}>删除视频</button> */}
</div>
}[propConfig.type]()}
</ElFormItem >
}))
}
}
9、组件自定义事件
(1)定义事件睡醒
每个组件有一个 events 对象,用于存储绑定的事件。
// 自定义事件
const events = {
redirect(url) {
if (url) {
window.open('https://' + url, '_blank');
}
},
alert(msg) {
if (msg) {
alert(msg)
}
},
}
const eventList = [
{
key: 'redirect',
label: 'url事件',
title: 'url地址',
hint: '请输入完整url地址',
event: events.redirect,
},
{
key: 'alert',
label: 'alert 事件',
title: '弹窗内容',
hint: '请输入内容',
event: events.alert
},
]
(2)添加事件
给每一个组件添加点击事件onclick,events.alert, events.redirect为渲染页面传过来的值
if (
(events.redirect != undefined && events.alert != undefined) ||
(events.redirect != undefined && events.alert == undefined) ||
(events.redirect == undefined && events.alert != undefined)) {
return <ElButton
style={{ height: size.height + 'px', width: size.width + 'px', fontSize: props.fontsize + 'px' }}
type={props.type}
onClick={() => this.handleClick(events.alert, events.redirect)}
size={props.size}>{props.text || '按钮'}</ElButton>
} else {
return <ElButton style={{ height: size.height + 'px', width: size.width + 'px', fontSize: props.fontsize + 'px' }} type={props.type} size={props.size}>{props.text || '按钮'}</ElButton>
}
再依次遍历events,将已添加的点击事件一次触发
handleClick(alert, redirect) {
// 循环触发绑定的事件
Object.keys(events).forEach(event => {
if (event == 'alert') events['alert'](alert)
if (event == 'redirect') events['redirect'](redirect)
})
}