JavaScript

超轻量级php框架startmvc

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

更新时间:2020-08-03 02:54:01 作者:startmvc
先简单说明一下,这个Demo引入的vue,iview的方式是标签引入的,没有用到webpack之类的构建工具..

先简单说明一下,这个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 = 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的表头 列: '资金<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 版本

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

vue iview 可编辑表格 vue表格编辑 iview可编辑表格