此项目适用于大多数自定义拖拽项目

项目要求:公司与多家医院对接,对于护理白板中的消息内容的样式有不同的需求,如果为此去进行定制化开发,将产生大量的人力与时间浪费,并且同一项目因此分出多个分支或项目进行管理,将会造成代码管理混乱,因此通过对页面进行自定义开发,从而避免以上问题。数据库中存储页面的DOM结构与展示数据,需要使用时进行实时渲染。

最先想到的是肯定就是后端渲染,每次请求之后刷新页面,方案一与方案二就是后端渲染,前端只负责获取页面,然后innerHTML。

方案一:模板引擎(Handlebars)

原因:由vue的模板引擎想到的的mustacheHandlebars作为mustache的扩展,功能更加丰富

优点:代码量小,对后端基本无要求,只需要提供存取接口,不需要对页面进行任何处理

缺点:作为初级的模板语法,与Vue中使用的模板语法不同的是,未添加Diff功能,因此只能通过innerHtml进行整体DOM替换,因此会造成页面首页白屏,并且整体替换会造成

废除理由:项目实施不愿意学习模板语法

方案二:富文本

老大提出的方案,希望实现所见即所得,类似于Notion这样在页面上直接编辑
原因:需要实现所见即所得

缺点:需要对富文本进行插件开发

方案三(最终方案):九宫格实现页面定位,拖拽实现模块的复用,重写Vue的render函数实现了模块的自定义渲染(算是符合了实施那边的需求,但算不上多么新颖的功能,只是各种技术的堆砌)

这项目有点像上一家公司的自定义驾驶舱,在项目内置一些配置好的图标,然后拖拽到驾驶舱页面,当时使用的是vue-drag-resize做的页面布局,但是到最后布局太过自由,上线时使用人员总是会将各个模块重叠起来,或者布局太过怪异,因此使用了vue-grid-layout进行页面中各模块的布局,先将页面布局定下来,不需要再去各种考虑布局了。
vuedraggable进行预制模块的拖拽,基本上符合了当前的需求

元数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
{
"name": "呼叫提醒",
"key": "call_reminder",
"vNode": {
"tag": "div",
"attr": {
"style": {
"width": "100%",
"height": "100%",
"overflow-y": "auto",
"padding": "16px 24px",
"box-sizing": "border-box",
"font-size": "12px",
},
},
"vNode": [
{
"tag": "div",
"attr": {
"style": {
"display": "flex",
"position": "fixed"
},
},
"vNode": [
{
"tag": "i",
"attr": {
"class": "img-box call_reminder",
"style": {
"width": "27px",
"height": "27px",
"margin-right": "10px",
},
},
"vNode": null,
},
{
"tag": "span",
"attr": {
"style": {
"height": "30px",
"font-size": "22px",
"font-family": "PingFangSC-Medium, PingFang SC",
"font-weight": 500,
"color": "#333333",
"line-height": "30px",
}
},
"type": "eval",
"vNode": "`呼叫提醒(${this.count||0})`", // 使用模板字符串,在渲染函数中自动转换
},
],
},
{
"tag": "div",
"attr": {
"style": {
"position": "absolute",
"top": "52px",
"right": "24px",
"bottom": "16px",
"left": "24px",
"overflow-y": "auto"
}
},
"vNode": [
{
"tag": "div",
"attr": {},
"child": {
"tag": "span",
"attr": {
"style": {
"width": "70px",
"text-align": "center",
"display": "inline-block",
"height": "36px",
"font-size": "20px",
"font-family": "PingFangSC-Regular, PingFang SC",
"font-weight": 400,
"color": "#333333",
"line-height": "36px",
},
},
},
"key": "call_reminder_data",
"keys": "bed_number",
"vNode": "tag",
},
{
"tag": "div",
"attr": { "style": { "width": "100%" } },
"child": {
"tag": "span",
"attr": {
"style": {
"width": "70px",
"text-align": "center",
"display": "inline-block",
"height": "36px",
"font-size": "20px",
"font-family": "PingFangSC-Regular, PingFang SC",
"font-weight": 400,
"color": "#FEAA02",
"line-height": "36px",
},
},
"vNode": "`${textArr[0]}号病房`"
},
"key": "call_reminder_room_data",
"keys": "room_name",
"vNode": "tag",
},
]
}
],
},
},

主要使用vue的$createElement函数和render函数,将元数据渲染成html模板,此方案相较于方案一,因为使用了Vue,有效避免了数据变化时的整屏数据变化

1
2
3
4
5
Vue.component('board', {
render: (h) => {
return h('div', {}, "vNode")
}
})

难点:
先期,元数据中没有VNode这个参数,使用的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 标签渲染
tag: (list, AST) => {
return list.map((v) => {
AST.vNode = v.text || "";
return AST;
});
}
// 表格渲染
table: (list, AST) => {
return list.map((v) => {
const textArr = v.split(",");
AST.vNode.map((item, i) => {
item.vNode = textArr[i];
return item;
});
return AST;
});
},

但是在数据结构保存到数据库时,函数会丢失,而且样式泰国复杂,只能修改成现在改写Vue的render函数,使用$createElement函数创建出虚拟DOM

标题栏需要显示当前数据的多少,因此使用了字符串模板和computed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
Vue.component('board', {
props: {
vNode: {
type: [Array, String, Object, Number],
default: () => {
tag: "div",
attr: {},
vNode: `标题${this.count}`
}
}
},
data() {
return {
list: []
}
},
computed: {
count() {
return this.list.length
}
},
render: (h) => {
const slots = this.$slots.default || [];
const vNode = this.toVnode(this.vNode, this.keyName
return h('div', {style: {width: "100%"}}, [vNode, ...slots]);
},
methods: {
toVnode(vNode) {
if (Array.isArray(vNode)) {
return vNode.map(v => this.objToVNode(v))
} else if (typeof vNode === 'object') {
return this.objToVNode(vNode)
} else {
return '' + vNode
}
},
objToVNode(AST) {
if (typeof AST === 'string') return ast
let { key, tag, keys, attr, vNode, child, type } = AST
const h = this.$createElement
if (Array.isArray(vNode)){
vNode = this.toVnode(vNode, key)
}
else if (child) {
switch (vNode) {
case "tag":
vNode = list.map(v => {
const _child = Object.assign({}, child)
// ...
return _child
})
break;
case "table":
vNode = list.map(v => {
const _child = Object.assign({}, child)
// ...
})
break;
default:
break;
}
// 使用深拷贝解除attr的响应式化(h的attr不支持响应式数据)
attr = Object.assign({}, attr)
// 绑定事件监听器
if (!attr.on) {
if (tag === "i") {
attr.on = {
click: (e) => {
if (this.isComponent)return
e.stopPropagation()
// 使用$EventBus绑定icon修改事件
this.$EventBus.$emit('changeIcon', {
attr,
key: this.keyName
})
}
}
} else {
attr.on = {
click: (e) => {
if (this.isComponent){
window.parent.postMessage(key)
return
}
e.stopPropagation()
if (!attr.style) {
attr.style = {}
}
// 绑定元素被选中事件
this.$EventBus.$emit('picEl', {
style: attr.style,
key: this.keyName
})
}
}
}
}
return this.objToVNode({key: key, tag, attr, vNode})
}
else if (type === "eval") {
vNode = eval(vNode)
}
return h(tag, attr, vNode)
}
}
})

方案四:

最近在用processon,想到了写新方案

因为之前一直使用的是即时数据渲染,因此当没有数据时,页面会显得光秃秃的,因此现在添加新功能:模块底图

底层使用canvas绘制出已经修改并保存过的样式

最后更新: 2022年11月03日 21:58