使用Vue渲染可配置表单--记一次问卷平台项目

近几天来了个紧急项目,想要做一个内部版本的问卷星。相当于可以编辑问卷并提供问卷展示,数据统计的这么一个平台。整个项目耗时不长,本着积淀和积累的原则,将过程中的思路和收获进行一下沉淀。由于公司原因,代码尚未开源。

不过沉淀了个动态配置表单的尝试: github,用于后台快速开发表单等需求,搭配element-ui进行使用,同时可通过后台进行配置生成表单等。

功能和效果

问卷编辑功能大概需要一下几点:

  • 根据不同题型添加问题
  • 区分问题的必选性
  • 问题排序,删除,复制功能
  • 选择题的选项编辑,排序,删除功能
  • 问卷渲染
  • 生成问卷二维码

效果

编辑效果

技术方案

Vue + VueRouter + ElementUI

使用element进行后台以及问卷表单渲染是再合适不过的了。极大的节省了需要进行表单样式修改的时间,同时,让动态渲染表单成为一件可能且容易的事情。

表单动态渲染

刚好在项目之前,有过一次动态配置表单的尝试: github 通过字段自动生成表单及验证。但此时的数据格式相当于在后台已经确定好的,针对可变切频繁变动的表单结构,确定数据结构如下:

数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
data: {
title: 问卷名称
desc: 问卷描述
questionList: [
{
type: 问题类型,
label: 问题描述,
required: 必选性,
options: [ //选项
{
label: 选项内容,
value: 选项值
}
...
]
}
...
]
}

表单渲染

最简单的 v-if 模式来满足我们的需求,之前有想过使用is进行渲染,但是不同表单配置项相差很大,很难进行通用。因此采用类似以下这种方式,配置详情可见element官网。

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 填空题 -->
<div v-if="question.type === 'input' || question.type === 'textarea'" class="question-content-wrap">
<el-row>
<el-col :xs="8" :sm="10">
<el-input
v-model="question.value"
:autosize="{ minRows: 2, maxRows: 4}"
class="question-input"
:type="question.type">
</el-input>
</el-col>
</el-row>
</div>

很简单就可以将表单根据配置渲染出来啦:

qss-preview

实现过程

思路理清楚了,就可以动手实践啦!

添加问题

首先,我需要各个问题的基本配置模板,以便于每次直接向questionList中直接添加相应的内容,为了方便存储及使用,将其放在store中,当

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
const state = {
baseSet: {
radio: {
type: 'radio',
label: '单选题',
required: true,
options: [...]
},
checkbox: ...
input: ...
}
}
//添加问题时,直接push进数组即可
const mutations = [
//添加问题
ADDQUESTIONLIST(state, data) {
state.qss.questionList.push(data);
}
]
//添加问题方法
addQuestion(type) {
this.addQuestionList(this. baseSet[type]);
},

注意

使用getter获取到我们对应的baseSet对象时,此对象为引用类型,并且,对象的属性,如options也同样为引用类型。我们若不进行处理,则会出现,创建两个相同类型的问题时,对其中某一问题选项进行修改,另一个选项也会进行修改。 因此我们需要对base对象进行简单的拷贝(只进行到数组内容即可)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export const clone = function(obj) {
var newObj = {};
for (let key in obj) {
var target = obj[key];
if (Object.prototype.toString.call(target) === "[object Object]") {
newObj[key] = clone(target);
} else {
if (Object.prototype.toString.call(target) === "[object Array]") {
newObj[key] = target.slice(0);
} else {
newObj[key] = target;
}
}
}
return newObj;
}
addQuestion(type) {
this.addQuestionList(clone(this. baseSet[type]));
},

排序/删除/复制

这三点基本就是简单的数组操作啦,此时的问题数据依旧是引用类型,直接对引用数组进行操作即可。简单的上移,下移排序,使用splice即可实现。其实这三点都是用splice实现的哈。

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
deleteQuestion(index) {
this.data.questionList.splice(index, 1);
},
copyQuestion(index) {
let list = this.data.questionList;
//复制时,同样需要对引用对象进行深拷贝
list.splice(index, 1, list[index], clone(list[index]));
},
moveQuestion(index, direct) {
let list = this.data.questionList;
if(direct === 'up') {
if(index < 1) {
this.$toast('已经是第一项!');
return;
}
list.splice(index - 1, 2, list[index], list[index - 1]);
} else {
if(index >= list.length - 1) {
this.$toast('已经是最后一项!');
return;
}
list.splice(index, 2, list[index + 1], list[index]);
}
}

生成二维码

使用qrcode.js,感谢大佬们为小辈们造出这么多好用的轮子,让我们站在巨人的肩膀上前行!

其他点

对于Vuex,使用computed获取getters or state,如何配合v-model使用?

我们都知道,针对Vue2.0后,使用computed获取getters or state,而针对计算属性,我们是无法进行写操作的,像这样

1
2
3
4
5
6
7
8
9
computed: {
...mapState({
qss: state => state.qss,
base: state => state.base
})
},
//以下代码是无效的
this.qss = 2;

因此,我们更无法将qss属性直接绑定在v-model上,很是苦恼。同事的一般处理方式是在data中书写相同的属性,在路由进入时对其进行初始化,当其修改时再写回store。这样写起来未免有点麻烦且不妥当。那么,该如何解决呢?

其实很简单,可以交给父组件呀。

我们常常会听到一个词,单向数据流,大概意思就是让数据单一方向流动,我们只对数据源进行修改,再让数据从数据源依次流动到子组件进行UI渲染。

其实就像我们使用ajax获取数据时,统一交给父组件一样,我们将统一获取到的数据,使用props进行向下分发即可,使用vuex亦是如此。子组件值进行对应值的修改。而针对props,v-model可以很方便的对其进行修改了。当然这些只是我的一点理解,如果有异议,可以一起讨论哈。