wenlele
1 year ago
14 changed files with 1252 additions and 69 deletions
@ -0,0 +1,582 @@ |
|||||
|
'use strict'; |
||||
|
import React, { useState, useEffect } from 'react'; |
||||
|
import PropTypes from 'prop-types'; |
||||
|
import { Button, Notification } from "@douyinfe/semi-ui"; |
||||
|
import moment from 'moment'; |
||||
|
import XLSX from 'xlsx'; |
||||
|
import { fromJS } from 'immutable'; |
||||
|
import { Request } from '@peace/utils'; |
||||
|
import FileSaver from 'file-saver'; |
||||
|
import { IconArrowDown } from '@douyinfe/semi-icons'; |
||||
|
|
||||
|
//通用前端导出组件 使用方法查看底部propTypes |
||||
|
const ExportData = ({...props}) => { |
||||
|
//const [form] = Form.useForm(); |
||||
|
const [exportLoading, setExportLoading] = useState(false); |
||||
|
const { customRender, title, exportType, style, showIcon } = props; |
||||
|
|
||||
|
|
||||
|
const loop = (data, keypath, values) => { // deal with array |
||||
|
let dkey = keypath.slice(0, 1)[0]; |
||||
|
if (dkey) { |
||||
|
let dvalue = data[dkey]; |
||||
|
let otherKeypath = keypath.slice(1); |
||||
|
if (Array.isArray(dvalue)) { |
||||
|
if (otherKeypath.length) { |
||||
|
let immutableData = fromJS(data); |
||||
|
for (let index = 0; index < dvalue.length; index++) { |
||||
|
let tmp = immutableData.getIn([dkey, index]).toJS(); |
||||
|
loop(tmp, otherKeypath, values); |
||||
|
} |
||||
|
} |
||||
|
} else { |
||||
|
values.push(dvalue); |
||||
|
} |
||||
|
} |
||||
|
return values; |
||||
|
}; |
||||
|
const getColumnData = (opts) => { |
||||
|
const { data, keypath, render, spliter, rawdata, valueEnum } = opts; |
||||
|
let v = null; |
||||
|
let outer = data[keypath[0]]; |
||||
|
if (Array.isArray(outer)) { |
||||
|
let values = loop(data, keypath, []); |
||||
|
v = rawdata ? values : values.join(spliter || ','); |
||||
|
} else { |
||||
|
v = fromJS(data).getIn(keypath) |
||||
|
} |
||||
|
//处理proTable 枚举 |
||||
|
if(valueEnum && valueEnum[v]?.text){ |
||||
|
v = valueEnum[v]?.text; |
||||
|
} |
||||
|
//处理render |
||||
|
// if (render && typeof render === 'function') { |
||||
|
// v = render(outer, data); |
||||
|
|
||||
|
// } |
||||
|
return v; |
||||
|
}; |
||||
|
const getDataSource = (attrs, filterData) => { |
||||
|
debugger |
||||
|
let dataSource = filterData.map(item => { |
||||
|
let record = {}; |
||||
|
attrs.forEach(attr => { |
||||
|
const { key, dataIndex, render, child, valueEnum } = attr; |
||||
|
if (child) { |
||||
|
|
||||
|
record[key] = getDataSource(child, item[key] || []); |
||||
|
} else { |
||||
|
let v = getColumnData({ |
||||
|
data: item, |
||||
|
keypath: Array.isArray(dataIndex) ? dataIndex : [dataIndex], |
||||
|
render: render || null, |
||||
|
valueEnum: valueEnum |
||||
|
}); |
||||
|
record[key] = v; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
}); |
||||
|
|
||||
|
return record; |
||||
|
}); |
||||
|
return dataSource; |
||||
|
|
||||
|
|
||||
|
}; |
||||
|
|
||||
|
const getNewColumns = (attrs) => { |
||||
|
return attrs.filter(f=> f.dataIndex).map(v=>{ |
||||
|
const { dataIndex } = v; |
||||
|
return { |
||||
|
...v, |
||||
|
key: Array.isArray(dataIndex) ? dataIndex.reduce((p,c)=>{ |
||||
|
p = `${p}${c.trim().replace(c[0], c[0].toUpperCase())}`; |
||||
|
return p |
||||
|
},'') : dataIndex |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
//暂时只处理两层 |
||||
|
const getFlatData = (attrs, filterData, dataToAoa, deep = 0) => { |
||||
|
|
||||
|
filterData.map(item => { |
||||
|
let cur = dataToAoa[deep] |
||||
|
if (!cur) { |
||||
|
cur = dataToAoa[deep] = [] |
||||
|
} |
||||
|
attrs.map((attr, index) => { |
||||
|
const { key, child } = attr; |
||||
|
if (child) { |
||||
|
if (Array.isArray(item[key])) { |
||||
|
//getFlatData(child,item[key],dataToAoa,deep) |
||||
|
|
||||
|
item[key].map((s, i) => { |
||||
|
if (i == 0) { |
||||
|
child.map(c => { |
||||
|
cur.push(s[c.key]); |
||||
|
}) |
||||
|
} else { |
||||
|
deep++ |
||||
|
let childCur = dataToAoa[deep] = [] |
||||
|
pushNull(childCur, index); |
||||
|
child.map(c => { |
||||
|
childCur.push(s[c.key]); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
}) |
||||
|
|
||||
|
} |
||||
|
} else { |
||||
|
cur.push(item[key]); |
||||
|
} |
||||
|
|
||||
|
}); |
||||
|
deep++ |
||||
|
}); |
||||
|
|
||||
|
|
||||
|
}; |
||||
|
|
||||
|
const getHeader = (headers, excelHeader, deep, perOffset) => { |
||||
|
let offset = 0 |
||||
|
let cur = excelHeader[deep] |
||||
|
if (!cur) { |
||||
|
cur = excelHeader[deep] = [] |
||||
|
} |
||||
|
pushNull(cur, perOffset - cur.length) |
||||
|
for (let i = 0; i < headers.length; i++) { |
||||
|
let head = headers[i] |
||||
|
cur.push(head.name) |
||||
|
if (head.hasOwnProperty('child') && Array.isArray(head.child) && head.child.length > 0) { |
||||
|
let childOffset = getHeader(head.child, excelHeader, deep + 1, cur.length - 1) |
||||
|
pushNull(cur, childOffset - 1) |
||||
|
offset += childOffset |
||||
|
} else { |
||||
|
offset++ |
||||
|
} |
||||
|
} |
||||
|
return offset; |
||||
|
} |
||||
|
|
||||
|
const pushNull = (arr, count) => { |
||||
|
for (let i = 0; i < count; i++) { |
||||
|
arr.push(null) |
||||
|
} |
||||
|
} |
||||
|
const fillNull = (arr) => { |
||||
|
let max = Math.max(...(arr.map(a => a.length))) |
||||
|
arr.filter(e => e.length < max).forEach(e => pushNull(e, max - e.length)) |
||||
|
} |
||||
|
const doMerges = (arr) => { |
||||
|
// 要么横向合并 要么纵向合并 |
||||
|
let deep = arr.length; |
||||
|
let merges = []; |
||||
|
for (let y = 0; y < deep; y++) { |
||||
|
// 先处理横向合并 |
||||
|
let row = arr[y]; |
||||
|
let colSpan = 0 |
||||
|
for (let x = 0; x < row.length; x++) { |
||||
|
if (row[x] === null) { |
||||
|
colSpan++ |
||||
|
if (((x + 1) === row.length) && (colSpan > 0 && x > colSpan)) { |
||||
|
merges.push({ s: { r: y, c: x - colSpan }, e: { r: y, c: x } }) |
||||
|
} |
||||
|
} else if (colSpan > 0 && x > colSpan) { |
||||
|
merges.push({ s: { r: y, c: x - colSpan - 1 }, e: { r: y, c: x - 1 } }) |
||||
|
colSpan = 0 |
||||
|
} else { |
||||
|
colSpan = 0 |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
// 再处理纵向合并 |
||||
|
let colLength = arr[0].length |
||||
|
for (let x = 0; x < colLength; x++) { |
||||
|
let rowSpan = 0 |
||||
|
for (let y = 0; y < deep; y++) { |
||||
|
if (arr[y][x] != null) { |
||||
|
rowSpan = 0 |
||||
|
} else { |
||||
|
rowSpan++; |
||||
|
} |
||||
|
} |
||||
|
if (rowSpan > 0) { |
||||
|
merges.push({ s: { r: deep - rowSpan - 1, c: x }, e: { r: deep - 1, c: x } }) |
||||
|
} |
||||
|
} |
||||
|
return merges; |
||||
|
} |
||||
|
|
||||
|
//内容暂只出了纵向合并 |
||||
|
const doContetMerges = (arr, headerLength) => { |
||||
|
let deep = arr.length; |
||||
|
let merges = []; |
||||
|
//处理纵向合并 |
||||
|
let colLength = arr[0].length |
||||
|
for (let x = 0; x < colLength; x++) { |
||||
|
let rowSpan = 0; |
||||
|
let mergY = 0; |
||||
|
for (let y = 0; y < deep; y++) { |
||||
|
if (rowSpan > 0) { |
||||
|
//如果还有null 继续加 |
||||
|
if (arr[y][x] === null) { |
||||
|
rowSpan = rowSpan + 1 |
||||
|
} else { |
||||
|
//不为null 增加merge |
||||
|
merges.push({ s: { r: headerLength + (y - rowSpan - 1), c: x }, e: { r: headerLength + y - 1, c: x } }); |
||||
|
rowSpan = 0; |
||||
|
} |
||||
|
} else { |
||||
|
if (arr[y][x] === null) { |
||||
|
rowSpan = rowSpan + 1 |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
if (rowSpan > 0) { |
||||
|
merges.push({ s: { r: headerLength + (deep - rowSpan - 1), c: x }, e: { r: headerLength + deep - 1, c: x } }) |
||||
|
rowSpan = 0; |
||||
|
} |
||||
|
} |
||||
|
return merges; |
||||
|
} |
||||
|
|
||||
|
//导出可以纵向合并单元格的数据 不建议使用 |
||||
|
const exportMergeExcel = async () => { |
||||
|
setExportLoading(true) |
||||
|
const { columns, data, fileName, exportUrl, exportQuery, exportBody, requestType, header, showYearMouth } = props || {}; |
||||
|
|
||||
|
let resultData = []; |
||||
|
if (exportUrl) { |
||||
|
|
||||
|
resultData = requestType == 'post' ? await Request.post(exportUrl, exportBody || {}, exportQuery || {}).then(data => { |
||||
|
//数据接口返回的结果 如果是对象 必须把返回数组放入rows |
||||
|
if (typeof data === 'object' && data.rows) { |
||||
|
|
||||
|
return data.rows |
||||
|
|
||||
|
} else { |
||||
|
return data; |
||||
|
} |
||||
|
}, err => { |
||||
|
Notification.error({ |
||||
|
content: '获取数据失败,导出失败!', |
||||
|
duration: 3, |
||||
|
}) |
||||
|
}) : await Request.get(exportUrl, exportQuery || {}).then(data => { |
||||
|
if (typeof data === 'object' && data.rows) { |
||||
|
|
||||
|
return data.rows |
||||
|
|
||||
|
} else { |
||||
|
return data; |
||||
|
} |
||||
|
}, err => { |
||||
|
Notification.error({ |
||||
|
content: '获取数据失败,导出失败!', |
||||
|
duration: 3, |
||||
|
}) |
||||
|
}); |
||||
|
if (!resultData) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
} else { |
||||
|
resultData = data |
||||
|
} |
||||
|
let excelHeader = []; |
||||
|
const newColumns = getNewColumns(columns); |
||||
|
getHeader(newColumns, excelHeader, 0, 0); |
||||
|
fillNull(excelHeader); |
||||
|
|
||||
|
//console.log(excelHeader); |
||||
|
|
||||
|
let loopData = getDataSource(newColumns, resultData); |
||||
|
//console.log(loopData) |
||||
|
|
||||
|
let dataToAoa = []; |
||||
|
getFlatData(newColumns, loopData, dataToAoa, 0); |
||||
|
fillNull(dataToAoa); |
||||
|
//console.log(dataToAoa); |
||||
|
|
||||
|
let aoa = [].concat(excelHeader, dataToAoa); |
||||
|
//console.log(aoa) |
||||
|
|
||||
|
let headerMerges = doMerges(excelHeader); |
||||
|
let contentMerages = doContetMerges(dataToAoa, excelHeader.length); |
||||
|
let merges = [].concat(headerMerges, contentMerages); |
||||
|
// console.log(contentMerages) |
||||
|
|
||||
|
// let opts = { |
||||
|
// defaultCellStyle: { |
||||
|
// font: { name: "宋体", sz: 11, color: { auto: 1 } }, |
||||
|
// border: { |
||||
|
// color: { auto: 1 } |
||||
|
// }, |
||||
|
// alignment: { |
||||
|
// /// 自动换行 |
||||
|
// wrapText: 1, |
||||
|
// // 居中 |
||||
|
// horizontal: "center", |
||||
|
// vertical: "center", |
||||
|
// indent: 0 |
||||
|
// } |
||||
|
// } |
||||
|
// } |
||||
|
let sheet = XLSX.utils.aoa_to_sheet(aoa); |
||||
|
// let newSheet = {}; |
||||
|
// for (let [key, value] of Object.entries(sheet)) { |
||||
|
// if(key == '!ref'){ |
||||
|
// newSheet[key] = value |
||||
|
// }else if(typeof value === 'object'){ |
||||
|
// newSheet[key] = { |
||||
|
// ...value, |
||||
|
// s: opts.defaultCellStyle |
||||
|
// } |
||||
|
// } |
||||
|
// } |
||||
|
const wpx = columns.map(c => { |
||||
|
return { |
||||
|
wpx: Number.parseInt(c.wpx) ? Number.parseInt(c.wpx) : 100 |
||||
|
} |
||||
|
}) |
||||
|
sheet['!cols'] = wpx; |
||||
|
sheet['!merges'] = merges; |
||||
|
|
||||
|
// 构建 workbook 对象 |
||||
|
const workbook = XLSX.utils.book_new(); |
||||
|
|
||||
|
const time = moment().format('YYYY-MM-DD'); |
||||
|
|
||||
|
|
||||
|
XLSX.utils.book_append_sheet(workbook, sheet, 'mySheet'); |
||||
|
// 导出 Excel |
||||
|
XLSX.writeFile(workbook, fileName ? `${fileName}-${time}.xlsx` : `导出数据-${time}.xlsx`); |
||||
|
setExportLoading(false); |
||||
|
Notification.success({ |
||||
|
content: `成功导出了 ${loopData.length || 0} 条数据`, |
||||
|
duration: 3, |
||||
|
}) |
||||
|
} |
||||
|
//FileSaver 方式导出可以自定义样式 columns可定义 headStyle, rowStyle |
||||
|
const exportFileSaver = async () => { |
||||
|
setExportLoading(true) |
||||
|
const { columns, data, fileName, exportUrl, exportQuery, exportBody, requestType } = props || {}; |
||||
|
let resultData = []; |
||||
|
if (exportUrl) { |
||||
|
resultData = requestType == 'post' ? await Request.post(exportUrl, exportBody || {}, exportQuery || {}).then(data => { |
||||
|
//数据接口返回的结果 如果是对象 必须把返回数组放入rows |
||||
|
if (typeof data === 'object') { |
||||
|
return data.data ? data.data : data.rows |
||||
|
} else { |
||||
|
return data; |
||||
|
} |
||||
|
}, err => { |
||||
|
Notification.error({ |
||||
|
content: '获取数据失败,导出失败!', |
||||
|
duration: 3, |
||||
|
}) |
||||
|
}) : await Request.get(exportUrl, exportQuery || {}).then(data => { |
||||
|
|
||||
|
if (typeof data === 'object' && data.rows) { |
||||
|
return data.rows |
||||
|
} else { |
||||
|
return data; |
||||
|
} |
||||
|
}, err => { |
||||
|
Notification.error({ |
||||
|
content: '获取数据失败,导出失败!', |
||||
|
duration: 3, |
||||
|
}) |
||||
|
}); |
||||
|
if (!resultData) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
} else { |
||||
|
resultData = data |
||||
|
} |
||||
|
const newColumns = getNewColumns(columns); |
||||
|
|
||||
|
const loopData = getDataSource(newColumns, resultData); |
||||
|
|
||||
|
let content = ''; |
||||
|
let header = '<tr>'; |
||||
|
//header += `<th><div>序号</div></th>`; |
||||
|
newColumns.map(colum => { |
||||
|
header += `<th style="${colum.headStyle || ''}"><div>${colum.title}</div></th>` |
||||
|
}); |
||||
|
header += '</tr>'; |
||||
|
loopData.map(data => { |
||||
|
content += `<tr>`; |
||||
|
newColumns.map(c => { |
||||
|
if (c.style) { |
||||
|
content += `<th style="${c.rowStyle || ''}"><div>${data[c.key] || ''}</div></th>` |
||||
|
} else { |
||||
|
content += `<th><div>${data[c.key] || ''}</div></th>` |
||||
|
} |
||||
|
}); |
||||
|
content += `</tr>`; |
||||
|
}) |
||||
|
|
||||
|
let exportTable = `\uFEFF |
||||
|
<table style='text-alagin:center' border="1"> |
||||
|
${header} |
||||
|
${content} |
||||
|
</table> |
||||
|
`; |
||||
|
const time = moment().format('YYYY-MM-DD'); |
||||
|
let tempStrs = new Blob([exportTable], { type: 'text/xls' }) |
||||
|
FileSaver.saveAs(tempStrs, fileName ? `${fileName}-${time}.xls` : `导出数据-${time}.xlsx`); |
||||
|
setExportLoading(false); |
||||
|
Notification.success({ |
||||
|
content: `成功导出了 ${loopData.length || 0} 条数据`, |
||||
|
duration: 3, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
//普通XLSX导出 |
||||
|
const exportExcel = async () => { |
||||
|
setExportLoading(true) |
||||
|
const { columns, data, fileName, exportUrl, exportQuery, exportBody, requestType } = props || {}; |
||||
|
|
||||
|
const newColumns = getNewColumns(columns); |
||||
|
|
||||
|
const _headers = newColumns |
||||
|
.map((item, i) => Object.assign({}, { key: item.key, title: item.title, position: String.fromCharCode(65 + i) + 1 })) |
||||
|
.reduce((prev, next) => Object.assign({}, prev, { [next.position]: { key: next.key, v: next.title } }), {}); |
||||
|
let resultData = []; |
||||
|
if (exportUrl) { |
||||
|
resultData = requestType == 'post' ? await Request.post(exportUrl, exportBody || {}, exportQuery || {}).then(data => { |
||||
|
//数据接口返回的结果 如果是对象 必须把返回数组放入rows |
||||
|
|
||||
|
if (typeof data === 'object' && (data.rows || data.data)) { |
||||
|
return data.data ? data.data : data.rows |
||||
|
} else { |
||||
|
return data; |
||||
|
} |
||||
|
}, err => { |
||||
|
Notification.error({ |
||||
|
content: '获取数据失败,导出失败!', |
||||
|
duration: 3, |
||||
|
}) |
||||
|
}) : await Request.get(exportUrl, exportQuery || {}).then(data => { |
||||
|
if (typeof data === 'object' && data.rows) { |
||||
|
return data.rows |
||||
|
|
||||
|
} else { |
||||
|
return data; |
||||
|
} |
||||
|
}, err => { |
||||
|
Notification.error({ |
||||
|
content: '获取数据失败,导出失败!', |
||||
|
duration: 3, |
||||
|
}) |
||||
|
}); |
||||
|
if (!resultData) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
} else { |
||||
|
resultData = data |
||||
|
} |
||||
|
|
||||
|
const loopData = getDataSource(newColumns, resultData); |
||||
|
|
||||
|
|
||||
|
const wpx = newColumns.map(c => { |
||||
|
return { |
||||
|
wpx: Number.parseInt(c.wpx) ? Number.parseInt(c.wpx) : 100 |
||||
|
} |
||||
|
}) |
||||
|
if (!(loopData.length > 0)) { |
||||
|
setExportLoading(false); |
||||
|
return; |
||||
|
} |
||||
|
const _data = loopData |
||||
|
.map((item, i) => newColumns.map((key, j) => Object.assign({}, { content: item[key.key], position: String.fromCharCode(65 + j) + (i + 2) }))) |
||||
|
// 对刚才的结果进行降维处理(二维数组变成一维数组) |
||||
|
.reduce((prev, next) => prev.concat(next)) |
||||
|
// 转换成 worksheet 需要的结构 |
||||
|
.reduce((prev, next) => Object.assign({}, prev, { [next.position]: { v: next.content } }), {}); |
||||
|
|
||||
|
// 合并 columns 和 data |
||||
|
const output = Object.assign({}, _headers, _data); |
||||
|
// 获取所有单元格的位置 |
||||
|
const outputPos = Object.keys(output); |
||||
|
// 计算出范围 ,["A1",..., "H2"] |
||||
|
const ref = `${outputPos[0]}:${outputPos[outputPos.length - 1]}`; |
||||
|
|
||||
|
// 构建 workbook 对象 |
||||
|
const workbook = { |
||||
|
SheetNames: ['mySheet'], |
||||
|
Sheets: { |
||||
|
mySheet: Object.assign( |
||||
|
{}, |
||||
|
output, |
||||
|
{ |
||||
|
'!ref': ref, |
||||
|
'!cols': wpx, |
||||
|
}, |
||||
|
), |
||||
|
}, |
||||
|
}; |
||||
|
const time = moment().format('YYYY-MM-DD'); |
||||
|
// 导出 Excel |
||||
|
XLSX.writeFile(workbook, fileName ? `${fileName}-${time}.xlsx` : `导出数据-${time}.xlsx`); |
||||
|
setExportLoading(false); |
||||
|
Notification.success({ |
||||
|
content: `成功导出了 ${loopData.length || 0} 条数据`, |
||||
|
duration: 3, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
|
||||
|
const handleExport = async () => { |
||||
|
switch (exportType) { |
||||
|
case 'fileSaver': |
||||
|
await exportFileSaver(); |
||||
|
break; |
||||
|
case 'xlsx': |
||||
|
await exportExcel(); |
||||
|
break; |
||||
|
case 'merge': |
||||
|
await exportMergeExcel(); |
||||
|
break; |
||||
|
default: |
||||
|
await exportExcel(); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
customRender ? |
||||
|
<span style={style} loading={exportLoading} onClick={handleExport}> |
||||
|
{customRender} |
||||
|
</span> : |
||||
|
|
||||
|
<Button style={style} loading={exportLoading} onClick={handleExport}> |
||||
|
{title || '导出'} |
||||
|
{showIcon && <IconArrowDown />} |
||||
|
</Button> |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
ExportData.propTypes = { |
||||
|
fileName: PropTypes.string, //导出文件名称前缀 |
||||
|
showIcon: PropTypes.bool, //导出按钮是否显示icon,默认不显示 |
||||
|
customRender: PropTypes.element, //自定义导出组件渲染 不传默认按钮样式 |
||||
|
style: PropTypes.object,//透传style |
||||
|
title: PropTypes.string, //导出按钮文字 |
||||
|
columns: PropTypes.array.isRequired, //导出显示的header数组 兼容antd columns 可直接拿table或者protable的columns使用 注:columns每列的属性wpx设置导出的execl每列的宽度值 默认 100 |
||||
|
data: PropTypes.array.isRequired, //导出的数据 兼容antd table 数组嵌套处理,如果传入exportUrl 则从接口获取数据导出,此参数无效 |
||||
|
exportUrl: PropTypes.string, //导出数据从接口获取的url地址 返回的数据1、数组必须支持columns的设置 ,2、如果是对象,数组需放在rows属性上 |
||||
|
exportBody: PropTypes.object, //导出数据接口body参数 |
||||
|
exportQuery: PropTypes.object, //导出数据从接口获取的url地址上的参数 |
||||
|
requestType: PropTypes.string, //请求类型 get,post,默认get |
||||
|
exportType: PropTypes.string, //导出执行类型函数 'fileSaver','xlsx','merge'纵向单元格合并 |
||||
|
}; |
||||
|
|
||||
|
export default ExportData; |
@ -1,66 +1,340 @@ |
|||||
import React, { useEffect, useState, useRef } from 'react'; |
import React, { useEffect, useState, useRef, useMemo } from 'react' |
||||
import { connect } from 'react-redux'; |
import { connect } from 'react-redux' |
||||
import ReactECharts from 'echarts-for-react'; |
import { Spin, Card, CardGroup, Form, Button, Table, Pagination, Tooltip } from '@douyinfe/semi-ui' |
||||
import echarts from 'echarts'; |
import ExportData from '../components/export-data' |
||||
import { Spin, Card, CardGroup, Form, Button,Table } from '@douyinfe/semi-ui'; |
import moment from 'moment' |
||||
|
|
||||
|
const Network = props => { |
||||
|
const { dispatch, actions, user, clientHeight, thingId, deviceListAlarms, devicesCardStatusList, project } = props |
||||
|
const { analysis } = actions |
||||
|
const form = useRef() //表单 |
||||
|
const [deployData, setDeployData] = useState([]) |
||||
|
const [deviceData, setDeviceData] = useState([]) |
||||
|
const [deviceMetasDeployed, setDeviceMetasDeployed] = useState([]) |
||||
|
const [sensorId, setSensorId] = useState([]) |
||||
|
const [sensorsDataItems, setSensorsDataItems] = useState({}) |
||||
|
const [tableData, setTableData] = useState([]) //最新一次的数据 |
||||
|
const [lastData, setLastData] = useState([]) //最终数据 |
||||
|
const [lastDataCopy, setLastDataCopy] = useState([]) //最终数据 |
||||
|
const [searchType, setSearchType] = useState('') |
||||
|
const [searchName, setSearchName] = useState('') |
||||
|
const [typeList, setTypeList] = useState([]) |
||||
|
const [query, setQuery] = useState({ limit: 10, page: 0 }) //页码信息 |
||||
|
|
||||
|
const DeviceTypes = { |
||||
|
'DTU': 'DTU', |
||||
|
'gateway': '网关', |
||||
|
'sensor': '传感器', |
||||
|
'acqUnit': '采集单元', |
||||
|
'dau.gateway': '分布式智能云采集网关', |
||||
|
'dau.node': '分布式智能云采集节点', |
||||
|
'tcp.dtu': '工作站', |
||||
|
} |
||||
|
|
||||
const Network = (props) => { |
useEffect(() => { |
||||
const { dispatch, actions, user, clientHeight } = props |
setLastData([]) |
||||
|
setLastDataCopy([]) |
||||
|
}, [project]) |
||||
|
|
||||
const form = useRef();//表单 |
useEffect(() => { |
||||
|
if (thingId) { |
||||
|
let dataList = [] |
||||
|
dispatch(analysis.getThingsDeploy(thingId)).then(rs => { |
||||
|
if (rs.success) { |
||||
|
setDeployData(rs.payload.data) |
||||
|
dataList = rs.payload.data |
||||
|
//列表渲染数据 |
||||
|
let da = [] |
||||
|
if (dataList.instances) { |
||||
|
Object.keys(dataList.instances).forEach(i => { |
||||
|
if (dataList.instances[i].type == 's.d') { |
||||
|
da.push({ |
||||
|
sensorId: i, |
||||
|
sensorName: dataList.instances[i]?.name, |
||||
|
deviceType: dataList?.instances[i]?.instance?.properties?.deviceType, |
||||
|
collectTime: '--', |
||||
|
data: '--', |
||||
|
iotCardStatus: '--', |
||||
|
status: '--', |
||||
|
option: '--', |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
dispatch(analysis.findDeviceMetaDeployed(thingId)).then(res => { |
||||
|
if (res.success) { |
||||
|
setDeviceMetasDeployed(res.payload.data) |
||||
|
const deviceMetaDeployed = res.payload.data |
||||
|
if (deviceMetaDeployed && dataList && deviceMetaDeployed.devices) { |
||||
|
const sensorsId = [] |
||||
|
let alarmSensorId = [] //所有设备的id |
||||
|
const sensorsDataItems = {} |
||||
|
for (const id in dataList.instances) { |
||||
|
alarmSensorId.push(id) |
||||
|
const instances = dataList.instances[id] |
||||
|
|
||||
useEffect(() => { |
if (instances.type == 's.d' && instances.instance.properties.deviceType == 'sensor') { |
||||
|
const meta = deviceMetaDeployed.devices.find(m => m.id == instances.instance.deviceMetaId) |
||||
|
sensorsDataItems[id] = { |
||||
|
items: {}, |
||||
|
deviceName: instances.name, |
||||
|
} |
||||
|
if (meta) { |
||||
|
sensorsDataItems[id].items = meta.capabilities[0].properties.reduce((p, n) => { |
||||
|
if (n.category == 'Output') { |
||||
|
p[n.name] = { name: n.showName, unit: n.unit } |
||||
|
} |
||||
|
return p |
||||
|
}, {}) |
||||
|
} |
||||
|
sensorsId.push(id) |
||||
|
} |
||||
|
} |
||||
|
dispatch(analysis.getDevicesAlarms({ deviceIds: alarmSensorId }, { limit: 5 })) |
||||
|
dispatch(analysis.findDevicesCardStatus({ deviceIds: alarmSensorId })) |
||||
|
setSensorsDataItems(sensorsDataItems) |
||||
|
setSensorId(sensorsId) |
||||
|
setDeviceData(da) |
||||
|
} |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
}, [thingId]) |
||||
|
|
||||
}, []) |
useEffect(async () => { |
||||
const columns = [ |
if (sensorId && sensorId.length && sensorsDataItems) { |
||||
{ |
const rs = await dispatch(analysis.findSensorLastData(sensorId)) |
||||
title: '设备名称', |
const tableData = [] |
||||
dataIndex: 'deviceName', |
if (rs.success) { |
||||
width: 200, |
rs.payload.data.forEach(sd => { |
||||
key:'deviceName' |
if (Object.keys(sensorsDataItems).length) { |
||||
}, |
let sensorDataItem = sensorsDataItems[sd.sensorId] |
||||
{ |
let sensorName = sensorDataItem && sensorDataItem.deviceName ? sensorDataItem.deviceName : '' |
||||
title: '设备类型', |
let msg = sd.data.length |
||||
dataIndex: 'deviceType', |
? sd.data[0] |
||||
width:200, |
: { |
||||
key:'deviceType' |
collectTime: null, |
||||
}, |
sensorName: sensorName, |
||||
{ |
data: { noData: '暂无数据' }, |
||||
title: '最后采集时间', |
} |
||||
dataIndex: 'collectTime', |
let dataStr = '' |
||||
width:200, |
let dataKeys = Object.keys(msg.data) |
||||
key:'collectTime' |
dataKeys.forEach(k => { |
||||
|
let item = sensorDataItem && sensorDataItem.items ? sensorDataItem.items[k] : null |
||||
|
if (item) { |
||||
|
dataStr += `${item.name}:${msg.data[k]}(${item.unit}); ` |
||||
|
} else if (k == 'noData') { |
||||
|
dataStr += msg.data[k] |
||||
|
} else { |
||||
|
dataStr += `${k}:${msg.data[k]};` |
||||
|
} |
||||
|
}) |
||||
|
let collectTime = msg.collectTime ? moment(msg.collectTime).format('YYYY-MM-DD HH:mm:ss') : '--' |
||||
|
tableData.push({ |
||||
|
sensorId: sd.sensorId, |
||||
|
sensorName: sensorName, |
||||
|
collectTime: collectTime, |
||||
|
data: dataStr, |
||||
|
deviceType: 'sensor', //传感器 |
||||
|
iotCardStatus: '--', |
||||
|
status: '--', |
||||
|
option: '--', |
||||
|
}) |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
setTableData(tableData) |
||||
|
} |
||||
|
}, [sensorId]) |
||||
|
useEffect(() => { |
||||
|
if (deviceData && deviceData.length && tableData && tableData.length) { |
||||
|
const dataD = deviceData?.map(p => { |
||||
|
const objRslt = tableData.find(q => q.sensorId == p.sensorId) |
||||
|
return { |
||||
|
sensorId: objRslt ? objRslt.sensorId : p.sensorId, |
||||
|
sensorName: objRslt ? objRslt.sensorName : p.sensorName, |
||||
|
collectTime: objRslt ? objRslt.collectTime : p.collectTime, |
||||
|
data: objRslt ? objRslt.data : p.data, |
||||
|
deviceType: DeviceTypes[objRslt ? objRslt.deviceType : p.deviceType], |
||||
|
iotCardStatus: |
||||
|
devicesCardStatusList && devicesCardStatusList.length |
||||
|
? devicesCardStatusList.find(v => v.deviceId == p.sensorId).status === 0 |
||||
|
? '正常' |
||||
|
: devicesCardStatusList.find(v => v.deviceId == p.sensorId).status === 1 |
||||
|
? '未激活' |
||||
|
: '停机' |
||||
|
: '--', |
||||
|
status: |
||||
|
deviceListAlarms && deviceListAlarms.length |
||||
|
? deviceListAlarms?.find(v => v.deviceId == p.sensorId) |
||||
|
? '异常' |
||||
|
: '正常' |
||||
|
: '正常', |
||||
|
option: objRslt ? objRslt.option : p.option, |
||||
|
} |
||||
|
}) |
||||
|
const typeList = dataD.reduce((p, c) => { |
||||
|
let isExist = p.some(q => q.label === c.deviceType) |
||||
|
if (!isExist) { |
||||
|
p.push({ label: c.deviceType, value: c.sensorId }) |
||||
|
} |
||||
|
return p |
||||
|
}, []) |
||||
|
setTypeList(typeList) |
||||
|
setLastData(dataD) |
||||
|
setLastDataCopy(dataD) |
||||
|
} |
||||
|
}, [deviceData, tableData]) |
||||
|
// const lastDataCopy=useMemo(()=>{ |
||||
|
// return lastData |
||||
|
// },[thingId]) |
||||
|
// const scroll = useMemo(() => ({ y: 400 }), []) |
||||
|
//名称回调事件 |
||||
|
const inputChange = e => { |
||||
|
setSearchName(e) |
||||
|
} |
||||
|
//选择设备类型下拉框回调 |
||||
|
const selectChange = e => { |
||||
|
setSearchType(typeList.find(f => f.value == e)?.label) |
||||
|
} |
||||
|
|
||||
|
//查询事件回调 |
||||
|
const searchHandler = () => { |
||||
|
setLastData( |
||||
|
searchName || searchType |
||||
|
? lastDataCopy.filter(f => f.sensorName.includes(searchName) && f.deviceType.includes(searchType)) |
||||
|
: lastDataCopy |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
const columns = [ |
||||
|
{ |
||||
|
title: '设备名称', |
||||
|
dataIndex: 'sensorName', |
||||
|
width: 200, |
||||
|
key: 'sensorName', |
||||
|
render: (_, r) => { |
||||
|
return ( |
||||
|
<> |
||||
|
<Tooltip content={r.sensorName}> |
||||
|
<div>{r.sensorName.length > 7 ? `${r.sensorName.substr(0, 7)}...` : r.sensorName}</div> |
||||
|
</Tooltip> |
||||
|
</> |
||||
|
) |
||||
}, |
}, |
||||
{ |
}, |
||||
title: '更新日期', |
{ |
||||
dataIndex: 'updateTime', |
title: '设备类型', |
||||
sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1), |
dataIndex: 'deviceType', |
||||
render: value => { |
width: 200, |
||||
return dateFns.format(new Date(value), 'yyyy-MM-dd'); |
key: 'deviceType', |
||||
}, |
}, |
||||
|
{ |
||||
|
title: '最后采集时间', |
||||
|
dataIndex: 'collectTime', |
||||
|
width: 200, |
||||
|
key: 'collectTime', |
||||
|
}, |
||||
|
{ |
||||
|
title: '数据', |
||||
|
dataIndex: 'data', |
||||
|
width: 200, |
||||
|
key: 'data', |
||||
|
render: (_, r) => { |
||||
|
return ( |
||||
|
<> |
||||
|
<Tooltip content={r.data}> |
||||
|
<div>{r.data.length > 6 ? `${r.data.substr(0, 6)}...` : r.data}</div> |
||||
|
</Tooltip> |
||||
|
</> |
||||
|
) |
||||
}, |
}, |
||||
]; |
}, |
||||
return ( |
{ |
||||
|
title: '物联网卡状态', |
||||
|
width: 200, |
||||
|
dataIndex: 'iotCardStatus', |
||||
|
key: 'iotCardStatus', |
||||
|
}, |
||||
|
{ |
||||
|
title: '状态', |
||||
|
width: 200, |
||||
|
dataIndex: 'status', |
||||
|
key: 'status', |
||||
|
}, |
||||
|
{ |
||||
|
title: '操作', |
||||
|
width: 200, |
||||
|
dataIndex: 'option', |
||||
|
key: 'option', |
||||
|
}, |
||||
|
] |
||||
|
return ( |
||||
|
<> |
||||
|
<div style={{ marginBottom: 12, display: 'flex' }}> |
||||
|
<div> |
||||
|
<Form> |
||||
|
<Form.Input |
||||
|
// suffix={<IconSearch />} |
||||
|
field='name' |
||||
|
pure |
||||
|
showClear |
||||
|
label='名称' |
||||
|
style={{ width: 260, marginRight: 12 }} |
||||
|
placeholder='请输入设备名称' |
||||
|
onChange={inputChange} |
||||
|
/> |
||||
|
<Form.Select |
||||
|
optionList={typeList} |
||||
|
field='type' |
||||
|
pure |
||||
|
showClear |
||||
|
label='设备类型' |
||||
|
onChange={selectChange} |
||||
|
style={{ width: 260, marginLeft: 12, marginRight: 12 }} |
||||
|
placeholder='请选择设备类型' |
||||
|
/> |
||||
|
<Button theme='solid' type='primary' htmlType='submit' onClick={searchHandler}> |
||||
|
查询 |
||||
|
</Button> |
||||
|
</Form> |
||||
|
</div> |
||||
|
<div style={{ marginLeft: 10 }}> |
||||
|
{' '} |
||||
|
{lastData.length ? ( |
||||
|
<ExportData |
||||
|
// showIcon |
||||
|
fileName='设备列表' |
||||
|
exportType='fileSaver' |
||||
|
data={lastData} |
||||
|
columns={columns} |
||||
|
key='export' |
||||
|
/> |
||||
|
) : ( |
||||
|
'' |
||||
|
)} |
||||
|
</div> |
||||
|
</div> |
||||
<Table |
<Table |
||||
columns={columns} |
// scroll={scroll} |
||||
dataSource={dataSource} |
columns={columns} |
||||
></Table> |
dataSource={lastData}></Table> |
||||
|
</> |
||||
) |
) |
||||
} |
} |
||||
|
|
||||
function mapStateToProps (state) { |
function mapStateToProps(state) { |
||||
const { auth, global, members, webSocket } = state; |
const { auth, global, members, webSocket, deviceListAlarms, devicesCardStatus } = state |
||||
return { |
return { |
||||
user: auth.user, |
user: auth.user, |
||||
actions: global.actions, |
actions: global.actions, |
||||
clientHeight: global.clientHeight |
clientHeight: global.clientHeight, |
||||
}; |
deviceListAlarms: deviceListAlarms?.data || [], |
||||
|
devicesCardStatusList: devicesCardStatus?.data || [], |
||||
|
} |
||||
} |
} |
||||
|
|
||||
export default connect(mapStateToProps)(Network); |
export default connect(mapStateToProps)(Network) |
||||
|
Loading…
Reference in new issue