16

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

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

功能和效果

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

  • 根据不同题型添加问题

  • 区分问题的必选性

  • 问题排序,删除,复制功能

  • 选择题的选项编辑,排序,删除功能

  • 问卷渲染

  • 生成问卷二维码

效果

图片
or
预览

技术方案

Vue + VueRouter + ElementUI

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

表单动态渲染

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

数据结构

data: {
  title: 问卷名称
  desc: 问卷描述
  questionList: [
    {
      type: 问题类型,
      label: 问题描述,
      required: 必选性,
      options: [ //选项
        {
          label: 选项内容,
          value: 选项值
        }
        ...
      ] 
    }
    ...
  ]
}

表单渲染

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

<!-- 填空题 -->
<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>

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

clipboard.png

实现过程

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

添加问题

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

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对象进行简单的拷贝(只进行到数组内容即可)

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实现的哈。

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,而针对计算属性,我们是无法进行写操作的,像这样

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可以很方便的对其进行修改了。当然这些只是我的一点理解,如果有异议,可以一起讨论哈。


Athon
1.7k 声望1.1k 粉丝