Browse Source

运维大屏

dev
wenlele 1 year ago
parent
commit
a7dcaeef28
  1. 51
      api/app/lib/controllers/project/group.js
  2. 3
      api/app/lib/routes/project/index.js
  3. BIN
      web/client/assets/images/projectGroup/chart.png
  4. 16
      web/client/src/sections/projectGroup/actions/group.js
  5. 181
      web/client/src/sections/projectGroup/containers/bigscreen.jsx
  6. 1
      web/client/src/utils/webapi.js

51
api/app/lib/controllers/project/group.js

@ -409,6 +409,56 @@ async function groupStatisticAlarm (ctx) {
}
}
async function groupProject (ctx) {
try {
const { models } = ctx.fs.dc;
const { groupId } = ctx.query
const { clickHouse } = ctx.app.fs
const findOne = await models.ProjectGroup.findOne({ where: { id: groupId } })
const proRes = await models.ProjectCorrelation.findAll({ where: { id: { $in: findOne.pomsProjectIds } } })
let pepProjectIds = new Set()
for (let p of proRes) {
if (p.pepProjectId) {
pepProjectIds.add(p.pepProjectId)
}
}
const pepProjectRes = pepProjectIds.size ?
await clickHouse.projectManage.query(
`
SELECT
t_pim_project.id AS id,
t_pim_project.project_name AS project_name,
t_pim_project.isdelete AS isdelete,
t_pim_project_construction.construction_status_id AS construction_status_id,
t_pim_project_state.construction_status AS construction_status
FROM t_pim_project
LEFT JOIN t_pim_project_construction
ON t_pim_project.id = t_pim_project_construction.project_id
LEFT JOIN t_pim_project_state
ON t_pim_project_construction.construction_status_id = t_pim_project_state.id
WHERE id IN (${[...pepProjectIds].join(',')}, -1)
`
).toPromise() :
[]
for (let p of proRes) {
const corPro = pepProjectRes.find(pp => pp.id == p.pepProjectId) || {}
p.dataValues.pepProjectName = corPro.project_name
}
ctx.status = 200;
ctx.body = proRes;
} catch (error) {
ctx.fs.logger.error(`path: ${ctx.path}, error: ${error}`);
ctx.status = 400;
ctx.body = {
message: typeof error == 'string' ? error : undefined
}
}
}
module.exports = {
groupList,
editGroup,
@ -416,4 +466,5 @@ module.exports = {
groupStatistic,
groupStatisticOnline,
groupStatisticAlarm,
groupProject,
};

3
api/app/lib/routes/project/index.js

@ -48,4 +48,7 @@ module.exports = function (app, router, opts) {
app.fs.api.logAttr['GET/project/group/statistic/alarm'] = { content: '获取项目分组告警统计信息', visible: true };
router.get('/project/group/statistic/alarm', projectGroup.groupStatisticAlarm);
app.fs.api.logAttr['GET/project/group/list'] = { content: '获取分组项目信息', visible: true };
router.get('/project/group/list', projectGroup.groupProject);
};

BIN
web/client/assets/images/projectGroup/chart.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

16
web/client/src/sections/projectGroup/actions/group.js

@ -65,8 +65,22 @@ export function groupStatisticAlarm (query = {}) {
query,
actionType: "GET_STATISTICALARM",
url: `${ApiTable.groupStatisticAlarm}`,
msg: { error: "获取项目分组告警统计信息失败" },
msg: { error: "获取项目分组告警统计信息失败" },
reducer: { name: "groupStatisticAlarm",
params: { noClear: true } },
});
}
export function groupProject (query = {}) {
return (dispatch) => basicAction({
type: "get",
dispatch: dispatch,
query,
actionType: "GET_GROUP_PROJECT",
url: `${ApiTable.groupProject}`,
msg: { error: "获取分组项目信息失败" },
reducer: { name: "groupProject",
params: { noClear: true } },
});
}

181
web/client/src/sections/projectGroup/containers/bigscreen.jsx

@ -11,6 +11,7 @@ import PerfectScrollbar from "perfect-scrollbar";
let interrupt
let repair
let overviewScrollbar;
const Bigscreen = ({ dispatch, actions, user, match, history, clientHeight, groupStatisticOnline }) => {
@ -18,13 +19,17 @@ const Bigscreen = ({ dispatch, actions, user, match, history, clientHeight, grou
const [online, setOnline] = useState([])
const [value, setValue] = useState([])
const [time, setTime] = useState([])
const [groupProject, setGroupProject] = useState([])
const [proportion, setProportion] = useState([])
const [formatter, setFormatter] = useState({})
const [alarmData, setAlarmData] = useState()//
const [biggest, setBiggest] = useState()//
const [mockData, setMockData] = useState()//
const [xData, setXData] = useState([])//
const self = useRef({ myChart: null });
useEffect(() => {
@ -32,7 +37,15 @@ const Bigscreen = ({ dispatch, actions, user, match, history, clientHeight, grou
statisticOnline(groupId)
dispatch(actions.projectGroup.groupStatisticAlarm({ groupId })).then(res => {
if (res.success) {
setMockData(res.data)
setMockData(res.payload.data)
}
})
dispatch(actions.projectGroup.groupProject({ groupId })).then(res => {
if (res.success) {
setGroupProject(res.payload.data?.map(v => ({ ...v, value: (Math.random() * 20).toFixed(0) })) || [])
setProportion([...res.payload.data?.slice(0, 3)?.map(v => ({ name: v.name || v.pepProjectName, value: (Math.random() * 20).toFixed(0) })), { value: 20, name: '其它' }])
}
})
@ -43,6 +56,12 @@ const Bigscreen = ({ dispatch, actions, user, match, history, clientHeight, grou
suppressScrollX: true,
});
}
const repairDom = document.getElementById("repair");
if (repairDom) {
repair = new PerfectScrollbar("#repair", {
suppressScrollX: true,
});
}
}, [])
@ -63,6 +82,10 @@ const Bigscreen = ({ dispatch, actions, user, match, history, clientHeight, grou
interrupt.update();
}
const repairDom = document.getElementById("repair");
if (repair && repairDom) {
interrupt.update();
}
})
useEffect(() => {
const maxCombinedValue = mockData?.reduce((max, item) => {
@ -119,7 +142,6 @@ const Bigscreen = ({ dispatch, actions, user, match, history, clientHeight, grou
let statisticOnline = (groupId) => {
dispatch(actions.projectGroup.groupStatisticOnline({ groupId })).then(res => {
console.log(res);
if (res.success) {
let Interrupt = []
res.payload.data?.forEach(v => {
@ -182,6 +204,43 @@ const Bigscreen = ({ dispatch, actions, user, match, history, clientHeight, grou
}
useEffect(() => {
let count = 0;
let currentIndex = -1;
if (!self.current.cityChart) return;
const timer = setInterval(() => {
count++;
if (count == 8) {
count = 1;
}
//
self.current.cityChart.dispatchAction({
type: "downplay",
seriesIndex: 0,
dataIndex: currentIndex,
});
currentIndex =
(currentIndex + 1) % proportion?.length
//
self.current.cityChart.dispatchAction({
type: "highlight",
seriesIndex: 0,
dataIndex: currentIndex,
});
// label
self.current.cityChart.dispatchAction({
type: "showTip",
seriesIndex: 0,
dataIndex: currentIndex,
});
}, 3000);
return () => {
clearInterval(timer);
};
}, [proportion]);
return (
<div className='project-group'>
@ -191,41 +250,101 @@ const Bigscreen = ({ dispatch, actions, user, match, history, clientHeight, grou
<div style={{ width: '100%', height: "45%", display: 'flex' }}>
<div style={{ width: "calc(50% - 8px)", height: "100%", marginRight: 16, display: 'flex' }}>
<Card title='项目工单占比' style={{ width: "calc(50% - 8px)", height: "100%", marginRight: 16 }}>
<div style={{ height: '100%' }}>
<div style={{ height: '100%', position: 'relative' }}>
<div style={{ height: clientHeight * 0.55 - 300, display: 'flex', justifyContent: "center", position: 'relative' }}>
<ReactECharts
option={{
tooltip: {
show: true,
trigger: "item",
position: "right",
backgroundColor: "rgba(0,0,0,0.7)",
textStyle: {
color: "#fff",
},
formatter: (values) => {
setFormatter(values?.data)
}
// `${values.seriesName}<br /> ${values.marker} ${values.name} <b>${values.value}</b>(${values.percent}%)`,
},
series: [
{
// name: 'Access From',
type: 'pie',
radius: ['60%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
// emphasis: {
// label: {
// show: true,
// fontSize: 44,
// fontWeight: 'bold'
// }
// },
labelLine: {
show: false
},
data: proportion || [],
}
]
}}
notMerge
onChartReady={(instance) => {
self.current.cityChart = instance;
}}
lazyUpdate
style={{ width: clientHeight * 0.55 - 300, height: clientHeight * 0.55 - 300 }}
/>
<img src='/assets/images/projectGroup/chart.png' style={{ height: clientHeight * 0.55 - 330, position: 'absolute', top: 16 }} />
<div style={{ position: 'absolute', top: 58, left: "auto", width: 100, textAlign: "center" }}>
<div style={{ fontFamily: 'DIN-Regular', fontWeight: 400, fontSize: 28, color: '#2F2F2F' }}>{formatter?.value}</div>
<div style={{
width: 100, fontFamily: 'SourceHanSansCN-Regular', fontWeight: 400,
fontSize: 14, color: '#2C66F3', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis'
}}>{formatter?.name}</div>
</div>
</div>
<div style={{ width: "100%", fontFamily: 'SourceHanSansCN-Regular', fontWeight: 400, fontSize: 14, position: 'absolute', bottom: 10 }}>
{proportion?.map((v, index) => {
let color = ['rgb(53 100 209)', 'rgb(138 201 15)', 'rgb(239 204 77)', 'rgb(233 107 107)']
return <div style={{ width: "50%", display: 'inline-block' }}>
<div style={{ display: "flex", justifyContent: "space-between", padding: '0 6px' }}>
<div title={v.name} style={{ width: 'calc(100% - 40px)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
<span style={{ background: color[index], display: 'inline-block', marginRight: 6, width: 6, height: 6 }}></span>{v.name}</div>
<div style={{ width: 34 }}>{v.value}</div>
</div>
</div>
})}
</div>
</div>
</Card>
<Card title='修复排名' style={{ width: "calc(50% - 8px)", height: "100%" }}>
<div style={{ height: '100%', fontFamily: 'SourceHanSansCN-Regular', fontWeight: 400, fontSize: 14, }}>
<div style={{ display: "flex", background: '#F6F9FF', height: 40, alignItems: 'center' }}>
<div style={{ textAlign: 'center', width: '33%' }}>序号</div>
<div style={{ textAlign: 'center', width: '33%' }}>工单名称</div>
<div style={{ textAlign: 'center', width: '33%' }}>修复时长</div>
</div>
<div id="interrupt" style={{ position: 'relative', height: clientHeight * 0.55 - 170 }}>
{[{ index: 1, name: 'wweq', time: 20 },
{ index: 2, name: 'wweq', time: 20 },
{ index: 3, name: 'wweq', time: 20 },
{ index: 4, name: 'wweq', time: 20 },
{ index: 5, name: 'wweq', time: 20 },
{ index: 6, name: 'wweq', time: 20 },
{ index: 7, name: 'wweq', time: 20 },
{ index: 1, name: 'wweq', time: 20 },
{ index: 1, name: 'wweq', time: 20 },
{ index: 1, name: 'wweq', time: 20 }].map((c, index) => {
let title
if (c.offline) {
if (c.offline >= 1440) title = Math.floor(c.offline / 1440) + "天"
if ((c.offline % 1440) >= 60) title = title + Math.floor(c.offline % 1440 / 60) + "时"
if (c.offline % 1440 % 60) title = title + c.offline % 1440 % 60 + "分"
}
return <div style={{ display: "flex", background: index % 2 == 1 ? "#F6F9FF" : '', height: 40, alignItems: 'center' }}>
<div style={{ textAlign: 'center', width: '33%', fontFamily: 'SourceHanSansCN-Regular', color: '#2C66F3', fontWeight: 400 }}>{c.name}</div>
<div style={{ textAlign: 'center', width: '33%' }}>{title}</div>
<div style={{ textAlign: 'center', width: '33%', fontFamily: 'SourceHanSansCN-Regular', color: '#F33B3B', fontWeight: 400 }}>{c.offnum + '/' + c.totnum}</div>
</div>
<div style={{ textAlign: 'center', width: '25%' }}>序号</div>
<div style={{ textAlign: 'center', width: '49%' }}>工单名称</div>
<div style={{ textAlign: 'center', width: '25%' }}>修复时长</div>
</div>
<div id="repair" style={{ position: 'relative', height: clientHeight * 0.55 - 220 }}>
{groupProject?.map((c, index) => {
return index < 10 ? <div style={{ display: "flex", background: index % 2 == 1 ? "#F6F9FF" : '', padding: 6, height: 50, alignItems: 'center' }}>
<div style={{ textAlign: 'center', width: '25%', fontFamily: 'SourceHanSansCN-Regular', color: '#2C66F3', fontWeight: 400 }}>
NO.{index + 1}</div>
<div title={c.name || c.pepProjectName + "(售后工单)"} style={{ textAlign: 'center', padding: '0 6px', width: '49%', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{c.name || c.pepProjectName}售后工单</div>
<div style={{ textAlign: 'center', width: '25%', fontFamily: 'SourceHanSansCN-Regular', fontWeight: 400 }}>
{c.value}h</div>
</div> : <></>
})}
</div>
</div>

1
web/client/src/utils/webapi.js

@ -41,6 +41,7 @@ export const ApiTable = {
groupStatistic: 'project/group/statistic',
groupStatisticOnline: 'project/group/statistic/online',
groupStatisticAlarm: 'project/group/statistic/alarm',
groupProject: "project/group/list",
//告警
getProjectPoms: 'project/poms', //获取已绑定项目

Loading…
Cancel
Save