From a7c03677562a6cdd3789430f0cbe53e66de1e090 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?zhaobing=E2=80=99?= Date: Tue, 28 Nov 2023 18:01:23 +0800 Subject: [PATCH 1/5] feat:fix bugs --- api/app/lib/controllers/analysis/network.js | 397 +++++++++++++++++- api/app/lib/routes/analysis/network.js | 2 + .../sections/install/containers/system.jsx | 2 +- .../projectGroup/containers/bigscreen.jsx | 2 +- 4 files changed, 400 insertions(+), 3 deletions(-) diff --git a/api/app/lib/controllers/analysis/network.js b/api/app/lib/controllers/analysis/network.js index 0e6a85e..e726941 100644 --- a/api/app/lib/controllers/analysis/network.js +++ b/api/app/lib/controllers/analysis/network.js @@ -1,8 +1,296 @@ 'use strict'; const moment = require('moment') +async function getMaintenceRecordRank(ctx) { + const sequelize = ctx.fs.dc.orm + const Sequelize = ctx.fs.dc.ORM; + const { clickHouse } = ctx.app.fs + const models = ctx.fs.dc.models + const { startTime, endTime } = ctx.query -async function getOrganizationsStruc (ctx) { + console.log(startTime, endTime, ctx.query, '1212312') + try { + const res = await sequelize.query(` + SELECT emrp.project_id,count(1) +FROM equipment_maintenance_record + RIGHT JOIN equipment_maintenance_record_project emrp + on equipment_maintenance_record.id = emrp.equipment_maintenance_record_id + where report_time BETWEEN :startTime AND :endTime +GROUP BY emrp.project_id + ` + , { + replacements: { + startTime: moment(startTime).format('YYYY-MM-DD HH:mm:ss'), + endTime: moment(endTime).format('YYYY-MM-DD HH:mm:ss ') + } + //, type: sequelize.QueryTypes.SELECT + } + ) + //查询equipment_maintenance_record返回的结果[{project_id: 22, count: 1}] + let projectList = [] + //存project的id + let projectIdList = [] + // console.log('resssss', res) + if (res.length > 0) { + res[0].forEach((item) => { + projectList.push({ project_id: item.project_id, count: Number(item.count) }) + projectIdList.push(item.project_id) + }) + } + const projectNameList = await models.ProjectCorrelation.findAll({ + attributes: + ['id', 'name'], + where: { + id: { $in: projectIdList }, + name: { + [Sequelize.Op.not]: null//有name的结果 + } + // del: false + } + }) || [] + //在ProjectCorrelation中查不到名字,去clickHouse中去查 + const projectNameList1 = await models.ProjectCorrelation.findAll({ + attributes: + ['id', 'name', 'pepProjectId'], + where: { + id: { $in: projectIdList }, + name: { + [Sequelize.Op.eq]: null//无name的结果 + } + // del: false + } + }) + //存放需要去查询clickHouse的id + let idList = new Set() + if (projectNameList1.length) { + projectNameList1.forEach((item) => { + idList.add(item.pepProjectId) + }) + } + //pepProject名称 + const projectManageName = idList.size ? await clickHouse.projectManage.query(` + SELECT id,project_name FROM t_pim_project + WHERE id IN (${[...idList].join(',')}, -1) + `).toPromise() : [] + // if (projectList.length) { + // projectList.forEach((item) => { + // projectManageName + // }) + // } + //存的是{id,projectName} + let project = [] + if (projectNameList1.length && projectManageName.length) { + projectManageName.forEach((item) => { + const pepObj = projectNameList1.find((item1) => { return item1.pepProjectId === item.id }) + project.push({ id: pepObj.id, projectName: item.project_name }) + }) + } + const resAll = project.concat(projectNameList) + let mergedArray = [] + if (resAll.length && projectList) { + mergedArray = projectList.map(obj1 => { + const matchingObj = resAll.find(obj2 => obj2.id === obj1.project_id); + return { id: obj1.project_id, pepProjectId: matchingObj.id, projectName: matchingObj.projectName || matchingObj.dataValues.name, count: obj1.count }; + }); + } + // console.log('ididididid', resAll) + // console.log('ididididid', project) + // console.log('ididididid', projectManageName) + // console.log('ididididid', projectNameList) + // console.log('ididididid', projectList) + + ctx.status = 200 + ctx.body = mergedArray + } catch (error) { + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); + ctx.status = 400; + ctx.body = { + message: '查询维修记录排名失败' + } + } +} + + +async function getMaintenceTotal(ctx) { + const sequelize = ctx.fs.dc.orm + const Sequelize = ctx.fs.dc.ORM; + const { clickHouse } = ctx.app.fs + const models = ctx.fs.dc.models + const { startTime, endTime } = ctx.query + try { + //所有维修记录 + const res = await sequelize.query(` + SELECT emrp.project_id, + count(case when record.status in ('维修中','待维修','维修完成') then record.id end) incomplete, + count(case when record.status in ('维修完成') then record.id end) completed + FROM equipment_maintenance_record record + RIGHT JOIN equipment_maintenance_record_project emrp + on record.id = emrp.equipment_maintenance_record_id + where report_time BETWEEN :startTime AND :endTime + GROUP BY emrp.project_id + ` + , { + replacements: { + startTime: moment(startTime).format('YYYY-MM-DD HH:mm:ss '), + endTime: moment(endTime).format('YYYY-MM-DD HH:mm:ss ') + } + //, type: sequelize.QueryTypes.SELECT + } + ) + //查询equipment_maintenance_record返回的结果[{project_id: 22,status:'' count: 1}] + let projectList = [] + //存project的id + let projectIdList = new Set() + // console.log('resssss', res) + if (res.length > 0) { + res[0].forEach((item) => { + projectList.push({ project_id: item.project_id, 'incomplete': Number(item.incomplete), completed: Number(item.completed) }) + projectIdList.add(item.project_id) + }) + } + // const result = projectList.reduce((acc, curr) => { + // if (curr.status === '待维修' || curr.status === '维修中') { + // const existingItem = acc.find(item => item.project_id === curr.project_id && item.status === '异常数'); + // if (existingItem) { + // existingItem.count += curr.count; + // } else { + // acc.push({ project_id: curr.project_id, status: '异常数', count: curr.count }); + // } + // } else if (curr.status === '维修完成') { + // const existingItem = acc.find(item => item.project_id === curr.project_id && item.status === '维修数'); + // if (existingItem) { + // existingItem.count += curr.count; + // } else { + // acc.push({ project_id: curr.project_id, status: '维修数', count: curr.count }); + // } + // } + // return acc; + // }, []) + //console.log('resssssresult', result) + const projectNameList = await models.ProjectCorrelation.findAll({ + attributes: + ['id', 'name'], + where: { + id: { $in: [...projectIdList] }, + name: { + [Sequelize.Op.not]: null//有name的结果 + } + // del: false + } + }) || [] + //在ProjectCorrelation中查不到名字,去clickHouse中去查 + const projectNameList1 = await models.ProjectCorrelation.findAll({ + attributes: + ['id', 'name', 'pepProjectId'], + where: { + id: { $in: [...projectIdList] }, + name: { + [Sequelize.Op.eq]: null//无name的结果 + } + // del: false + } + }) + //存放需要去查询clickHouse的id + let idList = new Set() + if (projectNameList1.length) { + projectNameList1.forEach((item) => { + idList.add(item.pepProjectId) + }) + } + //pepProject名称 + const projectManageName = idList.size ? await clickHouse.projectManage.query(` + SELECT id,project_name FROM t_pim_project + WHERE id IN (${[...idList].join(',')}, -1) + `).toPromise() : [] + let project = [] + if (projectNameList1.length && projectManageName.length) { + projectManageName.forEach((item) => { + const pepObj = projectNameList1.find((item1) => { return item1.pepProjectId === item.id }) + project.push({ id: pepObj.id, projectName: item.project_name }) + }) + } + //pg的数据和clcikHouse的数据(名字)合并 + const resAll = project.concat(projectNameList) + let mergedArray = [] + if (resAll.length && projectList) { + mergedArray = projectList.map(obj1 => { + const matchingObj = resAll.find(obj2 => obj2.id === obj1.project_id) + return { + id: obj1.project_id, incomplete: obj1.incomplete, completed: obj1.completed, pepProjectId: matchingObj.id, + projectName: matchingObj.projectName || matchingObj.dataValues.name + } + }); + } + + // console.log('ididididid', resAll) + // console.log('ididididid', project) + // console.log('ididididid', projectManageName) + // console.log('ididididid', projectNameList) + // console.log('ididididid', projectList) + ctx.status = 200 + ctx.body = mergedArray + } catch (error) { + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); + ctx.status = 400; + ctx.body = { + message: '查询维修记录统计失败' + } + } + + +} +async function getEquipmentCategory(ctx) { + const { startTime, endTime } = ctx.query + const Sequelize = ctx.fs.dc.ORM + const models = ctx.fs.dc.models + try { + const res = await models.EquipmentMaintenanceRecord.findAll({ + attributes: [ + 'equipment_category', + [Sequelize.fn('COUNT', Sequelize.col('equipment_category')), 'count'] + ], + where: { reportTime: { $between: [moment(startTime).format('YYYY-MM-DD HH:mm:ss '), moment(endTime).format('YYYY-MM-DD HH:mm:ss ')] } }, + group: ['equipment_category'] + }) + ctx.status = 200 + ctx.body = res + + } catch (error) { + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); + ctx.status = 400; + ctx.body = { + message: '查询设备类型失败' + } + } +} + + +async function getStatus(ctx) { + const { startTime, endTime } = ctx.query + const Sequelize = ctx.fs.dc.ORM + const models = ctx.fs.dc.models + try { + const res = await models.EquipmentMaintenanceRecord.findAll({ + attributes: [ + 'status', + [Sequelize.fn('COUNT', Sequelize.col('status')), 'count'] + ], + where: { reportTime: { $between: [moment(startTime).format('YYYY-MM-DD HH:mm:ss '), moment(endTime).format('YYYY-MM-DD HH:mm:ss ')] } }, + group: ['status'] + }) + ctx.status = 200 + ctx.body = res + + } catch (error) { + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); + ctx.status = 400; + ctx.body = { + message: '查询设备类型失败' + } + } +} + +async function getOrganizationsStruc(ctx) { try { const { utils: { anxinStrucIdRange } } = ctx.app.fs const { pepProjectId } = ctx.params @@ -59,8 +347,115 @@ async function getThingsDeploy (ctx) { } +async function findDeviceLastData(ctx, deviceIds) { + let rslt = []; + const clientRaws = ctx.app.fs.esclient[RAW_DATA]; + const clientVbraws = ctx.app.fs.esclient[VBRAW_DATA]; + if (deviceIds) { + for (let id of deviceIds) { + let params = { + index: clientRaws.config.index, + type: clientRaws.config.type, + body: { + query: { + constant_score: { + filter: { + bool: { + must: [ + { term: { "iota_device": id } }, + { range: { "collect_time": { lte: moment().toISOString() } } } + ] + } + } + } + }, + sort: [ + { + "collect_time": { + order: "desc" + } + } + ], + size: 1 + } + }; + let res = await clientRaws.search(params); + if (res.hits.hits.length == 0) { + params.index = clientVbraws.config.index; + params.type = clientVbraws.config.type; + res = await clientVbraws.search(params); + } + + let data = res.hits.hits.map(h => { + let source = h._source; + let data_ = source.data; + if (params.index == clientVbraws.config.index) { + let tempData = { "最大幅值": '_' }; + if (data_.raw && data_.raw.length) { + let maxAmplitude = data_.raw[0]; + for (let v of data_.raw) { + if (maxAmplitude < v) { + maxAmplitude = v; + } + } + tempData['最大幅值'] = maxAmplitude + '(gal)'; + } + data_ = tempData; + } + return { + collectTime: source.collect_time, + iotaDevice: source.iota_device, + iotaDeviceName: source.iota_device_name, + data: data_ + } + }); + rslt.push({ + sensorId: id, + data: data + }); + } + } + return rslt; +} +async function getDeviceLists(ctx) { + try { + const sensorIds = ctx.request.body.sensorIds; + let rslt = await findDeviceLastData(ctx, sensorIds); + ctx.status = 200; + ctx.body = rslt; + } catch (error) { + ctx.status = 400; + ctx.body = { + "name": "FindError", + "message": "原始数据查询失败" + } + } + +} + +// try { +// const { clickHouse } = ctx.app.fs +// const { thingIds } = ctx.params +// // const res = thingIds ? await clickHouse.iot.query( +// // `SELECT distinct dv.name,dv.thingId,dv.id,dv.deviceMetaId,re.properties +// // FROM iota.Device dv +// // LEFT JOIN iota.FilteredResource re +// // ON dv.deviceMetaId=re.key +// // WHERE thing_id in (${thingIds})`).toPromise() : [] + + +// } catch { +// ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); +// ctx.status = 400; +// ctx.body = { +// message: '查询设备列表失败' +// } +// } + + module.exports = { getOrganizationsStruc, getThingsDeploy, + getDeviceLists } \ No newline at end of file diff --git a/api/app/lib/routes/analysis/network.js b/api/app/lib/routes/analysis/network.js index a038608..94a55ef 100644 --- a/api/app/lib/routes/analysis/network.js +++ b/api/app/lib/routes/analysis/network.js @@ -9,6 +9,8 @@ module.exports = function (app, router, opts) { router.get('/things/:thingId/deploy', network.getThingsDeploy) + app.fs.api.logAttr['POST/sensors/last/data'] = { content: '原始数据查询失败', visible: true }; + router.post('/sensors/last/data', network.getDeviceLists) // app.fs.api.logAttr['GET/systemAvailability'] = { content: '获取系统可用性', visible: true }; // router.get('/systemAvailability', operationData.getSystemAvailability) diff --git a/web/client/src/sections/install/containers/system.jsx b/web/client/src/sections/install/containers/system.jsx index 0ef16bc..a012398 100644 --- a/web/client/src/sections/install/containers/system.jsx +++ b/web/client/src/sections/install/containers/system.jsx @@ -196,7 +196,7 @@ const Example = (props) => { // ) row.anxinProject.length >= 2 ? ( -
7 ? '112px' : '', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', display: index > 2 ? 'none' : '', color: item.projectState == -1 ? '#F93920' : '' }}> +
4 ? '70px' : '', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', display: index > 2 ? 'none' : '', color: item.projectState == -1 ? '#F93920' : '' }}> {item.name}
):( diff --git a/web/client/src/sections/projectGroup/containers/bigscreen.jsx b/web/client/src/sections/projectGroup/containers/bigscreen.jsx index f013cdb..6d5352d 100644 --- a/web/client/src/sections/projectGroup/containers/bigscreen.jsx +++ b/web/client/src/sections/projectGroup/containers/bigscreen.jsx @@ -313,7 +313,7 @@ const Bigscreen = (props) => { }, tooltip: { trigger: 'axis', - position: 'inside', + confine:true,//固定在图表中 formatter: function (params) { // 自定义提示框内容 // console.log(params); From 1af527df5fc74f3652986031fdf954823cdd6893 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?zhaobing=E2=80=99?= Date: Wed, 29 Nov 2023 09:51:53 +0800 Subject: [PATCH 2/5] feat:fix bugs --- api/app/lib/schedule/workOrder.js | 4 ++-- .../src/sections/projectGroup/containers/bigscreen.jsx | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/api/app/lib/schedule/workOrder.js b/api/app/lib/schedule/workOrder.js index e271a81..b116129 100644 --- a/api/app/lib/schedule/workOrder.js +++ b/api/app/lib/schedule/workOrder.js @@ -3,7 +3,7 @@ const schedule = require('node-schedule'); const moment = require('moment') const request = require('superagent'); -let isDev = true +let isDev = false module.exports = function (app, opts,ctx) { const workOrder = app.fs.scheduleInit( @@ -15,7 +15,7 @@ module.exports = function (app, opts,ctx) { async()=>{ try{ //前一次执行时间 - console.log('工单数据抽取开始') + console.log('工单数据抽取开始',moment().format('YYYY-MM-DD HH:mm:ss')) const username = "admin" const password = "fs-workflow" let lastExecutionTime = null; diff --git a/web/client/src/sections/projectGroup/containers/bigscreen.jsx b/web/client/src/sections/projectGroup/containers/bigscreen.jsx index 6d5352d..65e6138 100644 --- a/web/client/src/sections/projectGroup/containers/bigscreen.jsx +++ b/web/client/src/sections/projectGroup/containers/bigscreen.jsx @@ -36,7 +36,6 @@ const Bigscreen = (props) => { const [interruptData,setInterruptData]=useState([]) const [avgTmes,setAvgTimes]=useState([])//平均修复时长数组 // const [queryUserId, setQueryUserId] = useState('') - useEffect(() => { @@ -359,8 +358,8 @@ const Bigscreen = (props) => { areaStyle: { color: '#0e9cff26', }, - data: v.online?.map(f => [moment(f.collect_time).format('YYYY-MM-DD HH'), f.rate.toFixed(1)]) || [] - })) || [] + data: v.online.sort((a,b)=>new Date(b.collect_time)-new Date(a.collect_time))?.map(f => [moment(f.collect_time).format('YYYY-MM-DD HH'), f.rate.toFixed(1)]) || [] + })) || [] }} notMerge={true} lazyUpdate={true} From e09702da4af595467f7b48097ef046b9d9115c73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?zhaobing=E2=80=99?= Date: Wed, 29 Nov 2023 10:20:04 +0800 Subject: [PATCH 3/5] feat:fix bugs --- api/app/lib/schedule/workOrder.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/app/lib/schedule/workOrder.js b/api/app/lib/schedule/workOrder.js index b116129..a308fec 100644 --- a/api/app/lib/schedule/workOrder.js +++ b/api/app/lib/schedule/workOrder.js @@ -77,7 +77,7 @@ module.exports = function (app, opts,ctx) { formData: JSON.parse(f.formData) }) const res=await models.FormDataTable.create({ - projectId:parseData.pomsProjectId.value || null, + projectId:parseData?parseData.pomsProjectId.value : null, formname:procinstsVariables.body.find(t => t.name == 'fsEmisBusinessName') ? procinstsVariables.body.find(t => t.name == 'fsEmisBusinessName').value : '', state: f.state||null, endTime:f.endTime||null, From d996142cc9ff5bc267a9fe800ace884693c8ec05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?zhaobing=E2=80=99?= Date: Thu, 30 Nov 2023 20:27:22 +0800 Subject: [PATCH 4/5] =?UTF-8?q?fix=20bugs:=E5=91=8A=E8=AD=A6=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E5=85=A8=E9=80=89=E6=97=A0=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/client/src/sections/problem/components/tableData.jsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/web/client/src/sections/problem/components/tableData.jsx b/web/client/src/sections/problem/components/tableData.jsx index 5a3fa4c..d83a2f3 100644 --- a/web/client/src/sections/problem/components/tableData.jsx +++ b/web/client/src/sections/problem/components/tableData.jsx @@ -19,7 +19,6 @@ const TableData = ({alarmDataGroup, route, dispatch, actions, collectData, setSe const groupId = useRef() let title = { dataLnterrupt: "数据中断详情", dataAbnormal: "数据异常详情", strategyHit: "策略命中详情", videoAbnormal: "视频异常详情", useAbnormal: "应用异常详情", deviceAbnormal: "设备异常详情" } const [exportUrl, setExportUrl] = useState('') - useEffect(() => { switch (route) { case 'useAbnormal': @@ -27,6 +26,7 @@ const TableData = ({alarmDataGroup, route, dispatch, actions, collectData, setSe if (res.success) { let typeData = { element: "元素异常", apiError: "接口报错 ", timeout: "加载超时" } let tableDatas = res.payload.data?.rows.map(v => ({ + key: v.id, id: v.id, projectName: v.app?.projectCorrelations?.map(r => (r.name ? { id: r.id, name: r.name, state: 'PMOS' } : { id: r.id, name: r.pepProject?.projectName, state: r.pepProject?.constructionStatus @@ -52,6 +52,7 @@ const TableData = ({alarmDataGroup, route, dispatch, actions, collectData, setSe dispatch(problem.getAlarmVideoList({ ...search.current, pepProjectId: pepProjectId })).then((res) => { if (res.success) { let tableDatas = res.payload.data?.map(v => ({ + key: v.alarmId, id: v.alarmId, StructureName: v.struc, projectName: v.pomsProject?.map(r => (r.name ? { id: r.id, name: r.name, state: 'PMOS' } : { @@ -86,6 +87,7 @@ const TableData = ({alarmDataGroup, route, dispatch, actions, collectData, setSe if (res.success) { setCount(res.payload.data?.count || 0) let tableDatas = res.payload.data?.rows?.map(v => ({ + key: v.AlarmId, id: v.AlarmId, StructureName: v.StructureName, projectName: v.pomsProject?.map(r => (r.name ? { id: r.id, name: r.name, state: 'PMOS' } : { @@ -234,7 +236,6 @@ const TableData = ({alarmDataGroup, route, dispatch, actions, collectData, setSe break; } } - return ( <>
@@ -408,6 +409,7 @@ const TableData = ({alarmDataGroup, route, dispatch, actions, collectData, setSe // name: record.name, }), onSelect: (record, selected) => { + // console.log(`select row: ${selected}`, record); }, // onSelectAll: (selected, selectedRows) => { @@ -417,6 +419,7 @@ const TableData = ({alarmDataGroup, route, dispatch, actions, collectData, setSe // console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows); setSelected(selectedRows?.map(v => v.id)) }, + }} /> })()} From d2cce0733efcfa07e433b641726e7b01a004e5d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?zhaobing=E2=80=99?= Date: Fri, 1 Dec 2023 10:07:38 +0800 Subject: [PATCH 5/5] =?UTF-8?q?feat:=E4=B8=80=E5=9B=BE=E7=BB=9F=E6=8F=BD?= =?UTF-8?q?=E8=A1=A8=E6=A0=BC=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/app/lib/controllers/analysis/network.js | 298 ++++++--- api/app/lib/routes/analysis/network.js | 11 +- api/config.js | 15 + .../src/layout/containers/layout/index.jsx | 2 +- .../src/sections/analysis/actions/network.js | 57 ++ .../analysis/components/export-data.jsx | 582 ++++++++++++++++++ .../sections/analysis/containers/network.jsx | 3 +- .../analysis/containers/tableShow.jsx | 378 ++++++++++-- .../projectGroup/containers/bigscreen.jsx | 4 +- web/client/src/utils/webapi.js | 6 +- web/package.json | 3 +- 11 files changed, 1213 insertions(+), 146 deletions(-) create mode 100644 web/client/src/sections/analysis/components/export-data.jsx diff --git a/api/app/lib/controllers/analysis/network.js b/api/app/lib/controllers/analysis/network.js index e726941..db4a5a6 100644 --- a/api/app/lib/controllers/analysis/network.js +++ b/api/app/lib/controllers/analysis/network.js @@ -1,5 +1,8 @@ 'use strict'; const moment = require('moment') +const RAW_DATA = 'rawData'; +const VBRAW_DATA = 'vbRawData'; +const ALARM = 'alarm'; async function getMaintenceRecordRank(ctx) { const sequelize = ctx.fs.dc.orm @@ -313,7 +316,7 @@ async function getOrganizationsStruc(ctx) { } } -async function getThingsDeploy (ctx) { +async function getThingsDeploy(ctx) { let error = { name: 'FindError', message: '获取设备部署信息失败' }; let rslt = null, errStatus = null; let { thingId } = ctx.params; @@ -323,7 +326,7 @@ async function getThingsDeploy (ctx) { throw '缺少参数 thingId' } let iotaResponse = await ctx.app.fs.iotRequest.get(`things/${thingId}/deploys`) - rslt = iotaResponse + rslt = JSON.parse(iotaResponse) error = null; } catch (err) { @@ -347,115 +350,238 @@ async function getThingsDeploy (ctx) { } + +async function findDeviceMetaDeployed(ctx, next) { + let rslt = null; + const { iotaThingId } = ctx.params; + try { + let iotaResponse = await ctx.app.fs.iotRequest.get(`meta/things/${iotaThingId}/devices`) + rslt = JSON.parse(iotaResponse) + ctx.status = 200; + ctx.body = rslt; + } catch (err) { + ctx.status = 400; + ctx.body = { + "name": "FindError", + "message": "设备部署原型获取失败" + } + } + +}; + async function findDeviceLastData(ctx, deviceIds) { let rslt = []; const clientRaws = ctx.app.fs.esclient[RAW_DATA]; const clientVbraws = ctx.app.fs.esclient[VBRAW_DATA]; if (deviceIds) { - for (let id of deviceIds) { - let params = { - index: clientRaws.config.index, - type: clientRaws.config.type, - body: { - query: { - constant_score: { - filter: { - bool: { - must: [ - { term: { "iota_device": id } }, - { range: { "collect_time": { lte: moment().toISOString() } } } - ] - } - } - } - }, - sort: [ - { - "collect_time": { - order: "desc" - } - } - ], - size: 1 - } - }; - let res = await clientRaws.search(params); - if (res.hits.hits.length == 0) { - params.index = clientVbraws.config.index; - params.type = clientVbraws.config.type; - res = await clientVbraws.search(params); - } - - let data = res.hits.hits.map(h => { - let source = h._source; - let data_ = source.data; - if (params.index == clientVbraws.config.index) { - let tempData = { "最大幅值": '_' }; - if (data_.raw && data_.raw.length) { - let maxAmplitude = data_.raw[0]; - for (let v of data_.raw) { - if (maxAmplitude < v) { - maxAmplitude = v; - } - } - tempData['最大幅值'] = maxAmplitude + '(gal)'; - } - data_ = tempData; - } - return { - collectTime: source.collect_time, - iotaDevice: source.iota_device, - iotaDeviceName: source.iota_device_name, - data: data_ + for (let id of deviceIds) { + let params = { + index: clientRaws.config.index, + type: clientRaws.config.type, + body: { + query: { + constant_score: { + filter: { + bool: { + must: [ + { term: { "iota_device": id } }, + { range: { "collect_time": { lte: moment().toISOString() } } } + ] + } + } + } + }, + sort: [ + { + "collect_time": { + order: "desc" + } + } + ], + size: 1 + } + }; + let res = await clientRaws.search(params); + if (res.hits.hits.length == 0) { + params.index = clientVbraws.config.index; + params.type = clientVbraws.config.type; + res = await clientVbraws.search(params); + } + + let data = res.hits.hits.map(h => { + let source = h._source; + let data_ = source.data; + if (params.index == clientVbraws.config.index) { + let tempData = { "最大幅值": '_' }; + if (data_.raw && data_.raw.length) { + let maxAmplitude = data_.raw[0]; + for (let v of data_.raw) { + if (maxAmplitude < v) { + maxAmplitude = v; + } + } + tempData['最大幅值'] = maxAmplitude + '(gal)'; } - }); - rslt.push({ - sensorId: id, - data: data - }); - } + data_ = tempData; + } + return { + collectTime: source.collect_time, + iotaDevice: source.iota_device, + iotaDeviceName: source.iota_device_name, + data: data_ + } + }); + rslt.push({ + sensorId: id, + data: data + }); + } } return rslt; } -async function getDeviceLists(ctx) { + +async function findSensorLastData(ctx) { try { const sensorIds = ctx.request.body.sensorIds; - let rslt = await findDeviceLastData(ctx, sensorIds); + let rslt = await findDeviceLastData(ctx, sensorIds); + // let rslt = [{ sensorId: "2aa1cad1-a52d-4132-8d84-2475034d5bc8", data: [] }, + // { sensorId: "9f1702ff-560d-484e-8572-ef188ef73916", data: [] }, + // { + // sensorId: "360d9098-f2a5-4e1a-ab2b-0bcd3ddcef87", data: [{ + // collectTime: "2021-07-12T05:30:44.000Z", data: { readingNumber: 228 }, + // iotaDevice: "360d9098-f2a5-4e1a-ab2b-0bcd3ddcef87", iotaDeviceName: "水表" + // }] + // }, + // { sensorId: "8c3a636b-9b62-4486-bf54-4ac835aee094", data: [] }, + // { + // sensorId: "9ea4d6cd-f0dc-4604-bb85-112f2591d644", data: [{ + // collectTime: "2021-07-17T05:53:35.783Z", data: { DI4: 0, DI7: 0, DI5: 0, DI1: 1, DI6: 0, DI2: 1, DI8: 0, DI3: 1 }, + // iotaDevice: "9ea4d6cd-f0dc-4604-bb85-112f2591d644", iotaDeviceName: "控制器" + // }] + // }, + // { sensorId: "e18060b4-3c7f-4fad-8a1a-202b5c0bf00c", data: [] } + // ] + ctx.status = 200; + ctx.body = rslt; + } catch (error) { + ctx.status = 400; + ctx.body = { + "name": "FindError", + "message": "原始数据查询失败" + } + } +} + + +async function findAlarmsDevices(ctx, next) { + let rslt = [] + const deviceIds = ctx.request.body + const { limit, state } = ctx.query + try { + if (deviceIds.length) { + for (let deviceId of deviceIds) { + // es search + const client = ctx.app.fs.esclient[ALARM];//准备查询 + let params = { + index: client.config.index, + type: client.config.type, + size: limit || 9999, + body: { + "query": { + "bool": { + "must": [ + { + "match": { + "source_id": deviceId + } + }, + { + "terms": { + "state": [] + } + } + ] + } + }, + "sort": [ + { + "start_time": { + "order": "desc" + } + } + ] + } + } + if (state == 'new') { + let newState = [AlarmState.Creation, AlarmState.CountUpgrade, AlarmState.LevelUpgrade]; + params.body.query.bool.must[1].terms.state = newState; + } + let alarms = await client.search(params); + const timer = ctx.app.fs.timer; + function filterAlarmMsg(oriMsg) { + let msg = []; + for (let s of oriMsg) { + msg.push({ + alarmContent: s._source.alarm_content, + alarmCount: s._source.alarm_count, + deviceId: s._source.source_id, + startTime: timer.toCSTString(s._source.start_time), + endTime: timer.toCSTString(s._source.end_time), + }) + } + return msg; + } + rslt = rslt.concat(filterAlarmMsg(alarms.hits.hits)); + } + } + ctx.status = 200; ctx.body = rslt; - } catch (error) { + } catch (error) { + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); ctx.status = 400; ctx.body = { - "name": "FindError", - "message": "原始数据查询失败" + message: '告警信息查询失败' + } + } +} + +async function findDevicesCardStatus(ctx, next) { + + try { + let rlst = [] + const { clickHouse } = ctx.app.fs + const { deviceIds } = ctx.request.body + if (deviceIds && deviceIds.length) { + const id = `(${deviceIds.map(id => `'${id}'`).join(',')})` + rlst = await clickHouse.dataAlarm.query(` + SELECT cs.DeviceId,cs.Status,MAX(cs.Time) + FROM alarm.CardStatus cs + WHERE cs.DeviceId in ${id} + GROUP BY cs.DeviceId,cs.Status`).toPromise() } - } + ctx.status = 200; + ctx.body = rlst; + } catch (error) { + ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); + ctx.status = 400; + ctx.body = { + message: '物联网卡状态查询失败' + } + } } -// try { -// const { clickHouse } = ctx.app.fs -// const { thingIds } = ctx.params -// // const res = thingIds ? await clickHouse.iot.query( -// // `SELECT distinct dv.name,dv.thingId,dv.id,dv.deviceMetaId,re.properties -// // FROM iota.Device dv -// // LEFT JOIN iota.FilteredResource re -// // ON dv.deviceMetaId=re.key -// // WHERE thing_id in (${thingIds})`).toPromise() : [] -// } catch { -// ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`); -// ctx.status = 400; -// ctx.body = { -// message: '查询设备列表失败' -// } -// } module.exports = { getOrganizationsStruc, getThingsDeploy, - getDeviceLists + findSensorLastData, + findDeviceMetaDeployed, + findAlarmsDevices, + findDevicesCardStatus } \ No newline at end of file diff --git a/api/app/lib/routes/analysis/network.js b/api/app/lib/routes/analysis/network.js index 94a55ef..c2035df 100644 --- a/api/app/lib/routes/analysis/network.js +++ b/api/app/lib/routes/analysis/network.js @@ -8,10 +8,17 @@ module.exports = function (app, router, opts) { app.fs.api.logAttr['GET/things/:thingId/deploy'] = { content: '获取设备部署信息', visible: true }; router.get('/things/:thingId/deploy', network.getThingsDeploy) - + app.fs.api.logAttr['GET/meta/things/:iotaThingId/devices'] = { content: '原始数据查询失败', visible: false }; + router.get('/meta/things/:iotaThingId/devices', network.findDeviceMetaDeployed) + app.fs.api.logAttr['POST/sensors/last/data'] = { content: '原始数据查询失败', visible: true }; - router.post('/sensors/last/data', network.getDeviceLists) + router.post('/sensors/last/data', network.findSensorLastData) + + app.fs.api.logAttr['POST/devices/alarms'] = { content: '获取多个设备的告警信息', visible: false }; + router.post('/devices/alarms', network.findAlarmsDevices) + app.fs.api.logAttr['POST/devices/cardStatus'] = { content: '获取物联网卡状态', visible: false }; + router.post('/devices/cardStatus', network.findDevicesCardStatus); // app.fs.api.logAttr['GET/systemAvailability'] = { content: '获取系统可用性', visible: true }; // router.get('/systemAvailability', operationData.getSystemAvailability) diff --git a/api/config.js b/api/config.js index fe04db6..f582137 100644 --- a/api/config.js +++ b/api/config.js @@ -361,6 +361,21 @@ const product = { index: `${ES_CONTINUITY_NAME}_continue`, type: flags.esType ? flags.esType : '_doc' }, + rawData: { + rootURL: ANXINCLOUD_ES_NODES_REST.split(','), + index: `${PLATFORM_NAME}_raws`, + type: flags.esType ? flags.esType : '_doc' + }, + vbRawData: { + rootURL: ANXINCLOUD_ES_NODES_REST.split(','), + index: `${PLATFORM_NAME}_vbraws`, + type: flags.esType ? flags.esType : '_doc' + }, + alarm: { + rootURL: ANXINCLOUD_ES_NODES_REST.split(','), + index: `${PLATFORM_NAME}_alarms`, + type: flags.esType ? flags.esType : '_doc' + }, } } } diff --git a/web/client/src/layout/containers/layout/index.jsx b/web/client/src/layout/containers/layout/index.jsx index 58e7a78..2a772a6 100644 --- a/web/client/src/layout/containers/layout/index.jsx +++ b/web/client/src/layout/containers/layout/index.jsx @@ -114,7 +114,7 @@ const LayoutContainer = props => { } setAllItems(nextItems) setHeaderItems(topItems) - console.log('topItems',topItems) + // console.log('topItems',topItems) if (lastSelectedKeys) {//如果有缓存 for (let i = 0; i < nextItems.length; i++) { if (JSON.parse(lastSelectedKeys)[0] == nextItems[i].itemKey) { diff --git a/web/client/src/sections/analysis/actions/network.js b/web/client/src/sections/analysis/actions/network.js index 0e4e47b..f049135 100644 --- a/web/client/src/sections/analysis/actions/network.js +++ b/web/client/src/sections/analysis/actions/network.js @@ -27,6 +27,63 @@ export function getThingsDeploy (id) { } +export function findDeviceMetaDeployed (id) { + return dispatch => basicAction({ + type: 'get', + dispatch: dispatch, + actionType: 'FIND_DEVICE_META_DEPLOYED', + url: `${ApiTable.findDeviceMetaDeployed.replace('{iotaThingId}', id)}`, + msg: { error: '获取单个结构物的设备信息失败' }, + reducer: { name: 'deviceMetaDeployed',params: { noClear: true } }, + + }) +} + +export function findSensorLastData (data) { + return dispatch => basicAction({ + type: 'post', + data, + dispatch: dispatch, + actionType: 'FIND_SENSOR_LAST_DATA', + url: `${ApiTable.findSensorLastData}`, + msg: { error: '原始数据查询失败' }, + reducer: { name: 'sensorLastData' } + }) +} + +export function getDevicesAlarms(deviceIds,query) { + return dispatch => basicAction({ + type: 'post', + dispatch: dispatch, + data: deviceIds, + query: query, + actionType: 'REQUEST_POST_DEVICES_ALARMS', + url: `${ApiTable.getDevicesAlarms}?state=new`, + msg: { + error: '获取设备告警信息失败' + }, + reducer: { + name: 'deviceListAlarms' + } + }); +} + + +export function findDevicesCardStatus(deviceIds) { + return dispatch => basicAction({ + type: 'post', + dispatch: dispatch, + data: deviceIds, + actionType: 'FIND_DEVICES_CARD_STATUS', + url: `${ApiTable.findDevicesCardStatus}`, + msg: { + error: '获取物联网卡信息失败' + }, + reducer: { + name: 'devicesCardStatus' + } + }); +} // export function getSystemAvailability() { diff --git a/web/client/src/sections/analysis/components/export-data.jsx b/web/client/src/sections/analysis/components/export-data.jsx new file mode 100644 index 0000000..f610676 --- /dev/null +++ b/web/client/src/sections/analysis/components/export-data.jsx @@ -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 = ''; + //header += `
序号
`; + newColumns.map(colum => { + header += `
${colum.title}
` + }); + header += ''; + loopData.map(data => { + content += ``; + newColumns.map(c => { + if (c.style) { + content += `
${data[c.key] || ''}
` + } else { + content += `
${data[c.key] || ''}
` + } + }); + content += ``; + }) + + let exportTable = `\uFEFF + + ${header} + ${content} +
+ `; + 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 ? + + {customRender} + : + + + ) +} + +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; diff --git a/web/client/src/sections/analysis/containers/network.jsx b/web/client/src/sections/analysis/containers/network.jsx index aae0771..b4d8370 100644 --- a/web/client/src/sections/analysis/containers/network.jsx +++ b/web/client/src/sections/analysis/containers/network.jsx @@ -155,7 +155,8 @@ const Network = ({ } > {show == 'tree' && } - {show == 'table' && } + {show == 'table' && +}
diff --git a/web/client/src/sections/analysis/containers/tableShow.jsx b/web/client/src/sections/analysis/containers/tableShow.jsx index 437dd02..d4ee3e0 100644 --- a/web/client/src/sections/analysis/containers/tableShow.jsx +++ b/web/client/src/sections/analysis/containers/tableShow.jsx @@ -1,66 +1,340 @@ -import React, { useEffect, useState, useRef } from 'react'; -import { connect } from 'react-redux'; -import ReactECharts from 'echarts-for-react'; -import echarts from 'echarts'; -import { Spin, Card, CardGroup, Form, Button,Table } from '@douyinfe/semi-ui'; +import React, { useEffect, useState, useRef, useMemo } from 'react' +import { connect } from 'react-redux' +import { Spin, Card, CardGroup, Form, Button, Table, Pagination, Tooltip } from '@douyinfe/semi-ui' +import ExportData from '../components/export-data' +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) => { - const { dispatch, actions, user, clientHeight } = props + useEffect(() => { + 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]) - }, []) - const columns = [ - { - title: '设备名称', - dataIndex: 'deviceName', - width: 200, - key:'deviceName' - }, - { - title: '设备类型', - dataIndex: 'deviceType', - width:200, - key:'deviceType' - }, - { - title: '最后采集时间', - dataIndex: 'collectTime', - width:200, - key:'collectTime' - + useEffect(async () => { + if (sensorId && sensorId.length && sensorsDataItems) { + const rs = await dispatch(analysis.findSensorLastData(sensorId)) + const tableData = [] + if (rs.success) { + rs.payload.data.forEach(sd => { + if (Object.keys(sensorsDataItems).length) { + let sensorDataItem = sensorsDataItems[sd.sensorId] + let sensorName = sensorDataItem && sensorDataItem.deviceName ? sensorDataItem.deviceName : '' + let msg = sd.data.length + ? sd.data[0] + : { + collectTime: null, + sensorName: sensorName, + data: { noData: '暂无数据' }, + } + let dataStr = '' + let dataKeys = Object.keys(msg.data) + 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 ( + <> + +
{r.sensorName.length > 7 ? `${r.sensorName.substr(0, 7)}...` : r.sensorName}
+
+ + ) }, - { - title: '更新日期', - dataIndex: 'updateTime', - sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1), - render: value => { - return dateFns.format(new Date(value), 'yyyy-MM-dd'); - }, + }, + { + title: '设备类型', + dataIndex: 'deviceType', + width: 200, + key: 'deviceType', + }, + { + title: '最后采集时间', + dataIndex: 'collectTime', + width: 200, + key: 'collectTime', + }, + { + title: '数据', + dataIndex: 'data', + width: 200, + key: 'data', + render: (_, r) => { + return ( + <> + +
{r.data.length > 6 ? `${r.data.substr(0, 6)}...` : r.data}
+
+ + ) }, - ]; - return ( - + }, + { + title: '物联网卡状态', + width: 200, + dataIndex: 'iotCardStatus', + key: 'iotCardStatus', + }, + { + title: '状态', + width: 200, + dataIndex: 'status', + key: 'status', + }, + { + title: '操作', + width: 200, + dataIndex: 'option', + key: 'option', + }, + ] + return ( + <> +
+
+
+ } + field='name' + pure + showClear + label='名称' + style={{ width: 260, marginRight: 12 }} + placeholder='请输入设备名称' + onChange={inputChange} + /> + + + +
+
+ {' '} + {lastData.length ? ( + + ) : ( + '' + )} +
+
- - ) + // scroll={scroll} + columns={columns} + dataSource={lastData}> + + ) } -function mapStateToProps (state) { - const { auth, global, members, webSocket } = state; - return { - user: auth.user, - actions: global.actions, - clientHeight: global.clientHeight - }; +function mapStateToProps(state) { + const { auth, global, members, webSocket, deviceListAlarms, devicesCardStatus } = state + return { + user: auth.user, + actions: global.actions, + clientHeight: global.clientHeight, + deviceListAlarms: deviceListAlarms?.data || [], + devicesCardStatusList: devicesCardStatus?.data || [], + } } -export default connect(mapStateToProps)(Network); +export default connect(mapStateToProps)(Network) diff --git a/web/client/src/sections/projectGroup/containers/bigscreen.jsx b/web/client/src/sections/projectGroup/containers/bigscreen.jsx index 65e6138..212bb7a 100644 --- a/web/client/src/sections/projectGroup/containers/bigscreen.jsx +++ b/web/client/src/sections/projectGroup/containers/bigscreen.jsx @@ -159,7 +159,7 @@ const Bigscreen = (props) => { }) dispatch(actions.projectGroup.getWorkOrdersRepairRank({ projectIds: query })).then(res => { if (res.success) { - setGroupProject(res.payload.data?.slice(0, 10).map(v => ({ name: v.formname, startTime: moment(v?.startTime).format('YYYY-MM-DD'), duration: moment(v?.endTime).add(8, 'hours').diff(v?.startTime, 'hours') })) || []) + setGroupProject(res.payload.data?.slice(0, 10).map(v => ({ name: v.formname, startTime: moment(v?.startTime).format('YYYY-MM-DD'), duration: Math.round(moment(v?.endTime).add(8, 'hours').diff(v?.startTime, 'hours',true)) })) || []) } }) //修复平均时长 @@ -167,7 +167,7 @@ const Bigscreen = (props) => { if (res.success) { const data=res.payload.data?.map(v=>{ return {projectName:allProjects?.find(item => item.value == v.project_id)?.label, - avgTime: Math.ceil(v.avgTime / 60 / 60), + avgTime: Math.round(v.avgTime / 60 / 60), project_id:v.project_id } }) diff --git a/web/client/src/utils/webapi.js b/web/client/src/utils/webapi.js index 576abf6..5f3ab1d 100644 --- a/web/client/src/utils/webapi.js +++ b/web/client/src/utils/webapi.js @@ -154,8 +154,12 @@ export const ApiTable = { //分析-一图统揽 organizationsStruc: 'organizations/{pepProjectId}/struc', //获取项目下的结构物信息 thingsDeploy: 'things/{thingId}/deploy',//获取设备部署信息-组网数据 - + findSensorLastData:'sensors/last/data',//原始数据查询 + findDeviceMetaDeployed:'meta/things/{iotaThingId}/devices',//按找thingId查询数据 + getDevicesAlarms: 'devices/alarms',//告警数据 + findDevicesCardStatus:'devices/cardStatus',//查询物联网卡状态 respondRecord: 'respond-record', + //待办工单 workOrders: 'unfinished', //获取设备型号 diff --git a/web/package.json b/web/package.json index ba23dff..d1bb201 100644 --- a/web/package.json +++ b/web/package.json @@ -86,6 +86,7 @@ "webpack-cli": "^4.2.0", "webpack-dev-middleware": "^4.0.2", "webpack-dev-server": "^3.11.2", - "webpack-hot-middleware": "^2.25.0" + "webpack-hot-middleware": "^2.25.0", + "xlsx": "^0.18.5" } }