From fb8479988cfd1db1cfe202bda59d7b6e0924ccef Mon Sep 17 00:00:00 2001 From: Sergio Crisostomo Date: Tue, 17 Oct 2017 10:13:54 +0200 Subject: [PATCH 1/2] Improve export to CSV functionality - added`callback`option so we can get CSV data in case we do not want to download - added options: - `quoted` for cases when we have line breaks in content - `separator` for cases when we have commas in content --- src/components/table/table.vue | 5 ++-- src/utils/csv.js | 54 ++++++++++++++++++---------------- 2 files changed, 32 insertions(+), 27 deletions(-) diff --git a/src/components/table/table.vue b/src/components/table/table.vue index e482ca92..0cc801e7 100644 --- a/src/components/table/table.vue +++ b/src/components/table/table.vue @@ -714,8 +714,9 @@ let noHeader = false; if ('noHeader' in params) noHeader = params.noHeader; - const data = Csv(columns, datas, ',', noHeader); - ExportCsv.download(params.filename, data); + const data = Csv(columns, datas, params, noHeader); + if (params.callback) params.callback(data); + else ExportCsv.download(params.filename, data); } }, created () { diff --git a/src/utils/csv.js b/src/utils/csv.js index ff03c177..b0605415 100644 --- a/src/utils/csv.js +++ b/src/utils/csv.js @@ -1,25 +1,39 @@ -// https://github.com/Terminux/react-csv-downloader/blob/master/src/lib/csv.js +/* + inspired by https://www.npmjs.com/package/react-csv-downloader + now removed from Github +*/ const newLine = '\r\n'; +const appendLine = (content, row, { separator, quoted }) => { + const line = row.map(data => { + if (!quoted) return data; + // quote data + data = typeof data === 'string' ? data.replace(/"/g, '"') : data; + return `"${data}"`; + }); + content.push(line.join(separator)); +}; -export default function csv(columns, datas, separator = ',', noHeader = false) { +const defaults = { + separator: ',', + quoted: false +}; + +export default function csv(columns, datas, options, noHeader = false) { + options = Object.assign({}, defaults, options); let columnOrder; const content = []; const column = []; if (columns) { columnOrder = columns.map(v => { - if (typeof v === 'string') { - return v; - } + if (typeof v === 'string') return v; if (!noHeader) { - column.push((typeof v.title !== 'undefined') ? v.title : v.key); + column.push(typeof v.title !== 'undefined' ? v.title : v.key); } return v.key; }); - if (column.length > 0) { - content.push(column.join(separator)); - } + if (column.length > 0) appendLine(content, column, options); } else { columnOrder = []; datas.forEach(v => { @@ -29,27 +43,17 @@ export default function csv(columns, datas, separator = ',', noHeader = false) { }); if (columnOrder.length > 0) { columnOrder = columnOrder.filter((value, index, self) => self.indexOf(value) === index); - - if (!noHeader) { - content.push(columnOrder.join(separator)); - } + if (!noHeader) appendLine(content, columnOrder, options); } } if (Array.isArray(datas)) { - datas.map(v => { - if (Array.isArray(v)) { - return v; + datas.forEach(row => { + if (!Array.isArray(row)) { + row = columnOrder.map(k => (typeof row[k] !== 'undefined' ? row[k] : '')); } - return columnOrder.map(k => { - if (typeof v[k] !== 'undefined') { - return v[k]; - } - return ''; - }); - }).forEach(v => { - content.push(v.join(separator)); + appendLine(content, row, options); }); } return content.join(newLine); -} \ No newline at end of file +} From 437b8059d38ada7e913f105f0f691438c7e5bcf8 Mon Sep 17 00:00:00 2001 From: Sergio Crisostomo Date: Tue, 17 Oct 2017 10:14:10 +0200 Subject: [PATCH 2/2] Added Table to CSV example and tests --- examples/routers/table.vue | 119 +++++++--- test/unit/specs/assets/table/csvData.js | 287 ++++++++++++++++++++++++ test/unit/specs/table.spec.js | 47 ++++ 3 files changed, 417 insertions(+), 36 deletions(-) create mode 100644 test/unit/specs/assets/table/csvData.js create mode 100644 test/unit/specs/table.spec.js diff --git a/examples/routers/table.vue b/examples/routers/table.vue index 6f9c30a3..42db9730 100644 --- a/examples/routers/table.vue +++ b/examples/routers/table.vue @@ -164,43 +164,47 @@
- -
- - - 选项 1 - - - - 选项 2 - - - - 选项 3 - -
+ +
+ + + 选项 1 + + + + 选项 2 + + + + 选项 3 + +
-
- -
-
- - 首页 - 应用中心 - 某应用 - -
-
-
-
+
+ +
+
+ + 首页 + 应用中心 + 某应用 + +
+
+
+
+
+
+
+ + + Export to CSV + + - -
- 2011-2016 © TalkingData -
@@ -330,8 +334,44 @@ age: 26, address: '深圳市南山区深南大道' } + ], + columns2: [ + { + title: '姓名', + key: 'name' + }, + { + title: '年龄', + key: 'age' + }, + { + title: '地址', + key: 'address' + } + ], + data2: [ + { + name: '王小明', + age: 18, + address: '北京市朝\n阳区芍药居' + }, + { + name: '张小刚', + age: 25, + address: '北京市海,淀区西二旗' + }, + { + name: '李小红', + age: 30, + address: '上海市浦东\r新区世纪大道' + }, + { + name: '周小伟', + age: 26, + address: '深圳市南山区深南大道' + } ] - } + }; }, computed: { iconSize () { @@ -347,7 +387,14 @@ this.spanLeft = 5; this.spanRight = 19; } + }, + exportCSV () { + this.$refs.csvTable.exportCsv({ + filename: '原始数据', + separator: ';', + quoted: true + }); } } - } + }; diff --git a/test/unit/specs/assets/table/csvData.js b/test/unit/specs/assets/table/csvData.js new file mode 100644 index 00000000..1754001e --- /dev/null +++ b/test/unit/specs/assets/table/csvData.js @@ -0,0 +1,287 @@ +export const csvA = { + columns: [ + { + title: '名称', + key: 'name', + fixed: 'left', + width: 200 + }, + { + title: '展示', + key: 'show', + width: 150, + sortable: true + }, + { + title: '唤醒', + key: 'weak', + width: 150, + sortable: true + }, + { + title: '登录', + key: 'signin', + width: 150, + sortable: true + }, + { + title: '点击', + key: 'click', + width: 150, + sortable: true + }, + { + title: '激活', + key: 'active', + width: 150, + sortable: true + }, + { + title: '7日留存', + key: 'day7', + width: 150, + sortable: true + }, + { + title: '30日留存', + key: 'day30', + width: 150, + sortable: true + }, + { + title: '次日留存', + key: 'tomorrow', + width: 150, + sortable: true + }, + { + title: '日活跃', + key: 'day', + width: 150, + sortable: true + }, + { + title: '周活跃', + key: 'week', + width: 150, + sortable: true + }, + { + title: '月活跃', + key: 'month', + width: 150, + sortable: true + } + ], + data: [ + { + name: '推广名称1', + fav: 0, + show: 7302, + weak: 5627, + signin: 1563, + click: 4254, + active: 1438, + day7: 274, + day30: 285, + tomorrow: 1727, + day: 558, + week: 4440, + month: 5610 + }, + { + name: '推广名称2', + fav: 0, + show: 4720, + weak: 4086, + signin: 3792, + click: 8690, + active: 8470, + day7: 8172, + day30: 5197, + tomorrow: 1684, + day: 2593, + week: 2507, + month: 1537 + }, + { + name: '推广名称3', + fav: 0, + show: 7181, + weak: 8007, + signin: 8477, + click: 1879, + active: 16, + day7: 2249, + day30: 3450, + tomorrow: 377, + day: 1561, + week: 3219, + month: 1588 + }, + { + name: '推广名称4', + fav: 0, + show: 9911, + weak: 8976, + signin: 8807, + click: 8050, + active: 7668, + day7: 1547, + day30: 2357, + tomorrow: 7278, + day: 5309, + week: 1655, + month: 9043 + }, + { + name: '推广名称5', + fav: 0, + show: 934, + weak: 1394, + signin: 6463, + click: 5278, + active: 9256, + day7: 209, + day30: 3563, + tomorrow: 8285, + day: 1230, + week: 4840, + month: 9908 + }, + { + name: '推广名称6', + fav: 0, + show: 6856, + weak: 1608, + signin: 457, + click: 4949, + active: 2909, + day7: 4525, + day30: 6171, + tomorrow: 1920, + day: 1966, + week: 904, + month: 6851 + }, + { + name: '推广名称7', + fav: 0, + show: 5107, + weak: 6407, + signin: 4166, + click: 7970, + active: 1002, + day7: 8701, + day30: 9040, + tomorrow: 7632, + day: 4061, + week: 4359, + month: 3676 + }, + { + name: '推广名称8', + fav: 0, + show: 862, + weak: 6520, + signin: 6696, + click: 3209, + active: 6801, + day7: 6364, + day30: 6850, + tomorrow: 9408, + day: 2481, + week: 1479, + month: 2346 + }, + { + name: '推广名称9', + fav: 0, + show: 567, + weak: 5859, + signin: 128, + click: 6593, + active: 1971, + day7: 7596, + day30: 3546, + tomorrow: 6641, + day: 1611, + week: 5534, + month: 3190 + }, + { + name: '推广名称10', + fav: 0, + show: 3651, + weak: 1819, + signin: 4595, + click: 7499, + active: 7405, + day7: 8710, + day30: 5518, + tomorrow: 428, + day: 9768, + week: 2864, + month: 5811 + } + ], + expected: ` + 名称,展示,唤醒,登录,点击,激活,7日留存,30日留存,次日留存,日活跃,周活跃,月活跃 + 推广名称1,7302,5627,1563,4254,1438,274,285,1727,558,4440,5610 + 推广名称2,4720,4086,3792,8690,8470,8172,5197,1684,2593,2507,1537 + 推广名称3,7181,8007,8477,1879,16,2249,3450,377,1561,3219,1588 + 推广名称4,9911,8976,8807,8050,7668,1547,2357,7278,5309,1655,9043 + 推广名称5,934,1394,6463,5278,9256,209,3563,8285,1230,4840,9908 + 推广名称6,6856,1608,457,4949,2909,4525,6171,1920,1966,904,6851 + 推广名称7,5107,6407,4166,7970,1002,8701,9040,7632,4061,4359,3676 + 推广名称8,862,6520,6696,3209,6801,6364,6850,9408,2481,1479,2346 + 推广名称9,567,5859,128,6593,1971,7596,3546,6641,1611,5534,3190 + 推广名称10,3651,1819,4595,7499,7405,8710,5518,428,9768,2864,5811 + ` +}; + +export const csvB = { + columns: [ + { + title: '姓名', + key: 'name' + }, + { + title: '年龄', + key: 'age' + }, + { + title: '地址', + key: 'address' + } + ], + data: [ + { + name: '王小明', + age: 18, + address: '北京市朝\n阳区芍药居' + }, + { + name: '张小刚', + age: 25, + address: '北京市海,淀区西二旗' + }, + { + name: '李小红', + age: 30, + address: '上海市浦东\r新区世纪大道' + }, + { + name: '周小伟', + age: 26, + address: '深圳市南山区深南大道' + } + ], + expected: ` + "姓名";"年龄";"地址" + "王小明";"18";"北京市朝\n阳区芍药居" + "张小刚";"25";"北京市海,淀区西二旗" + "李小红";"30";"上海市浦东\r新区世纪大道" + "周小伟";"26";"深圳市南山区深南大道" + ` +}; diff --git a/test/unit/specs/table.spec.js b/test/unit/specs/table.spec.js new file mode 100644 index 00000000..755b4a3b --- /dev/null +++ b/test/unit/specs/table.spec.js @@ -0,0 +1,47 @@ +import { createVue, destroyVM } from '../util'; +import { csvA, csvB } from './assets/table/csvData.js'; + +const cleanCSV = (str) => str.split('\n').map(s => s.trim()).filter(Boolean).join('\n'); + +describe('Table.vue', () => { + let vm; + afterEach(() => { + destroyVM(vm); + }); + + describe('CSV export', () => { + it('should export simple data to CSV - test A', done => { + vm = createVue({ + template: '
', + data() { + return csvA; + }, + mounted() { + this.$refs.tableA.exportCsv({callback: data => { + expect(cleanCSV(data)).to.equal(cleanCSV(this.expected)); + expect(cleanCSV(data).length > 0).to.equal(true); + done(); + }}); + } + }); + }); + + it('should export data with commas and line breaks to CSV - test B', done => { + vm = createVue({ + template: '
', + data() { + return csvB; + }, + mounted() { + this.$refs.tableB.exportCsv({separator: ';', quoted: true, callback: data => { + expect(cleanCSV(data)).to.equal(cleanCSV(this.expected)); + expect(cleanCSV(data).length > 0).to.equal(true); + done(); + }}); + } + }); + }); + + }); + +});