vue+iview 實現可編輯表格

catkinmu發表於2018-10-30

先簡單說明一下,這個Demo引入的vue,iview的方式是標籤引入的,沒有用到webpack之類的構建工具…畢竟公司還在用angularjs+jq.這也是我第一次寫文章,大家看看思路就行了,要是有大佬指點指點就更好了

話不多說,先來個效果圖

vue+iview 實現可編輯表格

我們再看下極為簡單的目錄結構

vue+iview 實現可編輯表格
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="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: github.com/catkinmu/vu…

參考

來源:https://juejin.im/post/5bd7bd4be51d45662d5289e1

相關文章