You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
besure_web/src/views/iot/runoverview/components/StatusDistributionChart.vue

241 lines
5.7 KiB
Vue

<template>
<ContentWrap class="chart-panel">
<div class="chart-panel__header">
<div class="chart-panel__title">
{{ t('DataCollection.RunOverview.statusDistributionTitle') }}
</div>
<div class="chart-panel__actions">
<el-select model-value="hour" class="!w-120px">
<el-option value="hour" :label="t('DataCollection.RunOverview.granularityHour')" />
</el-select>
<el-button class="chart-panel__icon-btn">
<Icon icon="ep:data-analysis" />
</el-button>
<el-button class="chart-panel__icon-btn">
<Icon icon="ep:download" />
</el-button>
</div>
</div>
<div class="chart-panel__body">
<div class="chart-panel__main">
<Echart :options="barOption" height="320px" />
</div>
<div class="chart-panel__side">
<div class="chart-panel__side-title">{{ t('DataCollection.RunOverview.summaryTitle') }}</div>
<Echart :options="pieOption" height="320px" />
<div class="chart-panel__legend">
<div v-for="item in summary" :key="item.status" class="chart-panel__legend-item">
<span class="dot" :style="{ background: statusColors[item.status] }"></span>
<span class="label">{{ statusLabelMap[item.status] }}</span>
<span class="value">{{ item.percent.toFixed(2) }}% ({{ item.hours.toFixed(2) }}h)</span>
</div>
</div>
</div>
</div>
</ContentWrap>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { EChartsOption } from 'echarts'
import { Echart as EchartChart } from '@/components/Echart'
import type { HourlyStatusItem, RunStatus, StatusSummaryItem } from './types'
const Echart = EchartChart
const props = defineProps<{
hourlyStatus: HourlyStatusItem[]
summary: StatusSummaryItem[]
}>()
const { t } = useI18n()
const statusColors: Record<RunStatus, string> = {
running: '#67c35b',
standby: '#f5c243',
fault: '#ff6b6b',
offline: '#cfd4df'
}
const statusLabelMap = computed<Record<RunStatus, string>>(() => ({
running: t('DataCollection.RunOverview.legend.running'),
standby: t('DataCollection.RunOverview.legend.standby'),
fault: t('DataCollection.RunOverview.legend.fault'),
offline: t('DataCollection.RunOverview.legend.offline')
}))
const barOption = computed<EChartsOption>(() => ({
color: [
statusColors.running,
statusColors.standby,
statusColors.fault,
statusColors.offline
],
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
legend: {
top: 8,
itemWidth: 14,
itemHeight: 8,
textStyle: { color: '#667085', fontSize: 12 }
},
grid: { left: 48, right: 18, top: 46, bottom: 36 },
xAxis: {
type: 'category',
data: props.hourlyStatus.map((item) => item.hour),
axisTick: { show: false },
axisLine: { lineStyle: { color: '#d0d5dd' } },
axisLabel: { color: '#667085', fontSize: 11 }
},
yAxis: {
type: 'value',
min: 0,
max: 100,
interval: 20,
axisLabel: { color: '#667085', formatter: '{value}%' },
splitLine: { lineStyle: { color: '#eef2f6' } }
},
series: [
{
name: statusLabelMap.value.running,
type: 'bar',
stack: 'status',
barWidth: 18,
data: props.hourlyStatus.map((item) => item.running)
},
{
name: statusLabelMap.value.standby,
type: 'bar',
stack: 'status',
barWidth: 18,
data: props.hourlyStatus.map((item) => item.standby)
},
{
name: statusLabelMap.value.fault,
type: 'bar',
stack: 'status',
barWidth: 18,
data: props.hourlyStatus.map((item) => item.fault)
},
{
name: statusLabelMap.value.offline,
type: 'bar',
stack: 'status',
barWidth: 18,
data: props.hourlyStatus.map((item) => item.offline)
}
]
}))
const pieOption = computed<EChartsOption>(() => ({
color: props.summary.map((item) => statusColors[item.status]),
tooltip: { trigger: 'item', formatter: '{b}: {c}%' },
graphic: [
{
type: 'text',
left: 'center',
top: '42%',
style: {
text: `${t('DataCollection.RunOverview.totalTimeLabel')}\n24.00 h`,
textAlign: 'center',
fill: '#101828',
fontSize: 14,
fontWeight: 600,
lineHeight: 24
}
}
],
series: [
{
name: t('DataCollection.RunOverview.summaryTitle'),
type: 'pie',
radius: ['55%', '76%'],
center: ['50%', '50%'],
label: { show: false },
data: props.summary.map((item) => ({
name: statusLabelMap.value[item.status],
value: item.percent
}))
}
]
}))
</script>
<style scoped lang="scss">
.chart-panel {
margin-bottom: 16px;
}
.chart-panel__header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.chart-panel__title,
.chart-panel__side-title {
color: #101828;
font-size: 16px;
font-weight: 700;
}
.chart-panel__actions {
display: flex;
gap: 10px;
align-items: center;
}
.chart-panel__icon-btn {
width: 36px;
padding: 0;
}
.chart-panel__body {
display: grid;
grid-template-columns: minmax(0, 1.7fr) minmax(280px, 0.8fr);
gap: 12px;
}
.chart-panel__side {
padding: 8px 8px 0;
}
.chart-panel__legend {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: -8px;
}
.chart-panel__legend-item {
display: grid;
grid-template-columns: 12px 1fr auto;
gap: 8px;
align-items: center;
color: #344054;
font-size: 13px;
}
.dot {
width: 12px;
height: 12px;
border-radius: 50%;
}
.label {
color: #667085;
}
.value {
color: #101828;
font-weight: 600;
}
@media (max-width: 1100px) {
.chart-panel__body {
grid-template-columns: 1fr;
}
}
</style>