download-excel.vue 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. <template>
  2. <div :id="idName" @click="generate">
  3. <slot> Download {{ name }} </slot>
  4. </div>
  5. </template>
  6. <script>
  7. import download from "./download";
  8. export default {
  9. name: "downloadExcel",
  10. props: {
  11. // mime type [xls, csv]
  12. type: {
  13. type: String,
  14. default: "xls",
  15. },
  16. // Json to download
  17. data: {
  18. type: Array,
  19. required: false,
  20. default: null,
  21. },
  22. // fields inside the Json Object that you want to export
  23. // if no given, all the properties in the Json are exported
  24. fields: {
  25. type: Object,
  26. default: () => null,
  27. },
  28. // this prop is used to fix the problem with other components that use the
  29. // variable fields, like vee-validate. exportFields works exactly like fields
  30. exportFields: {
  31. type: Object,
  32. default: () => null,
  33. },
  34. // Use as fallback when the row has no field values
  35. defaultValue: {
  36. type: String,
  37. required: false,
  38. default: "",
  39. },
  40. // Title(s) for the data, could be a string or an array of strings (multiple titles)
  41. header: {
  42. default: null,
  43. },
  44. // Footer(s) for the data, could be a string or an array of strings (multiple footers)
  45. footer: {
  46. default: null,
  47. },
  48. // filename to export
  49. name: {
  50. type: String,
  51. default: "data.xls",
  52. },
  53. fetch: {
  54. type: Function,
  55. },
  56. meta: {
  57. type: Array,
  58. default: () => [],
  59. },
  60. worksheet: {
  61. type: String,
  62. default: "Sheet1",
  63. },
  64. //event before generate was called
  65. beforeGenerate: {
  66. type: Function,
  67. },
  68. //event before download pops up
  69. beforeFinish: {
  70. type: Function,
  71. },
  72. // Determine if CSV Data should be escaped
  73. escapeCsv: {
  74. type: Boolean,
  75. default: true,
  76. },
  77. // long number stringify
  78. stringifyLongNum: {
  79. type: Boolean,
  80. default: false,
  81. },
  82. },
  83. computed: {
  84. // unique identifier
  85. idName() {
  86. var now = new Date().getTime();
  87. return "export_" + now;
  88. },
  89. downloadFields() {
  90. if (this.fields) return this.fields;
  91. if (this.exportFields) return this.exportFields;
  92. },
  93. },
  94. methods: {
  95. async generate() {
  96. if (typeof this.beforeGenerate === "function") {
  97. await this.beforeGenerate();
  98. }
  99. let data = this.data;
  100. if (typeof this.fetch === "function" || !data) data = await this.fetch();
  101. if (!data || !data.length) {
  102. return;
  103. }
  104. let json = this.getProcessedJson(data, this.downloadFields);
  105. if (this.type === "html") {
  106. // this is mainly for testing
  107. return this.export(
  108. this.jsonToXLS(json),
  109. this.name.replace(".xls", ".html"),
  110. "text/html"
  111. );
  112. } else if (this.type === "csv") {
  113. return this.export(
  114. this.jsonToCSV(json),
  115. this.name.replace(".xls", ".csv"),
  116. "application/csv"
  117. );
  118. }
  119. return this.export(
  120. this.jsonToXLS(json),
  121. this.name,
  122. "application/vnd.ms-excel"
  123. );
  124. },
  125. /*
  126. Use downloadjs to generate the download link
  127. */
  128. export: async function (data, filename, mime) {
  129. let blob = this.base64ToBlob(data, mime);
  130. if (typeof this.beforeFinish === "function") await this.beforeFinish();
  131. download(blob, filename, mime);
  132. },
  133. /*
  134. jsonToXLS
  135. ---------------
  136. Transform json data into an xml document with MS Excel format, sadly
  137. it shows a prompt when it opens, that is a default behavior for
  138. Microsoft office and cannot be avoided. It's recommended to use CSV format instead.
  139. */
  140. jsonToXLS(data) {
  141. let xlsTemp =
  142. '<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns="http://www.w3.org/TR/REC-html40"><head><meta name=ProgId content=Excel.Sheet> <meta name=Generator content="Microsoft Excel 11"><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><!--[if gte mso 9]><xml><x:ExcelWorkbook><x:ExcelWorksheets><x:ExcelWorksheet><x:Name>${worksheet}</x:Name><x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions></x:ExcelWorksheet></x:ExcelWorksheets></x:ExcelWorkbook></xml><![endif]--><style>br {mso-data-placement: same-cell;}</style></head><body><table>${table}</table></body></html>';
  143. let xlsData = "<thead>";
  144. const colspan = Object.keys(data[0]).length;
  145. let _self = this;
  146. //Header
  147. const header = this.header || this.$attrs.title;
  148. if (header) {
  149. xlsData += this.parseExtraData(
  150. header,
  151. '<tr><th colspan="' + colspan + '">${data}</th></tr>'
  152. );
  153. }
  154. //Fields
  155. xlsData += "<tr>";
  156. for (let key in data[0]) {
  157. xlsData += "<th>" + key + "</th>";
  158. }
  159. xlsData += "</tr>";
  160. xlsData += "</thead>";
  161. //Data
  162. xlsData += "<tbody>";
  163. data.map(function (item, index) {
  164. xlsData += "<tr>";
  165. for (let key in item) {
  166. xlsData +=
  167. "<td>" +
  168. _self.preprocessLongNum(
  169. _self.valueReformattedForMultilines(item[key])
  170. ) +
  171. "</td>";
  172. }
  173. xlsData += "</tr>";
  174. });
  175. xlsData += "</tbody>";
  176. //Footer
  177. if (this.footer != null) {
  178. xlsData += "<tfoot>";
  179. xlsData += this.parseExtraData(
  180. this.footer,
  181. '<tr><td colspan="' + colspan + '">${data}</td></tr>'
  182. );
  183. xlsData += "</tfoot>";
  184. }
  185. return xlsTemp
  186. .replace("${table}", xlsData)
  187. .replace("${worksheet}", this.worksheet);
  188. },
  189. /*
  190. jsonToCSV
  191. ---------------
  192. Transform json data into an CSV file.
  193. */
  194. jsonToCSV(data) {
  195. let _self = this;
  196. var csvData = [];
  197. //Header
  198. const header = this.header || this.$attrs.title;
  199. if (header) {
  200. csvData.push(this.parseExtraData(header, "${data}\r\n"));
  201. }
  202. //Fields
  203. for (let key in data[0]) {
  204. csvData.push(key);
  205. csvData.push(",");
  206. }
  207. csvData.pop();
  208. csvData.push("\r\n");
  209. //Data
  210. data.map(function (item) {
  211. for (let key in item) {
  212. let escapedCSV = item[key] + "";
  213. // Escaped CSV data to string to avoid problems with numbers or other types of values
  214. // this is controlled by the prop escapeCsv
  215. if (_self.escapeCsv) {
  216. escapedCSV = '="' + escapedCSV + '"'; // cast Numbers to string
  217. if (escapedCSV.match(/[,"\n]/)) {
  218. escapedCSV = '"' + escapedCSV.replace(/\"/g, '""') + '"';
  219. }
  220. }
  221. csvData.push(escapedCSV);
  222. csvData.push(",");
  223. }
  224. csvData.pop();
  225. csvData.push("\r\n");
  226. });
  227. //Footer
  228. if (this.footer != null) {
  229. csvData.push(this.parseExtraData(this.footer, "${data}\r\n"));
  230. }
  231. return csvData.join("");
  232. },
  233. /*
  234. getProcessedJson
  235. ---------------
  236. Get only the data to export, if no fields are set return all the data
  237. */
  238. getProcessedJson(data, header) {
  239. let keys = this.getKeys(data, header);
  240. let newData = [];
  241. let _self = this;
  242. data.map(function (item, index) {
  243. let newItem = {};
  244. for (let label in keys) {
  245. let property = keys[label];
  246. newItem[label] = _self.getValue(property, item);
  247. }
  248. newData.push(newItem);
  249. });
  250. return newData;
  251. },
  252. getKeys(data, header) {
  253. if (header) {
  254. return header;
  255. }
  256. let keys = {};
  257. for (let key in data[0]) {
  258. keys[key] = key;
  259. }
  260. return keys;
  261. },
  262. /*
  263. parseExtraData
  264. ---------------
  265. Parse title and footer attribute to the csv format
  266. */
  267. parseExtraData(extraData, format) {
  268. let parseData = "";
  269. if (Array.isArray(extraData)) {
  270. for (var i = 0; i < extraData.length; i++) {
  271. if (extraData[i])
  272. parseData += format.replace("${data}", extraData[i]);
  273. }
  274. } else {
  275. parseData += format.replace("${data}", extraData);
  276. }
  277. return parseData;
  278. },
  279. getValue(key, item) {
  280. const field = typeof key !== "object" ? key : key.field;
  281. let indexes = typeof field !== "string" ? [] : field.split(".");
  282. let value = this.defaultValue;
  283. if (!field) value = item;
  284. else if (indexes.length > 1)
  285. value = this.getValueFromNestedItem(item, indexes);
  286. else value = this.parseValue(item[field]);
  287. if (key.hasOwnProperty("callback"))
  288. value = this.getValueFromCallback(value, key.callback);
  289. return value;
  290. },
  291. /*
  292. convert values with newline \n characters into <br/>
  293. */
  294. valueReformattedForMultilines(value) {
  295. if (typeof value == "string") return value.replace(/\n/gi, "<br/>");
  296. else return value;
  297. },
  298. preprocessLongNum(value) {
  299. if (this.stringifyLongNum) {
  300. if (String(value).startsWith("0x")) {
  301. return value;
  302. }
  303. if (!isNaN(value) && value != "") {
  304. if (value > 99999999999 || value < 0.0000000000001) {
  305. return '="' + value + '"';
  306. }
  307. }
  308. }
  309. return value;
  310. },
  311. getValueFromNestedItem(item, indexes) {
  312. let nestedItem = item;
  313. for (let index of indexes) {
  314. if (nestedItem) nestedItem = nestedItem[index];
  315. }
  316. return this.parseValue(nestedItem);
  317. },
  318. getValueFromCallback(item, callback) {
  319. if (typeof callback !== "function") return this.defaultValue;
  320. const value = callback(item);
  321. return this.parseValue(value);
  322. },
  323. parseValue(value) {
  324. return value || value === 0 || typeof value === "boolean"
  325. ? value
  326. : this.defaultValue;
  327. },
  328. base64ToBlob(data, mime) {
  329. let base64 = window.btoa(window.unescape(encodeURIComponent(data)));
  330. let bstr = atob(base64);
  331. let n = bstr.length;
  332. let u8arr = new Uint8ClampedArray(n);
  333. while (n--) {
  334. u8arr[n] = bstr.charCodeAt(n);
  335. }
  336. return new Blob([u8arr], { type: mime });
  337. },
  338. }, // end methods
  339. };
  340. </script>