Files
lc_frontend/src/views/screen/components/RiskStatisticsPanel.vue

385 lines
9.3 KiB
Vue
Raw Normal View History

2025-10-17 10:31:13 +08:00
<template>
<div class="left-bottom">
<div class="panel-title">
<div class="tabs">
2025-12-15 20:55:27 +08:00
<span class="tab" :class="{ active: activeTab === '安全类' }" @click="handleTabClick('安全类')">安全类</span>
2025-10-17 10:31:13 +08:00
<span class="divider">|</span>
2025-12-15 20:55:27 +08:00
<span class="tab" :class="{ active: activeTab === '工程类' }" @click="handleTabClick('工程类')">工程类</span>
2025-10-17 10:31:13 +08:00
</div>
</div>
2025-12-15 20:55:27 +08:00
<img class="title-line" src="@/assets/images/title_border_line.png" />
<div class="chart-grid">
<div class="chart-card" v-for="item in currentCharts" :key="item.title">
<div class="chart-title">{{ item.title }}</div>
<div class="chart-content">
<div class="chart-wrapper">
<Echart class="donut-chart" :options="buildOption(item)" />
<div class="chart-center">
<div class="center-title">总数</div>
<div class="center-value">{{ item.total }}</div>
</div>
</div>
<div class="legend">
<div class="legend-label-row">
<ElTooltip v-for="status in statusList" :key="status.key" :content="status.label" effect="dark" placement="right">
<div class="legend-item">
<span class="legend-dot" :style="{ background: status.color }"></span>
<span class="legend-label">{{ status.label }}</span>
<div class="legend-value">
{{ item.status[status.key] }}
</div>
</div>
</ElTooltip>
</div>
<div class="legend-value-row">
<div class="legend-value" v-for="status in statusList" :key="`${status.key}-value`">
{{ item.status[status.key] }}
</div>
</div>
</div>
</div>
2025-10-17 10:31:13 +08:00
</div>
</div>
</div>
</template>
<script setup lang="ts">
2025-12-15 20:55:27 +08:00
import { computed, ref, watch } from 'vue'
import { ElTooltip } from 'element-plus'
import type { EChartsOption } from 'echarts'
2025-10-17 10:31:13 +08:00
2025-12-15 20:55:27 +08:00
type TabType = '安全类' | '工程类'
type StatusKey = 'notStarted' | 'inProgress' | 'done' | 'voided'
2025-10-17 10:31:13 +08:00
2025-12-15 20:55:27 +08:00
interface ChartItem {
title: string
total: number
status: Record<StatusKey, number>
2025-10-17 10:31:13 +08:00
}
2025-12-15 20:55:27 +08:00
const statusList: { key: StatusKey; label: string; color: string }[] = [
{ key: 'notStarted', label: '未开始', color: '#2a59ff' },
{ key: 'inProgress', label: '进行中', color: '#ff8a00' },
{ key: 'done', label: '已完成', color: '#1bd9ff' },
{ key: 'voided', label: '已作废', color: '#9fa0a6' }
]
2025-10-17 10:31:13 +08:00
2025-12-15 20:55:27 +08:00
const defaultChart: ChartItem[] = [
{ title: '每日检查(维保类)', total: 6, status: { notStarted: 3, inProgress: 0, done: 3, voided: 0 } },
{ title: '每月检查(维保类)', total: 6, status: { notStarted: 3, inProgress: 0, done: 3, voided: 0 } },
{ title: '每年检查(维保类)', total: 6, status: { notStarted: 3, inProgress: 0, done: 3, voided: 0 } },
{ title: '每日检查(巡检类)', total: 6, status: { notStarted: 3, inProgress: 0, done: 3, voided: 0 } },
{ title: '每月检查(巡检类)', total: 6, status: { notStarted: 3, inProgress: 0, done: 3, voided: 0 } },
{ title: '每年检查(巡检类)', total: 6, status: { notStarted: 3, inProgress: 0, done: 3, voided: 0 } }
]
2025-10-17 10:31:13 +08:00
2025-12-15 20:55:27 +08:00
const tabCharts = ref<Record<TabType, ChartItem[]>>({
安全类: [...defaultChart],
工程类: [...defaultChart]
2025-10-17 10:31:13 +08:00
})
2025-12-15 20:55:27 +08:00
const props = defineProps<{
riskStatistics?: Record<TabType, ChartItem[]>
}>()
2025-10-17 10:31:13 +08:00
2025-12-15 20:55:27 +08:00
const activeTab = ref<TabType>('安全类')
const emit = defineEmits<{ tabChange: [tab: TabType] }>()
2025-10-17 10:31:13 +08:00
2025-12-15 20:55:27 +08:00
// 监听props变化更新图表数据
watch(() => props.riskStatistics, (newData) => {
if (newData) {
tabCharts.value = {
安全类: newData['安全类'] ? JSON.parse(JSON.stringify(newData['安全类'])) : [...defaultChart],
工程类: newData['工程类'] ? JSON.parse(JSON.stringify(newData['工程类'])) : [...defaultChart]
}
2025-10-17 10:31:13 +08:00
}
2025-12-15 20:55:27 +08:00
}, { deep: true, immediate: true })
2025-10-17 10:31:13 +08:00
2025-12-15 20:55:27 +08:00
const currentCharts = computed(() => tabCharts.value[activeTab.value])
2025-10-17 10:31:13 +08:00
2025-12-15 20:55:27 +08:00
const buildOption = (item: ChartItem): EChartsOption => ({
backgroundColor: 'transparent',
tooltip: { show: false },
series: [
{
type: 'pie' as const,
radius: ['40%', '60%'],
center: ['50%', '50%'],
startAngle: 90,
avoidLabelOverlap: false,
label: { show: false },
labelLine: { show: false },
data: [
{ value: item.status.notStarted, name: '未开始', itemStyle: { color: '#2a59ff' }, label: { show: false } },
{ value: item.status.inProgress, name: '进行中', itemStyle: { color: '#ff8a00' }, label: { show: false } },
{ value: item.status.done, name: '已完成', itemStyle: { color: '#1bd9ff' }, label: { show: false } },
{ value: item.status.voided, name: '已作废', itemStyle: { color: '#9fa0a6' }, label: { show: false } }
],
emphasis: { scale: true, scaleSize: 4 }
2025-10-17 10:31:13 +08:00
}
2025-12-15 20:55:27 +08:00
]
})
2025-10-17 10:31:13 +08:00
2025-12-15 20:55:27 +08:00
const handleTabClick = (tab: TabType) => {
if (activeTab.value === tab) return
2025-10-17 10:31:13 +08:00
activeTab.value = tab
emit('tabChange', tab)
}
</script>
<style scoped lang="scss">
.left-bottom {
background-image: url('@/assets/images/screen/left_top_2_img.png'), url('@/assets/images/screen/left_center_img.png'), url('@/assets/images/screen/left_bottom_img.png');
background-position: top center, left center, bottom center;
background-repeat: no-repeat, no-repeat, no-repeat;
background-size: 100% 90px, cover, 100% 68px;
flex: 1;
.panel-title {
margin: 4px 20px 0;
font-size: 0.8rem;
font-weight: bold;
color: #fff;
}
.tabs {
display: inline-flex;
align-items: center;
column-gap: 8px;
}
.tab {
padding: 2px 10px;
color: #fff;
cursor: pointer;
border-radius: 4px;
transition: all 0.2s ease-in-out;
user-select: none;
}
.tab:hover {
color: #1afb8f;
}
.tab.active {
color: #1afb8f;
background: rgb(26 251 143 / 12%);
border: 1px solid rgb(26 251 143 / 35%);
}
.divider {
margin: 0 2px;
color: #94a3b8;
}
2025-12-15 20:55:27 +08:00
.title-line {
margin: 8px 0;
}
.chart-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
padding: 0 12px 12px;
width: 66%;
// margin: 0 auto;
}
.chart-card {
background: rgba(18, 34, 63, 0.65);
border: 1px solid rgba(58, 112, 179, 0.55);
border-radius: 10px;
padding: 5px;
2025-10-17 10:31:13 +08:00
display: flex;
flex-direction: column;
2025-12-15 20:55:27 +08:00
align-items: center;
box-shadow: 0 4px 12px rgb(0 0 0 / 25%);
}
2025-10-17 10:31:13 +08:00
2025-12-15 20:55:27 +08:00
.chart-title {
width: 100%;
color: #ffffff;
font-size: 13px;
text-align: center;
margin-bottom: 6px;
}
.chart-content {
display: flex;
align-items: center;
// justify-content: flex-start;
// gap: 10px;
width: 100%;
}
.chart-wrapper {
position: relative;
flex: 0 0 90px;
width: 90px;
height: 90px;
}
.donut-chart {
width: 100%;
height: 100% !important; // 强制覆盖 EChart 默认高度,避免过高
2025-10-17 10:31:13 +08:00
}
2025-12-15 20:55:27 +08:00
.chart-center {
pointer-events: none;
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #ffffff;
text-align: center;
text-shadow: 0 2px 6px rgba(0, 0, 0, 0.45);
2025-10-17 10:31:13 +08:00
}
2025-12-15 20:55:27 +08:00
.legend {
display: flex;
flex-direction: column;
gap: 6px;
margin-top: 0;
width: auto;
}
.legend-label-row {
flex-wrap: nowrap;
gap: 10px;
justify-content: flex-start;
align-items: center;
color: #cbd5e1;
font-size: 12px;
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
white-space: nowrap;
}
.legend-dot {
width: 10px;
height: 10px;
border-radius: 50%;
// display: inline-block;
}
.legend-label {
color: #cbd5e1;
white-space: nowrap;
}
.legend-value-row {
display: none;
gap: 20px;
color: #ffffff;
font-size: 14px;
font-weight: 600;
justify-content: flex-start;
}
.legend-value {
min-width: 14px;
text-align: center;
2025-10-17 10:31:13 +08:00
}
}
2025-12-15 20:55:27 +08:00
@media (width <=1280px) {
.left-bottom .legend-label-row {
gap: 8px;
}
.left-bottom .legend-label {
display: none;
}
.left-bottom .legend-item {
gap: 4px;
}
2025-10-17 10:31:13 +08:00
2025-12-15 20:55:27 +08:00
.left-bottom .chart-wrapper {
flex-basis: 80px;
width: 80px;
height: 80px;
2025-10-17 10:31:13 +08:00
}
}
2025-12-15 20:55:27 +08:00
@media (width <=3000px) {
.left-bottom .legend-label {
display: none;
}
}
2025-10-17 10:31:13 +08:00
2025-12-15 20:55:27 +08:00
@media (width >3000px) {
.left-bottom .chart-content {
flex-direction: column;
align-items: center;
gap: 10px;
}
.left-bottom .legend {
align-items: center;
width: 100%;
}
.left-bottom .legend-label {
display: inline;
}
.left-bottom .legend-label-row {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
gap: 18px;
}
.left-bottom .legend-item {
flex-direction: column;
align-items: center;
gap: 6px;
white-space: nowrap;
}
.left-bottom .legend-label-row .legend-value {
display: block;
}
.left-bottom .legend-value-row {
display: none;
}
.left-bottom .chart-wrapper {
flex-basis: 110px;
width: 110px;
height: 110px;
2025-10-17 10:31:13 +08:00
}
}
2025-12-15 20:55:27 +08:00
@media (width <=1024px) {
.left-bottom .chart-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.left-bottom .donut-chart {
height: 80px !important;
}
}
2025-10-17 10:31:13 +08:00
2025-12-15 20:55:27 +08:00
@media (width <=768px) {
.left-bottom .chart-grid {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
.left-bottom .chart-card {
padding: 5px;
2025-10-17 10:31:13 +08:00
}
}
</style>
2025-12-15 20:55:27 +08:00