资讯专栏INFORMATION COLUMN

vue+iview 实现可编辑表格

Anleb / 453人阅读

摘要:先简单说明一下这个引入的的方式是标签引入的没有用到之类的构建工具毕竟公司还在用这也是我第一次写文章大家看看思路就行了要是有大佬指点指点就更好了话不多说先来个效果图我们再看下极为简单的目录结构实现的可编辑表格首页首页相关与业务无关的纯工具函数

先简单说明一下,这个Demo引入的vue,iview的方式是标签引入的,没有用到webpack之类的构建工具...
毕竟公司还在用angularjs+jq.
这也是我第一次写文章,大家看看思路就行了,要是有大佬指点指点就更好了

话不多说,先来个效果图

我们再看下极为简单的目录结构

IViewEditTable                ## vue+iview 实现的可编辑表格
└── index.html                ## 首页
└── js
    └── editTable.js          ## 首页JS
└── ivew                      ## iview相关
└── vue
    ├── axios.min.js          ## axios (ajax)
    ├── util.js               ## 与业务无关的纯工具函数包
    └── vue.min.js            ## vue (2.x)

首页html:




  
  可编辑表格
  


  

首页没什么说的,都是基本的架子. 这是需要渲染的数据及其说明:

{
  "Status": 1,
  "Total": 233,
  "Items": [{
    "ID": 1,
    "PID": 3,
    "PRJCODE": "2018-001",                                          //项目编号  不可编辑
    "PRJNAME": "淡化海水配套泵站",                                  //项目名称  文本输入框
    "PRJTYPE": "基础设施",                                          //项目类型  下拉选项
    "JSUNIT": "投资公司",                                           //建设单位  文本输入框
    "FLOW_TYPE_CODE":"A02",                                         //流程分类  下拉选项,与数据库以code形式交互
    "DATE_START": "2018-12-1",                                      //开工时间  日期选择
    "DATE_END": "2019-12-1",                                        //竣工时间  日期选择
    "CONTENT": "建设淡化海水配套泵站一座,占地面积约8500平方米",    //建设内容  多行输入框
    "INVEST_ALL": "1000"                                            //总投资    数字输入框
  }]
}

还有editTable.js的基本架子,$http是我为了方便在utils最后一行加入的 (angularjs用多了,习惯用$http)

Vue.prototype.utils = utils
window.$http = axios

editTable.js :

var vm = new Vue({
  el: "#editTableCtrl",
  data: function() {
    return {
      loading: true,
      //表格的数据源
      dataList: [],
      // 列
      columnsList: [],
      // 增加编辑状态, 保存状态, 用于操作数据 避免干扰原数据渲染
      cloneDataList: []
    }
  },
  methods: {
    getData: function() {
      var self = this;
      self.loading = true;
      $http.get("json/editTable.txt").then(function(res) {
        self.dataList = res.data.Items;
        self.loading = false;
      });
    },
  },
  created: function() {
    this.getData();
  }
});

我们再来按照iview的规则编写渲染的列:

//...

      /**
       * @name columnsList (浏览器 渲染的列)  
       * @author catkin
       * @see https://www.iviewui.com/components/table
       * @param 
       * { 
       *  titleHtml : 渲染带有html的表头 列: "资金来源"
       *  editable  : true,可编辑的列 必须有字段 
       *  option    : 渲染的下拉框列表,如果需要与数据库交互的值与显示的值不同,须使用[{value:"value",label:"label"}]的形式,下面有例子
       *  date      : 渲染成data类型 ,可选参数: 
       *              date | daterange: yyyy-MM-dd (默认)
       *              datetime | datetimerange: yyyy-MM-dd HH:mm:ss
       *              year: yyyy
       *              month: yyyy-MM
       *  input     : 渲染input类型 ,可选参数为html5所有类型 (额外增加 textarea 属性), 默认text
       *  handle    : 数组类型, 渲染操作方式,目前只支持 "edit", "delete"
       * }
       * @version 0.0.1
       */

columnsList: [{
    width: 80,
    type: "index",
    title: "序号",
    align: "center"
}, {
    align: "center",
    title: "项目编号",
    key: "PRJCODE"
}, {
    align: "center",
    title: "项目名称",
    titleHtml: "项目名称 ",
    key: "PRJNAME",
    editable: true
}, {
    align: "center",
    title: "项目分类",
    titleHtml: "项目分类 ",
    key: "PRJTYPE",
    option: ["产业项目", "基础设施", "民生项目", "住宅项目"],
    editable: true
}, {
    align: "center",
    title: "建设单位",
    titleHtml: "建设单位 ",
    key: "JSUNIT",
    editable: true
}, {
    align: "center",
    title: "流程分类",
    titleHtml: "流程分类 ",
    key: "FLOW_TYPE_CODE",
    option: [{
      value: "A01",
      label: "建筑-出让"
    }, {
      value: "A02",
      label: "建筑-划拨"
    }, {
      value: "B01",
      label: "市政-绿化"
    }, {
      value: "B02",
      label: "市政-管线"
    }],
    editable: true
}, {
    align: "center",
    title: "开工时间",
    titleHtml: "开工时间 ",
    key: "DATE_START",
    //这里在后面处理的时候会分割成["month","yyyy-MM"]的数组,分别代表iview的DatePicker组件选择日期的格式与数据库传过来时页面显示的格式
    date: "month_yyyy-MM",   
    editable: true
}, {
    align: "center",
    title: "竣工时间",
    titleHtml: "竣工时间 ",
    key: "DATE_END",
    date: "month_yyyy-MM",
    editable: true
}, {
    align: "center",
    title: "建设内容",
    titleHtml: "建设内容 ",
    key: "CONTENT",
    input: "textarea",
    editable: true
}, {
    align: "center",
    title: "总投资(万元)",
    titleHtml: "总投资
(万元) ", key: "INVEST_ALL", input: "number", editable: true }, { title: "操作", align: "center", width: 150, key: "handle", handle: ["edit", "delete"] }] //...

此时页面应该已经可以渲染出表格了

既然要编辑数据,并且我的需求是整行整行的编辑,而编辑的同时就会同步更新数据,那么问题来了
vue中, 数据更新,视图会随之更新. 想象一下,我在输入框中属于一个值,触发了数据更新,接着又触发了视图更新,那么我每次输入时,这个input都会失焦,毫无用户体验. 所以我们把可编辑的动态内容用cloneDataList渲染,静态显示的用dataList渲染
//...
self.dataList = res.data.Items;
// 简单的深拷贝,虽然map会返回新数组,但是数组元素也是引用类型,不能直接改,所以先深拷贝一份
self.cloneDataList = JSON.parse(JSON.stringify(self.dataList)).map(function(item) {
  // 给每行添加一个编辑状态 与 保存状态, 默认都是false
  item.editting = false;
  item.saving = false;
  return item;
});
//...

接下来,我们要根据columnsList做一次循环判断,根据相应的key写出不同的render函数

//全局添加
//根据value值找出数组中的对象元素
function findObjectInOption(value) {
  return function(item) {
    return item.value === value;
  }
}
//动态添加编辑按钮
var editButton = function(vm, h, currentRow, index) {
  return h("Button", {
    props: {
      size: "small",
      type: currentRow.editting ? "success" : "primary",
      loading: currentRow.saving
    },
    style: {
      margin: "0 5px"
    },
    on: {
      click: function() {
        // 点击按钮时改变当前行的编辑状态, 当数据被更新时,render函数会再次执行,详情参考https://cn.vuejs.org/v2/api/#render
        // handleBackdata是用来删除当前行的editting属性与saving属性
        var tempData = vm.handleBackdata(currentRow)
        if (!currentRow.editting) {
          currentRow.editting = true;
        } else {
          // 这里也是简单的点击编辑后的数据与原始数据做对比,一致则不做操作,其实更好的应该遍历所有属性并判断
          if (JSON.stringify(tempData) == JSON.stringify(vm.dataList[index])) {
            console.log("未更改");
            return currentRow.editting = false;
          }
          vm.saveData(currentRow, index)
          currentRow.saving = true;
        }
      }
    }
  }, currentRow.editting ? "保存" : "编辑");
};
//动态添加 删除 按钮
var deleteButton = function(vm, h, currentRow, index) {
  return h("Poptip", {
      props: {
        confirm: true,
        title: currentRow.WRAPDATASTATUS != "删除" ? "您确定要删除这条数据吗?" : "您确定要对条数据撤销删除吗?",
        transfer: true,
        placement: "left"
      },
      on: {
        "on-ok": function() {
          vm.deleteData(currentRow, index)
        }
      }
    },
    [
      h("Button", {
        style: {
          color: "#ed3f14",
          fontSize: "18px",
          padding: "2px 7px 0",
          border: "none",
          outline: "none",
          focus: {
            "-webkit-box-shadow": "none",
            "box-shadow": "none"
          }
        },
        domProps: {
          title: "删除"
        },
        props: {
          size: "small",
          type: "ghost",
          icon: "android-delete",
          placement: "left"
        }
      })
    ]);
};


//methods中添加
init: function() {
  console.log("init");
  var self = this;
  self.columnsList.forEach(function(item) {
    // 使用$set 可以触发视图更新
    // 如果含有titleHtml属性 将其值填入表头
    if (item.titleHtml) {
      self.$set(item, "renderHeader", function(h, params) {
        return h("span", {
          domProps: {
            innerHTML: params.column.titleHtml
          }
        });
      });
    }
    // 如果含有操作属性 添加相应按钮
    if (item.handle) {
      item.render = function(h, param) {
        var currentRow = self.cloneDataList[param.index];
        var children = [];
        item.handle.forEach(function(item) {
          if (item === "edit") {
            children.push(editButton(self, h, currentRow, param.index));
          } else if (item === "delete") {
            children.push(deleteButton(self, h, currentRow, param.index));
          }
        });
        return h("div", children);
      };
    }
    //如果含有editable属性并且为true
    if (item.editable) {
      item.render = function(h, params) {
        var currentRow = self.cloneDataList[params.index];
        // 非编辑状态
        if (!currentRow.editting) {
          // 日期类型多带带 渲染(利用工具暴力的formatDate格式化日期)
          if (item.date) {
            return h("span", self.utils.formatDate(currentRow[item.key], item.date.split("_")[1]))
          }
          // 下拉类型中value与label不一致时多带带渲染
          if (item.option && self.utils.isArray(item.option)) {
            // 我这里为了简单的判断了第一个元素为object的情况,其实最好用every来判断所有元素
            if (typeof item.option[0] === "object") {
              return h("span", item.option.find(findObjectInOption(currentRow[item.key])).label);
            }
          }
          return h("span", currentRow[item.key]);
        } else {
        // 编辑状态
          //如果含有option属性
          if (item.option && self.utils.isArray(item.option)) {
            return h("Select", {
              props: {
                // ***重点***:  这里要写currentRow[params.column.key],绑定的是cloneDataList里的数据
                value: currentRow[params.column.key]
              },
              on: {
                "on-change": function(value) {
                  self.$set(currentRow, params.column.key, value)
                }
              }
            }, item.option.map(function(item) {
              return h("Option", {
                props: {
                  value: item.value || item,
                  label: item.label || item
                }
              }, item.label || item);
            }));
          } else if (item.date) {
            //如果含有date属性
            return h("DatePicker", {
              props: {
                type: item.date.split("_")[0] || "date",
                clearable: false,
                value: currentRow[params.column.key]
              },
              on: {
                "on-change": function(value) {
                  self.$set(currentRow, params.column.key, value)
                }
              }
            });
          } else {
            // 默认input
            return h("Input", {
              props: {
                // type类型也是自定的属性
                type: item.input || "text",
                // rows只有在input 为textarea时才会起作用
                rows: 3,
                value: currentRow[params.column.key]
              },
              on: {
                "on-change"(event) {
                  self.$set(currentRow, params.column.key, event.target.value)
                }
              }
            });
          }
        }
      };
    }
  });
},
// 还原数据,用来与原始数据作对比的
handleBackdata: function(object) {
  var clonedData = JSON.parse(JSON.stringify(object));
  delete clonedData.editting;
  delete clonedData.saving;
  return clonedData;
}

到这里完成已经差不多了,补上保存数据与删除数据的函数

// 保存数据
saveData: function(currentRow, index) {
  var self = this;
  // 修改当前的原始数据, 就不需要再从服务端获取了
  this.$set(this.dataList, index, this.handleBackdata(currentRow))
  // 需要保存的数据
  // 模拟ajax
  setTimeout(function() {
    充值编辑与保存状态
    currentRow.saving = false;
    currentRow.editting = false;
    self.$Message.success("保存完成");
    console.log(self.dataList);
  }, 1000)
},
// 删除数据
deleteData: function(currentRow, index) {
  var self = this;
  console.log(currentRow.ID);
  setTimeout(function() {
    self.$delete(self.dataList, index)
    self.$delete(self.cloneDataList, index)
    vm.$Message.success("删除成功");
  }, 1000)
},

完整的editTable.js代码

// 根据数据中下拉的值找到对应的对象
function findObjectInOption(name) {
  return function(item) {
    return item.value === name;
  }
}
var editButton = function(vm, h, currentRow, index) {
  return h("Button", {
    props: {
      size: "small",
      type: currentRow.editting ? "success" : "primary",
      loading: currentRow.saving
    },
    style: {
      margin: "0 5px"
    },
    on: {
        click: function() {
          // 点击按钮时改变当前行的编辑状态,当数据被更新时,render函数会再次执行,详情参考https://cn.vuejs.org/v2/api/#render
          // handleBackdata是用来删除当前行的editting属性与saving属性
          var tempData = vm.handleBackdata(currentRow)
          if (!currentRow.editting) {
            currentRow.editting = true;
          } else {
            // 这里也是简单的点击编辑后的数据与原始数据做对比,一致则不做操作,其实更好的应该遍历所有属性并判断
            if (JSON.stringify(tempData) == JSON.stringify(vm.dataList[index])) {
              console.log("未更改");
              return currentRow.editting = false;
            }
            vm.saveData(currentRow, index)
            currentRow.saving = true;
          }
        }
    }
  }, currentRow.editting ? "保存" : "编辑");
};
//动态添加 删除 按钮
var deleteButton = function(vm, h, currentRow, index) {
  return h("Poptip", {
      props: {
        confirm: true,
        title: currentRow.WRAPDATASTATUS != "删除" ? "您确定要删除这条数据吗?" : "您确定要对条数据撤销删除吗?",
        transfer: true,
        placement: "left"
      },
      on: {
        "on-ok": function() {
          vm.deleteData(currentRow, index)
        }
      }
    },
    [
      h("Button", {
        style: {
          color: "#ed3f14",
          fontSize: "18px",
          padding: "2px 7px 0",
          border: "none",
          outline: "none",
          focus: {
            "-webkit-box-shadow": "none",
            "box-shadow": "none"
          }
        },
        domProps: {
          title: "删除"
        },
        props: {
          size: "small",
          type: "ghost",
          icon: "android-delete",
          placement: "left"
        }
      })
    ]);
};
var vm = new Vue({
  el: "#editTableCtrl",
  data: function() {
    return {
      loading: true,
      //表格的数据源
      dataList: [],
      /**
       * @name columnsList (浏览器 渲染的列)  
       * @author ch
       * @see https://www.iviewui.com/components/table
       * @param 
       * { 
       *  titleHtml : 渲染带有html的表头 列: "资金来源"
       *  editable  : true,可编辑的列 必须有字段 
       *  option    : 渲染的下拉框列表
       *  date      : 渲染成data类型 ,可选参数: 
       *              date | daterange: yyyy-MM-dd (默认)
       *              datetime | datetimerange: yyyy-MM-dd HH:mm:ss
       *              year: yyyy
       *              month: yyyy-MM
       *  input     : 渲染input类型 ,可选参数为html5所有类型 (额外增加 textarea 属性), 默认text
       *  handle    : 数组类型, 渲染操作方式,目前只支持 "edit", "delete"
       * }
       * @version 0.0.1
       */
      columnsList: [{
        width: 80,
        type: "index",
        title: "序号",
        align: "center"
      }, {
        align: "center",
        title: "项目编号",
        key: "PRJCODE"
      }, {
        align: "center",
        title: "项目名称",
        titleHtml: "项目名称 ",
        key: "PRJNAME",
        editable: true
      }, {
        align: "center",
        title: "项目分类",
        titleHtml: "项目分类 ",
        key: "PRJTYPE",
        option: ["产业项目", "基础设施", "民生项目", "住宅项目"],
        editable: true
      }, {
        align: "center",
        title: "建设单位",
        titleHtml: "建设单位 ",
        key: "JSUNIT",
        editable: true
      }, {
        align: "center",
        title: "流程分类",
        titleHtml: "流程分类 ",
        key: "FLOW_TYPE_CODE",
        option: [{
          value: "A01",
          label: "建筑-出让"
        }, {
          value: "A02",
          label: "建筑-划拨"
        }, {
          value: "B01",
          label: "市政-绿化"
        }, {
          value: "B02",
          label: "市政-管线"
        }],
        editable: true
      }, {
        align: "center",
        title: "开工时间",
        titleHtml: "开工时间 ",
        key: "DATE_START",
        //这里在后面处理的时候会分割成["month","yyyy-MM"]的数组,分别代表iview的DatePicker组件选择日期的格式与数据库传过来时页面显示的格式
        date: "month_yyyy-MM",
        editable: true
      }, {
        align: "center",
        title: "竣工时间",
        titleHtml: "竣工时间 ",
        key: "DATE_END",
        date: "month_yyyy-MM",
        editable: true
      }, {
        align: "center",
        title: "建设内容",
        titleHtml: "建设内容 ",
        key: "CONTENT",
        input: "textarea",
        editable: true
      }, {
        align: "center",
        title: "总投资(万元)",
        titleHtml: "总投资
(万元) ", key: "INVEST_ALL", input: "number", editable: true }, { title: "操作", align: "center", width: 150, key: "handle", handle: ["edit", "delete"] }], // 增加编辑状态, 保存状态, 用于操作数据 避免干扰原数据渲染 cloneDataList: [] } }, methods: { getData: function() { var self = this; self.loading = true; $http.get("json/editTable.txt").then(function(res) { // 给每行添加一个编辑状态 与 保存状态 self.dataList = res.data.Items; self.cloneDataList = JSON.parse(JSON.stringify(self.dataList)).map(function(item) { item.editting = false; item.saving = false; return item; }); self.loading = false; }); }, //初始化数据 //methods中添加 init: function() { console.log("init"); var self = this; self.columnsList.forEach(function(item) { // 使用$set 可以触发视图更新 // 如果含有titleHtml属性 将其值填入表头 if (item.titleHtml) { self.$set(item, "renderHeader", function(h, params) { return h("span", { domProps: { innerHTML: params.column.titleHtml } }); }); } // 如果含有操作属性 添加相应按钮 if (item.handle) { item.render = function(h, param) { var currentRow = self.cloneDataList[param.index]; var children = []; item.handle.forEach(function(item) { if (item === "edit") { children.push(editButton(self, h, currentRow, param.index)); } else if (item === "delete") { children.push(deleteButton(self, h, currentRow, param.index)); } }); return h("div", children); }; } //如果含有editable属性并且为true if (item.editable) { item.render = function(h, params) { var currentRow = self.cloneDataList[params.index]; // 非编辑状态 if (!currentRow.editting) { // 日期类型多带带 渲染(利用工具暴力的formatDate格式化日期) if (item.date) { return h("span", self.utils.formatDate(currentRow[item.key], item.date.split("_")[1])) } // 下拉类型中value与label不一致时多带带渲染 if (item.option && self.utils.isArray(item.option)) { // 我这里为了简单的判断了第一个元素为object的情况,其实最好用every来判断所有元素 if (typeof item.option[0] === "object") { return h("span", item.option.find(findObjectInOption(currentRow[item.key])).label); } } return h("span", currentRow[item.key]); } else { // 编辑状态 //如果含有option属性 if (item.option && self.utils.isArray(item.option)) { return h("Select", { props: { // ***重点***: 这里要写currentRow[params.column.key],绑定的是cloneDataList里的数据 value: currentRow[params.column.key] }, on: { "on-change": function(value) { self.$set(currentRow, params.column.key, value) } } }, item.option.map(function(item) { return h("Option", { props: { value: item.value || item, label: item.label || item } }, item.label || item); })); } else if (item.date) { //如果含有date属性 return h("DatePicker", { props: { type: item.date.split("_")[0] || "date", clearable: false, value: currentRow[params.column.key] }, on: { "on-change": function(value) { self.$set(currentRow, params.column.key, value) } } }); } else { // 默认input return h("Input", { props: { // type类型也是自定的属性 type: item.input || "text", // rows只有在input 为textarea时才会起作用 rows: 3, value: currentRow[params.column.key] }, on: { "on-change"(event) { self.$set(currentRow, params.column.key, event.target.value) } } }); } } }; } }); }, saveData: function(currentRow, index) { var self = this; // 修改当前的原始数据, 就不需要再从服务端获取了 this.$set(this.dataList, index, this.handleBackdata(currentRow)) // 需要保存的数据 // 模拟ajax setTimeout(function() { // 重置编辑与保存状态 currentRow.saving = false; currentRow.editting = false; self.$Message.success("保存完成"); console.log(self.dataList); }, 1000) }, // 删除数据 deleteData: function(currentRow, index) { var self = this; console.log(currentRow.ID); setTimeout(function() { self.$delete(self.dataList, index) self.$delete(self.cloneDataList, index) vm.$Message.success("删除成功"); }, 1000) }, // 还原数据,用来与原始数据作对比的 handleBackdata: function(object) { var clonedData = JSON.parse(JSON.stringify(object)); delete clonedData.editting; delete clonedData.saving; return clonedData; } }, created: function() { this.getData(); this.init(); } });
总结

两三天的时间搞的这些,刚开始也是各种懵逼.期间也试过用插槽来实现,但还是没有这个方法来的清晰(队友都能看懂), 总的来说就是在columnsList自定义一些属性,后面根据这些属性,在render函数里return不同的值,思路还是很简单的.

第一次写文章,并且我也是vue初学者,写的凑合看吧 ^_^ ,欢迎留言指正

本demo 连同之前学习vue时写的demo一起放在的我的github上,欢迎star...

github: https://github.com/catkinmu/v...

参考

vue: render文档

iview

iview admin1.x 版本

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/98843.html

相关文章

  • iView 发布后台管理系统 iview-admin,没错,它就是你想要的

    摘要:简介是基于,搭配使用组件库形成的一套后台集成解决方案,由前端可视化团队部分成员开发维护。遵守设计和开发约定,风格统一,设计考究,并且更多功能在不停开发中。 showImg(https://segmentfault.com/img/remote/1460000011603206); 简介 iView Admin 是基于 Vue.js,搭配使用 iView UI 组件库形成的一套后台集成解...

    HackerShell 评论0 收藏0
  • iview Table表格组件无法拆分单元格的解决思路

    摘要:因为我们项目中首要的是单元格拆分的,因此以拆分为例。使用函数对表格组件的表格列配置数据进行动态改造,普通单元格渲染标签呈现数据,要拆分的单元格渲染原生标签最后隐藏嵌套表格的边框及调整相关原生表格样式。 最近在开发的Vue项目中,使用了iview第三方UI库;对于表格组件的需求是最多的,但是在一些特定场景下,发现iview的表格组件没有单元格合并与拆分的API,搜了一下发现很多同学提问关...

    songze 评论0 收藏0
  • VUE UI框架对比 element-ui 与 iView

    摘要:而则是用到到指令结合的方式去生成,批量生成元素。表格操作列自定义渲染的时,使用的是的函数,直接在中插入对应模板表格分页都需要引入分页组件配合使用两者总体比较,要比简洁许多。 element VS iview(最近项目UI框架在选型 ,做了个分析, 不带有任何利益相关)主要从以下几个方面来做对比使用率(npm 平均下载频率,组件数量,star, issue…)API风格打包优化与设计师友...

    ZHAO_ 评论0 收藏0
  • iView 发布 3.0 版本,以及开发者社区等 5 款新产品

    摘要:相对时间组件锚点组件面板分割组件分割线组件单元格组件相对时间组件用于表示几分钟前几小时前等相对于此时此刻的时间描述。单元格组件在手机上比较常见,在上则常用于固定的侧边菜单项。开发者社区这是发布会最劲爆的一款产品了。 showImg(https://segmentfault.com/img/bVbeuj6?w=2864&h=1458); 7 月 28 日,我们成功地召开了 iView 3...

    FreeZinG 评论0 收藏0

发表评论

0条评论

Anleb

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<