首页 > 编程 > JavaScript > 正文

vue+iview 实现可编辑表格的示例代码

2019-11-19 12:36:27
字体:
来源:转载
供稿:网友

先简单说明一下,这个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:

<!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml"><head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>可编辑表格</title> <link href="iview/iview.css" rel="external nofollow" rel="stylesheet" /></head><body style="background-color: #f0f3f4;"> <div id="editTableCtrl"> <i-table :loading="loading" border :data="dataList" :columns="columnsList" stripe size="small"></i-table> </div> <script src="vue/axios.min.js"></script> <script src="vue/vue.min.js"></script> <script src="iview/iview.min.js"></script> <script src="vue/util.js"></script> <script src="js/editTable.js"></script></body></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 = utilswindow.$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的表头 列: '资金<em class="blue" style="color:red">来源</em>'    * 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: '项目名称 <i class="ivu-icon ivu-icon-edit"></i>',  key: 'PRJNAME',  editable: true}, {  align: 'center',  title: '项目分类',  titleHtml: '项目分类 <i class="ivu-icon ivu-icon-edit"></i>',  key: 'PRJTYPE',  option: ['产业项目', '基础设施', '民生项目', '住宅项目'],  editable: true}, {  align: 'center',  title: '建设单位',  titleHtml: '建设单位 <i class="ivu-icon ivu-icon-edit"></i>',  key: 'JSUNIT',  editable: true}, {  align: 'center',  title: '流程分类',  titleHtml: '流程分类 <i class="ivu-icon ivu-icon-edit"></i>',  key: 'FLOW_TYPE_CODE',  option: [{   value: 'A01',   label: '建筑-出让'  }, {   value: 'A02',   label: '建筑-划拨'  }, {   value: 'B01',   label: '市政-绿化'  }, {   value: 'B02',   label: '市政-管线'  }],  editable: true}, {  align: 'center',  title: '开工时间',  titleHtml: '开工时间 <i class="ivu-icon ivu-icon-edit"></i>',  key: 'DATE_START',  //这里在后面处理的时候会分割成['month','yyyy-MM']的数组,分别代表iview的DatePicker组件选择日期的格式与数据库传过来时页面显示的格式  date: 'month_yyyy-MM',    editable: true}, {  align: 'center',  title: '竣工时间',  titleHtml: '竣工时间 <i class="ivu-icon ivu-icon-edit"></i>',  key: 'DATE_END',  date: 'month_yyyy-MM',  editable: true}, {  align: 'center',  title: '建设内容',  titleHtml: '建设内容 <i class="ivu-icon ivu-icon-edit"></i>',  key: 'CONTENT',  input: 'textarea',  editable: true}, {  align: 'center',  title: '总投资(万元)',  titleHtml: '总投资<br />(万元) <i class="ivu-icon ivu-icon-edit"></i>',  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的表头 列: '资金<em class="blue" style="color:red">来源</em>'  * 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: '项目名称 <i class="ivu-icon ivu-icon-edit"></i>',  key: 'PRJNAME',  editable: true  }, {  align: 'center',  title: '项目分类',  titleHtml: '项目分类 <i class="ivu-icon ivu-icon-edit"></i>',  key: 'PRJTYPE',  option: ['产业项目', '基础设施', '民生项目', '住宅项目'],  editable: true  }, {  align: 'center',  title: '建设单位',  titleHtml: '建设单位 <i class="ivu-icon ivu-icon-edit"></i>',  key: 'JSUNIT',  editable: true  }, {  align: 'center',  title: '流程分类',  titleHtml: '流程分类 <i class="ivu-icon ivu-icon-edit"></i>',  key: 'FLOW_TYPE_CODE',  option: [{   value: 'A01',   label: '建筑-出让'  }, {   value: 'A02',   label: '建筑-划拨'  }, {   value: 'B01',   label: '市政-绿化'  }, {   value: 'B02',   label: '市政-管线'  }],  editable: true  }, {  align: 'center',  title: '开工时间',  titleHtml: '开工时间 <i class="ivu-icon ivu-icon-edit"></i>',  key: 'DATE_START',  //这里在后面处理的时候会分割成['month','yyyy-MM']的数组,分别代表iview的DatePicker组件选择日期的格式与数据库传过来时页面显示的格式  date: 'month_yyyy-MM',  editable: true  }, {  align: 'center',  title: '竣工时间',  titleHtml: '竣工时间 <i class="ivu-icon ivu-icon-edit"></i>',  key: 'DATE_END',  date: 'month_yyyy-MM',  editable: true  }, {  align: 'center',  title: '建设内容',  titleHtml: '建设内容 <i class="ivu-icon ivu-icon-edit"></i>',  key: 'CONTENT',  input: 'textarea',  editable: true  }, {  align: 'center',  title: '总投资(万元)',  titleHtml: '总投资<br />(万元) <i class="ivu-icon ivu-icon-edit"></i>',  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/vue.js-practice/tree/master/IViewEditTable

参考

vue: render文档
iview
iview admin1.x 版本

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持武林网。

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表