background
operating
: dear, I want to show the user request information, can you do ~ 😊
I
: hey? You tm talk talk, don’t be so close, I tm don’t like men, you find a product/back end to pull a meeting. Yue!!
operating
: kiss, don’t, come on, look for the product again need several days, you say is not, beg you.
I
:You get out of here, I’ll be careful you don’t even know your own mother. I need you to tidy it up. I’ll get it done for you right away. All right, sister.
I
: Pro, back-end big brother, the operation of the newly added needs, you make an interface!
The back-end bosses
: roll!
I
: Play operation recording (little brother, add a page ~)
The back-end bosses
: make!!!!! I’ll call you in two hours.
I
Yue!! Figure out a way to fix this. It’s not a big deal.
Note: Pure fiction, if any, pure similarity. Make!!!!!!
preface
In order to improve operations/products to meet the business needs of low logic and strong demonstration, such as operations support/user center, improve the input-output ratio, reduce development costs, and improve the flexibility of operations/products. Deal with yue!! Blunt!
Page component
Material area, editing area, display area, property modification area. Unified JSON data import and export. Matching relationship between back-end maintenance item-page address-JSON. Unified renderJson method was extracted from front-end rendering, and unified maintenance and management of NPM package was released.
Project initialization
Initialize the project through vUE/CLI
// Here we choose vue3
vue create lowcode
cd lowcode
yarn
yarn serve
Copy the code
Global configuration data
// data.json
{
"container": {
"width": "1000"."height": "1000"
},
"blocks": [{"top": 100."left": 100."zIndex": 1."key": "input"
},
{
"top": 200."left": 200."zIndex": 1."key": "text"
},
{
"top": 300."left": 300."zIndex": 1."key": "button"}}]Copy the code
Container is used to configure the size of the render area, and blocks is used to render the corresponding components and positions in the render area. The key finds the corresponding component in our materials section.
Material area
The material is dynamically configurable, so we need to put forward the configuration item of the material separately, which can be registered. Provides both preview mode and render mode.
// block-config.js
// Material registration
import { ElButton, ElInput } from 'element-plus'
const registerConfig = () = > {
const componentList = []
const componentMap = {}
return {
componentList,
componentMap,
register(component) {
componentList.push(component)
componentMap[component.key] = component
},
}
}
const registe = registerConfig()
registe.register({
key: 'text'.label: 'text'.preview: () = > 'Preview text'.render: () = > 'Render text',
})
registe.register({
key: 'button'.label: 'button'.preview: () = > <ElButton>Preview button</ElButton>,
render: () = > <ElButton>The render button</ElButton>,
})
registe.register({
key: 'input'.label: 'Input field'.preview: () = > <ElInput placeholder="Preview input box"></ElInput>,
render: () = > <ElInput placeholder="Render input box"></ElInput>,})export default registe
Copy the code
ComponentList for material area rendering, componentMap for rendering area rendering, global configuration through key one-to-one correspondence to find our registered group material building
Drag function in material area
// src/hook/useDragger
const useDragger = (containerRef, data) = > {
let currentComponent = null
const dragenter = (e) = > {
e.dataTransfer.dropEffect = 'move'
}
const dragover = (e) = > {
e.preventDefault()
}
const dragleave = (e) = > {
e.dataTransfer.dropEffect = 'none'
}
const drop = (e) = > {
console.log('currentComponent: ', currentComponent) data.value = { ... data.value,blocks: [
...data.value.blocks,
{
top: e.offsetY,
left: e.offsetX,
zIndex: 1.key: currentComponent.key,
alignCenter: true.// Indicate that the component needs to be in the middle of the mouse after the drag. Change the component position in the build render onMonted
},
],
}
currentComponent = null
}
const dragstart = (e, component) = > {
/** * dragover target is triggered when the element is passed, the default behavior must be disabled, otherwise it will not trigger drop * dragleave when it leaves the element, add a warning flag ** Drop release is triggered, add the component to the render area */
currentComponent = component
containerRef.value.addEventListener('dragenter', dragenter)
containerRef.value.addEventListener('dragover', dragover)
containerRef.value.addEventListener('dragleave', dragleave)
containerRef.value.addEventListener('drop', drop)
}
const dragend = () = > {
containerRef.value.removeEventListener('dragenter', dragenter)
containerRef.value.removeEventListener('dragover', dragover)
containerRef.value.removeEventListener('dragleave', dragleave)
containerRef.value.removeEventListener('drop', drop)
}
return {
dragstart,
dragend,
}
}
export default useDragger
// editor.jsx
const containerRef = ref(null)
const { dragstart, dragend } = useDragger(containerRef, data)
// editor-block.jsx
onMounted(() = > {
if (props.block.alignCenter) {
const { offsetWidth, offsetHeight } = blockRef.value
props.block.top = props.block.top - offsetHeight / 2
props.block.left = props.block.left - offsetWidth / 2
props.block.alignCenter = false}})Copy the code
ContainerRef is the DOM node in our render area that listens for DARG events, data is the rendered data that refers to our data.json, the material that’s being dragged in the left side of the currentComponent material area, I’m listening for the dragstart event in the left side of the material area, Retain the component that is being dragged
Drag and drop in the render area
Get focus hook
// useFoucs.js
import { computed } from 'vue'
const useFocus = (data, callback) = > {
const clearFoucsData = () = > {
data.value.blocks.forEach((block) = > {
block.focus = false})}const mousedown = (e, block) = > {
e.preventDefault()
e.stopPropagation()
if (e.shiftKey) {
if(foucsData. Value. Foucs. Length < =1) {
block.focus = true
} else{ block.focus = ! block.focus } }else {
if(! block.focus) { clearFoucsData() block.focus =true
}
}
callback(e)
}
const foucsData = computed(() = > {
const foucs = []
const unfoucs = []
data.value.blocks.forEach((block) = > {
block.focus ? foucs.push(block) : unfoucs.push(block)
})
return {
foucs,
unfoucs,
}
})
const containMousedown = () = > {
clearFoucsData()
}
return {
mousedown,
containMousedown,
foucsData,
}
}
export default useFocus
Copy the code
Listen to the mousedown event to get the clicked block data, and set blocket. focus to true. At the same time, calculate a foucsData data to facilitate the subsequent batch movement of selected blocks. Callback is important because when clicked, it will be immediately dragged, so you need a function that executes immediately to listen for mouse movements and modify component positions.
Render area component drag and drop
// useMove.js
const useMove = (foucsData) = > {
let dragSate = {
startX: 0.startY: 0,}const mousemove = (e) = > {
let moveX = e.clientX - dragSate.startX
let moveY = e.clientY - dragSate.startY
foucsData.value.foucs.forEach((block, index) = > {
block.top = dragSate.statePos[index].top + moveY
block.left = dragSate.statePos[index].left + moveX
})
}
const mouseup = () = > {
document.removeEventListener('mousemove', mousemove)
document.removeEventListener('mouseup', mouseup)
markLine.x = null
markLine.y = null
}
const mouseEvent = (e) = > {
dragSate = {
startX: e.clientX,
startY: e.clientY,
statePos: foucsData.value.foucs.map(({ top, left }) = > ({ top, left })),
}
document.addEventListener('mousemove', mousemove)
document.addEventListener('mouseup', mouseup)
}
return {
mouseEvent,
}
}
export default useMove
Copy the code
In the callback mentioned above, we listen to the mousemove of the page to calculate the offset of the mouse, and record the dragSate position of the mouse click. At the same time, we need to obtain the initial position of all selected blocks in foucsData, and modify the position of all blocks in move immediately to achieve the purpose of batch movement.
Render area helper line function
As seen above we need to find the last selected component, so inuseFoucs
The recordlastSelectBlock
To facilitate the calculation of the position relationship with unselected components. So this brings us to the hardest part, which is figuring out the position of the auxiliary line, right hereB
That means ourslastSelectBlock
The last component selected,A
Refers to our unselected component. Then IB
andA
Altogether10
Species, respectively:
Top to top top to bottom middle to middle bottom to top bottom left to left left to right middle to right right to left right to right
Here are the 10 mappings
// useFoucs.js
// add the following code
const mouseEvent = (e) = > {
const { width: BWidth, height: BHeight } = lastSelectBlock.value
dragSate = {
startX: e.clientX,
startY: e.clientY,
startTop: lastSelectBlock.value.top,
startLeft: lastSelectBlock.value.left,
statePos: foucsData.value.foucs.map(({ top, left }) = > ({ top, left })),
lines: (() = > {
const { unfoucs } = foucsData.value
let lines = { x: [].y: [] }
unfoucs.forEach((block) = > {
const { top: ATop, left: ALeft, width: AWidth, height: AHeight } = block
lines.y.push({ shotTop: ATop, top: ATop }) / / on top
lines.y.push({ shotTop: ATop, top: ATop - BHeight }) / / top to bottom
lines.y.push({ shotTop: ATop + AHeight / 2.top: ATop + AHeight / 2 - BHeight / 2 }) / / to
lines.y.push({ shotTop: ATop + AHeight, top: ATop + AHeight }) / / the bottom to the top
lines.y.push({ shotTop: ATop + AHeight, top: ATop + AHeight - BHeight }) / / to the bottom
lines.x.push({ shotLeft: ALeft, left: ALeft })
lines.x.push({ shotLeft: ALeft + AWidth, left: ALeft + AWidth })
lines.x.push({ shotLeft: ALeft + AWidth / 2.left: ALeft + AWidth / 2 - BWidth / 2 })
lines.x.push({ shotLeft: ALeft + AWidth, left: ALeft + AWidth - BWidth })
lines.x.push({ shotLeft: ALeft, left: ALeft - BWidth })
})
return lines
})(),
}
document.addEventListener('mousemove', mousemove)
document.addEventListener('mouseup', mouseup)
}
const markLine = reactive({
x: 0.y: 0,})const mousemove = (e) = > {
let moveX = e.clientX - dragSate.startX
let moveY = e.clientY - dragSate.startY
// Calculate the latest left and top values of the current element, and look in the generated line, when less than 5, display line
const left = e.clientX - dragSate.startX + dragSate.startLeft
const top = e.clientY - dragSate.startY + dragSate.startTop
let y = null
let x = null
// Look for lines in each move
for (let i = 0; i < dragSate.lines.y.length; i++) {
const { top: t, shotTop } = dragSate.lines.y[i]
if (Math.abs(t - top) < 5) {
y = shotTop
break}}for (let j = 0; j < dragSate.lines.y.length; j++) {
const { left: l, shotLeft } = dragSate.lines.x[j]
if (Math.abs(l - left) < 5) {
x = shotLeft
break
}
}
markLine.x = x
markLine.y = y
foucsData.value.foucs.forEach((block, index) = > {
block.top = dragSate.statePos[index].top + moveY
block.left = dragSate.statePos[index].left + moveX
})
}
Copy the code
Render area rollback and undo functions
// useCommand.js
/* eslint-disable no-debugger */
import { emitter } from '.. /common/util'
import { onUnmounted } from 'vue'
import deepcopy from 'deepcopy'
const useCommand = (data) = > {
const state = {
current: -1.// The current forward or backward pointer
queue: [].// Record the operation command
commands: {}, // Save the mapping between commands and executing functions
commandArr: [].// Store all commands
distroyArr: [].// Store the destruction command
}
// Button area function
const buttons = [
{
label: 'withdraw'.icon: 'icon-huitui'.handler: () = > {
state.commands.undo()
},
},
{
label: 'redo'.icon: 'icon-zhongzuo'.handler: () = > {
state.commands.redo()
},
},
]
const registry = (command) = > {
state.commandArr.push(command)
state.commands[command.name] = () = > {
const { redo, undo } = command.execute()
if (command.pushQuene) {
let { current, queue } = state
// This is important, there may be many steps in the middle of undo, because the operation only modifies the current value, so calculate the correct queue value on each drag.
if (queue.length > 0) {
state.queue = queue.slice(0, current + 1)
}
state.queue.push({ redo, undo })
state.current = current + 1
} else {
redo && redo()
undo && undo()
}
}
}
registry({
name: 'redo'.execute() {
return {
redo: () = > {
const item = state.queue[state.current + 1]
if (item && item.redo) {
item.redo()
state.current + 1
}
},
}
},
})
registry({
name: 'undo'.execute() {
return {
undo: () = > {
if (state.current >= 0) {
const item = state.queue[state.current]
if (item && item.undo) {
item.undo()
state.current--
}
}
},
}
},
})
registry({
name: 'drag'.pushQuene: true.init() {
this.before = null
const start = () = > {
this.before = deepcopy(data.value.blocks)
}
const end = () = > {
state.commands.drag()
}
emitter.on('start', start)
emitter.on('end', end)
return () = > {
emitter.off('start', start)
emitter.off('end', end)
}
},
execute() {
const before = this.before
const after = data.value.blocks
return {
redo(){ data.value = { ... data.value,blocks: after }
},
undo(){ data.value = { ... data.value,blocks: before }
},
}
},
})
// Initialize all init
state.commandArr.forEach((command) = > command.init && state.distroyArr.push(command.init()))
onUnmounted(() = > {
state.distroyArr.forEach((fn) = > fn && fn())
})
return {
buttons,
state,
}
}
export default useCommand
Copy the code
The logic here is a little complicated, but let me explain it. The first thing that’s stored in state is
CommandArr: [], // Store all commands distroyArr: [], // Store the destruction command
Queue stores the instructions for each operation. For example, execute returns two functions: redo fallback and undo undo, which modify data blocks so that the page falls back and is destroyed. The queue [{redo: fn, undo: fn}]. Commands is used to make a map of the commands we have registered. The actual execute function is called each time we call state.commands[name]. CommandArr is used to hold all registered commands and is used to initialize commands. The distroyArr command is used to unbind the command. Let’s look at the specific execution process:
Same thing with the undo button. performstate.commands.redo
Method can. Here we will not introduce the import and export functions, relatively simple, is to showdata
Data, applicationdata
Data process. There is also a problem that needs to be solved, that is, drag should also be able to back and undo. All we have to do ismounsemove
andmouseup
When to goEmit (' start ') and emit (' end ')
Can.
//useMove.js Added in Mousemoveif(! dragSate.dragging) { dragSate.dragging =true
emitter.emit('start'} added in mouseupif (dragSate.dragging) {
emitter.emit('end')}Copy the code
Edit area function
Edit area function Here is a short time to introduce the text component edit function. The editing capabilities of the other components are similar, with the most important being two-way data binding. So the idea of implementation is the same, I won’t list them all.
// editor-operator.jsx
/* eslint-disable no-unused-vars */
/* eslint-disable no-debugger */
import { defineComponent, inject, watch, reactive } from 'vue'
import {
ElForm,
ElFormItem,
ElButton,
ElInputNumber,
ElColorPicker,
ElSelect,
ElOption,
ElInput,
} from 'element-plus'
import deepcopy from 'deepcopy'
export default defineComponent({
props: {
block: { type: Object }, // The last element selected by the user
data: { type: Object }, // All current data
updateContainer: { type: Function },
updateBlock: { type: Function}},setup(props) {
console.log(props, '----props')
const config = inject('config') // Component configuration information
const state = reactive({
editData: {},})const reset = () = > {
console.log('props.block: ', props.block)
if(! props.block) {// specify the width and height of the container to bind
state.editData = deepcopy(props.data.container)
} else {
state.editData = deepcopy(props.block)
}
}
const apply = () = > {
if(! props.block) {// Change the size of the component containerprops.updateContainer({ ... props.data,container: state.editData })
} else {
// Change the configuration of the component
props.updateBlock(state.editData, props.block)
}
}
watch(() = > props.block, reset, { immediate: true })
return () = > {
let content = []
if(! props.block) { content.push(<div>
<ElFormItem label="Container width">
<ElInputNumber v-model={state.editData.width}></ElInputNumber>
</ElFormItem>
<ElFormItem label="Container height">
<ElInputNumber v-model={state.editData.height}></ElInputNumber>
</ElFormItem>
</div>)}else {
let component = config.componentMap[props.block.key]
console.log('component: ', component)
console.log('state: ', state)
if (component && component.props) {
// {text:{type:'xxx'},size:{},color:{}}
// {text:xxx,size:13px,color:#fff}
content.push(
Object.entries(component.props).map(([propName, propConfig]) = > {
return (
<ElFormItem label={propConfig.label}>
{{
input: () => <ElInput v-model={state.editData.props[propName]}></ElInput>,
color: () => (
<ElColorPicker v-model={state.editData.props[propName]}></ElColorPicker>
),
select: () => (
<ElSelect v-model={state.editData.props[propName]}>
{propConfig.options.map((opt) => {
return <ElOption label={opt.label} value={opt.value}></ElOption>
})}
</ElSelect>
),
table: () => (
<TableEditor
propConfig={propConfig}
v-model={state.editData.props[propName]}
></TableEditor>
),
}[propConfig.type]()}
</ElFormItem>)}}))if (component && component.model) {
// Name of the default label
content.push(
Object.entries(component.model).map(([modelName, label]) = > {
return (
<ElFormItem label={label}>
{/* model => {default:"username"} */}
<ElInput v-model={state.editData.model[modelName]}></ElInput>
</ElFormItem>)}))}}return (
<ElForm labelPosition="top" style="padding:30px">
{content}
<ElFormItem>
<ElButton type="primary" onClick={()= >The apply ()} > applications</ElButton>
<ElButton onClick={reset}>reset</ElButton>
</ElFormItem>
</ElForm>)}}})/ / block - config. Js
registe.register({
key: 'text'.label: 'text'.preview: () = > 'Preview text'.render: (config) = > {
const { props } = config
return (
<span style={{ color: props.color.fontSize: props.size}} >{props. Text | | 'rendering text'}</span>)},props: {
text: createInputProp('Text content'),
color: createColorProp('Font color'),
size: createSelectProp('Font size'[{label: '14px'.value: '14px' },
{ label: '20px'.value: '20px' },
{ label: '24px'.value: '24px'},]),}})/ / useCommand. Js
// With the history of the common mode
registry({
name: 'updateContainer'.// Update the entire container
pushQuene: true.execute(newValue) {
let state = {
before: data.value, // The current value
after: newValue, / / the new values
}
return {
redo: () = > {
data.value = state.after
},
undo: () = > {
data.value = state.before
},
}
},
})
registry({
name: 'updateBlock'.// Update a component
pushQuene: true.execute(newBlock, oldBlock) {
let state = {
before: data.value.blocks,
after: (() = > {
let blocks = [...data.value.blocks] // Make a copy for the new block
const index = data.value.blocks.indexOf(oldBlock) // Find the old need to look through the old
if (index > -1) {
blocks.splice(index, 1, newBlock)
}
return blocks
})(),
}
return {
redo: () = >{ data.value = { ... data.value,blocks: state.after }
},
undo: () = >{ data.value = { ... data.value,blocks: state.before }
},
}
},
})
/ / data. Json increases
{
"top": 200."left": 200."zIndex": 1."key": "text"."props": {
"text": "Text text"."color": "red"."size": "14px"}},/ / editor - block. JSX increase
const componentRender = component.render({
size: props.block.hasResize ? { width: props.block.width, height: props.block.height } : {},
props: props.block.props,
// model: props.block.model => {default:'username'} => {modelValue: FormData.username,"onUpdate:modelValue":v=> FormData.username = v}
model: Object.keys(component.model || {}).reduce((prev, modelName) = > {
let propName = props.block.model[modelName] // 'username'
prev[modelName] = {
modelValue: props.formData[propName], // zfjg
'onUpdate:modelValue': (v) = > (props.formData[propName] = v),
}
return prev
}, {}),
})
Copy the code
The whole logic of the process is, We have lastSelectBlock = props. Block so we loop through component = Config.componentmap [props. Block. key] Component corresponds to the material information registered in block-config. The corresponding text is:
We’re going to look at differentprops[key]
Values render different components and bind our values to the correspondingkey
Theta, and then we have thetablock
When rendering, we will calculate goodprops
Back to therender
function(Render is the render we defined in block-config)
, so that we can be inRender gets props data
Are worth modifying and applying.
Good write almost, roughly introduced the content of all the key points, hit so many words, point a praise and then go, pro!! If you need the above source code, please add group to get, this group in addition to not talk about technology, what all talk… Please leave a message if it has expired.