|
|
@@ -1,5 +1,10 @@
|
|
|
<script lang="ts" setup>
|
|
|
import * as echarts from 'echarts'
|
|
|
+import { kanbanApi } from '@/api/pms/qhse/index'
|
|
|
+import { IotDangerApi } from '@/api/pms/qhse/index'
|
|
|
+import { useUserStore } from '@/store/modules/user'
|
|
|
+
|
|
|
+const userStore = useUserStore()
|
|
|
import {
|
|
|
AlarmClock,
|
|
|
Checked,
|
|
|
@@ -27,21 +32,6 @@ defineOptions({
|
|
|
name: 'PmsQhseKanban'
|
|
|
})
|
|
|
|
|
|
-type SummaryCard = {
|
|
|
- title: string
|
|
|
- value: string
|
|
|
- note: string
|
|
|
- accent: string
|
|
|
- glow: string
|
|
|
- icon: any
|
|
|
-}
|
|
|
-
|
|
|
-type MetricBar = {
|
|
|
- label: string
|
|
|
- value: number
|
|
|
- color: string
|
|
|
-}
|
|
|
-
|
|
|
type RiskZone = {
|
|
|
title: string
|
|
|
desc: string
|
|
|
@@ -62,8 +52,11 @@ type BottomCard = {
|
|
|
lines: string[]
|
|
|
}
|
|
|
|
|
|
+type SafeDayMap = Record<string, number>
|
|
|
+
|
|
|
const wrapperRef = ref<HTMLDivElement>()
|
|
|
const hazardChartRef = ref<HTMLDivElement>()
|
|
|
+const safeDayChartRef = ref<HTMLDivElement>()
|
|
|
const socChartRef = ref<HTMLDivElement>()
|
|
|
const scale = ref(1)
|
|
|
const supportsZoom = ref(false)
|
|
|
@@ -71,30 +64,31 @@ const supportsZoom = ref(false)
|
|
|
let resizeObserver: ResizeObserver | null = null
|
|
|
let resizeRaf = 0
|
|
|
let hazardChart: echarts.ECharts | null = null
|
|
|
+let safeDayChart: echarts.ECharts | null = null
|
|
|
let socChart: echarts.ECharts | null = null
|
|
|
|
|
|
const pageTitle = 'QHSE管理看板'
|
|
|
|
|
|
-const summaryCards: SummaryCard[] = [
|
|
|
+const summaryCards = ref([
|
|
|
{
|
|
|
title: '风险总数(处)',
|
|
|
- value: '126',
|
|
|
- note: 'R:12',
|
|
|
+ value: 0,
|
|
|
+ note: '',
|
|
|
accent: '#ff5a62',
|
|
|
glow: 'rgba(255, 90, 98, 0.26)',
|
|
|
icon: Warning
|
|
|
},
|
|
|
{
|
|
|
title: '本月隐患(条)',
|
|
|
- value: '29',
|
|
|
- note: 'Exp:2',
|
|
|
+ value: 0,
|
|
|
+ note: '',
|
|
|
accent: '#ff9f2f',
|
|
|
glow: 'rgba(255, 159, 47, 0.24)',
|
|
|
icon: Opportunity
|
|
|
},
|
|
|
{
|
|
|
title: '隐患整改率',
|
|
|
- value: '92.3%',
|
|
|
+ value: '0%',
|
|
|
note: '',
|
|
|
accent: '#2ac7c9',
|
|
|
glow: 'rgba(42, 199, 201, 0.26)',
|
|
|
@@ -102,7 +96,7 @@ const summaryCards: SummaryCard[] = [
|
|
|
},
|
|
|
{
|
|
|
title: '本月作业许可',
|
|
|
- value: '11',
|
|
|
+ value: 0,
|
|
|
note: '',
|
|
|
accent: '#4f8dff',
|
|
|
glow: 'rgba(79, 141, 255, 0.22)',
|
|
|
@@ -110,31 +104,26 @@ const summaryCards: SummaryCard[] = [
|
|
|
},
|
|
|
{
|
|
|
title: '人员持证率',
|
|
|
- value: '97.8%',
|
|
|
+ value: 0,
|
|
|
note: 'Warn:3',
|
|
|
accent: '#f2b800',
|
|
|
glow: 'rgba(242, 184, 0, 0.22)',
|
|
|
icon: Postcard
|
|
|
}
|
|
|
-]
|
|
|
+])
|
|
|
|
|
|
-const hazardBars: MetricBar[] = [
|
|
|
- { label: '总数', value: 657, color: '#4f8dff' },
|
|
|
- { label: '已整改', value: 628, color: '#43c7ca' },
|
|
|
- { label: '未整改', value: 29, color: '#ff981f' }
|
|
|
-]
|
|
|
+const hazardBars = ref([
|
|
|
+ { label: '总数', value: 0, color: '#4f8dff' },
|
|
|
+ { label: '已整改', value: 0, color: '#43c7ca' },
|
|
|
+ { label: '未整改', value: 0, color: '#ff981f' }
|
|
|
+])
|
|
|
|
|
|
-const incidentStats = [
|
|
|
- { label: '安全事故', value: '0起', accent: '#2ac7c9' },
|
|
|
- { label: '安全生产天数', value: '3起', accent: '#f2c11a' }
|
|
|
-]
|
|
|
-
|
|
|
-const riskZones: RiskZone[] = [
|
|
|
- { title: '高危风险区', desc: '危化库 / 试压区 / 配电房', color: '#ff4c49' },
|
|
|
- { title: '中风险区', desc: '焊接 / 机加 / 吊装区', color: '#ff981f' },
|
|
|
- { title: '低风险区', desc: '物料库 / 维修 / 装卸区', color: '#f2c11a' },
|
|
|
- { title: '安全控制区', desc: '办公区 / 展厅 / 主通道', color: '#5794ff' }
|
|
|
-]
|
|
|
+const riskZones = ref([
|
|
|
+ { title: '', desc: '办公区 / 展厅 / 主通道', color: '#25b36a', value: 0 },
|
|
|
+ { title: '', desc: '物料库 / 维修 / 装卸区', color: '#3d7cff', value: 0 },
|
|
|
+ { title: '', desc: '焊接 / 机加 / 吊装区', color: '#ff9827', value: 0 },
|
|
|
+ { title: '', desc: '危化库 / 试压区 / 配电房', color: '#ff5b61', value: 0 }
|
|
|
+])
|
|
|
|
|
|
const permitStats: PermitStat[] = [
|
|
|
{ label: '个人防护', value: 18, color: '#4f8dff' },
|
|
|
@@ -222,6 +211,17 @@ function updateScale() {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
+const staticData = ref({})
|
|
|
+async function getStatic() {
|
|
|
+ const res = await IotDangerApi.getDangerStatistics(userStore.user.deptId)
|
|
|
+ staticData.value = res.classify
|
|
|
+
|
|
|
+ riskZones.value.forEach((zone, index: number) => {
|
|
|
+ zone.value = staticData.value[index].count
|
|
|
+ zone.title = `${staticData.value[index].classify}区`
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
onMounted(() => {
|
|
|
supportsZoom.value = typeof CSS !== 'undefined' && CSS.supports?.('zoom', '1') === true
|
|
|
nextTick(updateScale)
|
|
|
@@ -230,9 +230,11 @@ onMounted(() => {
|
|
|
resizeObserver.observe(wrapperRef.value)
|
|
|
}
|
|
|
initHazardChart()
|
|
|
+ initSafeDayChart()
|
|
|
initSocChart()
|
|
|
window.addEventListener('resize', updateScale)
|
|
|
window.addEventListener('resize', resizeHazardChart)
|
|
|
+ window.addEventListener('resize', resizeSafeDayChart)
|
|
|
window.addEventListener('resize', resizeSocChart)
|
|
|
})
|
|
|
|
|
|
@@ -240,9 +242,11 @@ onUnmounted(() => {
|
|
|
resizeObserver?.disconnect()
|
|
|
window.removeEventListener('resize', updateScale)
|
|
|
window.removeEventListener('resize', resizeHazardChart)
|
|
|
+ window.removeEventListener('resize', resizeSafeDayChart)
|
|
|
window.removeEventListener('resize', resizeSocChart)
|
|
|
cancelAnimationFrame(resizeRaf)
|
|
|
destroyHazardChart()
|
|
|
+ destroySafeDayChart()
|
|
|
destroySocChart()
|
|
|
})
|
|
|
|
|
|
@@ -271,7 +275,7 @@ function getHazardChartOption(): echarts.EChartsOption {
|
|
|
}),
|
|
|
xAxis: {
|
|
|
type: 'category',
|
|
|
- data: hazardBars.map((item) => item.label),
|
|
|
+ data: hazardBars.value.map((item) => item.label),
|
|
|
axisLine: {
|
|
|
show: false
|
|
|
},
|
|
|
@@ -309,7 +313,7 @@ function getHazardChartOption(): echarts.EChartsOption {
|
|
|
series: [
|
|
|
{
|
|
|
type: 'bar',
|
|
|
- data: hazardBars.map((item) => ({
|
|
|
+ data: hazardBars.value.map((item) => ({
|
|
|
value: item.value,
|
|
|
itemStyle: {
|
|
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
|
@@ -318,7 +322,7 @@ function getHazardChartOption(): echarts.EChartsOption {
|
|
|
]),
|
|
|
shadowBlur: 14,
|
|
|
shadowColor: item.color,
|
|
|
- borderRadius: [0, 0, 0, 0]
|
|
|
+ borderRadius: [5, 5, 0, 0]
|
|
|
}
|
|
|
})),
|
|
|
barWidth: 70,
|
|
|
@@ -359,6 +363,11 @@ function initHazardChart() {
|
|
|
hazardChart.setOption(getHazardChartOption(), true)
|
|
|
}
|
|
|
|
|
|
+function updateHazardChart() {
|
|
|
+ if (!hazardChart) return
|
|
|
+ hazardChart.setOption(getHazardChartOption(), true)
|
|
|
+}
|
|
|
+
|
|
|
function resizeHazardChart() {
|
|
|
hazardChart?.resize()
|
|
|
}
|
|
|
@@ -370,6 +379,133 @@ function destroyHazardChart() {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+function getSafeDayEntries() {
|
|
|
+ const safeDayMap = (safeDay.value ?? {}) as SafeDayMap
|
|
|
+
|
|
|
+ return Object.entries(safeDayMap)
|
|
|
+ .map(([label, value]) => ({
|
|
|
+ label,
|
|
|
+ value: Number(value) || 0
|
|
|
+ }))
|
|
|
+ .sort((a, b) => a.value - b.value)
|
|
|
+}
|
|
|
+
|
|
|
+function getSafeDayChartOption(): echarts.EChartsOption {
|
|
|
+ const entries = getSafeDayEntries()
|
|
|
+
|
|
|
+ return {
|
|
|
+ ...ANIMATION,
|
|
|
+ grid: {
|
|
|
+ left: 52,
|
|
|
+ right: 32,
|
|
|
+ top: 12,
|
|
|
+ bottom: 24,
|
|
|
+ containLabel: true
|
|
|
+ },
|
|
|
+ tooltip: createTooltip({
|
|
|
+ trigger: 'axis',
|
|
|
+ axisPointer: {
|
|
|
+ type: 'shadow',
|
|
|
+ shadowStyle: {
|
|
|
+ color: 'rgba(31, 91, 184, 0.08)'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ formatter(params: any) {
|
|
|
+ const item = Array.isArray(params) ? params[0] : params
|
|
|
+ return `${item.name}<br/>安全天数:${item.value}`
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ xAxis: {
|
|
|
+ type: 'value',
|
|
|
+ axisLine: {
|
|
|
+ show: false
|
|
|
+ },
|
|
|
+ axisTick: {
|
|
|
+ show: false
|
|
|
+ },
|
|
|
+ axisLabel: {
|
|
|
+ color: '#263854',
|
|
|
+ fontSize: 12,
|
|
|
+ fontFamily: FONT_FAMILY
|
|
|
+ },
|
|
|
+ splitLine: {
|
|
|
+ lineStyle: {
|
|
|
+ color: 'rgba(83, 114, 173, 0.6)',
|
|
|
+ type: 'dashed'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ type: 'category',
|
|
|
+ data: entries.map((item) => item.label),
|
|
|
+ axisLine: {
|
|
|
+ show: false
|
|
|
+ },
|
|
|
+ axisTick: {
|
|
|
+ show: false
|
|
|
+ },
|
|
|
+ axisLabel: {
|
|
|
+ color: '#16263d',
|
|
|
+ fontSize: 14,
|
|
|
+ fontWeight: 700,
|
|
|
+ fontFamily: FONT_FAMILY
|
|
|
+ }
|
|
|
+ },
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ type: 'bar',
|
|
|
+ data: entries.map((item) => item.value),
|
|
|
+ barWidth: 16,
|
|
|
+ showBackground: true,
|
|
|
+ backgroundStyle: {
|
|
|
+ color: 'rgba(108, 149, 228, 0.08)',
|
|
|
+ borderRadius: 6
|
|
|
+ },
|
|
|
+ itemStyle: {
|
|
|
+ borderRadius: 6,
|
|
|
+ color: new echarts.graphic.LinearGradient(1, 0, 0, 0, [
|
|
|
+ { offset: 0, color: '#78a0ec' },
|
|
|
+ { offset: 1, color: '#6a90dd' }
|
|
|
+ ])
|
|
|
+ },
|
|
|
+ emphasis: {
|
|
|
+ itemStyle: {
|
|
|
+ shadowBlur: 16,
|
|
|
+ shadowColor: 'rgba(106, 144, 221, 0.34)'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function initSafeDayChart() {
|
|
|
+ if (!safeDayChartRef.value) return
|
|
|
+ if (safeDayChart) {
|
|
|
+ safeDayChart.dispose()
|
|
|
+ }
|
|
|
+ safeDayChart = echarts.init(safeDayChartRef.value, undefined, {
|
|
|
+ renderer: CHART_RENDERER
|
|
|
+ })
|
|
|
+ safeDayChart.setOption(getSafeDayChartOption(), true)
|
|
|
+}
|
|
|
+
|
|
|
+function updateSafeDayChart() {
|
|
|
+ if (!safeDayChart) return
|
|
|
+ safeDayChart.setOption(getSafeDayChartOption(), true)
|
|
|
+}
|
|
|
+
|
|
|
+function resizeSafeDayChart() {
|
|
|
+ safeDayChart?.resize()
|
|
|
+}
|
|
|
+
|
|
|
+function destroySafeDayChart() {
|
|
|
+ if (safeDayChart) {
|
|
|
+ safeDayChart.dispose()
|
|
|
+ safeDayChart = null
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
function getSocChartOption(): echarts.EChartsOption {
|
|
|
return {
|
|
|
...ANIMATION,
|
|
|
@@ -486,6 +622,60 @@ function destroySocChart() {
|
|
|
socChart = null
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+const getKanban = async () => {
|
|
|
+ const data = await kanbanApi.getKanban()
|
|
|
+ return data
|
|
|
+}
|
|
|
+
|
|
|
+function formatPercent(numerator: number, denominator: number) {
|
|
|
+ if (!denominator || Number.isNaN(denominator)) {
|
|
|
+ return '0%'
|
|
|
+ }
|
|
|
+
|
|
|
+ return `${((numerator / denominator) * 100).toFixed(1)}%`
|
|
|
+}
|
|
|
+
|
|
|
+const summaryPanel = ref<any>(null)
|
|
|
+const safeDay = ref<SafeDayMap>({})
|
|
|
+onMounted(async () => {
|
|
|
+ summaryPanel.value = await getKanban()
|
|
|
+
|
|
|
+ summaryCards.value[0].value = summaryPanel.value.danger
|
|
|
+ summaryCards.value[1].value = summaryPanel.value.monthHazard
|
|
|
+ summaryCards.value[2].value = formatPercent(
|
|
|
+ summaryPanel.value.totalHazard - summaryPanel.value.todoHazard,
|
|
|
+ summaryPanel.value.totalHazard
|
|
|
+ )
|
|
|
+ summaryCards.value[3].value = summaryPanel.value.ptwCount
|
|
|
+ summaryCards.value[4].value = formatPercent(
|
|
|
+ summaryPanel.value.totdalCert - summaryPanel.value.expiredCert,
|
|
|
+ summaryPanel.value.totdalCert
|
|
|
+ )
|
|
|
+
|
|
|
+ summaryCards.value[4].note = `Warn: ${summaryPanel.value.warnCert}`
|
|
|
+
|
|
|
+ hazardBars.value[0].value = summaryPanel.value.totalHazard
|
|
|
+ hazardBars.value[1].value = summaryPanel.value.totalHazard - summaryPanel.value.todoHazard
|
|
|
+ hazardBars.value[2].value = summaryPanel.value.todoHazard
|
|
|
+
|
|
|
+ try {
|
|
|
+ safeDay.value = (await kanbanApi.getSafeDay(userStore.getUser.deptId)) || {}
|
|
|
+ } catch (error) {
|
|
|
+ console.log(error)
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ getStatic()
|
|
|
+ } catch (error) {
|
|
|
+ console.log(error)
|
|
|
+ }
|
|
|
+
|
|
|
+ nextTick(() => {
|
|
|
+ updateHazardChart()
|
|
|
+ updateSafeDayChart()
|
|
|
+ })
|
|
|
+})
|
|
|
</script>
|
|
|
|
|
|
<template>
|
|
|
@@ -531,38 +721,6 @@ function destroySocChart() {
|
|
|
</section>
|
|
|
|
|
|
<div class="board-main">
|
|
|
- <div class="left-column">
|
|
|
- <section class="panel board-panel kb-stage-card kb-stage-card--2">
|
|
|
- <div class="panel-title">
|
|
|
- <span class="icon-decorator"><span></span><span></span></span>
|
|
|
- 隐患排查治理统计
|
|
|
- </div>
|
|
|
- <div ref="hazardChartRef" class="chart-panel chart-panel--echart"></div>
|
|
|
- </section>
|
|
|
-
|
|
|
- <section class="panel board-panel kb-stage-card kb-stage-card--3">
|
|
|
- <div class="panel-title">
|
|
|
- <span class="icon-decorator"><span></span><span></span></span>
|
|
|
- 事故事件趋势(近12月)
|
|
|
- </div>
|
|
|
- <div class="incident-panel">
|
|
|
- <div class="incident-graphic">
|
|
|
- <div class="incident-graphic__axis"></div>
|
|
|
- <div class="incident-graphic__area"></div>
|
|
|
- <div class="incident-graphic__line"></div>
|
|
|
- </div>
|
|
|
- <div class="incident-metrics">
|
|
|
- <div v-for="item in incidentStats" :key="item.label" class="incident-metric">
|
|
|
- <span :style="{ color: item.label === '安全生产天数' ? '#259745' : '' }"
|
|
|
- >{{ item.label }}:</span
|
|
|
- >
|
|
|
- <strong :style="{ color: item.accent }">{{ item.value }}</strong>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </section>
|
|
|
- </div>
|
|
|
-
|
|
|
<div class="center-column">
|
|
|
<section class="panel board-panel board-panel--center kb-stage-card kb-stage-card--4">
|
|
|
<div class="panel-title panel-title--center">
|
|
|
@@ -574,12 +732,35 @@ function destroySocChart() {
|
|
|
<div class="risk-card__title">
|
|
|
<span class="risk-card__dot" :style="{ background: zone.color }"></span>
|
|
|
<span :style="{ color: zone.color }">{{ zone.title }}</span>
|
|
|
+ <!-- <span class="risk-card__count">{{ zone.value }}</span> -->
|
|
|
+ <CountTo
|
|
|
+ :duration="2600"
|
|
|
+ :end-val="zone.value"
|
|
|
+ :start-val="0"
|
|
|
+ :style="{ color: zone.color, fontSize: '28px' }" />
|
|
|
</div>
|
|
|
<div class="risk-card__desc">{{ zone.desc }}</div>
|
|
|
</article>
|
|
|
</div>
|
|
|
</section>
|
|
|
</div>
|
|
|
+ <div class="left-column">
|
|
|
+ <section class="panel board-panel kb-stage-card kb-stage-card--2">
|
|
|
+ <div class="panel-title">
|
|
|
+ <span class="icon-decorator"><span></span><span></span></span>
|
|
|
+ 隐患排查治理统计
|
|
|
+ </div>
|
|
|
+ <div ref="hazardChartRef" class="chart-panel chart-panel--echart"></div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section class="panel board-panel kb-stage-card kb-stage-card--3">
|
|
|
+ <div class="panel-title">
|
|
|
+ <span class="icon-decorator"><span></span><span></span></span>
|
|
|
+ 安全生产天数
|
|
|
+ </div>
|
|
|
+ <div ref="safeDayChartRef" class="safe-day-chart-panel"></div>
|
|
|
+ </section>
|
|
|
+ </div>
|
|
|
|
|
|
<div class="right-column">
|
|
|
<section class="panel board-panel kb-stage-card kb-stage-card--5">
|
|
|
@@ -745,64 +926,9 @@ function destroySocChart() {
|
|
|
height: 188px;
|
|
|
}
|
|
|
|
|
|
-.incident-panel {
|
|
|
- display: grid;
|
|
|
- margin-top: 50px;
|
|
|
- grid-template-columns: 180px 1fr;
|
|
|
- align-items: center;
|
|
|
- gap: 28px;
|
|
|
-}
|
|
|
-
|
|
|
-.incident-graphic {
|
|
|
- position: relative;
|
|
|
- width: 170px;
|
|
|
- height: 132px;
|
|
|
-}
|
|
|
-
|
|
|
-.incident-graphic__axis {
|
|
|
- position: absolute;
|
|
|
- inset: 24px 16px 16px 16px;
|
|
|
- border-bottom: 6px solid #4f8dff;
|
|
|
- border-left: 6px solid #4f8dff;
|
|
|
- border-radius: 2px;
|
|
|
- opacity: 0.92;
|
|
|
-}
|
|
|
-
|
|
|
-.incident-graphic__area {
|
|
|
- position: absolute;
|
|
|
- right: 22px;
|
|
|
- bottom: 24px;
|
|
|
- width: 102px;
|
|
|
- height: 68px;
|
|
|
- background: linear-gradient(180deg, rgb(79 141 255 / 88%) 0%, rgb(79 141 255 / 28%) 100%);
|
|
|
- clip-path: polygon(0 100%, 22% 50%, 54% 72%, 100% 0, 100% 100%);
|
|
|
-}
|
|
|
-
|
|
|
-.incident-graphic__line {
|
|
|
- position: absolute;
|
|
|
- right: 22px;
|
|
|
- bottom: 24px;
|
|
|
- width: 102px;
|
|
|
- height: 68px;
|
|
|
- border-top: 5px solid #4f8dff;
|
|
|
- clip-path: polygon(0 100%, 22% 50%, 54% 72%, 100% 0, 100% 5%, 54% 77%, 22% 55%, 0 100%);
|
|
|
-}
|
|
|
-
|
|
|
-.incident-metrics {
|
|
|
- display: grid;
|
|
|
- gap: 20px;
|
|
|
-}
|
|
|
-
|
|
|
-.incident-metric {
|
|
|
- font-size: 24px;
|
|
|
- font-weight: 700;
|
|
|
- color: #556b89;
|
|
|
-}
|
|
|
-
|
|
|
-.incident-metric strong {
|
|
|
- margin-left: 4px;
|
|
|
- font-size: 34px;
|
|
|
- line-height: 1;
|
|
|
+.safe-day-chart-panel {
|
|
|
+ height: 218px;
|
|
|
+ // margin-top: 18px;
|
|
|
}
|
|
|
|
|
|
.panel-title--center {
|