Files
water-management-system/frontend/public/operation-dashboard.html
T

849 lines
32 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>供水运营专题大屏</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://unpkg.com/echarts@5.5.0/dist/echarts.min.js"></script>
<script src="https://unpkg.com/echarts-liquidfill@3.0.0/dist/echarts-liquidfill.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/element-plus@2.8.0/dist/index.css">
<script src="https://unpkg.com/element-plus@2.8.0/dist/index.full.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
overflow: hidden;
}
.operation-dashboard {
padding: 20px;
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
color: #fff;
min-height: 100vh;
}
.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);
}
.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 .el-radio {
margin-right: 0;
}
.time-selector .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;
}
}
.map-placeholder {
text-align: center;
}
.map-placeholder .el-icon {
font-size: 48px;
margin-bottom: 10px;
}
</style>
</head>
<body>
<div id="app">
<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" style="width: 100%; height: 300px;"></div>
</div>
<!-- 水质分布 -->
<div class="chart-panel">
<div class="panel-header">
<h3>水质监测分布</h3>
</div>
<div class="chart-container" ref="waterQualityChart" style="width: 100%; height: 300px;"></div>
</div>
<!-- 报警统计 -->
<div class="chart-panel">
<div class="panel-header">
<h3>报警统计</h3>
</div>
<div class="chart-container" ref="alarmChart" style="width: 100%; height: 300px;"></div>
</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" style="width: 100%; height: 300px;"></div>
</div>
</div>
<!-- 右侧图表区域 -->
<div class="right-charts">
<!-- 营收分析 -->
<div class="chart-panel">
<div class="panel-header">
<h3>营收指标分析</h3>
</div>
<div class="chart-container" ref="revenueChart" style="width: 100%; height: 300px;"></div>
</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" style="width: 100%; height: 300px;"></div>
</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>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
currentTime: '',
onlineDevices: 156,
totalDevices: 178,
coreMetrics: [
{
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'
}
],
timeRange: 'day',
realTimeAlarms: [
{
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: '监控中'
}
],
bottomStats: [
{
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'
}
],
charts: {}
}
},
methods: {
updateTime() {
const now = new Date();
this.currentTime = now.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
},
initCharts() {
// 供水趋势图
const trendChart = echarts.init(document.getElementById('supplyTrendChart'));
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(document.getElementById('waterQualityChart'));
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 alarmChart = echarts.init(document.getElementById('alarmChart'));
alarmChart.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(document.getElementById('deviceStatusChart'));
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 revenueChart = echarts.init(document.getElementById('revenueChart'));
revenueChart.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 energyChart = echarts.init(document.getElementById('energyChart'));
energyChart.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)' }
])
}
}]
});
this.charts = {
trend: trendChart,
quality: qualityChart,
alarm: alarmChart,
device: deviceChart,
revenue: revenueChart,
energy: energyChart
};
}
},
mounted() {
this.updateTime();
setInterval(this.updateTime, 1000);
this.$nextTick(() => {
this.initCharts();
window.addEventListener('resize', () => {
Object.values(this.charts).forEach(chart => {
chart && chart.resize();
});
});
});
}
}).use(ElementPlus).mount('#app');
</script>
</body>
</html>