<template>
|
<div class="data-dashboard">
|
<!-- 顶部指标卡片区 -->
|
<div class="top-cards">
|
<div class="cards-container">
|
<!-- 时间周期选项卡片 -->
|
<div class="time-period-card">
|
<div class="card-title">时间周期</div>
|
<div class="time-tab-container">
|
<div
|
v-for="tab in timeTabs"
|
:key="tab.value"
|
class="time-tab"
|
:class="{ active: activeTime === tab.value }"
|
@click="handleTimeChange(tab)"
|
>
|
{{ tab.label }}
|
</div>
|
</div>
|
</div>
|
|
<!-- 超标数 -->
|
<div class="metric-card">
|
<div class="card-header">
|
<div class="card-title">{{ getPeriodLabel() }}超标数</div>
|
<div class="card-icon warning-icon">⚠</div>
|
</div>
|
<div class="card-value">{{ metrics.overStandardCount }}</div>
|
<div class="card-trend">
|
<span
|
class="trend-arrow"
|
:class="{ up: metrics.overStandardTrend > 0, down: metrics.overStandardTrend < 0 }"
|
>
|
{{ metrics.overStandardTrend > 0 ? '↑' : '↓' }}
|
</span>
|
<span class="trend-text">{{ Math.abs(metrics.overStandardTrend) }}%</span>
|
<span class="trend-label">{{ getCompareLabel() }}</span>
|
</div>
|
</div>
|
|
<!-- 在线率 -->
|
<div class="metric-card">
|
<div class="card-header">
|
<div class="card-title">{{ getPeriodLabel() }}在线率</div>
|
<div class="card-icon online-icon">🟢</div>
|
</div>
|
<div class="card-value">{{ metrics.onlineRate }}%</div>
|
<div class="card-trend">
|
<span
|
class="trend-arrow"
|
:class="{ up: metrics.onlineRateTrend > 0, down: metrics.onlineRateTrend < 0 }"
|
>
|
{{ metrics.onlineRateTrend > 0 ? '↑' : '↓' }}
|
</span>
|
<span class="trend-text">{{ Math.abs(metrics.onlineRateTrend) }}%</span>
|
<span class="trend-label">{{ getCompareLabel() }}</span>
|
</div>
|
</div>
|
|
<!-- 净化器运行效率 -->
|
<div class="metric-card">
|
<div class="card-header">
|
<div class="card-title">{{ getPeriodLabel() }}净化器运行效率</div>
|
<div class="card-icon efficiency-icon">⚙</div>
|
</div>
|
<div class="card-value">{{ metrics.purifierEfficiency }}%</div>
|
<div class="card-trend">
|
<span
|
class="trend-arrow"
|
:class="{
|
up: metrics.purifierEfficiencyTrend > 0,
|
down: metrics.purifierEfficiencyTrend < 0,
|
}"
|
>
|
{{ metrics.purifierEfficiencyTrend > 0 ? '↑' : '↓' }}
|
</span>
|
<span class="trend-text">{{ Math.abs(metrics.purifierEfficiencyTrend) }}%</span>
|
<span class="trend-label">{{ getCompareLabel() }}</span>
|
</div>
|
</div>
|
|
<!-- 任务完成率 -->
|
<div class="metric-card">
|
<div class="card-header">
|
<div class="card-title">{{ getPeriodLabel() }}任务完成率</div>
|
<div class="card-icon task-icon">✅</div>
|
</div>
|
<div class="card-value">{{ metrics.taskCompletionRate }}%</div>
|
<div class="card-trend">
|
<span
|
class="trend-arrow"
|
:class="{
|
up: metrics.taskCompletionRateTrend > 0,
|
down: metrics.taskCompletionRateTrend < 0,
|
}"
|
>
|
{{ metrics.taskCompletionRateTrend > 0 ? '↑' : '↓' }}
|
</span>
|
<span class="trend-text">{{ Math.abs(metrics.taskCompletionRateTrend) }}%</span>
|
<span class="trend-label">{{ getCompareLabel() }}</span>
|
</div>
|
</div>
|
</div>
|
</div>
|
|
<!-- 中部GIS地图区 -->
|
<div class="map-section">
|
<div id="map" class="map-container"></div>
|
|
<!-- 地图点位弹窗 -->
|
<el-dialog v-model="dialogVisible" title="企业实时数据" width="400px">
|
<div class="dialog-content">
|
<el-descriptions :column="1" border>
|
<el-descriptions-item label="企业名称">{{
|
selectedPoint.enterpriseName
|
}}</el-descriptions-item>
|
<el-descriptions-item label="设备编号">{{
|
selectedPoint.deviceId
|
}}</el-descriptions-item>
|
<el-descriptions-item label="油烟浓度"
|
>{{ selectedPoint.oilSmokeConcentration }} mg/m³</el-descriptions-item
|
>
|
<el-descriptions-item label="颗粒物"
|
>{{ selectedPoint.particulateMatter }} mg/m³</el-descriptions-item
|
>
|
<el-descriptions-item label="非甲烷总烃"
|
>{{ selectedPoint.nonMethaneHydrocarbon }} mg/m³</el-descriptions-item
|
>
|
<el-descriptions-item label="监测时间">{{
|
selectedPoint.monitoringTime
|
}}</el-descriptions-item>
|
<el-descriptions-item label="超标情况">
|
<el-tag :type="selectedPoint.isOverStandard ? 'danger' : 'success'">
|
{{ selectedPoint.isOverStandard ? '超标' : '正常' }}
|
</el-tag>
|
</el-descriptions-item>
|
</el-descriptions>
|
</div>
|
<template #footer>
|
<span class="dialog-footer">
|
<el-button @click="dialogVisible = false">关闭</el-button>
|
<el-button type="primary" @click="viewDetails">查看详情</el-button>
|
</span>
|
</template>
|
</el-dialog>
|
</div>
|
|
<!-- 右侧实时监测总览区 -->
|
<div class="overview-section">
|
<div class="overview-card">
|
<div class="overview-title">实时监测总览</div>
|
|
<div class="overview-items-container">
|
<div class="overview-item">
|
<div class="overview-label">餐饮店铺总数</div>
|
<div class="overview-value">{{ overview.totalShops }}</div>
|
</div>
|
|
<div class="overview-item">
|
<div class="overview-label">在线设备数</div>
|
<div class="overview-value">{{ overview.onlineDevices }}</div>
|
</div>
|
|
<div class="overview-item">
|
<div class="overview-label">离线设备数</div>
|
<div class="overview-value">{{ overview.offlineDevices }}</div>
|
</div>
|
</div>
|
|
<!-- 设备状态饼图 -->
|
<div class="device-status-chart">
|
<canvas id="deviceStatusChart"></canvas>
|
</div>
|
</div>
|
</div>
|
</div>
|
</template>
|
|
<script>
|
import * as echarts from 'echarts'
|
|
export default {
|
name: 'DataDashboard',
|
data() {
|
return {
|
activeTime: 'day',
|
timeTabs: [
|
{ label: '日', value: 'day' },
|
{ label: '周', value: 'week' },
|
{ label: '月', value: 'month' },
|
],
|
dialogVisible: false,
|
selectedPoint: {
|
enterpriseName: '',
|
deviceId: '',
|
oilSmokeConcentration: 0,
|
particulateMatter: 0,
|
nonMethaneHydrocarbon: 0,
|
monitoringTime: '',
|
isOverStandard: false,
|
},
|
metrics: {
|
overStandardCount: 12,
|
overStandardTrend: 5,
|
onlineRate: 92,
|
onlineRateTrend: 2,
|
purifierEfficiency: 85,
|
purifierEfficiencyTrend: -3,
|
taskCompletionRate: 78,
|
taskCompletionRateTrend: 10,
|
},
|
overview: {
|
totalShops: 245,
|
onlineDevices: 220,
|
offlineDevices: 25,
|
},
|
map: null,
|
refreshTimer: null,
|
}
|
},
|
mounted() {
|
this.initMap()
|
this.initDeviceStatusChart()
|
this.startAutoRefresh()
|
},
|
beforeUnmount() {
|
if (this.refreshTimer) {
|
clearInterval(this.refreshTimer)
|
}
|
},
|
methods: {
|
handleTimeChange(tab) {
|
this.activeTime = tab.value
|
// 模拟切换时间周期后的数据更新
|
this.updateMetrics()
|
},
|
getPeriodLabel() {
|
switch (this.activeTime) {
|
case 'day':
|
return '今日'
|
case 'week':
|
return '本周'
|
case 'month':
|
return '本月'
|
default:
|
return '今日'
|
}
|
},
|
getCompareLabel() {
|
switch (this.activeTime) {
|
case 'day':
|
return '较昨日'
|
case 'week':
|
return '较上周'
|
case 'month':
|
return '较上月'
|
default:
|
return '较昨日'
|
}
|
},
|
updateMetrics() {
|
// 这里应该根据选择的时间周期从接口获取数据
|
// 模拟数据更新
|
setTimeout(() => {
|
this.metrics = {
|
overStandardCount: Math.floor(Math.random() * 30),
|
overStandardTrend: Math.floor(Math.random() * 20) - 10,
|
onlineRate: Math.floor(Math.random() * 20) + 80,
|
onlineRateTrend: Math.floor(Math.random() * 10) - 5,
|
purifierEfficiency: Math.floor(Math.random() * 30) + 70,
|
purifierEfficiencyTrend: Math.floor(Math.random() * 10) - 5,
|
taskCompletionRate: Math.floor(Math.random() * 40) + 60,
|
taskCompletionRateTrend: Math.floor(Math.random() * 15) - 7,
|
}
|
}, 300)
|
},
|
initMap() {
|
// 这里应该初始化真实的GIS地图
|
// 模拟地图初始化
|
const mapElement = document.getElementById('map')
|
if (mapElement) {
|
mapElement.innerHTML = '<div class="map-placeholder">GIS地图加载中...</div>'
|
// 实际项目中这里应该使用真实的地图API,如高德地图、百度地图等
|
}
|
},
|
initDeviceStatusChart() {
|
const chartDom = document.getElementById('deviceStatusChart')
|
if (chartDom) {
|
const chart = echarts.init(chartDom)
|
const option = {
|
tooltip: {
|
trigger: 'item',
|
formatter: '{b}: {c} ({d}%)',
|
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
borderColor: '#ebeef5',
|
borderWidth: 1,
|
textStyle: {
|
color: '#303133',
|
},
|
},
|
legend: {
|
bottom: '0%',
|
left: 'center',
|
textStyle: {
|
color: '#606266',
|
fontSize: 12,
|
},
|
},
|
series: [
|
{
|
name: '设备状态',
|
type: 'pie',
|
radius: ['40%', '65%'],
|
center: ['50%', '45%'],
|
avoidLabelOverlap: false,
|
itemStyle: {
|
borderRadius: 8,
|
borderColor: '#ffffff',
|
borderWidth: 2,
|
shadowBlur: 10,
|
shadowOffsetX: 0,
|
shadowColor: 'rgba(0, 0, 0, 0.1)',
|
},
|
label: {
|
show: true,
|
position: 'outside',
|
formatter: '{b}: {d}%',
|
fontSize: 12,
|
color: '#606266',
|
},
|
labelLine: {
|
show: true,
|
length: 10,
|
length2: 20,
|
lineStyle: {
|
color: '#606266',
|
width: 1,
|
},
|
},
|
data: [
|
{
|
value: this.overview.onlineDevices,
|
name: '在线',
|
itemStyle: {
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
{ offset: 0, color: '#67c23a' },
|
{ offset: 1, color: '#409eff' },
|
]),
|
},
|
},
|
{
|
value: this.overview.offlineDevices,
|
name: '离线',
|
itemStyle: {
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
{ offset: 0, color: '#f56c6c' },
|
{ offset: 1, color: '#e6a23c' },
|
]),
|
},
|
},
|
],
|
},
|
],
|
}
|
chart.setOption(option)
|
|
// 响应式调整
|
window.addEventListener('resize', () => {
|
chart.resize()
|
})
|
}
|
},
|
startAutoRefresh() {
|
// 每30秒自动刷新数据
|
this.refreshTimer = setInterval(() => {
|
this.updateMetrics()
|
// 这里应该同时更新地图点位数据
|
}, 30000)
|
},
|
viewDetails() {
|
// 跳转到企业监控详情页
|
this.$router.push('/monitor/enterprise-detail')
|
},
|
},
|
}
|
</script>
|
|
<style scoped>
|
.data-dashboard {
|
width: 100%;
|
height: 100vh;
|
background-color: #f5f7fa;
|
color: #303133;
|
padding: 20px;
|
box-sizing: border-box;
|
display: grid;
|
grid-template-rows: auto 1fr;
|
grid-template-columns: 1fr 300px;
|
grid-template-areas:
|
'top overview'
|
'map overview';
|
gap: 20px;
|
}
|
|
.top-cards {
|
grid-area: top;
|
}
|
|
.time-period-card {
|
background-color: #ffffff;
|
border: 1px solid #ebeef5;
|
border-radius: 12px;
|
padding: 24px;
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.08);
|
display: flex;
|
flex-direction: column;
|
justify-content: center;
|
}
|
|
.time-period-card .card-title {
|
font-size: 14px;
|
color: #909399;
|
font-weight: 500;
|
margin-bottom: 16px;
|
text-align: center;
|
}
|
|
.time-tab-container {
|
display: flex;
|
flex-direction: column;
|
gap: 8px;
|
width: 100%;
|
}
|
|
.time-tab {
|
padding: 2px 4px;
|
border-radius: 8px;
|
cursor: pointer;
|
font-size: 14px;
|
font-weight: 500;
|
transition: all 0.3s ease;
|
color: #606266;
|
text-align: center;
|
border: 1px solid #ebeef5;
|
background-color: #f9f9f9;
|
}
|
|
.time-tab.active {
|
background-color: #409eff;
|
color: #ffffff;
|
border-color: #409eff;
|
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
|
}
|
|
.time-tab:hover:not(.active) {
|
color: #409eff;
|
border-color: #c6e2ff;
|
background-color: #ecf5ff;
|
}
|
|
.time-tab.active {
|
background-color: #409eff;
|
color: #ffffff;
|
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
|
}
|
|
.time-tab:hover:not(.active) {
|
color: #409eff;
|
background-color: rgba(64, 158, 255, 0.1);
|
}
|
|
.cards-container {
|
display: grid;
|
grid-template-columns: 120px repeat(4, 1fr);
|
gap: 20px;
|
}
|
|
.metric-card {
|
background-color: #ffffff;
|
border: 1px solid #ebeef5;
|
border-radius: 12px;
|
padding: 24px;
|
transition: all 0.3s ease;
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.08);
|
position: relative;
|
overflow: hidden;
|
}
|
|
.metric-card::before {
|
content: '';
|
position: absolute;
|
top: 0;
|
left: 0;
|
width: 4px;
|
height: 100%;
|
background-color: #409eff;
|
}
|
|
.metric-card:nth-child(1)::before {
|
background-color: #f56c6c;
|
}
|
|
.metric-card:nth-child(2)::before {
|
background-color: #67c23a;
|
}
|
|
.metric-card:nth-child(3)::before {
|
background-color: #e6a23c;
|
}
|
|
.metric-card:nth-child(4)::before {
|
background-color: #909399;
|
}
|
|
.metric-card:hover {
|
transform: translateY(-5px);
|
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.12);
|
}
|
|
.card-header {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
margin-bottom: 16px;
|
}
|
|
.card-title {
|
font-size: 14px;
|
color: #909399;
|
font-weight: 500;
|
}
|
|
.card-icon {
|
font-size: 20px;
|
}
|
|
.card-value {
|
font-size: 36px;
|
font-weight: bold;
|
margin: 16px 0;
|
color: #303133;
|
line-height: 1.2;
|
}
|
|
.card-trend {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
font-size: 13px;
|
padding-top: 12px;
|
border-top: 1px solid #f0f2f5;
|
}
|
|
.trend-arrow {
|
font-size: 16px;
|
font-weight: bold;
|
}
|
|
.trend-arrow.up {
|
color: #67c23a;
|
}
|
|
.trend-arrow.down {
|
color: #f56c6c;
|
}
|
|
.trend-text {
|
color: #606266;
|
font-weight: 500;
|
}
|
|
.trend-label {
|
color: #909399;
|
font-size: 12px;
|
}
|
|
.map-section {
|
grid-area: map;
|
background-color: #ffffff;
|
border: 1px solid #ebeef5;
|
border-radius: 8px;
|
position: relative;
|
overflow: hidden;
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
}
|
|
.map-container {
|
width: 100%;
|
height: 100%;
|
position: relative;
|
}
|
|
.map-placeholder {
|
width: 100%;
|
height: 100%;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
color: #909399;
|
font-size: 18px;
|
}
|
|
.overview-section {
|
grid-area: overview;
|
}
|
|
.overview-card {
|
background-color: #ffffff;
|
border: 1px solid #ebeef5;
|
border-radius: 8px;
|
padding: 20px 0px;
|
display: flex;
|
flex-direction: column;
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
}
|
|
.overview-title {
|
font-size: 16px;
|
font-weight: bold;
|
margin-bottom: 20px;
|
text-align: center;
|
color: #303133;
|
}
|
|
.overview-items-container {
|
display: flex;
|
justify-content: space-between;
|
align-items: center;
|
width: 100%;
|
}
|
|
.overview-item {
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
flex: 1;
|
text-align: center;
|
padding: 12px 0;
|
}
|
|
.overview-item:last-child {
|
border-right: none;
|
}
|
|
.overview-label {
|
font-size: 14px;
|
color: #606266;
|
font-weight: 500;
|
margin-bottom: 8px;
|
}
|
|
.overview-value {
|
font-size: 24px;
|
font-weight: bold;
|
color: #303133;
|
}
|
|
.device-status-chart {
|
flex: 1;
|
margin-top: 20px;
|
min-height: 200px;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
/* background-color: #c6e2ff; */
|
}
|
|
.dialog-content {
|
color: #333;
|
}
|
|
.dialog-footer {
|
text-align: right;
|
}
|
|
/* 闪烁效果 */
|
@keyframes blink {
|
0%,
|
100% {
|
opacity: 1;
|
}
|
50% {
|
opacity: 0.5;
|
}
|
}
|
|
.blink {
|
animation: blink 1s infinite;
|
}
|
|
/* 响应式设计 */
|
@media (max-width: 1200px) {
|
.data-dashboard {
|
grid-template-columns: 1fr;
|
grid-template-areas:
|
'top'
|
'map'
|
'overview';
|
height: auto;
|
}
|
|
.cards-container {
|
grid-template-columns: 160px repeat(2, 1fr);
|
}
|
|
.overview-section {
|
height: 300px;
|
}
|
}
|
|
@media (max-width: 768px) {
|
.cards-container {
|
grid-template-columns: 1fr;
|
grid-template-rows: auto repeat(4, auto);
|
}
|
|
.time-period-card {
|
order: -1;
|
}
|
|
.time-tab-container {
|
flex-direction: row;
|
}
|
|
.time-tab {
|
flex: 1;
|
padding: 8px 0;
|
}
|
}
|
</style>
|