# Easyui框架-右键拷贝+datagrid导出为xlsx文件 **Repository Path**: a20a20/easyui-ExpandFunc ## Basic Information - **Project Name**: Easyui框架-右键拷贝+datagrid导出为xlsx文件 - **Description**: 根据业务需求对easyui两个常用功能的封装; 1、Easyui Datagrid右键拷贝(复制到Excel) 2、一个函数导出各种datagird,适配冻结列表和多表头的数据表,带颜色的cell等。 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2023-01-30 - **Last Updated**: 2023-01-30 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README --- typora-root-url: md-img --- # Easyui Datagrid右键拷贝(复制到Excel) ## 1、简介 在数据row上点击鼠标右键,生成菜单——拷贝当前单元格、拷贝当前行、拷贝选中行、拷贝所有(包括表头),适配**冻结列表**和**多表头**的数据表。 实例文件:index-copy.html 冻结列.html 表头Group.html ![](md-img/1612247394532.png) ![](md-img/1612253637973.png) ![](md-img/1612253520470.png) ![](md-img/1612253571916.png) ![](md-img/1612253862672.png) ![](md-img/1612254069931.png) ![](md-img/单元格带换行的数据.png) 数据经过“\t”和“\n”处理,可以直接复制复制到Excel中。 用法:直接通过script标签引用js文件(***必须写在body标签内***)。 ```js $("#tt").datagrid({ // 此onRowContextMenu不用用在html标签的data-options的字符串中,请用jS代码设置 onRowContextMenu: function (e, index, row) { // 函数内部用到了当前的this,所以同call改变指向 onRowContextMenu_Copy.call(this, e, index, row); }, }); ``` ## 2、需求 **在已经写好的界面中,不在编写其他代码,直接引用函数。** ## 3、实现 ### 1、**生成右键菜单的HTML结构** ```js /**@desc 生成随机右键菜单的id值 */ const menuID = "menu" + Math.ceil(Math.random() * 10000).toString(); !(function () { //创建菜单 const menu = document.body.appendChild(document.createElement("div")); menu.id = menuID; menu.classList.add("easyui-menu"); menu.style.display = "none"; menu.innerHTML = `
拷贝此格
拷贝此行
拷贝选中
拷贝所有行
`; })(); ``` **注释**:!()()为立即执行函数,在引入js文件后立即执行(1、插入element到body中,js引用必须写在body标签内,否则appendChild不成功。2、因为easyui的渲染问题必须先生成节点、否则找不到节点)。 ### 2、编写datagrid的`onRowContextMenu`(右键)方法函数 首先,onRowContextMenu方法有三个参数 **function (e, index, row){}** ​ e ——为当前事件的DOM,index——为当前数据的行号(索引从0开始),row——为当前事件行的数据(**全部**数据,可能比页面实现的多(顺序也可能不一样)。例如:`{id:1,name:"张三"}`类型为object)。 现在,我们有了当前右键行的数据。 其次,我们需要**选中行**的数据和**当前页所有行**的数据。在datagrid中有 **getSelections** 和 **getRows** 两个方法, | 方法 | 参数 | 描述 | | :-----------: | :--: | :----------------------------------------------------------: | | getRows | none | 返回当前页的所有行。 | | getSelections | none | 返回所有被选中的行,当没有记录被选中的时候将返回一个空数组。 | 两者返回都是数组,数组的每一项都是对应行的值(数据表加载的全部值) ![](md-img/1612252464878.png) ```json [ {code: "001", name: "名称1", qita: "qwewqe", price: "2323"}, {code: "002", name: "名称2", qita: "", price: "4612"} ] ``` 我们可以直接通过 “.”数据名称(`*.name`)直接获取想要的数据,简单的一个表还可以。但,当复用在其他表上,每个表的`field`值都不同,所以直接引用是不行的。 好在,datagird中 有 **options** 方法, | 方法名 | 参数 | 描述 | | :-----: | :--: | :------------: | | options | none | 返回属性对象。 | ![](md-img/1612252655447.png) 方法返回的数据Columns数组中 有了 `field` 和 `title` 属性,通过`filed`值我们就可以找到 **`getSelections`** 和 **`getRows`** 返回数据中的具体值,而且`columns`中数据的顺序是跟显示的顺序一致。并且我们也得到了 `title` 就可以复制表头了。 **现在:**编写右键函数 ```js /** * function --> 数据表添加右键copy * * @example 用法: onRowContextMenu: function (e, index, row) { * onRowContextMenu_Copy.call(this, e, index, row) * } * @param {Event} e * @param {int} index * @param {object} row */ function onRowContextMenu_Copy(e, index, row) { let that = this; // this为当前元素 // 阻止默认右键 e.preventDefault(); // 判断是否点击到行内,index ->当前所点击的行号 if (index != -1) { $("#" + menuID).menu("show", { //显示右键菜单 left: e.pageX, //在鼠标点击处显示菜单 top: e.pageY, hideOnUnhover: false, // 当设置为false时,在鼠标离开菜单的时候将不会自动隐藏菜单。 onClick: function (item) { switch (item.id) { case "btn_CopyCell": copyToClipboard(e.target.innerText); break; case "btn_CopyLine": let rowValue = switchGriddataToText({ datagridName: that.id, rowType: 1, rowArray: [row], title: false, }); copyToClipboard(rowValue); break; case "btn_CopySelect": if ($("#" + that.id).datagrid("getSelections").length === 0) { $.messager.show({ title: "消息", msg: "没有选中行", timeout: 700, showType: "fade", style: { top: window.innerHeight / 5, }, width: 200, height: 120, }); } else { let rowsValue = switchGriddataToText({ datagridName: that.id, rowType: 2, title: false }); copyToClipboard(rowsValue); } break; case "btn_CopyAllLines": let rowsAllValue = switchGriddataToText({ datagridName: that.id, rowType: 3, title: true }); copyToClipboard(rowsAllValue); break; default: break; } }, }); } } ``` onRowContextMenuCopy 函数的三个参数是 datagrid 中 onRowContextMenu 函数的参数, 右键菜单的 onClick 事件, | 方法名 | 参数 | 描述 | | :-----: | :--: | :------------------------: | | onClick | item | 在菜单项被点击的时候触发。 | ![](md-img/1612254525905.png) 根据 `item` 中的 `id` 可以判断 执行不同的 方法。 我们看到通过不同 id 执行的函数有两个,一个是 **`switchGriddataToText`** ,该函数将datagrid数据转换成字符串,另一个 **`copyToClipboard`** ,是将字符串复制到系统剪切板。 下面,进入到我们的 **`switchGriddataToText`** 。 ### 3、编写数据处理函数 —— `switchGriddataToText` 首先,首先,`switchGriddataToText({ datagridName, rowType, rowArray, title = false })`方法的参数是一个对象,对象的好处是可以不按顺序传值,也可以省略值。 - `@param {int} datagridName` -> 表格id值 * `@param {int} rowType` -> 1,2,3 1:获取一行 2: 获取选中 3: 获取全部 * `@param {Array} rowArray` -> 当`rowType:1`时,传入数组,例如`,[{id:1,name:"www"}]` * `@param {boolen} title` -> 是否含表头 根据需求不同可以自定义修改拓展参数,这样可以直接复制,复制到哪用到哪里! **为了让我们的函数能够,重复并在多样表格中应用,我们考虑,easyui中有哪些表格,** 常见的,普通表格、冻结列表格、多表头表格(本函数只针对这三种,还需处理隐藏列) 我们分析一下,这三种表格的不同,可以 通过 `$("#tt").datagrid("options")`在控制台中打印观察。 ![](md-img/1612255309141.png) ![](md-img/1612255356231.png) 观察差别是在,`columns` 和 `frozenColumns` ,之前我们说过希望的通过 `filed` 属性获得数据的值,现在 `filed` 在这两个当中,通过编写数据表格,我们知道冻结列都是在数据表格的左边,所以先判断有没有冻结列,其次看`columns` ,正常单行表头`columns` 数组元素是1,两行是2,三行是3,所以我们判断`columns` 的长度来判断是否是多表头。**声明一个数组 `prop` 来存储含`filed`和`title`的数据(判断是否存在`title`,是因为表可能有复选框;判断hidden是否为true,因为可能存在隐藏列(一般根据用户权限是否显示))**。先放`frozenColumns`,再放`columns`。 ![](md-img/1612256599216.png) **多表头** 的 columns ,通过判断 `colspan` 来添加 当前数组的下一个数组里面的值(`colspan`的值,对应下一个数组选取多少个值)(可以对应插入数据的顺序)例如,上面图片的数据 `columns[0]`的第三项才有`colspan`, 这里相当一个占位符,遇见 `colspan`大于1的,向`columns[1]`中查找,如果`columns[1]`中也遇见 `colspan`大于1的,向`columns[2]`中查找... 所以用到回调函数解决此问题。 ```js /** * 将datagrid的数据转换成 string * * @param {int} datagridName -> 表格id值 * @param {int} rowType -> 1,2,3 1:获取一行 2: 获取选中 3: 获取全部 * @param {Array} rowArray -> 当rowType:1时,传入数组,例如,[{id:1,name:"www"}] * @param {boolen} title -> boolen 是否含表头 */ function switchGriddataToText({ datagridName, rowType, rowArray, title = false }) { /**@desc 存储返回的string */ let rowsValue = ""; /**@desc 储存表头信息的 filed */ let prop = []; const state = $("#" + datagridName).datagrid("options"); // 判断是否是空数据 if (state.columns == "") return ""; //判断是否存在冻结列 if (state.frozenColumns != "") { for (let item of state.frozenColumns[0]) { // 判断title 、 field 以及隐藏列,符合判断语句则跳出此次循环 if (!item.title || (!item.title && !item.field) || (item.hidden && item.hidden == true)) continue; prop.push(item); } } let flag = 0; // 默认columns[0]中的title for (let item of state.columns[0]) { // 判断title 、 field 以及隐藏列,符合判断语句则跳出此次循环 if (!item.title || (!item.title && !item.field) || (item.hidden && item.hidden == true)) continue; // 判断是否存在多表头 if (state.columns.length > 1) { function add(item, flag) { if (item.title && item.field && (!item.hidden || item.hidden != true)) { prop.push(item); } else if (!item.field && item.colspan > 1) { flag += 1; for (const item2 of state.columns[flag]) { add(item2, flag); } } } add(item, flag); } else { prop.push(item); } } ......(省略) } ``` 现在已经把所有还有 `field` 和 `title` 属性并且不是隐藏列( hidden )的数据项添加到 `prop` 数组中了,我们可以根据 `field` 来遍历数据了。 ```js /** * 将datagrid的数据转换成 string * * @param {int} datagridName -> 表格id值 * @param {int} rowType -> 1,2,3 1:获取一行 2: 获取选中 3: 获取全部 * @param {Array} rowArray -> 当rowType:1时,传入数组,例如,[{id:1,name:"www"}] * @param {boolen} title -> boolen 是否含表头 */ function switchGriddataToText({ datagridName, rowType, rowArray, title = false }) { ......(省略) // datagrid数据 const rows = rowType == 3 ? $("#" + datagridName).datagrid("getRows") : rowType == 2 ? $("#" + datagridName).datagrid("getSelections") : rowArray; // 根据表头的 field 属性 , 遍历内容 for (let $row of rows) { for (let index in prop) { // 当数据是一行的最后一个 if (index == prop.length - 1) { if ($row[prop[index].field] == null || $row[prop[index].field] == "") { rowsValue += "\n"; } else { // 如果存在格式化函数(formatter方法:原始数据 --->页面显示的数据) if (prop[index].formatter != undefined) { rowsValue += prop[index].formatter($row[prop[index].field]) + "\n"; } else { // 如果数据中存在换行符 if ($row[prop[index].field].toString().trim().includes("\n")) { rowsValue += '"' + $row[prop[index].field].toString().trim() + '"' + "\n"; } else { rowsValue += $row[prop[index].field].toString().trim() + "\n"; } } } } else { if ($row[prop[index].field] == null || $row[prop[index].field] == "") { rowsValue += "\t "; } else { // 如果存在格式化函数(formatter方法:原始数据 --->页面显示的数据) if (prop[index].formatter != undefined) { rowsValue += prop[index].formatter($row[prop[index].field]) + "\t"; } else { // 如果数据中存在换行符 if ($row[prop[index].field].toString().trim().includes("\n")) { rowsValue += '"' + $row[prop[index].field].toString().trim() + '"' + "\t"; } else { rowsValue += $row[prop[index].field].toString().trim() + "\t"; } } } } } } return rowsValue.slice(0, -1); } ``` rows 的格式是 `[{id:1,name:"www"},{id:2,name:"xxx"}]` 是数组,所以第一层用 `for of` 遍历,里面每一项是对象用 `for in` 遍历(`都可以替换成for`循环)。具体的数据拼接、转换可以根据业务需求改变。**最终**,将数据拼接到 `rowsValue` 。 (**1、**通过获取的prop数据,根据 `prop[index].formatter` 来判断某一列的数据是否经过格式化处理,如果有,就将我们得到的原始数据,处理一下。 ​ **2、**对每一个单元格的数据进行 `toString().trim()` ,防止数据前后有制表符,不然复制到excel会多空格 ​ **3、**再判断数据中间是否有换行符 ` ↵ ` ,如果有,字符串前后加上 `""` ,不加上excel会处理字符串中间的 ` ↵ ` 。但,这种处理方式,复制到不是excel的地方,只复制单元格没事,复制一行或所有就会出现下图的问题。不过一般这样复制数据都是到单元格。) ![](md-img/1612682384299.png) **现在**,我们在来看标题内容的处理(**重点**) (每个人想法不同,可能有不同的处理逻辑) ```js /** * 将datagrid的数据转换成 string * * @param {int} datagridName -> 表格id值 * @param {int} rowType -> 1,2,3 1:获取一行 2: 获取选中 3: 获取全部 * @param {Array} rowArray -> 当rowType:1时,传入数组,例如,[{id:1,name:"www"}] * @param {boolen} title -> boolen 是否含表头 */ function switchGriddataToText({ datagridName, rowType, rowArray, title = false }) { ......(省略) // 如果需要表头 if (title) { // 判断是否是多表头 if (state.columns.length > 1) { let arr = []; //表头数组 为了判断 不插入重复值 let flag = 0; // colspan之前有多少项 // 根据columns.length得出多表头有几列,循环几次,把数据加入数组中 for (let i = 0; i < state.columns.length; i++) { const element = state.columns[i]; //判断是否存在冻结列 if (state.frozenColumns != "") { for (let item of state.frozenColumns[0]) { // 判断是否有title属性 if (!item.title || (!item.title && !item.field)) continue; if (arr.includes(item.title + "\t ")) { arr.push("\t"); } else { arr.push(item.title + "\t "); } } } // 除冻结列 遇到colspan>1的数据 的之前还有几个空格 for (let k = 0; k < flag; k++) { arr.push("\t"); } for (let j = 0; j < element.length; j++) { const el = element[j]; if (el.title) { if (arr.includes(el.title + "\t ")) { if (j == element.length - 1) { arr.push(""); } else { arr.push("\t"); } } else { if (j == element.length - 1) { arr.push(el.title); } else { arr.push(el.title + "\t "); } } if (el.colspan && el.colspan > 1) { flag += j; for (let i = 1; i < el.colspan; i++) { if (j == element.length - 1) { arr.push(""); } else { arr.push("\t"); } } } } else { if (j == element.length - 1) { arr.push(""); } else { arr.push("\t"); } } } arr.push("\n"); } rowsValue = arr.join(""); } else { for (let index in prop) { if (index == prop.length - 1) { rowsValue += prop[index].title + "\n"; } else { rowsValue += prop[index].title + "\t "; } } } } } ``` 1、首先判断是否是多表头 `if (state.columns.length > 1)` 是的话,开始循环处理,前面说到 `columns` 的长度就是表头的行数,所以我们根据有多少行,来循环多少次,处理每一行的数据。 2、(每次及每行的处理)判断是否有冻结列,因为easyui冻结列都在最左边,且按照 `frozenColumns` 数组的顺序排序,所以直接添加,以为循环下一行还是要判断所以用到 `arr =[]` ,用来去重,是重复的就为 `\t` (其实是根据 `columns` 里面每一个数组项里面的`rowspan`,看占几行) ![](md-img/1612320280294.png) 用到 `for` 循环处理是为了判断到最后一项,不在添加 `\t` ,因为每一行最后加的是 `\n` 。 3、普通一行的表头,直接根据我们处理好的 `prop` 里面的 `title` 直接添加到 `rowsValue` 。 到目前为止,我们的数据处理已经完成。可能还有修改的空间(比如,更复杂的数据表,更好更简便的代码)。有时间有精力可以接着研究(造轮子😂)。下面进入到剪切板。 ### 4、 将数据复制到剪切板 - `copyToClipboard` 剪切板函数 `function copyToClipboard(str)` 直接一个类型为string的字符串参数。 函数用到了最新的 [`navigator.clipboard`](https://developer.mozilla.org/zh-cn/docs/web/api/navigator/clipboard)API ,有兼容行问题,所以加了 [document.execCommand - Web API 接口参考 | MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/Document/execCommand) ```js /** * 复制 str 到 剪切板 * 兼容 */ function copyToClipboard(str) { if (!navigator.clipboard) { textArea = document.createElement("textArea"); textArea.innerHTML = str; textArea.value = str; document.body.appendChild(textArea); textArea.select(); try { if (document.execCommand("Copy")) { $.messager.show({ title: "消息", msg: "复制成功", timeout: 700, showType: "fade", style: { top: window.innerHeight / 5, }, width: 200, height: 100, }); } else { $.messager.show({ title: "消息", msg: "复制失败", timeout: 700, showType: "fade", style: { top: window.innerHeight / 5, }, width: 200, height: 100, }); } } catch (err) { $.messager.show({ title: "消息", msg: "复制失败:" + err, timeout: 700, showType: "fade", style: { top: window.innerHeight / 5, }, width: 200, height: 120, }); } document.body.removeChild(textArea); } else { navigator.clipboard.writeText(str).then( function () { let msg = str == "" ? "复制内容为空" : "复制成功"; $.messager.show({ title: "消息", msg: msg, timeout: 700, showType: "fade", style: { top: window.innerHeight / 5, }, width: 200, height: 120, }); }, function () { $.messager.show({ title: "消息", msg: "复制失败", timeout: 700, showType: "fade", style: { top: window.innerHeight / 5, }, width: 200, height: 120, }); } ); } } ``` ## 4、结束 [项目连接(gitee)](https://gitee.com/Jeexu/easyui-ExpandFunc) 最后,因为刚接触easyui,根据项目需求封装了一下这个功能,还有很多改进的地方,不过本着能用就行理念,就这样了,现在还能用上easyui的项目,就凑合用吧。