# 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







数据经过“\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 | 返回所有被选中的行,当没有记录被选中的时候将返回一个空数组。 |
两者返回都是数组,数组的每一项都是对应行的值(数据表加载的全部值)

```json
[
{code: "001", name: "名称1", qita: "qwewqe", price: "2323"},
{code: "002", name: "名称2", qita: "", price: "4612"}
]
```
我们可以直接通过 “.”数据名称(`*.name`)直接获取想要的数据,简单的一个表还可以。但,当复用在其他表上,每个表的`field`值都不同,所以直接引用是不行的。
好在,datagird中 有 **options** 方法,
| 方法名 | 参数 | 描述 |
| :-----: | :--: | :------------: |
| options | none | 返回属性对象。 |

方法返回的数据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 | 在菜单项被点击的时候触发。 |

根据 `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")`在控制台中打印观察。


观察差别是在,`columns` 和 `frozenColumns` ,之前我们说过希望的通过 `filed` 属性获得数据的值,现在 `filed` 在这两个当中,通过编写数据表格,我们知道冻结列都是在数据表格的左边,所以先判断有没有冻结列,其次看`columns` ,正常单行表头`columns` 数组元素是1,两行是2,三行是3,所以我们判断`columns` 的长度来判断是否是多表头。**声明一个数组 `prop` 来存储含`filed`和`title`的数据(判断是否存在`title`,是因为表可能有复选框;判断hidden是否为true,因为可能存在隐藏列(一般根据用户权限是否显示))**。先放`frozenColumns`,再放`columns`。

**多表头** 的 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的地方,只复制单元格没事,复制一行或所有就会出现下图的问题。不过一般这样复制数据都是到单元格。)

**现在**,我们在来看标题内容的处理(**重点**)
(每个人想法不同,可能有不同的处理逻辑)
```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`,看占几行)

用到 `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的项目,就凑合用吧。