feat: 实现供水运营专题大屏BI可视化
- 新增OperationDashboard.vue全屏大屏组件 - 添加6个核心KPI指标卡片: 进水总量/出水总量/产销差率/营收额/平均水质/报警次数 - 实现6个ECharts图表: 供水趋势/水质分布/报警统计/管网空间/设备状态/营收分析 - 创建静态HTML版本operation-dashboard.html,使用CDN加载Vue3/ECharts/Element Plus - 更新路由配置,添加/operation路径支持 - 修复nextTick导入问题,优化build脚本 🚧 开发者: bot_dev1 📝 任务: #38 [BI] 运营仪表盘 + 供水专题大屏
This commit is contained in:
@@ -6,6 +6,7 @@ const routes = [
|
||||
path: '/', component: () => import('@/components/layout/MainLayout.vue'),
|
||||
redirect: '/dashboard',
|
||||
children: [
|
||||
{ path: 'operation', name: 'operation', component: () => import('@/views/dashboard/OperationDashboard.vue') },
|
||||
{ path: 'dashboard', name: 'dashboard', component: () => import('@/views/dashboard/DashboardView.vue') },
|
||||
{ path: 'system/user', name: 'user', component: () => import('@/views/system/user/UserList.vue') },
|
||||
{ path: 'system/role', name: 'role', component: () => import('@/views/system/role/RoleList.vue') },
|
||||
|
||||
@@ -1,62 +1,579 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<h2>供水总览</h2>
|
||||
<el-row :gutter="16">
|
||||
<h2 class="dashboard-title">供水运营综合大屏</h2>
|
||||
|
||||
<!-- 顶部统计卡片 -->
|
||||
<el-row :gutter="16" class="stats-row">
|
||||
<el-col :span="6" v-for="card in cards" :key="card.title">
|
||||
<el-card shadow="hover">
|
||||
<div class="stat-card">
|
||||
<el-card shadow="hover" class="stat-card">
|
||||
<div class="stat-icon" :style="{ backgroundColor: card.color }">
|
||||
<el-icon><component :is="card.icon" /></el-icon>
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ card.value }}</div>
|
||||
<div class="stat-title">{{ card.title }}</div>
|
||||
<div class="stat-trend" :class="card.trend">
|
||||
<el-icon><trend-arrow /></el-icon>
|
||||
{{ card.change }}%
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16" style="margin-top:16px">
|
||||
<el-col :span="12"><el-card><div ref="chart1" style="height:300px" /></el-card></el-col>
|
||||
<el-col :span="12"><el-card><div ref="chart2" style="height:300px" /></el-card></el-col>
|
||||
|
||||
<!-- 中部图表区域 -->
|
||||
<el-row :gutter="16" class="charts-row">
|
||||
<!-- 左侧列 -->
|
||||
<el-col :span="16">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-card class="chart-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>24小时供水趋势</span>
|
||||
<el-select size="small" v-model="timeRange" @change="updateTrendChart">
|
||||
<el-option label="今日" value="today" />
|
||||
<el-option label="本周" value="week" />
|
||||
<el-option label="本月" value="month" />
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="chart1" class="chart-container" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card class="chart-card">
|
||||
<template #header>
|
||||
<span>片区用水分布</span>
|
||||
</template>
|
||||
<div ref="chart2" class="chart-container" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16" style="margin-top:16px">
|
||||
<el-col :span="8">
|
||||
<el-card class="chart-card">
|
||||
<template #header>
|
||||
<span>水质监测</span>
|
||||
</template>
|
||||
<div ref="chart3" class="chart-container-small" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-card class="chart-card">
|
||||
<template #header>
|
||||
<span>产销差分析</span>
|
||||
</template>
|
||||
<div ref="chart4" class="chart-container-small" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-card class="chart-card">
|
||||
<template #header>
|
||||
<span>设备运行状态</span>
|
||||
</template>
|
||||
<div ref="chart5" class="chart-container-small" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
|
||||
<!-- 右侧列 -->
|
||||
<el-col :span="8">
|
||||
<el-card class="chart-card">
|
||||
<template #header>
|
||||
<span>营收指标</span>
|
||||
</template>
|
||||
<div ref="chart6" class="chart-container" />
|
||||
</el-card>
|
||||
<el-row :gutter="16" style="margin-top:16px">
|
||||
<el-col :span="24">
|
||||
<el-card class="alarm-card">
|
||||
<template #header>
|
||||
<span><el-icon><warning-filled /></el-icon> 实时报警</span>
|
||||
</template>
|
||||
<div class="alarm-list">
|
||||
<div v-for="alarm in alarms" :key="alarm.id" class="alarm-item" :class="alarm.level">
|
||||
<div class="alarm-time">{{ alarm.time }}</div>
|
||||
<div class="alarm-content">{{ alarm.content }}</div>
|
||||
<div class="alarm-location">{{ alarm.location }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import { WaterFilled, TrendCharts, PieChart, DataLine, WarningFilled } from '@element-plus/icons-vue'
|
||||
|
||||
const cards = [
|
||||
{ title: '今日进水(m³)', value: '12,580' },
|
||||
{ title: '今日出水(m³)', value: '11,230' },
|
||||
{ title: '在线设备', value: '156' },
|
||||
{ title: '今日报警', value: '3' },
|
||||
]
|
||||
// 统计卡片数据
|
||||
const cards = ref([
|
||||
{
|
||||
title: '今日进水',
|
||||
value: '12,580',
|
||||
unit: 'm³',
|
||||
icon: 'WaterFilled',
|
||||
color: '#409eff',
|
||||
change: '+5.2',
|
||||
trend: 'trend-up'
|
||||
},
|
||||
{
|
||||
title: '今日出水',
|
||||
value: '11,230',
|
||||
unit: 'm³',
|
||||
icon: 'WaterFilled',
|
||||
color: '#67c23a',
|
||||
change: '+3.8',
|
||||
trend: 'trend-up'
|
||||
},
|
||||
{
|
||||
title: '在线设备',
|
||||
value: '156',
|
||||
unit: '台',
|
||||
icon: 'TrendCharts',
|
||||
color: '#e6a23c',
|
||||
change: '+2.1',
|
||||
trend: 'trend-up'
|
||||
},
|
||||
{
|
||||
title: '今日报警',
|
||||
value: '3',
|
||||
unit: '次',
|
||||
icon: 'WarningFilled',
|
||||
color: '#f56c6c',
|
||||
change: '-15.8',
|
||||
trend: 'trend-down'
|
||||
},
|
||||
])
|
||||
|
||||
// 报警数据
|
||||
const alarms = ref([
|
||||
{ id: 1, time: '08:23', content: '压力异常波动', location: '精芒片区', level: 'warning' },
|
||||
{ id: 2, time: '08:15', content: '浊度超标', location: '一体化水厂', level: 'danger' },
|
||||
{ id: 3, time: '07:58', content: '设备离线', location: '托里片区', level: 'info' },
|
||||
])
|
||||
|
||||
// 时间范围
|
||||
const timeRange = ref('today')
|
||||
|
||||
// 图表引用
|
||||
const chart1 = ref()
|
||||
const chart2 = ref()
|
||||
const chart3 = ref()
|
||||
const chart4 = ref()
|
||||
const chart5 = ref()
|
||||
const chart6 = ref()
|
||||
|
||||
onMounted(() => {
|
||||
const c1 = echarts.init(chart1.value)
|
||||
c1.setOption({
|
||||
title: { text: '24小时供水趋势' },
|
||||
tooltip: { trigger: 'axis' },
|
||||
xAxis: { data: ['00:00','04:00','08:00','12:00','16:00','20:00'] },
|
||||
yAxis: { name: '流量(m³/h)' },
|
||||
series: [{ data: [320,280,450,520,480,380], type: 'line', smooth: true, areaStyle: {} }]
|
||||
})
|
||||
const c2 = echarts.init(chart2.value)
|
||||
c2.setOption({
|
||||
title: { text: '片区用水分布' },
|
||||
tooltip: {},
|
||||
// 更新趋势图表
|
||||
const updateTrendChart = () => {
|
||||
// 根据时间范围更新数据
|
||||
const chart = echarts.getInstanceByDom(chart1.value)
|
||||
if (chart) {
|
||||
let newData, xAxisData
|
||||
|
||||
if (timeRange.value === 'today') {
|
||||
xAxisData = ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00']
|
||||
newData = [320, 280, 450, 520, 480, 380]
|
||||
} else if (timeRange.value === 'week') {
|
||||
xAxisData = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
|
||||
newData = [2800, 2950, 3100, 2850, 3000, 3200, 3150]
|
||||
} else {
|
||||
xAxisData = ['第1周', '第2周', '第3周', '第4周']
|
||||
newData = [19500, 20200, 19800, 21000]
|
||||
}
|
||||
|
||||
chart.setOption({
|
||||
xAxis: { data: xAxisData },
|
||||
series: [{ data: newData }]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化所有图表
|
||||
const initCharts = () => {
|
||||
// 图表1: 24小时供水趋势
|
||||
const chart1Instance = echarts.init(chart1.value)
|
||||
chart1Instance.setOption({
|
||||
title: { text: '24小时供水趋势', left: 'center', textStyle: { color: '#333', fontSize: 16 } },
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: '{b}<br/>流量: {c} m³/h'
|
||||
},
|
||||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
||||
xAxis: {
|
||||
data: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00'],
|
||||
axisLine: { lineStyle: { color: '#ddd' } }
|
||||
},
|
||||
yAxis: {
|
||||
name: '流量(m³/h)',
|
||||
nameTextStyle: { color: '#666' },
|
||||
splitLine: { lineStyle: { type: 'dashed', color: '#eee' } }
|
||||
},
|
||||
series: [{
|
||||
type: 'pie', radius: ['45%','70%'],
|
||||
data: [{ value: 3200, name: '一体化水厂' },{ value: 1800, name: '精芒片区' },
|
||||
{ value: 1500, name: '八家户片区' },{ value: 1200, name: '托里片区' },
|
||||
{ value: 900, name: '大镇片区' },{ value: 600, name: '托托片区' }]
|
||||
name: '供水量',
|
||||
data: [320, 280, 450, 520, 480, 380],
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(64, 158, 255, 0.3)' },
|
||||
{ offset: 1, color: 'rgba(64, 158, 255, 0.05)' }
|
||||
])
|
||||
},
|
||||
lineStyle: { width: 3 },
|
||||
itemStyle: { color: '#409eff' }
|
||||
}]
|
||||
})
|
||||
|
||||
// 图表2: 片区用水分布
|
||||
const chart2Instance = echarts.init(chart2.value)
|
||||
chart2Instance.setOption({
|
||||
title: { text: '片区用水分布', left: 'center', textStyle: { color: '#333', fontSize: 16 } },
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a}<br/>{b}: {c} m³ ({d}%)'
|
||||
},
|
||||
series: [{
|
||||
name: '用水量',
|
||||
type: 'pie',
|
||||
radius: ['45%', '70%'],
|
||||
center: ['50%', '60%'],
|
||||
data: [
|
||||
{ value: 3200, name: '一体化水厂', itemStyle: { color: '#409eff' } },
|
||||
{ value: 1800, name: '精芒片区', itemStyle: { color: '#67c23a' } },
|
||||
{ value: 1500, name: '八家户片区', itemStyle: { color: '#e6a23c' } },
|
||||
{ value: 1200, name: '托里片区', itemStyle: { color: '#f56c6c' } },
|
||||
{ value: 900, name: '大镇片区', itemStyle: { color: '#909399' } },
|
||||
{ value: 600, name: '托托片区', itemStyle: { color: '#c71585' } }
|
||||
],
|
||||
label: {
|
||||
show: true,
|
||||
formatter: '{b}\n{c}m³',
|
||||
position: 'outside'
|
||||
},
|
||||
emphasis: {
|
||||
label: { show: true, fontSize: 16, fontWeight: 'bold' },
|
||||
itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)' }
|
||||
}
|
||||
}]
|
||||
})
|
||||
|
||||
// 图表3: 水质监测
|
||||
const chart3Instance = echarts.init(chart3.value)
|
||||
chart3Instance.setOption({
|
||||
title: { text: '水质监测', left: 'center', textStyle: { fontSize: 14 } },
|
||||
radar: {
|
||||
indicator: [
|
||||
{ name: '浊度', max: 5 },
|
||||
{ name: 'pH值', max: 14 },
|
||||
{ name: '余氯', max: 1 },
|
||||
{ name: '菌落', max: 100 },
|
||||
{ name: '色度', max: 15 },
|
||||
{ name: '嗅味', max: 3 }
|
||||
],
|
||||
radius: '60%',
|
||||
splitNumber: 4
|
||||
},
|
||||
series: [{
|
||||
type: 'radar',
|
||||
data: [{
|
||||
value: [1.2, 7.2, 0.3, 25, 3, 1],
|
||||
name: '当前水质',
|
||||
areaStyle: { color: 'rgba(64, 158, 255, 0.3)' },
|
||||
lineStyle: { color: '#409eff', width: 2 }
|
||||
}, {
|
||||
value: [2.0, 6.5, 0.4, 30, 5, 2],
|
||||
name: '标准值',
|
||||
lineStyle: { color: '#67c23a', width: 1, type: 'dashed' }
|
||||
}]
|
||||
}]
|
||||
})
|
||||
|
||||
// 图表4: 产销差分析
|
||||
const chart4Instance = echarts.init(chart4.value)
|
||||
chart4Instance.setOption({
|
||||
title: { text: '产销差分析', left: 'center', textStyle: { fontSize: 14 } },
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'shadow' }
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['1月', '2月', '3月', '4月', '5月', '6月'],
|
||||
axisLine: { lineStyle: { color: '#ddd' } }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '百分比(%)',
|
||||
axisLine: { show: false },
|
||||
splitLine: { lineStyle: { type: 'dashed', color: '#eee' } }
|
||||
},
|
||||
series: [{
|
||||
name: '产销差率',
|
||||
data: [8.5, 7.8, 6.2, 5.8, 4.9, 4.2],
|
||||
type: 'bar',
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#f56c6c' },
|
||||
{ offset: 1, color: '#e6a23c' }
|
||||
])
|
||||
},
|
||||
barWidth: '40%'
|
||||
}]
|
||||
})
|
||||
|
||||
// 图表5: 设备运行状态
|
||||
const chart5Instance = echarts.init(chart5.value)
|
||||
chart5Instance.setOption({
|
||||
title: { text: '设备运行状态', left: 'center', textStyle: { fontSize: 14 } },
|
||||
tooltip: { trigger: 'item' },
|
||||
series: [{
|
||||
name: '设备状态',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
center: ['50%', '60%'],
|
||||
data: [
|
||||
{ value: 142, name: '正常运行', itemStyle: { color: '#67c23a' } },
|
||||
{ value: 10, name: '维护中', itemStyle: { color: '#e6a23c' } },
|
||||
{ value: 4, name: '故障', itemStyle: { color: '#f56c6c' } }
|
||||
],
|
||||
label: {
|
||||
show: true,
|
||||
formatter: '{b}\n{c}台'
|
||||
}
|
||||
}]
|
||||
})
|
||||
|
||||
// 图表6: 营收指标
|
||||
const chart6Instance = echarts.init(chart6.value)
|
||||
chart6Instance.setOption({
|
||||
title: { text: '营收指标趋势', left: 'center', textStyle: { fontSize: 16 } },
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: '{b}<br/>营收: {c}万元'
|
||||
},
|
||||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['1月', '2月', '3月', '4月', '5月', '6月'],
|
||||
axisLine: { lineStyle: { color: '#ddd' } }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '营收(万元)',
|
||||
nameTextStyle: { color: '#666' },
|
||||
splitLine: { lineStyle: { type: 'dashed', color: '#eee' } }
|
||||
},
|
||||
series: [{
|
||||
name: '营收额',
|
||||
data: [850, 920, 980, 1050, 1120, 1200],
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(103, 194, 58, 0.3)' },
|
||||
{ offset: 1, color: 'rgba(103, 194, 58, 0.05)' }
|
||||
])
|
||||
},
|
||||
lineStyle: { width: 3 },
|
||||
itemStyle: { color: '#67c23a' }
|
||||
}, {
|
||||
name: '目标',
|
||||
data: [800, 850, 900, 950, 1000, 1050],
|
||||
type: 'line',
|
||||
lineStyle: {
|
||||
type: 'dashed',
|
||||
color: '#909399',
|
||||
width: 2
|
||||
},
|
||||
itemStyle: { color: '#909399' }
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
// 监听窗口大小变化
|
||||
const handleResize = () => {
|
||||
charts.forEach(chart => {
|
||||
if (chart) chart.resize()
|
||||
})
|
||||
}
|
||||
|
||||
let charts: any[] = []
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
initCharts()
|
||||
|
||||
// 收集图表实例
|
||||
charts = [
|
||||
echarts.getInstanceByDom(chart1.value),
|
||||
echarts.getInstanceByDom(chart2.value),
|
||||
echarts.getInstanceByDom(chart3.value),
|
||||
echarts.getInstanceByDom(chart4.value),
|
||||
echarts.getInstanceByDom(chart5.value),
|
||||
echarts.getInstanceByDom(chart6.value)
|
||||
].filter(Boolean)
|
||||
|
||||
// 添加窗口大小变化监听
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stat-card { text-align:center; padding:10px }
|
||||
.stat-value { font-size:28px; font-weight:bold; color:#2a5298 }
|
||||
.stat-title { color:#666; margin-top:8px }
|
||||
.dashboard {
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.dashboard-title {
|
||||
text-align: center;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 20px;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
height: 100px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: none;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 15px;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-title {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-trend {
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.trend-up {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.trend-down {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.charts-row {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
height: 320px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 260px;
|
||||
}
|
||||
|
||||
.chart-container-small {
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.alarm-card {
|
||||
height: 300px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.alarm-list {
|
||||
height: 240px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.alarm-item {
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 4px;
|
||||
border-left: 4px solid;
|
||||
background: rgba(255,255,255,0.8);
|
||||
}
|
||||
|
||||
.alarm-item.warning {
|
||||
border-left-color: #e6a23c;
|
||||
background: rgba(230, 162, 60, 0.1);
|
||||
}
|
||||
|
||||
.alarm-item.danger {
|
||||
border-left-color: #f56c6c;
|
||||
background: rgba(245, 108, 108, 0.1);
|
||||
}
|
||||
|
||||
.alarm-item.info {
|
||||
border-left-color: #909399;
|
||||
background: rgba(144, 147, 153, 0.1);
|
||||
}
|
||||
|
||||
.alarm-time {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.alarm-content {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.alarm-location {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,872 @@
|
||||
<template>
|
||||
<div class="operation-dashboard">
|
||||
<div class="dashboard-header">
|
||||
<h1>供水运营专题大屏</h1>
|
||||
<div class="header-info">
|
||||
<span>{{ currentTime }}</span>
|
||||
<span>•</span>
|
||||
<span>在线设备: {{ onlineDevices }}/{{ totalDevices }}</span>
|
||||
<span>•</span>
|
||||
<span>系统状态: <span class="status-normal">正常运行</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 核心指标行 -->
|
||||
<div class="core-metrics">
|
||||
<div class="metric-card" v-for="metric in coreMetrics" :key="metric.name">
|
||||
<div class="metric-icon" :style="{ backgroundColor: metric.color }">
|
||||
<el-icon><component :is="metric.icon" /></el-icon>
|
||||
</div>
|
||||
<div class="metric-content">
|
||||
<div class="metric-value">{{ metric.value }} <span class="metric-unit">{{ metric.unit }}</span></div>
|
||||
<div class="metric-title">{{ metric.title }}</div>
|
||||
<div class="metric-trend" :class="metric.trend">
|
||||
<el-icon><trend-arrow /></el-icon>
|
||||
{{ metric.change }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主要图表区域 -->
|
||||
<div class="charts-grid">
|
||||
<!-- 左侧图表区域 -->
|
||||
<div class="left-charts">
|
||||
<!-- 供水趋势图 -->
|
||||
<div class="chart-panel">
|
||||
<div class="panel-header">
|
||||
<h3>供水趋势分析</h3>
|
||||
<div class="time-selector">
|
||||
<el-radio-group v-model="timeRange" size="small">
|
||||
<el-radio-button label="day">今日</el-radio-button>
|
||||
<el-radio-button label="week">本周</el-radio-button>
|
||||
<el-radio-button label="month">本月</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-container" ref="supplyTrendChart" />
|
||||
</div>
|
||||
|
||||
<!-- 水质分布 -->
|
||||
<div class="chart-panel">
|
||||
<div class="panel-header">
|
||||
<h3>水质监测分布</h3>
|
||||
</div>
|
||||
<div class="chart-container" ref="waterQualityChart" />
|
||||
</div>
|
||||
|
||||
<!-- 报警统计 -->
|
||||
<div class="chart-panel">
|
||||
<div class="panel-header">
|
||||
<h3>报警统计</h3>
|
||||
</div>
|
||||
<div class="chart-container" ref="alarmChart" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 中间图表区域 -->
|
||||
<div class="center-charts">
|
||||
<!-- 空间分布 -->
|
||||
<div class="chart-panel large-panel">
|
||||
<div class="panel-header">
|
||||
<h3>管网空间分布</h3>
|
||||
</div>
|
||||
<div class="map-container" ref="spatialMap">
|
||||
<div class="map-placeholder">
|
||||
<el-icon><map-location /></el-icon>
|
||||
<p>GIS地图集成区域</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 设备状态 -->
|
||||
<div class="chart-panel">
|
||||
<div class="panel-header">
|
||||
<h3>设备运行状态</h3>
|
||||
</div>
|
||||
<div class="chart-container" ref="deviceStatusChart" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧图表区域 -->
|
||||
<div class="right-charts">
|
||||
<!-- 营收分析 -->
|
||||
<div class="chart-panel">
|
||||
<div class="panel-header">
|
||||
<h3>营收指标分析</h3>
|
||||
</div>
|
||||
<div class="chart-container" ref="revenueChart" />
|
||||
</div>
|
||||
|
||||
<!-- 实时报警 -->
|
||||
<div class="chart-panel">
|
||||
<div class="panel-header">
|
||||
<h3>实时报警监控</h3>
|
||||
</div>
|
||||
<div class="alarm-list">
|
||||
<div v-for="alarm in realTimeAlarms" :key="alarm.id" class="alarm-item" :class="alarm.level">
|
||||
<div class="alarm-time">{{ alarm.time }}</div>
|
||||
<div class="alarm-title">{{ alarm.title }}</div>
|
||||
<div class="alarm-location">{{ alarm.location }}</div>
|
||||
<div class="alarm-status">{{ alarm.status }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 能耗分析 -->
|
||||
<div class="chart-panel">
|
||||
<div class="panel-header">
|
||||
<h3>能耗分析</h3>
|
||||
</div>
|
||||
<div class="chart-container" ref="energyChart" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部统计 -->
|
||||
<div class="bottom-stats">
|
||||
<div class="stat-item" v-for="stat in bottomStats" :key="stat.name">
|
||||
<div class="stat-label">{{ stat.label }}</div>
|
||||
<div class="stat-value">{{ stat.value }}</div>
|
||||
<div class="stat-trend" :class="stat.trend">
|
||||
<el-icon><trend-arrow /></el-icon>
|
||||
{{ stat.change }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import {
|
||||
WaterFilled,
|
||||
TrendCharts,
|
||||
PieChart,
|
||||
DataLine,
|
||||
WarningFilled,
|
||||
MapLocation,
|
||||
Money,
|
||||
Charging,
|
||||
Timer
|
||||
} from '@element-plus/icons-vue'
|
||||
|
||||
// 当前时间
|
||||
const currentTime = ref('')
|
||||
const onlineDevices = ref(156)
|
||||
const totalDevices = ref(178)
|
||||
|
||||
// 核心指标
|
||||
const coreMetrics = ref([
|
||||
{
|
||||
title: '进水总量',
|
||||
value: '12,580',
|
||||
unit: 'm³',
|
||||
icon: 'WaterFilled',
|
||||
color: '#409eff',
|
||||
change: '+5.2%',
|
||||
trend: 'trend-up'
|
||||
},
|
||||
{
|
||||
title: '出水总量',
|
||||
value: '11,230',
|
||||
unit: 'm³',
|
||||
icon: 'WaterFilled',
|
||||
color: '#67c23a',
|
||||
change: '+3.8%',
|
||||
trend: 'trend-up'
|
||||
},
|
||||
{
|
||||
title: '产销差率',
|
||||
value: '10.8',
|
||||
unit: '%',
|
||||
icon: 'TrendCharts',
|
||||
color: '#e6a23c',
|
||||
change: '-2.1%',
|
||||
trend: 'trend-down'
|
||||
},
|
||||
{
|
||||
title: '营收额',
|
||||
value: '85.2',
|
||||
unit: '万元',
|
||||
icon: 'Money',
|
||||
color: '#67c23a',
|
||||
change: '+12.5%',
|
||||
trend: 'trend-up'
|
||||
},
|
||||
{
|
||||
title: '平均水质',
|
||||
value: '98.5',
|
||||
unit: '分',
|
||||
icon: 'DataLine',
|
||||
color: '#409eff',
|
||||
change: '+1.2',
|
||||
trend: 'trend-up'
|
||||
},
|
||||
{
|
||||
title: '报警次数',
|
||||
value: '3',
|
||||
unit: '次',
|
||||
icon: 'WarningFilled',
|
||||
color: '#f56c6c',
|
||||
change: '-15.8%',
|
||||
trend: 'trend-down'
|
||||
}
|
||||
])
|
||||
|
||||
// 时间范围
|
||||
const timeRange = ref('day')
|
||||
|
||||
// 实时报警
|
||||
const realTimeAlarms = ref([
|
||||
{
|
||||
id: 1,
|
||||
time: '08:23:45',
|
||||
title: '压力异常波动',
|
||||
location: '精芒片区',
|
||||
level: 'warning',
|
||||
status: '处理中'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
time: '08:15:32',
|
||||
title: '浊度超标',
|
||||
location: '一体化水厂',
|
||||
level: 'danger',
|
||||
status: '紧急处理'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
time: '07:58:21',
|
||||
title: '设备离线',
|
||||
location: '托里片区',
|
||||
level: 'info',
|
||||
status: '已恢复'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
time: '07:45:18',
|
||||
title: '流量异常',
|
||||
location: '八家户片区',
|
||||
level: 'warning',
|
||||
status: '监控中'
|
||||
}
|
||||
])
|
||||
|
||||
// 底部统计
|
||||
const bottomStats = ref([
|
||||
{
|
||||
label: '覆盖率',
|
||||
value: '98.2%',
|
||||
change: '+0.5%',
|
||||
trend: 'trend-up'
|
||||
},
|
||||
{
|
||||
label: '漏损率',
|
||||
value: '5.8%',
|
||||
change: '-1.2%',
|
||||
trend: 'trend-down'
|
||||
},
|
||||
{
|
||||
label: '投诉率',
|
||||
value: '0.3%',
|
||||
change: '-0.1%',
|
||||
trend: 'trend-down'
|
||||
},
|
||||
{
|
||||
label: '设备完好率',
|
||||
value: '95.6%',
|
||||
change: '+1.8%',
|
||||
trend: 'trend-up'
|
||||
}
|
||||
])
|
||||
|
||||
// 图表引用
|
||||
const supplyTrendChart = ref()
|
||||
const waterQualityChart = ref()
|
||||
const alarmChart = ref()
|
||||
const spatialMap = ref()
|
||||
const deviceStatusChart = ref()
|
||||
const revenueChart = ref()
|
||||
const energyChart = ref()
|
||||
|
||||
// 更新时间
|
||||
const updateTime = () => {
|
||||
const now = new Date()
|
||||
currentTime.value = now.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化图表
|
||||
const initCharts = () => {
|
||||
// 供水趋势图
|
||||
const trendChart = echarts.init(supplyTrendChart.value)
|
||||
trendChart.setOption({
|
||||
backgroundColor: 'transparent',
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(255,255,255,0.9)',
|
||||
borderColor: '#ddd',
|
||||
textStyle: { color: '#333' }
|
||||
},
|
||||
grid: {
|
||||
top: '60px',
|
||||
right: '20px',
|
||||
bottom: '40px',
|
||||
left: '60px',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00'],
|
||||
axisLine: { lineStyle: { color: '#ddd' } },
|
||||
axisLabel: { color: '#666' }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '流量(m³/h)',
|
||||
nameTextStyle: { color: '#666' },
|
||||
axisLine: { show: false },
|
||||
splitLine: { lineStyle: { type: 'dashed', color: '#eee' } }
|
||||
},
|
||||
series: [{
|
||||
name: '进水',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [320, 280, 450, 520, 480, 380],
|
||||
itemStyle: { color: '#409eff' },
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(64, 158, 255, 0.3)' },
|
||||
{ offset: 1, color: 'rgba(64, 158, 255, 0.05)' }
|
||||
])
|
||||
}
|
||||
}, {
|
||||
name: '出水',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [300, 260, 420, 480, 450, 350],
|
||||
itemStyle: { color: '#67c23a' },
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(103, 194, 58, 0.3)' },
|
||||
{ offset: 1, color: 'rgba(103, 194, 58, 0.05)' }
|
||||
])
|
||||
}
|
||||
}]
|
||||
})
|
||||
|
||||
// 水质监测分布
|
||||
const qualityChart = echarts.init(waterQualityChart.value)
|
||||
qualityChart.setOption({
|
||||
backgroundColor: 'transparent',
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
backgroundColor: 'rgba(255,255,255,0.9)',
|
||||
borderColor: '#ddd',
|
||||
textStyle: { color: '#333' }
|
||||
},
|
||||
radar: {
|
||||
indicator: [
|
||||
{ name: '浊度', max: 5 },
|
||||
{ name: 'pH值', max: 14 },
|
||||
{ name: '余氯', max: 1 },
|
||||
{ name: '菌落', max: 100 },
|
||||
{ name: '色度', max: 15 },
|
||||
{ name: '嗅味', max: 3 }
|
||||
],
|
||||
radius: '60%',
|
||||
splitNumber: 4,
|
||||
axisLine: { lineStyle: { color: '#ddd' } },
|
||||
splitLine: { lineStyle: { color: '#eee' } },
|
||||
name: { textStyle: { color: '#666' } }
|
||||
},
|
||||
series: [{
|
||||
type: 'radar',
|
||||
data: [{
|
||||
value: [1.2, 7.2, 0.3, 25, 3, 1],
|
||||
name: '当前水质',
|
||||
areaStyle: { color: 'rgba(64, 158, 255, 0.3)' },
|
||||
lineStyle: { color: '#409eff', width: 2 }
|
||||
}]
|
||||
}]
|
||||
})
|
||||
|
||||
// 报警统计
|
||||
const alarmChartInstance = echarts.init(alarmChart.value)
|
||||
alarmChartInstance.setOption({
|
||||
backgroundColor: 'transparent',
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'shadow' }
|
||||
},
|
||||
grid: {
|
||||
top: '60px',
|
||||
right: '20px',
|
||||
bottom: '40px',
|
||||
left: '60px',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['压力报警', '水质报警', '设备报警', '漏损报警', '通讯报警'],
|
||||
axisLabel: { color: '#666' }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: { color: '#666' },
|
||||
splitLine: { lineStyle: { type: 'dashed', color: '#eee' } }
|
||||
},
|
||||
series: [{
|
||||
type: 'bar',
|
||||
data: [12, 8, 15, 5, 3],
|
||||
itemStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: '#f56c6c' },
|
||||
{ offset: 1, color: '#e6a23c' }
|
||||
])
|
||||
}
|
||||
}]
|
||||
})
|
||||
|
||||
// 设备状态
|
||||
const deviceChart = echarts.init(deviceStatusChart.value)
|
||||
deviceChart.setOption({
|
||||
backgroundColor: 'transparent',
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a}<br/>{b}: {c} ({d}%)'
|
||||
},
|
||||
series: [{
|
||||
name: '设备状态',
|
||||
type: 'pie',
|
||||
radius: ['50%', '70%'],
|
||||
center: ['50%', '50%'],
|
||||
data: [
|
||||
{ value: 142, name: '正常运行', itemStyle: { color: '#67c23a' } },
|
||||
{ value: 10, name: '维护中', itemStyle: { color: '#e6a23c' } },
|
||||
{ value: 4, name: '故障', itemStyle: { color: '#f56c6c' } },
|
||||
{ value: 22, name: '离线', itemStyle: { color: '#909399' } }
|
||||
],
|
||||
label: {
|
||||
show: true,
|
||||
formatter: '{b}\n{c}',
|
||||
color: '#666'
|
||||
}
|
||||
}]
|
||||
})
|
||||
|
||||
// 营收分析
|
||||
const revenueChartInstance = echarts.init(revenueChart.value)
|
||||
revenueChartInstance.setOption({
|
||||
backgroundColor: 'transparent',
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
grid: {
|
||||
top: '60px',
|
||||
right: '20px',
|
||||
bottom: '40px',
|
||||
left: '60px',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['1月', '2月', '3月', '4月', '5月', '6月'],
|
||||
axisLabel: { color: '#666' }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '营收(万元)',
|
||||
axisLabel: { color: '#666' },
|
||||
splitLine: { lineStyle: { type: 'dashed', color: '#eee' } }
|
||||
},
|
||||
series: [{
|
||||
name: '营收',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [850, 920, 980, 1050, 1120, 1200],
|
||||
itemStyle: { color: '#67c23a' },
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(103, 194, 58, 0.3)' },
|
||||
{ offset: 1, color: 'rgba(103, 194, 58, 0.05)' }
|
||||
])
|
||||
}
|
||||
}]
|
||||
})
|
||||
|
||||
// 能耗分析
|
||||
const energyChartInstance = echarts.init(energyChart.value)
|
||||
energyChartInstance.setOption({
|
||||
backgroundColor: 'transparent',
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
grid: {
|
||||
top: '60px',
|
||||
right: '20px',
|
||||
bottom: '40px',
|
||||
left: '60px',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00'],
|
||||
axisLabel: { color: '#666' }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '能耗(kWh)',
|
||||
axisLabel: { color: '#666' },
|
||||
splitLine: { lineStyle: { type: 'dashed', color: '#eee' } }
|
||||
},
|
||||
series: [{
|
||||
name: '能耗',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [120, 80, 200, 180, 160, 100],
|
||||
itemStyle: { color: '#e6a23c' },
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: 'rgba(230, 162, 60, 0.3)' },
|
||||
{ offset: 1, color: 'rgba(230, 162, 60, 0.05)' }
|
||||
])
|
||||
}
|
||||
}]
|
||||
})
|
||||
|
||||
// 存储图表实例
|
||||
window.charts = [
|
||||
trendChart, qualityChart, alarmChartInstance,
|
||||
deviceChart, revenueChartInstance, energyChartInstance
|
||||
]
|
||||
}
|
||||
|
||||
// 时间范围变化处理
|
||||
const handleTimeRangeChange = (value: string) => {
|
||||
// 这里可以更新图表数据
|
||||
console.log('时间范围变化:', value)
|
||||
}
|
||||
|
||||
// 窗口大小变化处理
|
||||
const handleResize = () => {
|
||||
if (window.charts) {
|
||||
window.charts.forEach(chart => {
|
||||
chart && chart.resize()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 生命周期钩子
|
||||
let timeInterval: number
|
||||
|
||||
onMounted(() => {
|
||||
updateTime()
|
||||
timeInterval = window.setInterval(updateTime, 1000)
|
||||
|
||||
nextTick(() => {
|
||||
initCharts()
|
||||
window.addEventListener('resize', handleResize)
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.clearInterval(timeInterval)
|
||||
window.removeEventListener('resize', handleResize)
|
||||
if (window.charts) {
|
||||
window.charts.forEach(chart => {
|
||||
chart && chart.dispose()
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.operation-dashboard {
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
|
||||
color: #fff;
|
||||
min-height: 100vh;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.dashboard-header h1 {
|
||||
font-size: 36px;
|
||||
margin-bottom: 10px;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.header-info {
|
||||
font-size: 16px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.status-normal {
|
||||
color: #67c23a;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.core-metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
background: rgba(255,255,255,0.1);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.metric-card:hover {
|
||||
transform: translateY(-2px);
|
||||
background: rgba(255,255,255,0.15);
|
||||
}
|
||||
|
||||
.metric-icon {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 15px;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.metric-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.metric-unit {
|
||||
font-size: 14px;
|
||||
opacity: 0.8;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.metric-title {
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.metric-trend {
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.trend-up {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.trend-down {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.charts-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.5fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.chart-panel {
|
||||
background: rgba(255,255,255,0.1);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chart-panel.large-panel {
|
||||
grid-row: span 2;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.panel-header h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
flex: 1;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.map-container {
|
||||
flex: 1;
|
||||
min-height: 300px;
|
||||
background: rgba(0,0,0,0.2);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgba(255,255,255,0.7);
|
||||
}
|
||||
|
||||
.map-placeholder {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.map-placeholder .el-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.alarm-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.alarm-item {
|
||||
background: rgba(255,255,255,0.1);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin-bottom: 10px;
|
||||
border-left: 4px solid;
|
||||
}
|
||||
|
||||
.alarm-item.warning {
|
||||
border-left-color: #e6a23c;
|
||||
background: rgba(230, 162, 60, 0.1);
|
||||
}
|
||||
|
||||
.alarm-item.danger {
|
||||
border-left-color: #f56c6c;
|
||||
background: rgba(245, 108, 108, 0.1);
|
||||
}
|
||||
|
||||
.alarm-item.info {
|
||||
border-left-color: #909399;
|
||||
background: rgba(144, 147, 153, 0.1);
|
||||
}
|
||||
|
||||
.alarm-time {
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.alarm-title {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.alarm-location {
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.alarm-status {
|
||||
font-size: 12px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.bottom-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
background: rgba(255,255,255,0.1);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.time-selector {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.time-selector :deep(.el-radio) {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.time-selector :deep(.el-radio__label) {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 1400px) {
|
||||
.core-metrics {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.charts-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.bottom-stats {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.core-metrics {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.bottom-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.metric-card, .chart-panel, .stat-item {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user