项目背景
随着地下轨道交通建设的快速发展,隧道安全监测成为保障运营安全的关键环节。传统的隧道检测主要依靠人工巡检,效率低下且存在安全隐患。为提高检测效率和准确性,隧道检测机器人应运而生,而如何高效地展示和分析机器人采集的数据,成为行业内的重要需求。
本项目基于Vue 3 + ECharts开发了一款专门针对隧道检测机器人的数据可视化大屏,通过直观、动态的数据展示,帮助运维人员实时了解隧道状况和机器人工作状态,提升隧道安全监测的效率和准确性。
功能特点
- 实时数据可视化:展示隧道检测机器人的实时工作状态和检测数据
- 多维度数据展示:通过图表、表格等多种形式展示点云数据、病害信息等
- 病害识别与标记:支持实时识别和标记隧道中的裂缝、渗漏水等病害
- 历史数据对比:提供历史图像和数据的对比分析功能
- 趋势预测:基于历史数据进行病害发展趋势预测
- 告警预警:实时监测并预警异常情况
- 响应式设计:适配不同屏幕尺寸,满足多种显示环境需求
技术栈
- 前端框架:Vue 3 (Composition API +
<script setup>) - 数据可视化:ECharts 5.4.3
- 样式设计:CSS3 (Flexbox, Grid, 响应式布局)
- 构建工具:无需构建工具,单文件HTML直接运行
核心功能模块
1. 系统概览
系统概览模块展示隧道基本信息、机器人实时状态和检测模式等关键信息。运维人员可以快速了解当前检测任务的基本情况和机器人的工作状态。
<div class="system-overview">
<div class="overview-section">
<h3>隧道信息</h3>
<div class="info-grid">
<div class="info-item">
<span class="label">隧道名称:</span>
<span class="value">{{ tunnelInfo.name }}</span>
</div>
<!-- 更多隧道信息 -->
</div>
</div>
<div class="overview-section">
<h3>机器人状态</h3>
<div class="info-grid">
<!-- 机器人状态信息 -->
</div>
</div>
<div class="overview-section">
<h3>检测模式</h3>
<!-- 检测模式信息 -->
</div>
</div>
2. 实时检测数据
该模块包含点云数据实时可视化和病害识别结果两部分。通过ECharts的3D散点图模拟点云数据,直观展示隧道内壁的三维信息,同时通过表格形式展示实时识别出的病害信息。
// 点云数据可视化(使用散点图模拟)
const pointCloudChart = echarts.init(document.getElementById('pointCloudChart'))
const pointCloudData = []
for (let i = 0; i < 1000; i++) {
const value = [
Math.random() * 100,
Math.random() * 100,
Math.random() * 100
]
// 根据值设置颜色,模拟裂缝风险
let color = '#1890ff' // 正常区域蓝色
if (value[2] > 70) {
color = '#f5222d' // 确认裂缝红色
} else if (value[2] > 50) {
color = '#faad14' // 疑似裂缝黄色
}
pointCloudData.push({
value: value,
itemStyle: {
color: color
}
})
}
3. 机器人系统状态
展示机器人吸附系统、传动系统和控制系统的实时状态,帮助运维人员监控机器人的运行状况,及时发现和处理异常情况。
4. 视觉图像与病害标注
显示机器人实时采集的隧道图像,并在图像上标注识别出的病害位置和类型。支持历史图像对比功能,便于观察病害的发展变化。
5. 历史数据与趋势分析
提供历史检测数据的查询和分析功能,包括病害类型分布统计和病害发展趋势预测,帮助运维人员制定更有针对性的维护计划。
6. 告警预警
实时监测系统运行状态和检测数据,当发现异常情况时及时发出告警,并在界面上显示告警信息,确保运维人员能够快速响应和处理。
交互设计
本项目采用直观的交互设计,主要包括以下几种交互方式:
- 表格行点击:点击病害表格中的行,在对应图像上显示病害标记
- 日期选择与对比:选择不同日期进行历史图像对比
- 数据筛选:通过下拉菜单筛选历史数据
- 图表交互:支持图表的缩放、平移等基本交互
响应式设计
为适应不同的显示环境,本项目采用响应式设计,通过CSS媒体查询调整布局和组件大小,确保在不同屏幕尺寸下都能提供良好的用户体验。
/* 响应式布局 */
@media (max-width: 1366px) {
.main-content {
flex-direction: column;
}
.left-panel,
.right-panel {
width: 100%;
}
.point-cloud-container,
.system-status-grid {
flex-direction: column;
height: auto;
}
.point-cloud-container .chart,
.system-status-grid .small-chart {
height: 150px;
}
.alarm-list {
flex-wrap: wrap;
}
}
使用方法
-
环境要求:
- 现代浏览器(Chrome、Firefox、Safari、Edge等)
- 无需安装额外软件或插件
-
使用步骤:
- 下载本项目中的HTML文件
- 在浏览器中直接打开该文件
- 界面加载后即可查看和交互
实现细节
1. 数据模拟
为了演示目的,本项目使用了模拟数据。在实际应用中,可以通过API接口连接到后端系统,获取真实的检测数据。
// 模拟裂缝数据
const crackData = ref([
{
position: 'K3+560 环号 18',
length: '0.36m',
width: '1.64mm',
error: '7.34%',
riskLevel: 3,
riskLevelText: '三级'
},
// 更多模拟数据
])
2. 图表初始化与管理
使用Vue的生命周期钩子函数初始化和管理图表,确保在组件挂载后正确渲染图表,并在组件卸载前清理资源。
// 组件挂载后初始化
onMounted(() => {
initCharts()
animateWorkflow()
})
// 组件卸载前清理定时器
onBeforeUnmount(() => {
if (workflowTimer) {
clearInterval(workflowTimer)
}
})
3. 工作流程动画
通过定时器实现工作流程的动态演示,直观展示机器人的工作状态和进度。
// 模拟工作流程动画
const animateWorkflow = () => {
workflowTimer = setInterval(() => {
currentWorkflowStep.value = (currentWorkflowStep.value + 1) % workflowSteps.value.length
}, 2000)
}
项目优势
- 直观易用:简洁明了的界面设计,使运维人员能够快速上手和使用
- 功能全面:涵盖了隧道检测的各个关键环节,提供全方位的数据支持
- 实时性强:实时展示检测数据和机器人状态,便于及时发现和处理问题
- 分析深入:提供历史数据对比和趋势预测功能,帮助进行更深入的分析
- 部署简单:单文件HTML设计,无需复杂的部署过程
未来改进方向
- 后端集成:接入真实的后端API,获取实时检测数据
- AI增强:集成更先进的AI算法,提高病害识别的准确性
- 数据导出:添加数据导出功能,支持Excel、PDF等格式
- 多端适配:优化移动端体验,支持在平板和手机上查看
- 权限管理:添加用户权限管理功能,适用于多角色场景
结语
隧道检测机器人数据可视化大屏的开发和应用,不仅提高了隧道安全监测的效率和准确性,也为运维人员提供了更直观、更全面的数据支持。本项目采用现代化的前端技术栈,实现了丰富的数据可视化功能,具有良好的用户体验和扩展性。
希望本项目能够为隧道安全监测领域的技术人员提供一些参考和帮助,共同推动隧道检测技术的发展和应用。
隧道检测机器人数据可视化大屏 /* 基础样式 */ body { margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background-color: #f0f2f5; } /* 可视化屏幕 */ .visualization-screen { width: 100%; height: 100vh; background-color: #f0f2f5; display: flex; flex-direction: column; position: relative; overflow: hidden; } /* 系统概览模块 */ .system-overview { height: 15%; background-color: #0e1a2b; color: #fff; padding: 10px 20px; display: flex; justify-content: space-between; align-items: center; border-bottom: 2px solid #1890ff; } .overview-section { flex: 1; } .overview-section h3 { margin: 0 0 10px 0; font-size: 16px; color: #1890ff; } .info-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; } .info-item { display: flex; align-items: center; font-size: 14px; } .info-item .label { color: #909399; margin-right: 8px; } .info-item .value { color: #fff; font-weight: 500; } .info-item .value.with-icon { display: flex; align-items: center; } .info-item .value i { margin-right: 5px; } .progress-container { position: relative; width: 120px; height: 20px; background-color: #1f2937; border-radius: 10px; overflow: hidden; } .progress-bar { height: 100%; background-color: #1890ff; transition: width 0.3s ease; } .progress-text { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #fff; font-size: 12px; font-weight: bold; } /* 主内容区域 */ .main-content { flex: 1; display: flex; padding: 10px; gap: 10px; overflow: hidden; } /* 左侧面板 */ .left-panel { width: 70%; display: flex; flex-direction: column; gap: 10px; overflow: hidden; } /* 右侧面板 */ .right-panel { width: 30%; display: flex; flex-direction: column; gap: 10px; overflow: hidden; } /* 通用模块样式 */ .module { background-color: #fff; border-radius: 6px; padding: 15px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); display: flex; flex-direction: column; overflow: hidden; } .module h2 { margin: 0 0 15px 0; font-size: 18px; color: #0e1a2b; border-bottom: 1px solid #e8e8e8; padding-bottom: 10px; } .sub-module { margin-bottom: 20px; } .sub-module h3 { margin: 0 0 10px 0; font-size: 16px; color: #1890ff; } /* 实时检测数据模块 */ .real-time-detection { flex: 2; overflow: hidden; display: flex; flex-direction: column; } .point-cloud-container { display: flex; gap: 15px; margin-bottom: 15px; height: 200px; } .point-cloud-container .chart { flex: 2; height: 100%; } .point-cloud-params { flex: 1; display: flex; flex-direction: column; justify-content: space-around; } .param-item { display: flex; align-items: center; font-size: 14px; } .param-item .label { color: #606266; margin-right: 8px; } .param-item .value { color: #0e1a2b; font-weight: 500; } /* 病害表格 */ .disease-section { margin-bottom: 15px; } .disease-section h4 { margin: 0 0 10px 0; font-size: 14px; color: #606266; } .table-container { overflow-x: auto; } .disease-table { width: 100%; border-collapse: collapse; font-size: 12px; } .disease-table th, .disease-table td { padding: 8px; text-align: left; border-bottom: 1px solid #e8e8e8; } .disease-table th { background-color: #f5f7fa; color: #606266; font-weight: 500; } .disease-table tr:hover { background-color: #f5f7fa; cursor: pointer; } .risk-level { padding: 2px 6px; border-radius: 3px; font-size: 11px; font-weight: 500; } .risk-1 { background-color: #f0f9ff; color: #52c41a; } .risk-2 { background-color: #fff7e6; color: #faad14; } .risk-3 { background-color: #fff2f0; color: #f5222d; } .water-level { padding: 2px 6px; border-radius: 3px; font-size: 11px; font-weight: 500; } .level-1 { background-color: #f0f9ff; color: #52c41a; } .level-2 { background-color: #fff7e6; color: #faad14; } .level-3 { background-color: #fff2f0; color: #f5222d; } /* 机器人系统状态模块 */ .robot-status { flex: 1; overflow: hidden; } .system-status-grid { display: flex; gap: 15px; margin-bottom: 15px; height: 120px; } .system-status-grid .small-chart { flex: 2; height: 100%; } .self-check-results { flex: 1; display: flex; flex-direction: column; justify-content: space-around; } .self-check-item { display: flex; align-items: center; font-size: 14px; } .self-check-item .label { color: #606266; margin-right: 8px; } .self-check-item .status { padding: 2px 6px; border-radius: 3px; font-size: 11px; font-weight: 500; } .self-check-item .status.normal { background-color: #f0f9ff; color: #52c41a; } .self-check-item .status.warning { background-color: #fff7e6; color: #faad14; } .self-check-item .status.error { background-color: #fff2f0; color: #f5222d; } /* 视觉图像与病害标注模块 */ .visual-image { flex: 2; overflow: hidden; } .camera-container { display: flex; flex-direction: column; gap: 10px; margin-bottom: 15px; } .camera-view { position: relative; width: 100%; height: 150px; background-color: #f5f7fa; border: 1px solid #e8e8e8; border-radius: 4px; overflow: hidden; } .camera-placeholder { width: 100%; height: 100%; object-fit: cover; } .image-info { position: absolute; bottom: 0; left: 0; right: 0; background-color: rgba(0, 0, 0, 0.6); color: #fff; padding: 5px 10px; font-size: 11px; display: flex; justify-content: space-between; } .disease-marker { position: absolute; background-color: rgba(245, 34, 45, 0.7); color: #fff; padding: 4px 8px; border-radius: 4px; font-size: 11px; transform: translate(-50%, -50%); pointer-events: none; } /* 历史图像对比 */ .history-comparison { margin-bottom: 15px; } .date-selector { margin-bottom: 10px; display: flex; align-items: center; } .date-selector label { margin-right: 8px; font-size: 14px; color: #606266; } .date-selector input { padding: 4px 8px; border: 1px solid #dcdfe6; border-radius: 4px; font-size: 14px; } .compare-btn { padding: 4px 12px; background-color: #1890ff; color: #fff; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; margin-left: 10px; } .compare-btn:hover { background-color: #40a9ff; } .comparison-view { display: flex; gap: 10px; height: 120px; } .compare-image-container { flex: 1; position: relative; background-color: #f5f7fa; border: 1px solid #e8e8e8; border-radius: 4px; overflow: hidden; } .date-label { position: absolute; top: 0; left: 0; background-color: rgba(0, 0, 0, 0.6); color: #fff; padding: 2px 6px; font-size: 11px; z-index: 1; } .history-placeholder { width: 100%; height: 100%; object-fit: cover; } /* 历史数据与趋势分析模块 */ .history-trend { flex: 1; overflow: hidden; } .filter-controls { display: flex; gap: 10px; margin-bottom: 15px; flex-wrap: wrap; } .filter-controls select { padding: 4px 8px; border: 1px solid #dcdfe6; border-radius: 4px; font-size: 14px; flex: 1; } .query-btn { padding: 4px 12px; background-color: #1890ff; color: #fff; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; white-space: nowrap; } .query-btn:hover { background-color: #40a9ff; } /* 告警预警模块 */ .alarm-module { height: 5%; background-color: #0e1a2b; color: #fff; padding: 5px 20px; border-top: 2px solid #1890ff; overflow: hidden; } .alarm-module h2 { margin: 0 0 5px 0; font-size: 14px; color: #1890ff; display: inline-block; margin-right: 20px; border-bottom: none; padding-bottom: 0; } .alarm-content { display: flex; align-items: center; height: calc(100% - 24px); } .alarm-summary { display: flex; gap: 20px; margin-right: 30px; } .alarm-count一级, .alarm-count二级, .alarm-count三级 { display: flex; align-items: center; gap: 5px; } .alarm-count一级 .count { color: #f5222d; font-weight: bold; } .alarm-count二级 .count { color: #faad14; font-weight: bold; } .alarm-count三级 .count { color: #1890ff; font-weight: bold; } .alarm-list { flex: 1; display: flex; gap: 15px; overflow-x: auto; } .alarm-item { display: flex; align-items: center; gap: 8px; padding: 2px 10px; background-color: rgba(255, 255, 255, 0.1); border-radius: 15px; white-space: nowrap; font-size: 13px; } .alarm-item一级 { border-left: 3px solid #f5222d; } .alarm-item二级 { border-left: 3px solid #faad14; } .alarm-item三级 { border-left: 3px solid #1890ff; } .alarm-icon一级 { color: #f5222d; } .alarm-icon二级 { color: #faad14; } .alarm-icon三级 { color: #1890ff; } .alarm-info { display: flex; flex-direction: column; } .alarm-type { font-weight: 500; } .alarm-desc { font-size: 11px; color: #909399; } .alarm-time { font-size: 11px; color: #909399; } /* 工作流程动态演示 */ .mini-workflow { position: absolute; bottom: 70px; right: 20px; background-color: rgba(14, 26, 43, 0.9); color: #fff; padding: 10px; border-radius: 6px; width: 250px; z-index: 100; } .mini-workflow h4 { margin: 0 0 10px 0; font-size: 12px; color: #1890ff; text-align: center; } .workflow-steps { display: flex; align-items: center; justify-content: space-between; } .workflow-step { display: flex; flex-direction: column; align-items: center; font-size: 10px; color: #909399; } .workflow-step.active { color: #1890ff; animation: pulse 1s infinite; } .step-arrow { color: #606266; font-size: 12px; } @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.6; } 100% { opacity: 1; } } /* 响应式布局 */ @media (max-width: 1366px) { .main-content { flex-direction: column; } .left-panel, .right-panel { width: 100%; } .point-cloud-container, .system-status-grid { flex-direction: column; height: auto; } .point-cloud-container .chart, .system-status-grid .small-chart { height: 150px; } .alarm-list { flex-wrap: wrap; } } /* 图标样式 */ .icon-battery:before { content: "🔋"; } .icon-signal:before { content: "📶"; } .icon-arrow:before { content: "→"; } .icon-adsorption:before { content: "🔧"; } .icon-scan:before { content: "🔍"; } .icon-drive:before { content: "⚙️"; } .icon-process:before { content: "📊"; } .icon-identify:before { content: "🔍"; } .alarm-icon:before { content: "⚠️"; } </style>隧道信息
<div class="overview-section">
<h3>机器人状态</h3>
<div class="info-grid">
<div class="info-item">
<span class="label">机器人编号:</span>
<span class="value">{{ robotStatus.id }}</span>
</div>
<div class="info-item">
<span class="label">检测进度:</span>
<div class="progress-container">
<div class="progress-bar" :style="{ width: robotStatus.progress + '%' }"></div>
<span class="progress-text">{{ robotStatus.progress }}%</span>
</div>
</div>
<div class="info-item">
<span class="label">剩余续航:</span>
<span class="value with-icon">
<i class="icon-battery"></i>{{ robotStatus.battery }}
</span>
</div>
<div class="info-item">
<span class="label">网络状态:</span>
<span class="value with-icon">
<i class="icon-signal"></i>{{ robotStatus.network }}
</span>
</div>
</div>
</div>
<div class="overview-section">
<h3>检测模式</h3>
<div class="info-grid">
<div class="info-item">
<span class="label">检测精度等级:</span>
<span class="value">{{ detectionMode.precision }}</span>
</div>
<div class="info-item">
<span class="label">数据采集类型:</span>
<span class="value">{{ detectionMode.dataType }}</span>
</div>
</div>
</div>
</div>
<!-- 主内容区域 -->
<div class="main-content">
<!-- 左侧区域 -->
<div class="left-panel">
<!-- 实时检测数据模块 -->
<div class="module real-time-detection">
<h2>实时检测数据</h2>
<!-- 点云数据实时可视化 -->
<div class="sub-module">
<h3>点云数据实时可视化</h3>
<div class="point-cloud-container">
<div class="chart" id="pointCloudChart"></div>
<div class="point-cloud-params">
<div class="param-item">
<span class="label">点云密度:</span>
<span class="value">{{ pointCloudParams.density }}</span>
</div>
<div class="param-item">
<span class="label">拼接精度:</span>
<span class="value">{{ pointCloudParams.accuracy }}</span>
</div>
<div class="param-item">
<span class="label">无效点占比:</span>
<span class="value">{{ pointCloudParams.invalidRatio }}</span>
</div>
</div>
</div>
</div>
<!-- 病害实时识别结果 -->
<div class="sub-module">
<h3>病害实时识别结果</h3>
<!-- 裂缝检测 -->
<div class="disease-section">
<h4>裂缝检测</h4>
<div class="table-container">
<table class="disease-table">
<thead>
<tr>
<th>里程位置</th>
<th>裂缝长度</th>
<th>最大宽度</th>
<th>相对误差</th>
<th>风险等级</th>
</tr>
</thead>
<tbody>
<tr v-for="(crack, index) in crackData" :key="index" @click="selectDisease('crack', index)">
<td>{{ crack.position }}</td>
<td>{{ crack.length }}</td>
<td>{{ crack.width }}</td>
<td>{{ crack.error }}</td>
<td>
<span :class="['risk-level', 'risk-' + crack.riskLevel]">
{{ crack.riskLevelText }}
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 其他病害类型 -->
<div class="disease-section">
<h4>渗漏水检测</h4>
<div class="table-container">
<table class="disease-table">
<thead>
<tr>
<th>位置</th>
<th>面积</th>
<th>漏水等级</th>
<th>关联裂缝ID</th>
</tr>
</thead>
<tbody>
<tr v-for="(leak, index) in leakageData" :key="index" @click="selectDisease('leakage', index)">
<td>{{ leak.position }}</td>
<td>{{ leak.area }}</td>
<td>
<span :class="['water-level', 'level-' + leak.level]">
{{ leak.levelText }}
</span>
</td>
<td>{{ leak.relatedCrack }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- 机器人系统状态模块 -->
<div class="module robot-status">
<h2>机器人系统状态</h2>
<!-- 吸附系统状态 -->
<div class="sub-module">
<h3>吸附系统状态</h3>
<div class="system-status-grid">
<div class="status-item">
<span class="label">吸附模式:</span>
<span class="value">{{ adsorptionSystem.mode }}</span>
</div>
<div class="chart small-chart" id="adsorptionChart"></div>
</div>
</div>
<!-- 传动系统状态 -->
<div class="sub-module">
<h3>传动系统状态</h3>
<div class="system-status-grid">
<div class="chart small-chart" id="transmissionChart"></div>
</div>
</div>
<!-- 控制系统状态 -->
<div class="sub-module">
<h3>控制系统状态</h3>
<div class="system-status-grid">
<div class="chart small-chart" id="controlSystemChart"></div>
<div class="self-check-results">
<div v-for="(item, index) in selfCheckResults" :key="index" class="self-check-item">
<span class="label">{{ item.system }}:</span>
<span :class="['status', item.status]">
{{ item.statusText }}
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 右侧区域 -->
<div class="right-panel">
<!-- 视觉图像与病害标注模块 -->
<div class="module visual-image">
<h2>视觉图像与病害标注</h2>
<!-- 实时图像采集 -->
<div class="sub-module">
<h3>实时图像采集</h3>
<div class="camera-container">
<div class="camera-view">
<div class="camera-placeholder" style="background: linear-gradient(135deg, #0e1a2b 0%, #1890ff 100%); width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; color: white; font-size: 16px;">
CCD相机1图像
</div>
<div class="image-info">
<span>位置:{{ currentImageInfo.position }}</span>
<span>时间:{{ currentImageInfo.time }}</span>
<span>光照:{{ currentImageInfo.light }}</span>
</div>
<div v-if="selectedDisease" class="disease-marker" :style="selectedDiseaseMarkerStyle">
{{ selectedDisease.type }}: {{ selectedDisease.value }}
</div>
</div>
<div class="camera-view">
<div class="camera-placeholder" style="background: linear-gradient(135deg, #0e1a2b 0%, #52c41a 100%); width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; color: white; font-size: 16px;">
CCD相机2图像
</div>
<div class="image-info">
<span>位置:{{ currentImageInfo.position }}</span>
<span>时间:{{ currentImageInfo.time }}</span>
<span>光照:{{ currentImageInfo.light }}</span>
</div>
</div>
</div>
</div>
<!-- 历史图像对比 -->
<div class="sub-module">
<h3>历史图像对比</h3>
<div class="history-comparison">
<div class="date-selector">
<label>选择时间1:</label>
<input type="date" v-model="compareDate1" />
</div>
<div class="date-selector">
<label>选择时间2:</label>
<input type="date" v-model="compareDate2" />
</div>
<button @click="compareImages" class="compare-btn">对比</button>
<div class="comparison-view">
<div class="compare-image-container">
<span class="date-label">{{ compareDate1 }}</span>
<div class="history-placeholder" style="background: linear-gradient(135deg, #0e1a2b 0%, #1890ff 100%); width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; color: white; font-size: 14px;">
历史图像1
</div>
</div>
<div class="compare-image-container">
<span class="date-label">{{ compareDate2 }}</span>
<div class="history-placeholder" style="background: linear-gradient(135deg, #0e1a2b 0%, #52c41a 100%); width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; color: white; font-size: 14px;">
历史图像2
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 历史数据与趋势分析模块 -->
<div class="module history-trend">
<h2>历史数据与趋势分析</h2>
<!-- 历史检测数据查询 -->
<div class="sub-module">
<h3>历史检测数据查询</h3>
<div class="filter-controls">
<select v-model="historyFilter.tunnel">
<option value="">选择隧道区间</option>
<option value="厦门地铁三号线">厦门地铁三号线</option>
<option value="其他隧道">其他隧道</option>
</select>
<select v-model="historyFilter.timeRange">
<option value="">选择时间范围</option>
<option value="202401-202410">2024年1-10月</option>
<option value="202406-202410">2024年6-10月</option>
</select>
<select v-model="historyFilter.diseaseType">
<option value="">选择病害类型</option>
<option value="crack">裂缝</option>
<option value="leakage">渗漏水</option>
<option value="falling">掉块</option>
<option value="dislocation">错台</option>
</select>
<button @click="queryHistoryData" class="query-btn">查询</button>
</div>
<div class="chart small-chart" id="diseaseTypeChart"></div>
</div>
<!-- 病害趋势预测 -->
<div class="sub-module">
<h3>病害趋势预测</h3>
<select v-model="selectedDiseaseForTrend">
<option value="">选择病害ID</option>
<option value="Crack-012">Crack-012</option>
<option value="Crack-015">Crack-015</option>
</select>
<div class="chart small-chart" id="trendPredictionChart"></div>
</div>
</div>
</div>
</div>
<!-- 告警预警模块 -->
<div class="alarm-module">
<h2>告警预警</h2>
<div class="alarm-content">
<div class="alarm-summary">
<div class="alarm-count一级">
<span class="count">{{ alarmCounts.level1 }}</span>
<span class="label">一级告警</span>
</div>
<div class="alarm-count二级">
<span class="count">{{ alarmCounts.level2 }}</span>
<span class="label">二级告警</span>
</div>
<div class="alarm-count三级">
<span class="count">{{ alarmCounts.level3 }}</span>
<span class="label">三级告警</span>
</div>
</div>
<div class="alarm-list">
<div v-for="(alarm, index) in alarmList" :key="index" class="alarm-item" :class="'alarm-item' + alarm.level">
<i :class="['alarm-icon', 'alarm-icon' + alarm.level]"></i>
<div class="alarm-info">
<span class="alarm-type">{{ alarm.type }}</span>
<span class="alarm-desc">{{ alarm.description }}</span>
</div>
<span class="alarm-time">{{ alarm.time }}</span>
</div>
</div>
</div>
</div>
<!-- 工作流程动态演示 -->
<div class="mini-workflow">
<h4>工作流程</h4>
<div class="workflow-steps">
<div
v-for="(step, index) in workflowSteps"
:key="index"
class="workflow-step"
:class="{ active: currentWorkflowStep === index }"
>
<i :class="['step-icon', step.icon]"></i>
<span class="step-label">{{ step.label }}</span>
</div>
<div v-for="index in workflowSteps.length - 1" :key="'arrow-' + index" class="step-arrow">
<i class="icon-arrow"></i>
</div>
</div>
</div>
</div>
</div>
<script>
const { createApp, ref, computed, onMounted, onBeforeUnmount } = Vue;
createApp({
setup() {
// 系统概览数据
const tunnelInfo = ref({
name: '厦门地铁三号线某区间左线',
mileage: 'K3+200-K5+200',
liningType: '钢筋混凝土',
innerDiameter: '5.5m'
})
const robotStatus = ref({
id: 'PABI-001',
progress: 65,
battery: '2.3h',
network: '-65dBm'
})
const detectionMode = ref({
precision: '高精度模式:0.08mm/m 分辨率',
dataType: '激光点云 + 视觉图像'
})
// 点云数据参数
const pointCloudParams = ref({
density: '800 点/cm²',
accuracy: '0.0015m',
invalidRatio: '2.3%'
})
// 病害数据
const crackData = ref([
{
position: 'K3+560 环号 18',
length: '0.36m',
width: '1.64mm',
error: '7.34%',
riskLevel: 3,
riskLevelText: '三级'
},
{
position: 'K3+620 环号 22',
length: '0.25m',
width: '0.8mm',
error: '6.12%',
riskLevel: 2,
riskLevelText: '二级'
},
{
position: 'K3+750 环号 35',
length: '0.18m',
width: '0.3mm',
error: '5.89%',
riskLevel: 1,
riskLevelText: '一级'
}
])
const leakageData = ref([
{
position: 'K3+620 环号 22',
area: '0.8m²',
level: 2,
levelText: '线流',
relatedCrack: 'Crack-012'
}
])
// 吸附系统状态
const adsorptionSystem = ref({
mode: '磁吸附'
})
// 自检结果
const selfCheckResults = ref([
{ system: '吸附', status: 'normal', statusText: '正常' },
{ system: '传动', status: 'normal', statusText: '正常' },
{ system: '检测', status: 'warning', statusText: 'CCD相机2信号弱' },
{ system: '通信', status: 'normal', statusText: '正常' }
])
// 当前图像信息
const currentImageInfo = ref({
position: 'K3+560 环号 18',
time: '2024-12-15 10:30:45',
light: '35lux'
})
// 历史对比日期
const compareDate1 = ref('2024-05-01')
const compareDate2 = ref('2024-10-01')
// 历史数据筛选条件
const historyFilter = ref({
tunnel: '',
timeRange: '',
diseaseType: ''
})
// 选择的病害趋势ID
const selectedDiseaseForTrend = ref('')
// 告警数据
const alarmCounts = ref({
level1: 2,
level2: 3,
level3: 5
})
const alarmList = ref([
{
level: '一级',
type: '吸附力不足',
description: '吸附模块3吸附力280N,位置K3+800',
time: '10:32:15'
},
{
level: '一级',
type: '检测模块故障',
description: 'CCD相机2信号弱',
time: '10:30:45'
},
{
level: '二级',
type: '裂缝宽度超限',
description: 'Crack-012宽度1.64mm>1mm',
time: '10:28:30'
}
])
// 工作流程
const workflowSteps = ref([
{ icon: 'icon-adsorption', label: '吸附稳定' },
{ icon: 'icon-scan', label: '周向扫描' },
{ icon: 'icon-drive', label: '轴向行进' },
{ icon: 'icon-process', label: '数据处理' },
{ icon: 'icon-identify', label: '病害识别' }
])
const currentWorkflowStep = ref(1)
let workflowTimer = null
// 选择的病害
const selectedDisease = ref(null)
// 计算病害标记样式
const selectedDiseaseMarkerStyle = computed(() => {
if (!selectedDisease.value) return {}
return {
top: selectedDisease.value.y + '%',
left: selectedDisease.value.x + '%'
}
})
// 选择病害
const selectDisease = (type, index) => {
let diseaseData = type === 'crack' ? crackData.value : leakageData.value
selectedDisease.value = {
type: type === 'crack' ? '裂缝' : '渗漏水',
value: diseaseData[index].position,
x: 30 + Math.random() * 40, // 随机位置用于演示
y: 30 + Math.random() * 40
}
}
// 对比图像
const compareImages = () => {
console.log('对比图像:', compareDate1.value, compareDate2.value)
// 实际项目中这里会根据选择的日期加载对应的历史图像
}
// 查询历史数据
const queryHistoryData = () => {
console.log('查询历史数据:', historyFilter.value)
// 实际项目中这里会根据筛选条件查询历史数据
}
// 初始化图表配置
const initCharts = () => {
// 点云数据可视化(使用散点图模拟)
const pointCloudChart = echarts.init(document.getElementById('pointCloudChart'))
const pointCloudData = []
for (let i = 0; i < 1000; i++) {
const value = [
Math.random() * 100,
Math.random() * 100,
Math.random() * 100
]
// 根据值设置颜色,模拟裂缝风险
let color = '#1890ff' // 正常区域蓝色
if (value[2] > 70) {
color = '#f5222d' // 确认裂缝红色
} else if (value[2] > 50) {
color = '#faad14' // 疑似裂缝黄色
}
pointCloudData.push({
value: value,
itemStyle: {
color: color
}
})
}
const pointCloudOption = {
tooltip: {},
xAxis3D: {
type: 'value'
},
yAxis3D: {
type: 'value'
},
zAxis3D: {
type: 'value'
},
grid3D: {
viewControl: {
projection: 'perspective'
},
light: {
main: {
intensity: 1.2
},
ambient: {
intensity: 0.3
}
}
},
series: [
{
type: 'scatter3D',
data: pointCloudData,
symbolSize: 2,
emphasis: {
itemStyle: {
borderWidth: 1
}
}
}
]
}
pointCloudChart.setOption(pointCloudOption)
// 吸附力条形图
const adsorptionChart = echarts.init(document.getElementById('adsorptionChart'))
const adsorptionOption = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: ['模块1', '模块2', '模块3', '模块4', '模块5', '模块6']
},
yAxis: {
type: 'value',
name: '吸附力(N)'
},
series: [
{
name: '吸附力',
type: 'bar',
data: [780, 820, 750, 800, 790, 810],
itemStyle: {
color: function(params) {
return params.value < 300 ? '#f5222d' : '#52c41a'
}
}
}
]
}
adsorptionChart.setOption(adsorptionOption)
// 传动系统仪表盘
const transmissionChart = echarts.init(document.getElementById('transmissionChart'))
const transmissionOption = {
tooltip: {
formatter: '{b}: {c}'
},
series: [
{
type: 'gauge',
startAngle: 180,
endAngle: 0,
min: 0,
max: 30,
splitNumber: 6,
radius: '60%',
axisLine: {
lineStyle: {
color: [
[0.5, '#52c41a'],
[0.8, '#faad14'],
[1, '#f5222d']
],
width: 6
}
},
pointer: {
icon: 'path://M12.8,0.7l12,40.1H0.7L12.8,0.7z',
length: '12%',
width: 20,
offsetCenter: [0, '-60%'],
itemStyle: {
color: 'auto'
}
},
axisTick: {
length: 12,
lineStyle: {
color: 'auto',
width: 1
}
},
splitLine: {
length: 20,
lineStyle: {
color: 'auto',
width: 2
}
},
axisLabel: {
color: '#464646',
fontSize: 10,
distance: -60,
formatter: function(value) {
if (value === 0) {
return '0';
} else if (value === 10) {
return '10';
} else if (value === 20) {
return '20';
} else if (value === 30) {
return '30';
}
return '';
}
},
title: {
offsetCenter: [0, '-10%'],
fontSize: 12
},
detail: {
fontSize: 20,
offsetCenter: [0, '20%'],
valueAnimation: true,
formatter: function(value) {
return Math.round(value) + 'rpm';
},
color: 'auto'
},
data: [
{
value: 15,
name: '周向转速'
}
]
}
]
}
transmissionChart.setOption(transmissionOption)
// 控制系统环形图
const controlSystemChart = echarts.init(document.getElementById('controlSystemChart'))
const controlSystemOption = {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 10,
data: ['GPU', 'CPU', '内存']
},
series: [
{
name: '系统负载',
type: 'pie',
radius: ['50%', '70%'],
avoidLabelOverlap: false,
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: '18',
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: [
{ value: 65, name: 'GPU', itemStyle: { color: '#1890ff' } },
{ value: 40, name: 'CPU', itemStyle: { color: '#52c41a' } },
{ value: 60, name: '内存', itemStyle: { color: '#faad14' } }
]
}
]
}
controlSystemChart.setOption(controlSystemOption)
// 病害类型饼图
const diseaseTypeChart = echarts.init(document.getElementById('diseaseTypeChart'))
const diseaseTypeOption = {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 10,
data: ['裂缝', '渗漏水', '掉块', '错台']
},
series: [
{
name: '病害类型',
type: 'pie',
radius: '60%',
data: [
{ value: 45, name: '裂缝', itemStyle: { color: '#1890ff' } },
{ value: 25, name: '渗漏水', itemStyle: { color: '#52c41a' } },
{ value: 20, name: '掉块', itemStyle: { color: '#faad14' } },
{ value: 10, name: '错台', itemStyle: { color: '#722ed1' } }
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
}
diseaseTypeChart.setOption(diseaseTypeOption)
// 趋势预测折线图
const trendPredictionChart = echarts.init(document.getElementById('trendPredictionChart'))
const trendPredictionOption = {
tooltip: {
trigger: 'axis'
},
legend: {
data: ['历史数据', '预测数据']
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
},
yAxis: {
type: 'value',
name: '裂缝宽度(mm)'
},
series: [
{
name: '历史数据',
type: 'line',
data: [0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.64, null, null],
itemStyle: {
color: '#1890ff'
}
},
{
name: '预测数据',
type: 'line',
data: [null, null, null, null, null, null, null, null, null, null, 1.8, 2.0],
lineStyle: {
type: 'dashed'
},
itemStyle: {
color: '#f5222d'
}
}
],
markArea: {
silent: true,
data: [
[
{
xAxis: '11月',
itemStyle: {
color: 'rgba(245, 34, 45, 0.1)'
}
},
{
xAxis: '12月'
}
]
]
}
}
trendPredictionChart.setOption(trendPredictionOption)
// 窗口大小改变时重绘图表
window.addEventListener('resize', function() {
pointCloudChart.resize()
adsorptionChart.resize()
transmissionChart.resize()
controlSystemChart.resize()
diseaseTypeChart.resize()
trendPredictionChart.resize()
})
}
// 模拟工作流程动画
const animateWorkflow = () => {
workflowTimer = setInterval(() => {
currentWorkflowStep.value = (currentWorkflowStep.value + 1) % workflowSteps.value.length
}, 2000)
}
// 组件挂载后初始化
onMounted(() => {
initCharts()
animateWorkflow()
})
// 组件卸载前清理定时器
onBeforeUnmount(() => {
if (workflowTimer) {
clearInterval(workflowTimer)
}
})
return {
tunnelInfo,
robotStatus,
detectionMode,
pointCloudParams,
crackData,
leakageData,
adsorptionSystem,
selfCheckResults,
currentImageInfo,
compareDate1,
compareDate2,
historyFilter,
selectedDiseaseForTrend,
alarmCounts,
alarmList,
workflowSteps,
currentWorkflowStep,
selectedDisease,
selectedDiseaseMarkerStyle,
selectDisease,
compareImages,
queryHistoryData
}
}
}).mount('#app')
</script>