Files
ljwx-health/ljwx-bigscreen/bigscreen/bigScreen/templates/bigscreen_main copy 7.html
T
bruno ce2937c9d1 feat: 添加 ljwx-bigscreen/bigscreen/bigScreen 目录所有重要代码文件
- 🐍 添加所有 Python 源码文件(*.py)
- 📄 添加所有 HTML 模板文件(templates/)
- 🗂️ 添加控制器、模型、测试等目录结构
- 📋 添加文档文件(*.md)和配置文件
- ⚙️ 更新 .gitignore 配置,只忽略日志和 SQL 文件
- 🚀 确保所有重要代码都被 Git 跟踪和版本控制

根据要求:ljwx-bigscreen/bigscreen/bigScreen 里面除了 log 和 sql 都需要提交
2025-08-14 21:09:12 +08:00

4428 lines
166 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>智能健康数据分析平台</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #001529;
color: #fff;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
overflow: hidden;
}
.container {
display: grid;
grid-template-columns: 350px 1fr 350px;
grid-gap: 20px;
padding: 0 20px 20px 20px;
height: calc(100vh - 60px); /* 减去标题栏高度 */
overflow: hidden;
}
.panel {
background: rgba(0, 21, 41, 0.7);
border: 1px solid rgba(0, 228, 255, 0.2);
border-radius: 4px;
padding: 12px;
margin: 0;
display: flex;
flex-direction: column;
}
.side-container {
display: flex;
flex-direction: column;
gap: 15px;
height: 100%;
}
.panel:nth-child(1) {
height: 30%;
}
.panel:nth-child(2) {
height: 35%;
}
.panel:nth-child(3) {
height: 35%;
}
.panel-header {
color: #00e4ff;
font-size: 16px;
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
}
.panel-content {
display: flex;
flex-direction: column;
height: 100%;
}
.map-container {
height: calc(100% - 20px);
margin-top: 20px;
background: rgba(0, 21, 41, 0.7);
border: 1px solid rgba(0, 228, 255, 0.2);
border-radius: 4px;
position: relative;
}
/* 数据卡片样式 */
.data-card {
background: rgba(0, 228, 255, 0.1);
border-radius: 4px;
padding: 10px;
margin-bottom: 10px;
}
.data-card-value {
font-size: 24px;
color: #00e4ff;
margin: 5px 0;
}
/* 滚动条美化 */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: rgba(0, 228, 255, 0.1);
border-radius: 3px;
}
::-webkit-scrollbar-thumb {
background: rgba(0, 228, 255, 0.3);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(0, 228, 255, 0.5);
}
/* 图表容器样式 */
.chart-container {
position: relative;
flex: 1;
width: 100%;
height: calc(100% - 40px); /* 减少高度偏移,给图表更多空间 */
min-height: 200px; /* 设置最小高度 */
}
/* 告警图表专用样式 */
#alertTypeChart, #alertLevelChart, #alertStatusChart, #alertTrendChart {
width: 100% !important;
height: 100% !important;
min-height: 120px !important;
}
/* 告警面板网格布局优化 */
.alert-charts-grid {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
gap: 8px;
height: calc(100% - 60px);
min-height: 250px;
}
/* 响应式调整 */
@media screen and (max-width: 1600px) {
.container {
grid-template-columns: 300px 1fr 300px;
}
}
@media screen and (max-width: 1200px) {
.container {
grid-template-columns: 250px 1fr 250px;
}
}
/* 在已有的 style 标签中添加 */
.header-title {
position: relative;
width: 100%;
height: 60px;
background: linear-gradient(90deg, rgba(0,21,41,0.8) 0%, rgba(0,91,171,0.8) 50%, rgba(0,21,41,0.8) 100%);
display: flex;
justify-content: center;
align-items: center;
border-bottom: 1px solid rgba(0,228,255,0.3);
margin-bottom: 20px;
}
.header-title h1 {
color: #00e4ff;
font-size: 28px;
font-weight: normal;
text-shadow: 0 0 10px rgba(0,228,255,0.5);
letter-spacing: 2px;
}
.header-title::before,
.header-title::after {
content: '';
position: absolute;
width: 200px;
height: 1px;
background: linear-gradient(90deg, transparent, #00e4ff, transparent);
bottom: -1px;
}
.header-title::before {
left: 0;
}
.header-title::after {
right: 0;
}
/* 添加装饰元素 */
.header-decoration {
position: absolute;
width: 120px;
height: 30px;
top: 50%;
transform: translateY(-50%);
}
.header-decoration.left {
left: 50px;
background: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjAiIGhlaWdodD0iMzAiPjxwYXRoIGQ9Ik0wIDE1aDEyME0zMCAwdjMwTTYwIDB2MzBNOTAgMHYzMCIgc3Ryb2tlPSIjMDBlNGZmIiBzdHJva2Utd2lkdGg9IjEiIG9wYWNpdHk9IjAuMyIvPjwvc3ZnPg==');
}
.header-decoration.right {
right: 50px;
background: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjAiIGhlaWdodD0iMzAiPjxwYXRoIGQ9Ik0wIDE1aDEyME0zMCAwdjMwTTYwIDB2MzBNOTAgMHYzMCIgc3Ryb2tlPSIjMDBlNGZmIiBzdHJva2Utd2lkdGg9IjEiIG9wYWNpdHk9IjAuMyIvPjwvc3ZnPg==');
transform: translateY(-50%) scaleX(-1);
}
/* 添加人员管理面板特殊调整 */
.personnel-management {
background: rgba(1, 19, 38, 0.8);
border: 1px solid rgba(0, 228, 255, 0.2);
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
}
.personnel-management:hover {
border-color: rgba(0, 228, 255, 0.4);
box-shadow: 0 0 15px rgba(0, 228, 255, 0.2);
}
.personnel-management .panel-content {
display: flex;
flex-direction: column;
height: 100%;
}
.personnel-management .data-overview {
display: flex;
justify-content: space-between;
padding: 8px 20px;
margin-bottom: 5px;
border-bottom: 1px solid rgba(0, 228, 255, 0.1);
}
.personnel-management .overview-item {
text-align: center;
display: flex;
align-items: center;
gap: 8px;
}
.personnel-management .overview-item .label {
font-size: 14px;
color: rgba(255, 255, 255, 0.7);
margin-bottom: 0;
}
.personnel-management .overview-item .number {
font-size: 20px;
color: #00e4ff;
font-weight: 600;
}
.personnel-management .department-chart {
flex: 1;
height: calc(100% - 50px); /* 给图表更多空间 */
min-height: 0;
}
/* 响应式调整 */
@media screen and (max-height: 800px) {
.personnel-management .overview-item .number {
font-size: 20px;
}
.personnel-management .overview-item .label {
font-size: 11px;
}
}
/* 在已有的style标签中添加 */
.message-count {
font-size: 14px;
color: #00e4ff;
background: rgba(0, 228, 255, 0.1);
padding: 2px 8px;
border-radius: 12px;
margin-left: 10px;
}
.message-list {
height: 100%;
overflow-y: auto;
padding-right: 5px;
}
.message-item {
background: rgba(0, 228, 255, 0.05);
border-left: 3px solid #00e4ff;
margin-bottom: 10px;
padding: 10px;
border-radius: 0 4px 4px 0;
transition: all 0.3s ease;
}
.message-item:hover {
background: rgba(0, 228, 255, 0.1);
}
.message-header {
display: flex;
justify-content: space-between;
margin-bottom: 5px;
font-size: 12px;
color: rgba(255, 255, 255, 0.7);
}
.message-time {
color: #00e4ff;
}
.message-content {
color: #fff;
font-size: 13px;
line-height: 1.4;
word-break: break-all;
}
/* 自定义滚动条样式 */
.message-list::-webkit-scrollbar {
width: 4px;
}
.message-list::-webkit-scrollbar-track {
background: rgba(0, 228, 255, 0.05);
}
.message-list::-webkit-scrollbar-thumb {
background: rgba(0, 228, 255, 0.3);
border-radius: 2px;
}
.message-list::-webkit-scrollbar-thumb:hover {
background: rgba(0, 228, 255, 0.5);
}
.device-stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
padding: 8px;
margin-bottom: 10px;
height: auto;
}
.device-stat-item {
background: rgba(0, 228, 255, 0.1);
border-radius: 4px;
padding: 8px;
text-align: center;
}
.stat-label {
font-size: 12px;
color: rgba(255, 255, 255, 0.7);
display: block;
margin-bottom: 4px;
}
.stat-value {
font-size: 18px;
color: #00e4ff;
font-weight: 600;
}
.total-score {
font-size: 16px;
color: #00e4ff;
background: rgba(0, 228, 255, 0.1);
padding: 4px 12px;
border-radius: 12px;
}
/* 在 <style> 标签中添加以下样式 */
.modal-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
.modal-content {
position: relative;
width: 90%;
height: 90%;
background: rgba(0, 21, 41, 0.95);
border: 1px solid rgba(0, 228, 255, 0.3);
border-radius: 8px;
padding: 20px;
box-shadow: 0 0 20px rgba(0, 228, 255, 0.2);
}
.modal-close {
position: absolute;
top: 10px;
right: 10px;
background: transparent;
border: none;
color: #00e4ff;
font-size: 20px;
cursor: pointer;
z-index: 9999;
padding: 5px 10px;
transition: all 0.3s ease;
}
.modal-close:hover {
transform: scale(1.1);
color: #ff4444;
}
.user-view-iframe {
width: 100%;
height: 100%;
border: none;
border-radius: 4px;
background: rgba(1, 19, 38, 0.8);
}
/* 添加动画效果 */
.modal-container {
animation: fadeIn 0.3s ease;
}
.modal-content {
animation: slideIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideIn {
from {
transform: translateY(-20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.filter-panel {
position: absolute;
top: 20px;
right: 20px;
z-index: 1000;
background: rgba(0, 21, 41, 0.9);
backdrop-filter: blur(8px);
padding: 15px;
border-radius: 8px;
width: 280px;
border: 1px solid rgba(0, 228, 255, 0.2);
box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
}
.filter-container {
display: flex;
flex-direction: column;
gap: 10px;
}
.filter-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid rgba(0, 228, 255, 0.2);
}
.filter-header span {
color: #00e4ff;
font-size: 14px;
font-weight: bold;
}
.filter-select {
font-family: monospace;
white-space: pre;
width: 100%;
padding: 8px;
margin-bottom: 10px;
background: rgba(0, 0, 0, 0.3);
color: #fff;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 4px;
transition: all 0.3s ease;
}
.filter-select:hover {
border-color: rgba(0, 228, 255, 0.4);
}
.filter-select:focus {
border-color: rgba(0, 228, 255, 0.6);
outline: none;
box-shadow: 0 0 5px rgba(0, 228, 255, 0.2);
}
.filter-select option {
font-family: monospace;
white-space: pre;
background: #1a1a1a;
color: #fff;
padding: 8px;
}
/* 添加美化的弹出框样式 */
.custom-alert {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0, 21, 41, 0.95);
border: 1px solid rgba(0, 228, 255, 0.3);
border-radius: 8px;
padding: 20px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
z-index: 10000;
min-width: 300px;
text-align: center;
animation: alertFadeIn 0.3s ease;
}
.custom-alert-content {
color: #fff;
font-size: 16px;
margin-bottom: 20px;
}
.custom-alert-button {
background: rgba(0, 228, 255, 0.2);
border: 1px solid rgba(0, 228, 255, 0.4);
color: #00e4ff;
padding: 8px 20px;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
}
.custom-alert-button:hover {
background: rgba(0, 228, 255, 0.3);
border-color: rgba(0, 228, 255, 0.6);
}
@keyframes alertFadeIn {
from {
opacity: 0;
transform: translate(-50%, -60%);
}
to {
opacity: 1;
transform: translate(-50%, -50%);
}
}
/* 添加遮罩层样式 */
.custom-alert-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(2px);
z-index: 9999;
animation: overlayFadeIn 0.3s ease;
}
@keyframes overlayFadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* 现代科技风格信息面板 */
.info-panel {
position: fixed;
right: 20px;
top: 50%;
transform: translateY(-50%);
width: 360px;
background: rgba(13, 17, 38, 0.95);
border: 1px solid rgba(0, 234, 255, 0.3);
border-radius: 12px;
box-shadow: 0 0 20px rgba(0, 234, 255, 0.2);
backdrop-filter: blur(10px);
z-index: 1000;
transition: all 0.3s ease;
overflow: hidden;
}
.info-panel-header {
padding: 15px 20px;
background: linear-gradient(90deg, rgba(0, 234, 255, 0.1), rgba(0, 234, 255, 0.05));
border-bottom: 1px solid rgba(0, 234, 255, 0.2);
display: flex;
justify-content: space-between;
align-items: center;
}
.info-panel-title {
color: #00eaff;
font-size: 18px;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
}
.info-panel-close {
background: none;
border: none;
color: #00eaff;
font-size: 20px;
cursor: pointer;
padding: 5px;
transition: all 0.2s ease;
}
.info-panel-close:hover {
color: #fff;
transform: rotate(90deg);
}
.info-panel-content {
padding: 20px;
max-height: 70vh;
overflow-y: auto;
}
.info-item {
background: rgba(0, 234, 255, 0.05);
border: 1px solid rgba(0, 234, 255, 0.1);
border-radius: 8px;
padding: 15px;
margin-bottom: 12px;
transition: all 0.2s ease;
}
.info-item:hover {
background: rgba(0, 234, 255, 0.1);
transform: translateX(-5px);
}
.info-item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.info-item-title {
color: #00eaff;
font-size: 16px;
font-weight: 500;
}
.info-item-time {
color: rgba(255, 255, 255, 0.6);
font-size: 14px;
}
.info-item-content {
color: #fff;
font-size: 14px;
line-height: 1.6;
}
.info-item-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid rgba(0, 234, 255, 0.1);
}
.info-item-location {
color: rgba(255, 255, 255, 0.6);
font-size: 13px;
}
.info-item-action {
background: #00eaff;
color: #0d1126;
border: none;
padding: 6px 12px;
border-radius: 4px;
font-size: 13px;
cursor: pointer;
transition: all 0.2s ease;
}
.info-item-action:hover {
background: #38f9d7;
}
/* 健康信息样式 */
.health-info {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
margin-top: 10px;
}
.health-item {
background: rgba(0, 234, 255, 0.05);
padding: 8px;
border-radius: 6px;
text-align: center;
}
.health-label {
color: rgba(255, 255, 255, 0.6);
font-size: 12px;
margin-bottom: 4px;
}
.health-value {
color: #00eaff;
font-size: 16px;
font-weight: 500;
}
/* 滚动条样式 */
.info-panel-content::-webkit-scrollbar {
width: 6px;
}
.info-panel-content::-webkit-scrollbar-track {
background: rgba(0, 234, 255, 0.05);
}
.info-panel-content::-webkit-scrollbar-thumb {
background: rgba(0, 234, 255, 0.3);
border-radius: 3px;
}
.info-panel-content::-webkit-scrollbar-thumb:hover {
background: rgba(0, 234, 255, 0.5);
}
.info-item-highlight { border:2px solid #ffbb00; box-shadow:0 0 12px #ffbb0088; }
/* 人员管理面板专用样式 */
.personnel-management .panel-header h3 {
color: #00e4ff;
font-size: 16px;
margin: 0;
font-weight: 600;
}
/* 统计概览卡片样式 */
.personnel-management .panel-content > div:first-child {
transition: all 0.3s ease;
cursor: pointer;
}
.personnel-management .panel-content > div:first-child:hover {
background: rgba(0, 228, 255, 0.15) !important;
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 228, 255, 0.2);
}
/* 图表容器优化 */
#departmentDistribution, #userStatusChart {
transition: all 0.3s ease;
}
#departmentDistribution:hover, #userStatusChart:hover {
border-color: rgba(0, 228, 255, 0.6) !important;
box-shadow: 0 4px 15px rgba(0, 228, 255, 0.2) !important;
}
</style>
</head>
<body>
<!-- 添加标题栏 -->
<div class="header-title">
<div class="header-decoration left"></div>
<h1>{{ BIGSCREEN_TITLE }}</h1>
<div class="header-decoration right"></div>
</div>
<div class="container">
<!-- 左侧面板 -->
<div class="side-container">
<div class="panel">
<div class="panel-header">
<span>健康评分</span>
<span class="total-score">总分:88.9</span>
</div>
<div class="panel-content">
<div id="healthScoreChart" class="chart-container"></div>
</div>
</div>
<div class="panel personnel-management">
<div class="panel-header">
<h3>人员管理</h3>
</div>
<div class="panel-content">
<!-- 统计概览卡片 -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; padding: 8px 12px; background: rgba(0,228,255,0.1); border-radius: 6px; border-left: 4px solid #00e4ff;">
<div style="display: flex; align-items: center; gap: 20px;">
<div style="text-align: center;">
<div style="color: #00e4ff; font-size: 18px; font-weight: bold;" id="totalUsers">0</div>
<div style="color: rgba(255,255,255,0.8); font-size: 11px;">总人数</div>
</div>
<div style="text-align: center;">
<div style="color: #00ff9d; font-size: 18px; font-weight: bold;" id="totalBindDevices">0</div>
<div style="color: rgba(255,255,255,0.8); font-size: 11px;">已绑定设备</div>
</div>
<div style="text-align: center;">
<div style="color: #ffbb00; font-size: 18px; font-weight: bold;" id="onlineRate">0%</div>
<div style="color: rgba(255,255,255,0.8); font-size: 11px;">在线率</div>
</div>
</div>
<div style="color: rgba(255,255,255,0.6); font-size: 12px; cursor: pointer;" onclick="showPersonnelDetails()">
详情 →
</div>
</div>
<!-- 快速统计卡片 -->
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; margin-bottom: 10px; height: 45px;">
<div style="background: rgba(0,228,255,0.1); border-radius: 4px; padding: 4px; text-align: center; border-left: 3px solid #00e4ff; cursor: pointer; transition: all 0.3s ease;" onclick="filterByDepartment()" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 12px rgba(0,228,255,0.3)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='none'">
<div style="color: #00e4ff; font-size: 14px; font-weight: bold;" id="activeDeptCount">0</div>
<div style="color: #fff; font-size: 9px;">活跃部门</div>
</div>
<div style="background: rgba(0,255,157,0.1); border-radius: 4px; padding: 4px; text-align: center; border-left: 3px solid #00ff9d; cursor: pointer; transition: all 0.3s ease;" onclick="filterByOnlineStatus()" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 12px rgba(0,255,157,0.3)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='none'">
<div style="color: #00ff9d; font-size: 14px; font-weight: bold;" id="onlineUsers">0</div>
<div style="color: #fff; font-size: 9px;">在线人员</div>
</div>
<div style="background: rgba(255,187,0,0.1); border-radius: 4px; padding: 4px; text-align: center; border-left: 3px solid #ffbb00; cursor: pointer; transition: all 0.3s ease;" onclick="filterByDeviceStatus()" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 12px rgba(255,187,0,0.3)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='none'">
<div style="color: #ffbb00; font-size: 14px; font-weight: bold;" id="boundDevices">0</div>
<div style="color: #fff; font-size: 9px;">绑定设备</div>
</div>
<div style="background: rgba(255,68,68,0.1); border-radius: 4px; padding: 4px; text-align: center; border-left: 3px solid #ff4444; cursor: pointer; transition: all 0.3s ease;" onclick="filterByAlertStatus()" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 12px rgba(255,68,68,0.3)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='none'">
<div style="color: #ff4444; font-size: 14px; font-weight: bold;" id="alertUsers">0</div>
<div style="color: #fff; font-size: 9px;">告警人员</div>
</div>
</div>
<!-- 图表区域 -->
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; height: calc(100% - 120px);">
<div id="departmentDistribution" style="background: rgba(0,21,41,0.4); border: 1px solid rgba(0,228,255,0.3); border-radius: 6px; position: relative; transition: all 0.3s ease;" onmouseover="this.style.borderColor='rgba(0,228,255,0.6)'; this.style.boxShadow='0 4px 15px rgba(0,228,255,0.2)'" onmouseout="this.style.borderColor='rgba(0,228,255,0.3)'; this.style.boxShadow='none'">
<div style="position: absolute; top: 5px; left: 8px; color: #00e4ff; font-size: 12px; font-weight: bold; z-index: 10;">部门分布</div>
</div>
<div id="userStatusChart" style="background: rgba(0,21,41,0.4); border: 1px solid rgba(0,228,255,0.3); border-radius: 6px; position: relative; transition: all 0.3s ease;" onmouseover="this.style.borderColor='rgba(0,228,255,0.6)'; this.style.boxShadow='0 4px 15px rgba(0,228,255,0.2)'" onmouseout="this.style.borderColor='rgba(0,228,255,0.3)'; this.style.boxShadow='none'">
<div style="position: absolute; top: 5px; left: 8px; color: #00e4ff; font-size: 12px; font-weight: bold; z-index: 10;">在线状态</div>
</div>
</div>
</div>
</div>
<div class="panel">
<div class="panel-header">
<span>告警信息</span>
</div>
<div class="panel-content">
<div id="alertList" class="chart-container"></div>
</div>
</div>
</div>
<!-- 中间地图区域 -->
<div class="map-container" id="map-container">
<!-- 筛选面板 -->
<div class="filter-panel">
<div class="filter-header">
<span>人员筛选</span>
</div>
<div class="filter-container">
<select id="deptSelect" class="filter-select">
<option value="">选择部门</option>
</select>
<select id="userSelect" class="filter-select">
<option value="">选择用户</option>
</select>
</div>
</div>
<!-- 保留原有地图代码 -->
</div>
<!-- 右侧面板 -->
<div class="side-container">
<div class="panel">
<div class="panel-header">
<span>手表管理</span>
<span class="label">手表总数</span>
<span class="number" id="totalWatchDevices">35</span>
</div>
<div class="panel-content">
<div id="statsChart" class="chart-container"></div>
</div>
</div>
<!-- 将原来的健康指标面板改为消息信息面板 -->
<div class="panel">
<div class="panel-header">
<span>消息信息</span>
<span class="message-count" id="messageCount">0</span>
</div>
<div class="panel-content">
<div id="messageList" class="message-list"></div>
</div>
</div>
<div class="panel">
<div class="panel-header">
<span>健康数据分析</span>
</div>
<div class="panel-content">
<div id="trendChart" class="chart-container"></div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<script>
window.difyChatbotConfig = {
token: 'CdVhQIms3fZkQuTW',
baseUrl: 'http://192.168.1.83',
systemVariables: {
// user_id: 'YOU CAN DEFINE USER ID HERE',
// conversation_id: 'YOU CAN DEFINE CONVERSATION ID HERE, IT MUST BE A VALID UUID',
},
}
</script>
<script
src="http://192.168.1.83/embed.min.js"
id="CdVhQIms3fZkQuTW"
defer>
</script>
<style>
#dify-chatbot-bubble-button {
background-color: #1C64F2 !important;
}
#dify-chatbot-bubble-window {
width: 24rem !important;
height: 40rem !important;
}
</style>
<script>
// 在全局作用域声明图表变量
let globalCharts = null;
let currentDept = '', currentUser = '';
const ALERT_TYPE_MAP = {
heart_rate: '心率',
blood_pressure: '血压',
stress: '压力',
blood_oxygen: '血氧',
temperature: '体温',
one_key_alarm: '一键报警',
fall_down: '跌倒',
sleep: '睡眠'
};
const ALERT_SEVERITY_MAP = {
critical: '严重',
high: '高',
medium: '中',
low: '低'
};
const ALERT_STATUS_MAP = {
pending: '待处理',
responded: '已响应',
resolved: '已解决'
};
function translateAlertType(type) {
return ALERT_TYPE_MAP[type] || type;
}
function translateAlertSeverity(severity) {
return ALERT_SEVERITY_MAP[severity] || severity;
}
function translateAlertStatus(status) {
return ALERT_STATUS_MAP[status] || status;
}
const ALERT_TYPE_COLOR = {
heart_rate: '#ff6b6b',
blood_pressure: '#ffb347',
stress: '#f7b731',
blood_oxygen: '#00e4ff',
temperature: '#ffd700',
one_key_alarm: '#ffe066',
fall_down: '#00cfff',
sleep: '#7ecfff'
};
const ALERT_SEVERITY_COLOR = {
critical: '#ff6b6b',
high: '#ffb347',
medium: '#f7b731',
low: '#00e4ff'
};
const ALERT_STATUS_COLOR = {
pending: '#ff6b6b',
responded: '#ffb347',
resolved: '#00e4ff'
};
function getAlertTypeColor(type) {
return ALERT_TYPE_COLOR[type] || '#7ecfff';
}
function getAlertSeverityColor(severity) {
return ALERT_SEVERITY_COLOR[severity] || '#7ecfff';
}
function getAlertStatusColor(status) {
return ALERT_STATUS_COLOR[status] || '#7ecfff';
}
// 添加日期格式化函数
function formatDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
// 修改initCharts函数
function initCharts() {
try {
let charts = {
healthScore: null,
stats: null,
trend: null,
alert: null
};
// 检查并初始化健康评分图表
const healthScoreContainer = document.getElementById('healthScoreChart');
if (healthScoreContainer) {
charts.healthScore = echarts.init(healthScoreContainer);
// 从URL获取customerId参数
const urlParams = new URLSearchParams(window.location.search);
const customerId = urlParams.get('customerId') || '1';
// 获取日期范围
const today = new Date();
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 7);
const startDate = formatDate(yesterday);
const endDate = formatDate(today);
// 调用接口获取健康评分数据
fetch(`/health_data/score?orgId=${customerId}&startDate=${startDate}&endDate=${endDate}`)
.then(response => response.json())
.then(result => {
if (result.success && result.data && result.data.healthScores) {
const factors = result.data.healthScores.factors;
// 更新总分显示
const totalScoreElement = document.querySelector('.total-score');
if (totalScoreElement) {
totalScoreElement.textContent = `总分:${result.data.summary.overallScore}`;
}
const healthScoreOption = {
tooltip: {
trigger: 'axis',
formatter: function(params) {
return params[0].name + '<br/>' +
params[0].marker + params[0].seriesName + ':' + params[0].value;
}
},
radar: {
radius: '65%',
center: ['50%', '55%'],
indicator: [
{ name: `心率 ${factors.heartRate?.score || 0}分`, max: 100 },
{ name: `血氧 ${factors.bloodOxygen?.score || 0}分`, max: 100 },
{ name: `体温 ${factors.temperature?.score || 0}分`, max: 100 },
{ name: `步数 ${factors.step?.score || 0}分`, max: 100 },
{ name: `卡路里 ${factors.calorie?.score || 0}分`, max: 100 },
{ name: `收缩压 ${factors.pressureHigh?.score || 0}分`, max: 100 },
{ name: `舒张压 ${factors.pressureLow?.score || 0}分`, max: 100 },
{ name: `压力 ${factors.stress?.score || 0}分`, max: 100 }
],
name: {
textStyle: {
color: '#00e4ff',
fontSize: 12,
padding: [3, 5]
},
rich: {
value: {
color: '#00e4ff',
fontSize: 12,
fontWeight: 'normal'
}
}
},
splitArea: {
show: true,
areaStyle: {
color: ['rgba(0,228,255,0.1)', 'rgba(0,228,255,0.2)']
}
},
axisLine: {
lineStyle: {
color: 'rgba(0,228,255,0.5)'
}
},
splitLine: {
lineStyle: {
color: 'rgba(0,228,255,0.3)'
}
}
},
series: [{
name: '健康指标',
type: 'radar',
data: [{
value: [
factors.heartRate?.score || 0,
factors.bloodOxygen?.score || 0,
factors.temperature?.score || 0,
factors.step?.score || 0,
factors.calorie?.score || 0,
factors.pressureHigh?.score || 0,
factors.pressureLow?.score || 0,
factors.stress?.score || 0
],
name: '当前状态',
itemStyle: {
color: '#00e4ff'
},
areaStyle: {
color: 'rgba(0,228,255,0.4)'
}
}]
}]
};
charts.healthScore.setOption(healthScoreOption);
} else {
// 如果没有数据,显示0分
const totalScoreElement = document.querySelector('.total-score');
if (totalScoreElement) {
totalScoreElement.textContent = '总分:0';
}
const healthScoreOption = {
tooltip: {
trigger: 'axis',
formatter: function(params) {
return params[0].name + '<br/>' +
params[0].marker + params[0].seriesName + ':' + params[0].value;
}
},
radar: {
radius: '65%',
center: ['50%', '55%'],
indicator: [
{ name: '心率 0分', max: 100 },
{ name: '血氧 0分', max: 100 },
{ name: '体温 0分', max: 100 },
{ name: '步数 0分', max: 100 },
{ name: '卡路里 0分', max: 100 },
{ name: '收缩压 0分', max: 100 },
{ name: '舒张压 0分', max: 100 },
{ name: '压力 0分', max: 100 }
],
name: {
textStyle: {
color: '#00e4ff',
fontSize: 12,
padding: [3, 5]
},
rich: {
value: {
color: '#00e4ff',
fontSize: 12,
fontWeight: 'normal'
}
}
},
splitArea: {
show: true,
areaStyle: {
color: ['rgba(0,228,255,0.1)', 'rgba(0,228,255,0.2)']
}
},
axisLine: {
lineStyle: {
color: 'rgba(0,228,255,0.5)'
}
},
splitLine: {
lineStyle: {
color: 'rgba(0,228,255,0.3)'
}
}
},
series: [{
name: '健康指标',
type: 'radar',
data: [{
value: [0, 0, 0, 0, 0, 0, 0, 0],
name: '当前状态',
itemStyle: {
color: '#00e4ff'
},
areaStyle: {
color: 'rgba(0,228,255,0.4)'
}
}]
}]
};
charts.healthScore.setOption(healthScoreOption);
}
})
.catch(error => {
console.error('Error fetching health data:', error);
// 发生错误时也显示0分
const totalScoreElement = document.querySelector('.total-score');
if (totalScoreElement) {
totalScoreElement.textContent = '总分:0';
}
// 设置默认的0分图表
const healthScoreOption = {
tooltip: {
trigger: 'axis',
formatter: function(params) {
return params[0].name + '<br/>' +
params[0].marker + params[0].seriesName + ':' + params[0].value;
}
},
radar: {
radius: '65%',
center: ['50%', '55%'],
indicator: [
{ name: '心率 0分', max: 100 },
{ name: '血氧 0分', max: 100 },
{ name: '体温 0分', max: 100 },
{ name: '步数 0分', max: 100 },
{ name: '卡路里 0分', max: 100 },
{ name: '收缩压 0分', max: 100 },
{ name: '舒张压 0分', max: 100 },
{ name: '压力 0分', max: 100 }
],
name: {
textStyle: {
color: '#00e4ff',
fontSize: 12,
padding: [3, 5]
},
rich: {
value: {
color: '#00e4ff',
fontSize: 12,
fontWeight: 'normal'
}
}
},
splitArea: {
show: true,
areaStyle: {
color: ['rgba(0,228,255,0.1)', 'rgba(0,228,255,0.2)']
}
},
axisLine: {
lineStyle: {
color: 'rgba(0,228,255,0.5)'
}
},
splitLine: {
lineStyle: {
color: 'rgba(0,228,255,0.3)'
}
}
},
series: [{
name: '健康指标',
type: 'radar',
data: [{
value: [0, 0, 0, 0, 0, 0, 0, 0],
name: '当前状态',
itemStyle: {
color: '#00e4ff'
},
areaStyle: {
color: 'rgba(0,228,255,0.4)'
}
}]
}]
};
charts.healthScore.setOption(healthScoreOption);
});
}
// 检查并初始化数据统计图表
const statsContainer = document.getElementById('statsChart');
if (statsContainer) {
charts.stats = echarts.init(statsContainer);
const statsOption = {
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
right: 10,
top: 'center',
textStyle: {
color: '#fff'
}
},
series: [{
name: '设备状态',
type: 'pie',
radius: ['40%', '70%'],
center: ['40%', '50%'],
avoidLabelOverlap: true,
itemStyle: {
borderColor: '#fff',
borderWidth: 1
},
label: {
show: true,
color: '#fff'
},
data: [
{ value: 35, name: '在线' },
{ value: 10, name: '离线' },
{ value: 15, name: '充电中' },
{ value: 20, name: '佩戴中' }
],
color: ['#00e4ff', '#ff4444', '#ffbb00', '#00ff9d']
}]
};
charts.stats.setOption(statsOption);
}
// 检查并初始化趋势分析图表
// 检查并初始化预警信息图表
const alertContainer = document.getElementById('alertList');
if (alertContainer) {
charts.alert = echarts.init(alertContainer);
const alertOption = {
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(0,21,41,0.9)',
borderColor: 'rgba(0,228,255,0.2)',
textStyle: {
color: '#fff'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
show: false
},
yAxis: {
type: 'category',
data: ['心率', '血氧', '体温', '步数', '卡路里', '距离'],
axisLabel: {
color: '#fff',
fontSize: 12
},
axisLine: {
show: false
},
axisTick: {
show: false
}
},
series: [{
type: 'bar',
data: [
{value: 8.02, itemStyle: {color: '#00e4ff'}},
{value: 8.09, itemStyle: {color: '#00e4ff'}},
{value: 2.87, itemStyle: {color: '#3498db'}},
{value: 0.85, itemStyle: {color: '#f39c12'}},
{value: 80.44, itemStyle: {color: '#00e4ff'}},
{value: 2.56, itemStyle: {color: '#e74c3c'}}
],
barWidth: '60%',
label: {
show: true,
position: 'right',
color: '#fff',
formatter: '{c}%'
}
}]
};
charts.alert.setOption(alertOption);
}
// 添加窗口resize事件监听
window.addEventListener('resize', () => {
Object.values(charts).forEach(chart => {
if (chart) {
chart.resize();
}
});
});
// 返回图表实例对象,以便其他地方可能需要使用
return charts;
} catch (error) {
console.error('Error initializing charts:', error);
}
}
// 修改初始化调用
document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
globalCharts = initCharts(); // 存储图表实例
refreshData(); // 初始加载数据
// 每分钟刷新一次数据
setInterval(refreshData, 60000);
}, 100);
});
// 修改消息列表初始化
function initMessageList(data) {
const messageList = document.getElementById('messageList');
const messageCount = document.getElementById('messageCount');
if (!messageList || !messageCount) return;
// 从message_info中获取数据
//console.log('initMessageList.data', data);
const messages = data.message_info.messages || [];
// 过滤出状态为pending的消息
const pendingMessages = messages.filter(msg => msg.message_status === 'pending' || msg.message_status === '1');
// 更新消息计数
messageCount.textContent = pendingMessages.length;
// 清空现有消息
messageList.innerHTML = '';
// 创建消息容器
const messageContainer = document.createElement('div');
messageContainer.className = 'message-container';
// 添加所有消息
pendingMessages.forEach(message => {
const messageElement = document.createElement('div');
messageElement.className = 'message-item';
messageElement.innerHTML = `
<div class="message-header">
<span>${message.department_name || '未知部门'}-${message.user_name || '系统'}</span>
<span class="message-time">${message.received_time || new Date().toLocaleString()}</span>
</div>
<div class="message-content">${message.message || '无消息内容'}</div>
`;
messageContainer.appendChild(messageElement);
});
// 如果没有消息,显示提示信息
if (pendingMessages.length === 0) {
messageList.innerHTML = '<div class="no-messages">暂无待处理消息</div>';
return;
}
// 将消息容器添加到列表中
messageList.appendChild(messageContainer);
// 添加样式
const style = document.createElement('style');
style.textContent = `
#messageList {
height: 280px; /* 固定高度,刚好容纳4条消息 */
overflow: hidden;
position: relative;
margin-bottom: 10px; /* 添加底部间距 */
}
.message-container {
position: relative;
animation: scrollMessages linear infinite;
}
.message-item {
height: 70px; /* 固定每条消息的高度 */
padding: 10px;
border-bottom: 1px solid rgba(0, 240, 255, 0.1);
background: rgba(0, 15, 29, 0.6);
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.message-item:last-child {
border-bottom: none;
}
.message-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: rgba(255, 255, 255, 0.7);
height: 20px;
}
.message-time {
color: rgba(0, 240, 255, 0.7);
}
.message-content {
color: #fff;
font-size: 14px;
line-height: 1.3;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
height: 36px;
}
.no-messages {
text-align: center;
padding: 20px;
color: rgba(255, 255, 255, 0.5);
}
@keyframes scrollMessages {
0% {
transform: translateY(0);
}
100% {
transform: translateY(calc(-100% + 280px)); /* 减去容器高度 */
}
}
`;
document.head.appendChild(style);
// 计算动画持续时间(基于消息数量)
const animationDuration = pendingMessages.length * 3; // 每条消息显示3秒
messageContainer.style.animationDuration = `${animationDuration}s`;
// 鼠标悬停控制
messageList.addEventListener('mouseenter', () => {
messageContainer.style.animationPlayState = 'paused';
});
messageList.addEventListener('mouseleave', () => {
messageContainer.style.animationPlayState = 'running';
});
// 监听动画结束,重新开始
messageContainer.addEventListener('animationend', () => {
messageContainer.style.animation = 'none';
setTimeout(() => {
messageContainer.style.animation = `scrollMessages ${animationDuration}s linear infinite`;
}, 10);
});
}
</script>
<script src="https://webapi.amap.com/maps?v=2.0&key=7de0c6b90cb13571ce931ca75a66c6e7"></script>
<script src="https://webapi.amap.com/loca?v=2.0.0&key=7de0c6b90cb13571ce931ca75a66c6e7"></script>
<script src="/static/js/map-optimizer.js"></script> <!-- 地图优化模块 -->
<script>
// Declare infoWindow globally
// let infoWindow; // 移除
let geoLevelF,geoLevelE,geo,map,loca,breathGreen,breathRed,breathYellow;//变量合并
async function getFirstCoordinates(map, deptId, userId) {
try {
console.log('getFirstCoordinates.deptId',deptId);
console.log('getFirstCoordinates.userId',userId);
const response = await fetch(`./generateAlertJson?customerId=${deptId}&userId=${userId}`);
console.log('getFirstCoordinates.response',response);
const result = await response.json();
if (result.features && result.features.length > 0) {
const coordinates = result.features[0].geometry.coordinates;
map.setCenter(coordinates);
console.log('getFirstCoordinates.coordinates',coordinates);
return coordinates;
} else {
const response1 = await fetch(`./generateHealthJson?customerId=${deptId}&userId=${userId}`);
const result1 = await response1.json();
if (result1.features && result1.features.length > 0) {
const coordinates = result1.features[0].geometry.coordinates;
map.setCenter(coordinates);
console.log('getFirstCoordinates.coordinates',coordinates);
return coordinates;
} else {
return [114.01508952, 22.54036796, 0];
}
}
} catch (error) {
console.error('Error accessing coordinates:', error);
return [114.01508952, 22.54036796, 0]; // 默认坐标
}
}
async function updateGeoJSONSources(deptId, userId) {
try {
console.log('Updating GeoJSON sources...', { deptId, userId });
// 构建URL参数
const params = new URLSearchParams({
customerId: deptId || '1',
userId: userId || '-1'
}).toString();
// 获取数据
console.log('updateGeoJSONSources.params',`./generateHealthJson?${params}`);
const [criticalData, highData, healthData] = await Promise.all([
fetch(`./generateAlertJson?${params}&severityLevel=critical`).then(res => res.json()),
fetch(`./generateAlertJson?${params}&severityLevel=high`).then(res => res.json()),
fetch(`./generateHealthJson?${params}`).then(res => res.json())
]);
// 创建新的数据源
const newGeoLevelF = new Loca.GeoJSONSource({
data: criticalData
});
const newGeoLevelE = new Loca.GeoJSONSource({
data: highData
});
const newGeo = new Loca.GeoJSONSource({
data: healthData
});
// 更新图层数据源
breathRed.setSource(newGeoLevelF);
breathYellow.setSource(newGeoLevelE);
breathGreen.setSource(newGeo);
// 更新地图中心点
if (healthData.features && healthData.features.length > 0) {
const coordinates = healthData.features[0].geometry.coordinates;
console.log('updateGeoJSONSources.coordinates',coordinates);
map.setCenter(coordinates);
}
// 重新启动动画
loca.animate.start();
console.log('GeoJSON sources updated successfully');
} catch (error) {
console.error('Error updating GeoJSON sources:', error);
}
}
function initializeMap(deptId, userId) {
//console.log(AMap); // 如果打印 `undefined`,说明 AMap 没有正确加载
//console.log(Loca); // 如果打印 `undefined`,说明 Loca 没有正确加载
//console.log('initializeMap.deptId',deptId);
//console.log('initializeMap.userId',userId);
//console.log('initializeMap.severityLevel',`./generateAlertJson?customerId=${deptId}&userId=${userId}&severityLevel=critical`);
geoLevelF = new Loca.GeoJSONSource({
url: `./generateAlertJson?customerId=${deptId}&userId=${userId}&severityLevel=critical`
});
// 黄色告警点
geoLevelE = new Loca.GeoJSONSource({
url: `./generateAlertJson?customerId=${deptId}&userId=${userId}&severityLevel=high`
});
// 绿色健康点
geo = new Loca.GeoJSONSource({
url: `./generateHealthJson?customerId=${deptId}&userId=${userId}`
});
console.log('geo',geo);
let current_coordinates = {
'longitude': 114.01508952,
'latitude': 22.58036796,
'altitude': 0
}
//console.log('current_coordinates',current_coordinates);
map = window.map = new AMap.Map('map-container', {
zoom: 17,
center: [current_coordinates['longitude'],current_coordinates['latitude']],
pitch: 45,
showLabel: false,
mapStyle: 'amap://styles/blue',
viewMode: '3D',
});
//const api_coordinates = getFirstCoordinates(map,deptId,userId);
loca = new Loca.Container({
map,
});
breathGreen = new Loca.ScatterLayer({
loca,
zIndex: 113,
opacity: 1,
visible: true,
zooms: [2, 22],
});
breathGreen.setSource(geo);
//console.log("BreathGreen GeoJSON data:", breathGreen);
breathGreen.setStyle({
unit: 'meter',
color: 'rgb(39, 207, 14)',
size: [10, 10],
borderWidth: 0,
exture: 'https://a.amap.com/Loca/static/loca-v2/demos/images/breath_green.png',
//texture: 'http://localhost:5001/static/images/avarta.png',
duration: 500,
animate: true,
});
// 添加到地图中
loca.add(breathGreen);
// 创建红色呼吸点图层
breathRed = new Loca.ScatterLayer({
loca,
zIndex: 113,
opacity: 1,
visible: true,
zooms: [2, 22],
});
//console.log('Health data:',geoLevelF);
breathRed.setSource(geoLevelF);
//console.log("BreathRed GeoJSON data:", breathRed);
breathRed.setStyle({
unit: 'meter',
size: [60, 60],
borderWidth: 0,
texture: 'https://a.amap.com/Loca/static/loca-v2/demos/images/breath_red.png',
duration: 500,
animate: true,
});
// 添加到地图中
loca.add(breathRed);
// 添加点击事件弹出信息框
map.on('click',e=>{const p=e.pixel.toArray();let f=breathRed.queryFeature(p)||breathYellow.queryFeature(p)||breathGreen.queryFeature(p);if(f&&f.coordinates){showCustomMapInfo(f)}else{removeCustomMapInfo()}});//优先级修正
breathYellow = new Loca.ScatterLayer({
loca,
zIndex: 112,
opacity: 1,
visible: true,
zooms: [2, 22],
});
breathYellow.setSource(geoLevelE);
//console.log("BreathYellow GeoJSON data:", breathYellow);
breathYellow.setStyle({
unit: 'meter',
size: [50, 50],
borderWidth: 0,
texture: 'https://a.amap.com/Loca/static/loca-v2/demos/images/breath_yellow.png',
duration: 1000,
animate: true,
});
loca.add(breathYellow);
//console.log("GeoJSON data for BreathRed:", geoLevelF);
//console.log("Map center:", map.getCenter());
// 启动渲染动画
loca.animate.start();
// 添加点击事件,显示信息框
// var infoWindow = new AMap.InfoWindow({
// offset: new AMap.Pixel(0, -30),
// });
// var dat = new Loca.Dat();
// dat.addLayer(breathGreen, '绿色');
// dat.addLayer(breathRed, '红色');
// dat.addLayer(breathYellow, '黄色');
// Function to fetch weather data
}
initializeMap('{{ customerId }}','-1');
async function reverseGeocode(lng, lat) {
try {
const response = await fetch(`https://restapi.amap.com/v3/geocode/regeo?key=d45f28e481665db5b1145a5aa989e68a&location=${lng},${lat}`);
const data = await response.json();
if (data.status === '1') {
const address = data.regeocode.formatted_address;
console.log('Address:', address);
return address;
} else {
console.error('Failed to reverse geocode:', data.info);
return null;
}
} catch (error) {
console.error('Error during reverse geocoding:', error);
return null;
}
}
function handleAlert(alertId) {
fetch(`./dealAlert?alertId=${alertId}`)
.then(response => response.json())
.then(data => {
if (data.success) {
console.log('Alert processed:', data);
showCustomAlert(data.message, () => {
location.reload();
});
} else {
console.error('Failed to process alert:', data.message);
showCustomAlert('处理失败,请重试', null);
}
})
.catch(error => {
console.error('Error processing alert:', error);
showCustomAlert('处理过程中发生错误,请重试', null);
});
}
// 添加自定义弹出框函数
function showCustomAlert(message, callback) {
// 创建遮罩层
const overlay = document.createElement('div');
overlay.className = 'custom-alert-overlay';
document.body.appendChild(overlay);
// 创建弹出框
const alertBox = document.createElement('div');
alertBox.className = 'custom-alert';
alertBox.innerHTML = `
<div class="custom-alert-content">${message}</div>
<button class="custom-alert-button">确定</button>
`;
document.body.appendChild(alertBox);
// 添加确定按钮点击事件
const confirmButton = alertBox.querySelector('.custom-alert-button');
confirmButton.onclick = () => {
document.body.removeChild(overlay);
document.body.removeChild(alertBox);
if (callback) callback();
};
}
function mapAlertStatus(alertStatus) {
switch (alertStatus) {
case 'responded':
return '已处理';
case 'pending':
return '未处理';
case 'closed':
return '已关闭';
default:
return '未知';
}
}
// 添加在其他 JavaScript 代码之前
function toggleFilter() {
const filterPanel = document.querySelector('.filter-panel');
const filterContainer = document.querySelector('.filter-container');
const toggleButton = document.querySelector('.filter-toggle i');
console.log('toggleFilter', toggleButton);
if (filterPanel.classList.contains('collapsed')) {
// 展开面板
filterPanel.classList.remove('collapsed');
toggleButton.classList.remove('fa-search');
toggleButton.classList.add('fa-times');
filterContainer.style.display = 'flex';
} else {
// 收起面板
filterPanel.classList.add('collapsed');
toggleButton.classList.remove('fa-times');
toggleButton.classList.add('fa-search');
filterContainer.style.display = 'none';
}
}
// 添加点击外部关闭筛选面板的功能
document.addEventListener('click', function(event) {
const filterPanel = document.querySelector('.filter-panel');
const filterToggle = document.querySelector('.filter-toggle');
// 如果点击的不是筛选面板内的元素且面板是展开的
if (!filterPanel.contains(event.target) &&
!filterToggle.contains(event.target) &&
!filterPanel.classList.contains('collapsed')) {
toggleFilter();
}
});
// 阻止筛选面板内的点击事件冒泡
document.querySelector('.filter-panel').addEventListener('click', function(event) {
event.stopPropagation();
});
// 初始化人员管理面板
function initPersonnelManagementPanel(data) {
// 更新总览数据
document.getElementById('totalUsers').innerText = data.user_info.totalUsers;
document.getElementById('totalBindDevices').innerText = data.user_info.totalDevices;
// 初始化部门分布图表
initDepartmentDistribution(data);
}
// 修改初始化部门分布图表函数 - 专业版
function initDepartmentDistribution(data) {
console.log('initDepartmentDistribution 开始执行,数据:', data); // 调试信息
const userInfo = data.user_info || {};
// 更新统计卡片数据
const totalUsers = userInfo.totalUsers || 0;
const totalDevices = userInfo.totalDevices || 0;
const departmentCount = userInfo.departmentCount || {};
const activeDeptCount = Object.keys(departmentCount).length;
// 模拟在线用户数据(实际应从后端获取)
const onlineUsers = Math.floor(totalUsers * 0.75); // 假设75%在线
const onlineRate = totalUsers > 0 ? ((onlineUsers / totalUsers) * 100).toFixed(1) : 0;
const alertUsers = Math.floor(totalUsers * 0.1); // 假设10%有告警
// 更新统计数据
document.getElementById('totalUsers').textContent = totalUsers;
document.getElementById('totalBindDevices').textContent = totalDevices;
document.getElementById('onlineRate').textContent = onlineRate + '%';
document.getElementById('activeDeptCount').textContent = activeDeptCount;
document.getElementById('onlineUsers').textContent = onlineUsers;
document.getElementById('boundDevices').textContent = totalDevices;
document.getElementById('alertUsers').textContent = alertUsers;
// 1. 部门分布图表 - 环形图
const departmentChart = echarts.init(document.getElementById('departmentDistribution'));
// 处理部门数据
const departmentData = Object.entries(departmentCount)
.map(([name, value]) => ({ name, value }))
.sort((a, b) => b.value - a.value);
// 如果没有数据,显示默认状态
const hasDeptData = departmentData.length > 0 && departmentData.some(d => d.value > 0);
const displayDeptData = hasDeptData ? departmentData : [
{ name: '暂无部门', value: 1 }
];
const deptColors = ['#00e4ff', '#00a8ff', '#0080ff', '#48cae4', '#90e0ef', '#a8dadc', '#457b9d'];
const departmentOption = {
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(0,21,41,0.95)',
borderColor: '#00e4ff',
borderWidth: 1,
textStyle: { color: '#fff', fontSize: 11 },
formatter: function(params) {
if (!hasDeptData) return '暂无数据';
return `${params.name}<br/>人数: ${params.value}人<br/>占比: ${params.percent}%`;
}
},
series: [{
type: 'pie',
radius: ['40%', '75%'],
center: ['50%', '60%'],
avoidLabelOverlap: true,
itemStyle: {
borderColor: 'rgba(0,21,41,0.8)',
borderWidth: 2,
shadowBlur: 8,
shadowColor: 'rgba(0,228,255,0.3)'
},
label: {
show: true,
position: 'outside',
color: '#fff',
fontSize: 9,
formatter: function(params) {
if (!hasDeptData) return '';
return params.value > 0 ? `${params.name}\n${params.value}人` : '';
},
lineHeight: 10
},
labelLine: {
show: true,
length: 6,
length2: 4,
lineStyle: { color: 'rgba(255,255,255,0.5)', width: 1 }
},
emphasis: {
itemStyle: {
shadowBlur: 15,
shadowOffsetX: 0,
shadowColor: 'rgba(0,228,255,0.6)',
scale: 1.05
}
},
data: displayDeptData.map((item, index) => ({
...item,
itemStyle: { color: deptColors[index % deptColors.length] }
}))
}]
};
departmentChart.setOption(departmentOption);
// 2. 用户状态图表 - 柱状图
const statusChart = echarts.init(document.getElementById('userStatusChart'));
// 模拟状态数据
const statusData = {
online: onlineUsers,
offline: totalUsers - onlineUsers,
bound: totalDevices,
unbound: totalUsers - totalDevices,
alert: alertUsers,
normal: totalUsers - alertUsers
};
const statusOption = {
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(0,21,41,0.95)',
borderColor: '#00e4ff',
textStyle: { color: '#fff', fontSize: 11 },
formatter: function(params) {
if (!params || !params[0]) return '';
const data = params[0];
return `${data.name}: ${data.value}人`;
}
},
grid: {
top: 25,
left: 25,
right: 15,
bottom: 20,
containLabel: true
},
xAxis: {
type: 'category',
data: ['在线', '离线', '绑定', '未绑定', '告警', '正常'],
axisLabel: {
color: '#7ecfff',
fontSize: 9,
interval: 0,
rotate: 0
},
axisLine: { lineStyle: { color: 'rgba(126,207,255,0.3)' } },
axisTick: { show: false }
},
yAxis: {
type: 'value',
axisLabel: { color: '#7ecfff', fontSize: 9 },
splitLine: { lineStyle: { color: 'rgba(126,207,255,0.1)', type: 'dashed' } },
axisLine: { show: false }
},
series: [{
type: 'bar',
data: [
{ value: statusData.online, itemStyle: { color: '#00ff9d' } },
{ value: statusData.offline, itemStyle: { color: '#666' } },
{ value: statusData.bound, itemStyle: { color: '#ffbb00' } },
{ value: statusData.unbound, itemStyle: { color: '#ff8800' } },
{ value: statusData.alert, itemStyle: { color: '#ff4444' } },
{ value: statusData.normal, itemStyle: { color: '#00e4ff' } }
],
barWidth: '60%',
label: {
show: true,
position: 'top',
color: '#fff',
fontSize: 9,
formatter: '{c}'
},
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowColor: 'rgba(0,228,255,0.5)'
}
}
}]
};
statusChart.setOption(statusOption);
// 自适应大小
const resizeCharts = () => {
try {
departmentChart && departmentChart.resize();
statusChart && statusChart.resize();
} catch (e) {
console.warn('人员管理图表resize失败:', e);
}
};
window.addEventListener('resize', resizeCharts);
// 延迟执行确保图表正确渲染
setTimeout(() => {
resizeCharts();
console.log('人员管理图表初始化完成');
}, 200);
// 图表点击交互
departmentChart.on('click', function(params) {
if (hasDeptData && params.data && params.data.value > 0) {
showDepartmentDetails(params.data.name, params.data.value);
}
});
statusChart.on('click', function(params) {
const statusName = params.name;
const statusValue = params.value;
showStatusDetails(statusName, statusValue);
});
return { departmentChart, statusChart };
}
function getPastDateStr(daysAgo) {
const d = new Date();
d.setDate(d.getDate() - daysAgo);
return d.toISOString().slice(0,10);
}
function loadBaselineTrendChart(orgId) {
const colorMap = {
'心率': '#00e4ff',
'血氧': '#00ff9d',
'体温': '#ffbb00',
'压力': '#ff4757',
'睡眠': '#00a8ff',
};
const endDate = getPastDateStr(0), startDate = getPastDateStr(6);
fetch(`/health_data/chart/baseline?orgId=${orgId}&startDate=${startDate}&endDate=${endDate}`)
.then(r=>r.json())
.then(result=>{
const {dates, metrics} = result;
const legendNames = metrics.map(m=>m.name);
const series = metrics.map(m=>({
name: m.name,
type: 'line',
data: m.values,
smooth: true,
symbol: 'circle',
symbolSize: 8,
lineStyle: { width: 1, color: colorMap[m.name]||'#fff' },
itemStyle: { color: colorMap[m.name]||'#fff' },
connectNulls: true
}));
const option = {
backgroundColor: 'transparent',
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(10,24,48,0.98)',
borderColor: '#00e4ff',
borderWidth: 1,
textStyle: { color: '#fff', fontSize: 12 },
extraCssText: 'box-shadow:0 0 10px #00e4ff44;',
formatter: params => {
let t = `<div style='font-size:12px;margin-bottom:6px;'>${params[0].axisValue}</div>`;
params.forEach(p=>{
t += `<div style="display:flex;align-items:center;margin-bottom:2px;">
<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${p.color};margin-right:8px;"></span>
<span>${p.seriesName}</span>
<span style="font-weight:300;margin-left:10px;">${p.data==null?'-':p.data}</span>
</div>`;
});
return t;
}
},
legend: {
data: legendNames,
top: 10,
left: 'center',
icon: 'circle',
itemWidth: 10,
itemHeight: 10,
textStyle: { color: '#7ecfff', fontSize: 12, fontWeight: 500 }
},
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: {
type: 'category',
data: dates,
boundaryGap: false,
axisLabel: { color: '#7ecfff', fontSize: 14 },
axisLine: { lineStyle: { color: 'rgba(0,228,255,0.15)' } },
splitLine: { show: false }
},
yAxis: {
type: 'value',
axisLabel: { color: '#7ecfff', fontSize: 14 },
axisLine: { lineStyle: { color: 'rgba(0,228,255,0.15)' } },
splitLine: { lineStyle: { color: 'rgba(0,228,255,0.08)', type: 'dashed' } }
},
series
};
echarts.init(document.getElementById('trendChart')).setOption(option);
});
}
// 修改数据刷新函数
function refreshData() {
// 从 URL 获取 customerId 参数
const urlParams = new URLSearchParams(window.location.search);
console.log('urlParams', urlParams.get('customerId'));
const customerId = urlParams.get('customerId') || '1';
loadBaselineTrendChart(customerId);
fetch(`/get_total_info?customer_id=${customerId}`)
.then(response => response.json())
.then(result => {
if (result.success) {
const data = result.data;
console.log('Refreshing data:', data);
lastTotalInfo = data;
updateMapData(lastTotalInfo);
// 刷新所有图表
if (globalCharts) {
// 更新健康评分图表
if (globalCharts.healthScore) {
// 获取日期范围
const today = new Date();
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 7);
const startDate = formatDate(yesterday);
const endDate = formatDate(today);
fetch(`/health_data/score?orgId=${customerId}&startDate=${startDate}&endDate=${endDate}`)
.then(response => response.json())
.then(result => {
if (result.success && result.data && result.data.healthScores) {
const factors = result.data.healthScores.factors;
console.log('factors',factors);
// 更新总分显示
const totalScoreElement = document.querySelector('.total-score');
if (totalScoreElement) {
totalScoreElement.textContent = `总分:${result.data.summary.overallScore}`;
}
const healthScoreOption = {
tooltip: {
trigger: 'axis',
formatter: function(params) {
return params[0].name + '<br/>' +
params[0].marker + params[0].seriesName + ':' + params[0].value;
}
},
radar: {
radius: '65%',
center: ['50%', '55%'],
indicator: [
{ name: `心率 ${factors.heartRate?.score || 0}分`, max: 100 },
{ name: `血氧 ${factors.bloodOxygen?.score || 0}分`, max: 100 },
{ name: `体温 ${factors.temperature?.score || 0}分`, max: 100 },
{ name: `步数 ${factors.step?.score || 0}分`, max: 100 },
{ name: `卡路里 ${factors.calorie?.score || 0}分`, max: 100 },
{ name: `收缩压 ${factors.pressureHigh?.score || 0}分`, max: 100 },
{ name: `舒张压 ${factors.pressureLow?.score || 0}分`, max: 100 },
{ name: `压力 ${factors.stress?.score || 0}分`, max: 100 }
],
name: {
textStyle: {
color: '#00e4ff',
fontSize: 12,
padding: [3, 5]
},
rich: {
value: {
color: '#00e4ff',
fontSize: 12,
fontWeight: 'normal'
}
}
},
splitArea: {
show: true,
areaStyle: {
color: ['rgba(0,228,255,0.1)', 'rgba(0,228,255,0.2)']
}
},
axisLine: {
lineStyle: {
color: 'rgba(0,228,255,0.5)'
}
},
splitLine: {
lineStyle: {
color: 'rgba(0,228,255,0.3)'
}
}
},
series: [{
name: '健康指标',
type: 'radar',
data: [{
value: [
factors.heartRate?.score || 0,
factors.bloodOxygen?.score || 0,
factors.temperature?.score || 0,
factors.step?.score || 0,
factors.calorie?.score || 0,
factors.pressureHigh?.score || 0,
factors.pressureLow?.score || 0,
factors.stress?.score || 0
],
name: '当前状态',
itemStyle: {
color: '#00e4ff'
},
areaStyle: {
color: 'rgba(0,228,255,0.4)'
}
}]
}]
};
globalCharts.healthScore.setOption(healthScoreOption);
} else {
// 如果没有数据,显示0分
const totalScoreElement = document.querySelector('.total-score');
if (totalScoreElement) {
totalScoreElement.textContent = '总分:0';
}
const healthScoreOption = {
tooltip: {
trigger: 'axis',
formatter: function(params) {
return params[0].name + '<br/>' +
params[0].marker + params[0].seriesName + ':' + params[0].value;
}
},
radar: {
radius: '65%',
center: ['50%', '55%'],
indicator: [
{ name: '心率 0分', max: 100 },
{ name: '血氧 0分', max: 100 },
{ name: '体温 0分', max: 100 },
{ name: '步数 0分', max: 100 },
{ name: '卡路里 0分', max: 100 },
{ name: '收缩压 0分', max: 100 },
{ name: '舒张压 0分', max: 100 },
{ name: '压力 0分', max: 100 }
],
name: {
textStyle: {
color: '#00e4ff',
fontSize: 12,
padding: [3, 5]
},
rich: {
value: {
color: '#00e4ff',
fontSize: 12,
fontWeight: 'normal'
}
}
},
splitArea: {
show: true,
areaStyle: {
color: ['rgba(0,228,255,0.1)', 'rgba(0,228,255,0.2)']
}
},
axisLine: {
lineStyle: {
color: 'rgba(0,228,255,0.5)'
}
},
splitLine: {
lineStyle: {
color: 'rgba(0,228,255,0.3)'
}
}
},
series: [{
name: '健康指标',
type: 'radar',
data: [{
value: [0, 0, 0, 0, 0, 0, 0, 0],
name: '当前状态',
itemStyle: {
color: '#00e4ff'
},
areaStyle: {
color: 'rgba(0,228,255,0.4)'
}
}]
}]
};
globalCharts.healthScore.setOption(healthScoreOption);
}
})
.catch(error => {
console.error('Error fetching health data:', error);
// 发生错误时也显示0分
const totalScoreElement = document.querySelector('.total-score');
if (totalScoreElement) {
totalScoreElement.textContent = '总分:0';
}
// 设置默认的0分图表
const healthScoreOption = {
tooltip: {
trigger: 'axis',
formatter: function(params) {
return params[0].name + '<br/>' +
params[0].marker + params[0].seriesName + ':' + params[0].value;
}
},
radar: {
radius: '65%',
center: ['50%', '55%'],
indicator: [
{ name: '心率 0分', max: 100 },
{ name: '血氧 0分', max: 100 },
{ name: '体温 0分', max: 100 },
{ name: '步数 0分', max: 100 },
{ name: '卡路里 0分', max: 100 },
{ name: '收缩压 0分', max: 100 },
{ name: '舒张压 0分', max: 100 },
{ name: '压力 0分', max: 100 }
],
name: {
textStyle: {
color: '#00e4ff',
fontSize: 12,
padding: [3, 5]
},
rich: {
value: {
color: '#00e4ff',
fontSize: 12,
fontWeight: 'normal'
}
}
},
splitArea: {
show: true,
areaStyle: {
color: ['rgba(0,228,255,0.1)', 'rgba(0,228,255,0.2)']
}
},
axisLine: {
lineStyle: {
color: 'rgba(0,228,255,0.5)'
}
},
splitLine: {
lineStyle: {
color: 'rgba(0,228,255,0.3)'
}
}
},
series: [{
name: '健康指标',
type: 'radar',
data: [{
value: [0, 0, 0, 0, 0, 0, 0, 0],
name: '当前状态',
itemStyle: {
color: '#00e4ff'
},
areaStyle: {
color: 'rgba(0,228,255,0.4)'
}
}]
}]
};
globalCharts.healthScore.setOption(healthScoreOption);
});
}
}
// 更新人员管理面板
initPersonnelManagementPanel(data);
// 更新告警信息图表
initAlertChart(data);
// 更新设备管理图表
initDeviceChart(data);
// 更新消息列表
initMessageList(data);
// 为各个面板添加点击事件
setupPanelClickEvents(customerId);
}
})
.catch(error => console.error('Error fetching data:', error));
}
// 将面板点击事件设置抽取为单独的函数
function setupPanelClickEvents(customerId) {
// 1. 告警信息面板
const alertPanel = document.querySelector('.panel:has(#alertList)');
if (alertPanel) {
alertPanel.style.cursor = 'pointer';
alertPanel.onclick = function() {
createModalWindow(`/alert_view.html?customerId=${customerId}`);
};
}
// 2. 设备管理面板
const devicePanel = document.querySelector('.panel:has(#statsChart)');
if (devicePanel) {
devicePanel.style.cursor = 'pointer';
devicePanel.onclick = function() {
createModalWindow(`/device_view.html?customerId=${customerId}`);
};
}
// 3. 消息信息面板
const messagePanel = document.querySelector('.panel:has(#messageList)');
if (messagePanel) {
messagePanel.style.cursor = 'pointer';
messagePanel.onclick = function() {
createModalWindow(`/message_view.html?customerId=${customerId}`);
};
}
// 4. 趋势分析面板
const trendPanel = document.querySelector('.panel:has(#trendChart)');
if (trendPanel) {
trendPanel.style.cursor = 'pointer';
trendPanel.onclick = function() {
createModalWindow(`/health_main?customerId=${customerId}`);
};
}
// 5. 人员管理面板
const personnelPanel = document.querySelector('.panel:has(#departmentDistribution)');
if (personnelPanel) {
personnelPanel.style.cursor = 'pointer';
personnelPanel.onclick = function() {
createModalWindow(`/user_view.html?customerId=${customerId}`);
};
}
// 6. 健康评分面板
const scorePanel = document.querySelector('.panel:has(#healthScoreChart)');
if (scorePanel) {
scorePanel.style.cursor = 'pointer';
scorePanel.onclick = function() {
createModalWindow(`/user_health_data_analysis.html?customerId=${customerId}`);
};
}
}
// 修改初始化调用
document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
globalCharts = initCharts(); // 存储图表实例
refreshData(); // 初始加载数据
// 每分钟刷新一次数据
setInterval(refreshData, 60000);
}, 100);
});
// 添加通用的创建模态窗口函数
function createModalWindow(url) {
const modalContainer = document.createElement('div');
modalContainer.className = 'modal-container';
modalContainer.innerHTML = `
<div class="modal-content">
<button class="modal-close">✖</button>
<div class="modal-header">
<div class="filter-controls">
<div class="select-group">
<select id="modalDeptSelect" class="modal-select">
<option value="">选择部门</option>
</select>
<select id="modalUserSelect" class="modal-select">
<option value="">选择用户</option>
</select>
</div>
<div class="date-picker" style="display: none;">
<!-- 预留时间选择器位置 -->
</div>
</div>
</div>
<iframe src="${url}" class="user-view-iframe"></iframe>
</div>
`;
document.body.appendChild(modalContainer);
// 添加样式
const style = document.createElement('style');
style.textContent = `
.modal-header {
position: absolute;
top: 10px;
right: 50px;
z-index: 2;
display: flex;
align-items: center;
gap: 20px;
}
.filter-controls {
display: flex;
align-items: center;
gap: 15px;
}
.select-group {
display: flex;
gap: 10px;
}
.modal-select {
background: rgba(0, 21, 41, 0.8);
border: 1px solid rgba(0, 228, 255, 0.3);
border-radius: 4px;
color: #fff;
padding: 5px 10px;
font-size: 14px;
min-width: 120px;
cursor: pointer;
transition: all 0.3s ease;
}
.modal-select:hover {
border-color: rgba(0, 228, 255, 0.6);
}
.modal-select option {
background: rgba(0, 21, 41, 0.9);
color: #fff;
}
.user-view-iframe {
margin-top: 10px;
}
`;
document.head.appendChild(style);
// 存储部门数据的映射关系
const departmentMap = new Map();
// 获取部门数据并填充选择框
fetch(`/get_departments?orgId={{ customerId }}`)
.then(response => response.json())
.then(response => {
if (response.success && response.data) {
const deptSelect = document.getElementById('modalDeptSelect');
// 递归添加部门选项并保存映射关系
function addDepartmentOptions(departments, level = 0) {
departments.forEach(dept => {
const option = document.createElement('option');
option.value = dept.id;
const indent = ' '.repeat(level);
option.textContent = indent + dept.name;
deptSelect.appendChild(option);
// 保存部门ID和名称的映射
departmentMap.set(dept.id.toString(), dept.name); // 确保key为字符串类型
if (dept.children && dept.children.length > 0) {
addDepartmentOptions(dept.children, level + 1);
}
});
}
addDepartmentOptions(response.data);
}
})
.catch(error => console.error('Error fetching departments:', error));
// 部门选择变化时更新用户列表
const deptSelect = document.getElementById('modalDeptSelect');
const userSelect = document.getElementById('modalUserSelect');
deptSelect.addEventListener('change', function() {
const selectedDeptId = this.value;
const selectedDeptName = selectedDeptId ? departmentMap.get(selectedDeptId.toString()) : ''; // 确保ID为字符串类型进行查找
userSelect.innerHTML = '<option value="">选择用户</option>';
if (selectedDeptId) {
fetch(`/fetch_users?orgId=${selectedDeptId}`)
.then(response => response.json())
.then(data => {
if (data) {
data.forEach(user => {
const option = document.createElement('option');
option.value = user.id;
option.textContent = user.user_name;
// 将用户名存储在data属性中
option.dataset.userName = user.user_name;
userSelect.appendChild(option);
});
// 添加"全部用户"选项
const allOption = document.createElement('option');
allOption.value = 'all';
allOption.textContent = '全部用户';
userSelect.appendChild(allOption);
}
})
.catch(error => console.error('Error fetching users:', error));
}
// 更新 iframe URL,包含部门信息
updateIframeUrl(selectedDeptId, selectedDeptName);
});
// 用户选择变化时触发事件
userSelect.addEventListener('change', function() {
const selectedDeptId = deptSelect.value;
const selectedDeptName = selectedDeptId ? departmentMap.get(selectedDeptId.toString()) : ''; // 确保ID为字符串类型进行查找
const selectedUserId = this.value;
const selectedOption = this.options[this.selectedIndex];
const selectedUserName = selectedOption.dataset.userName || '';
// 更新 iframe URL,包含部门和用户信息
updateIframeUrl(selectedDeptId, selectedDeptName, selectedUserId, selectedUserName);
});
// 更新 iframe URL 的辅助函数
function updateIframeUrl(deptId, deptName, userId, userName) {
const iframe = modalContainer.querySelector('iframe');
let newUrl = new URL(iframe.src);
// 保持原有的 customerId 参数
const customerId = newUrl.searchParams.get('customerId');
// 重置 URL 参数
newUrl.search = '';
// 重新添加所有必要的参数
if (customerId) {
newUrl.searchParams.set('customerId', customerId);
}
if (deptId) {
newUrl.searchParams.set('deptId', deptId);
newUrl.searchParams.set('deptName', encodeURIComponent(deptName || ''));
}
if (userId && userId !== 'all') {
newUrl.searchParams.set('userId', userId);
newUrl.searchParams.set('userName', encodeURIComponent(userName || ''));
}
// 更新 iframe 的 src
iframe.src = newUrl.toString();
// 如果页面有刷新数据的函数,尝试调用它
try {
iframe.contentWindow.fetchData && iframe.contentWindow.fetchData();
} catch (e) {
console.log('No fetchData function found in iframe or cross-origin restrictions apply');
}
}
// 添加关闭事件
const closeBtn = modalContainer.querySelector('.modal-close');
closeBtn.onclick = () => {
modalContainer.remove();
style.remove();
};
// 点击遮罩层关闭
modalContainer.onclick = (e) => {
if (e.target === modalContainer) {
modalContainer.remove();
style.remove();
}
};
}
// 添加面板样式
const panelStyle = document.createElement('style');
panelStyle.textContent = `
.panel {
cursor: pointer;
transition: all 0.3s ease;
}
.panel:hover {
border-color: rgba(0, 228, 255, 0.4);
box-shadow: 0 0 15px rgba(0, 228, 255, 0.2);
}
.modal-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
animation: fadeIn 0.3s ease;
}
.modal-content {
position: relative;
width: 90%;
height: 90%;
background: rgba(0, 21, 41, 0.95);
border: 1px solid rgba(0, 228, 255, 0.3);
border-radius: 8px;
padding: 20px;
box-shadow: 0 0 20px rgba(0, 228, 255, 0.2);
animation: slideIn 0.3s ease;
}
.modal-close {
position: absolute;
top: 10px;
right: 10px;
background: transparent;
border: none;
color: #00e4ff;
font-size: 20px;
cursor: pointer;
z-index: 1;
padding: 5px 10px;
transition: all 0.3s ease;
}
.modal-close:hover {
transform: scale(1.1);
color: #ff4444;
}
.user-view-iframe {
width: 100%;
height: 100%;
border: none;
border-radius: 4px;
background: rgba(1, 19, 38, 0.8);
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideIn {
from {
transform: translateY(-20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
`;
document.head.appendChild(panelStyle);
// 初始化告警信息图表 - 专业版
function initAlertChart(data) {
console.log('initAlertChart 开始执行,数据:', data); // 调试信息
const alertContainer = document.getElementById('alertList');
if (!alertContainer) {
console.warn('告警容器 #alertList 未找到');
return;
}
// 清空容器并创建专业布局
alertContainer.innerHTML = `
<div style="position: relative; height: 100%; padding: 8px;">
<!-- 告警状态总览 -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; padding: 8px 12px; background: rgba(0,228,255,0.1); border-radius: 6px; border-left: 4px solid #00e4ff;">
<div style="display: flex; align-items: center; gap: 15px;">
<div class="alert-stat-item">
<span style="color: #ff4444; font-size: 20px; font-weight: bold;" id="criticalCount">0</span>
<span style="color: #fff; font-size: 12px; margin-left: 4px;">严重</span>
</div>
<div class="alert-stat-item">
<span style="color: #ffbb00; font-size: 18px; font-weight: bold;" id="mediumCount">0</span>
<span style="color: #fff; font-size: 12px; margin-left: 4px;">中等</span>
</div>
<div class="alert-stat-item">
<span style="color: #ff6666; font-size: 16px; font-weight: bold;" id="pendingCount">0</span>
<span style="color: #fff; font-size: 12px; margin-left: 4px;">待处理</span>
</div>
</div>
<div style="background: rgba(255,68,68,0.2); padding: 4px 8px; border-radius: 12px; animation: pulse 2s infinite;" id="alertBadge">
<span style="color: #ff4444; font-size: 11px; font-weight: bold;">🚨 实时监控</span>
</div>
</div>
<!-- 图表区域 -->
<div class="alert-charts-grid">
<div id="alertTypeChart" style="background: rgba(0,21,41,0.4); border: 1px solid rgba(0,228,255,0.3); border-radius: 6px; position: relative;">
<div style="position: absolute; top: 5px; left: 8px; color: #00e4ff; font-size: 12px; font-weight: bold; z-index: 10;">告警类型分布</div>
</div>
<div id="alertLevelChart" style="background: rgba(0,21,41,0.4); border: 1px solid rgba(0,228,255,0.3); border-radius: 6px; position: relative;">
<div style="position: absolute; top: 5px; left: 8px; color: #00e4ff; font-size: 12px; font-weight: bold; z-index: 10;">严重程度分析</div>
</div>
<div id="alertStatusChart" style="background: rgba(0,21,41,0.4); border: 1px solid rgba(0,228,255,0.3); border-radius: 6px; position: relative;">
<div style="position: absolute; top: 5px; left: 8px; color: #00e4ff; font-size: 12px; font-weight: bold; z-index: 10;">处理状态</div>
</div>
<div id="alertTrendChart" style="background: rgba(0,21,41,0.4); border: 1px solid rgba(0,228,255,0.3); border-radius: 6px; position: relative;">
<div style="position: absolute; top: 5px; left: 8px; color: #00e4ff; font-size: 12px; font-weight: bold; z-index: 10;">24小时趋势</div>
</div>
</div>
</div>
`;
const alertInfo = data.alert_info || {};
const alerts = alertInfo.alerts || [];
console.log('告警信息:', alertInfo); // 调试信息
console.log('告警列表:', alerts); // 调试信息
// 更新统计数据
const criticalCount = alertInfo.alertLevelCount?.critical || 0;
const mediumCount = alertInfo.alertLevelCount?.medium || 0;
const pendingCount = alertInfo.alertStatusCount?.pending || 0;
document.getElementById('criticalCount').textContent = criticalCount;
document.getElementById('mediumCount').textContent = mediumCount;
document.getElementById('pendingCount').textContent = pendingCount;
// 更新告警徽章
const badge = document.getElementById('alertBadge');
if (criticalCount > 0) {
badge.innerHTML = '<span style="color: #ff4444; font-size: 11px; font-weight: bold;">🔴 严重告警</span>';
badge.style.background = 'rgba(255,68,68,0.3)';
} else if (pendingCount > 0) {
badge.innerHTML = '<span style="color: #ffbb00; font-size: 11px; font-weight: bold;">⚠️ 待处理</span>';
badge.style.background = 'rgba(255,187,0,0.3)';
} else {
badge.innerHTML = '<span style="color: #00ff9d; font-size: 11px; font-weight: bold;">✅ 正常</span>';
badge.style.background = 'rgba(0,255,157,0.3)';
}
// 1. 告警类型分布图 - 水平条形图
const typeChart = echarts.init(document.getElementById('alertTypeChart'));
const alertTypes = Object.keys(alertInfo.alertTypeCount || {});
const alertValues = Object.values(alertInfo.alertTypeCount || {});
// 如果没有数据,显示默认数据
const hasTypeData = alertTypes.length > 0 && alertValues.some(v => v > 0);
const displayTypes = hasTypeData ? alertTypes : ['heart_rate', 'blood_pressure', 'temperature'];
const displayValues = hasTypeData ? alertValues : [0, 0, 0];
const typeColors = {
'temperature': '#ffd700',
'stress': '#ff8800',
'heart_rate': '#00e4ff',
'blood_pressure': '#ffbb00',
'blood_oxygen': '#ff6666'
};
const typeOption = {
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(0,21,41,0.95)',
borderColor: '#00e4ff',
borderWidth: 1,
textStyle: { color: '#fff', fontSize: 11 },
formatter: function(params) {
if (!params || !params[0]) return '';
const data = params[0];
const total = displayValues.reduce((a, b) => a + b, 0);
const percent = total > 0 ? (data.value / total * 100).toFixed(1) : 0;
return `${translateAlertType(data.name)}<br/>告警: ${data.value}次 (${percent}%)`;
}
},
grid: { top: 25, left: 45, right: 15, bottom: 15 },
xAxis: {
type: 'value',
axisLabel: { color: '#7ecfff', fontSize: 10 },
splitLine: { lineStyle: { color: 'rgba(126,207,255,0.1)' } },
axisLine: { show: false },
max: Math.max(...displayValues, 1)
},
yAxis: {
type: 'category',
data: displayTypes.map(t => translateAlertType(t)),
axisLabel: { color: '#fff', fontSize: 10 },
axisLine: { show: false },
axisTick: { show: false }
},
series: [{
type: 'bar',
data: displayTypes.map((type, index) => ({
value: displayValues[index],
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: typeColors[type] || '#00e4ff' },
{ offset: 1, color: (typeColors[type] || '#00e4ff') + '88' }
])
}
})),
barWidth: '65%',
label: {
show: true,
position: 'right',
color: '#fff',
fontSize: 10,
formatter: '{c}'
}
}]
};
typeChart.setOption(typeOption);
// 2. 告警级别分布图 - 环形图
const levelChart = echarts.init(document.getElementById('alertLevelChart'));
const levelEntries = Object.entries(alertInfo.alertLevelCount || {});
const hasLevelData = levelEntries.length > 0 && levelEntries.some(([_, count]) => count > 0);
const levelData = hasLevelData ?
levelEntries.map(([level, count]) => ({
name: level === 'critical' ? '严重' : level === 'medium' ? '中等' : '轻微',
value: count,
itemStyle: {
color: level === 'critical' ? '#ff4444' : level === 'medium' ? '#ffbb00' : '#00e4ff'
}
})) :
[
{ name: '严重', value: 0, itemStyle: { color: '#ff4444' } },
{ name: '中等', value: 0, itemStyle: { color: '#ffbb00' } },
{ name: '正常', value: 1, itemStyle: { color: '#00e4ff' } }
];
const levelOption = {
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(0,21,41,0.95)',
borderColor: '#00e4ff',
textStyle: { color: '#fff', fontSize: 11 },
formatter: function(params) {
return `${params.name}<br/>数量: ${params.value}次<br/>占比: ${params.percent}%`;
}
},
series: [{
type: 'pie',
radius: ['35%', '65%'],
center: ['50%', '55%'],
data: levelData,
label: {
show: true,
position: 'outside',
color: '#fff',
fontSize: 10,
formatter: function(params) {
return hasLevelData ? `${params.name}\n${params.value}次` : `${params.name}`;
},
lineHeight: 12
},
labelLine: {
show: true,
length: 8,
length2: 5,
lineStyle: { color: 'rgba(255,255,255,0.5)' }
},
emphasis: {
itemStyle: {
shadowBlur: 15,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.8)',
scale: 1.05
}
}
}]
};
levelChart.setOption(levelOption);
// 3. 告警状态分布图 - 仪表盘样式
const statusChart = echarts.init(document.getElementById('alertStatusChart'));
const totalAlerts = (alertInfo.alertStatusCount?.pending || 0) + (alertInfo.alertStatusCount?.responded || 0);
const pendingPercent = totalAlerts > 0 ? ((alertInfo.alertStatusCount?.pending || 0) / totalAlerts * 100).toFixed(1) : 0;
const statusOption = {
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(0,21,41,0.95)',
borderColor: '#00e4ff',
textStyle: { color: '#fff', fontSize: 11 }
},
series: [{
type: 'gauge',
radius: '75%',
center: ['50%', '55%'],
startAngle: 180,
endAngle: 0,
min: 0,
max: 100,
splitNumber: 4,
axisLine: {
lineStyle: {
width: 8,
color: [
[0.3, '#00e4ff'],
[0.7, '#ffbb00'],
[1, '#ff4444']
]
}
},
pointer: {
icon: 'circle',
length: '60%',
width: 4,
offsetCenter: [0, '5%'],
itemStyle: { color: '#fff' }
},
axisTick: { show: false },
splitLine: { show: false },
axisLabel: {
show: true,
distance: -25,
color: '#7ecfff',
fontSize: 9,
formatter: function(value) {
if (value === 0) return '正常';
if (value === 50) return '中等';
if (value === 100) return '严重';
return '';
}
},
detail: {
valueAnimation: true,
formatter: function(value) {
return `{value|${value}%}\n{name|待处理率}`;
},
rich: {
value: {
fontSize: 16,
fontWeight: 'bold',
color: pendingPercent > 70 ? '#ff4444' : pendingPercent > 30 ? '#ffbb00' : '#00e4ff'
},
name: {
fontSize: 10,
color: '#7ecfff',
padding: [5, 0, 0, 0]
}
},
offsetCenter: [0, '20%']
},
data: [{ value: pendingPercent }]
}]
};
statusChart.setOption(statusOption);
// 4. 24小时告警趋势图 - 修复数据处理
const trendChart = echarts.init(document.getElementById('alertTrendChart'));
// 处理时间数据,按小时统计
const hourlyData = {};
const now = new Date();
// 初始化24小时数据
for (let i = 0; i < 24; i++) {
hourlyData[i] = 0;
}
// 统计告警数据
if (alerts && alerts.length > 0) {
alerts.forEach(alert => {
try {
let alertTime;
if (alert.alert_timestamp) {
// 尝试解析时间戳
alertTime = new Date(alert.alert_timestamp);
if (isNaN(alertTime.getTime())) {
// 如果解析失败,尝试其他格式
alertTime = new Date(alert.alert_timestamp.replace(/-/g, '/'));
}
} else if (alert.timestamp) {
alertTime = new Date(alert.timestamp);
} else {
alertTime = now; // 默认当前时间
}
if (!isNaN(alertTime.getTime())) {
const hour = alertTime.getHours();
hourlyData[hour] = (hourlyData[hour] || 0) + 1;
}
} catch (e) {
console.warn('解析告警时间失败:', alert, e);
}
});
}
const hours = Array.from({length: 24}, (_, i) => i);
const hourlyValues = hours.map(h => hourlyData[h] || 0);
const maxValue = Math.max(...hourlyValues, 1);
const trendOption = {
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(0,21,41,0.95)',
borderColor: '#00e4ff',
textStyle: { color: '#fff', fontSize: 11 },
formatter: function(params) {
if (!params || !params[0]) return '';
const data = params[0];
return `${data.name}:00<br/>告警数量: ${data.value}次`;
}
},
grid: { top: 25, left: 25, right: 15, bottom: 20 },
xAxis: {
type: 'category',
data: hours.map(h => h.toString().padStart(2, '0')),
axisLabel: {
color: '#7ecfff',
fontSize: 9,
interval: 3
},
axisLine: { lineStyle: { color: 'rgba(126,207,255,0.3)' } },
axisTick: { show: false }
},
yAxis: {
type: 'value',
max: Math.max(maxValue + 1, 3),
axisLabel: { color: '#7ecfff', fontSize: 9 },
splitLine: { lineStyle: { color: 'rgba(126,207,255,0.1)', type: 'dashed' } },
axisLine: { show: false }
},
series: [{
type: 'line',
data: hourlyValues,
smooth: true,
lineStyle: {
color: '#00e4ff',
width: 2,
shadowColor: 'rgba(0,228,255,0.3)',
shadowBlur: 5
},
itemStyle: {
color: '#00e4ff',
borderColor: '#fff',
borderWidth: 1
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(0,228,255,0.4)' },
{ offset: 1, color: 'rgba(0,228,255,0.05)' }
])
},
symbol: 'circle',
symbolSize: function(value) {
return value > 0 ? 4 : 2;
},
emphasis: {
itemStyle: {
color: '#fff',
borderColor: '#00e4ff',
borderWidth: 2,
shadowColor: 'rgba(0,228,255,0.6)',
shadowBlur: 8
},
scale: 1.2
}
}]
};
trendChart.setOption(trendOption);
// 添加动画样式
const style = document.createElement('style');
style.textContent = `
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.05); opacity: 0.8; }
}
.alert-stat-item {
display: flex;
align-items: baseline;
transition: all 0.3s ease;
}
.alert-stat-item:hover {
transform: translateY(-2px);
}
#alertBadge {
cursor: pointer;
transition: all 0.3s ease;
}
#alertBadge:hover {
transform: scale(1.1);
box-shadow: 0 0 10px rgba(0,228,255,0.5);
}
`;
document.head.appendChild(style);
// 自适应大小 - 延迟执行确保DOM已渲染
setTimeout(() => {
const resizeCharts = () => {
try {
typeChart && typeChart.resize();
levelChart && levelChart.resize();
statusChart && statusChart.resize();
trendChart && trendChart.resize();
} catch (e) {
console.warn('图表resize失败:', e);
}
};
resizeCharts(); // 立即执行一次
window.addEventListener('resize', resizeCharts);
}, 100);
// 添加点击事件显示详细告警列表
badge.onclick = () => showAlertDetails(alerts);
// 图表点击交互
typeChart.on('click', function(params) {
if (hasTypeData && params.dataIndex < alertTypes.length) {
const alertType = alertTypes[params.dataIndex];
const filteredAlerts = alerts.filter(alert => alert.alert_type === alertType);
showAlertDetails(filteredAlerts, `${translateAlertType(alertType)}告警详情`);
}
});
// 确保图表正确渲染 - 延迟执行
setTimeout(() => {
try {
typeChart.resize();
levelChart.resize();
statusChart.resize();
trendChart.resize();
console.log('告警图表初始化完成');
} catch (e) {
console.warn('告警图表初始化失败:', e);
}
}, 200);
return { typeChart, levelChart, statusChart, trendChart };
}
// 显示详细告警列表
function showAlertDetails(alerts, title = '📋 详细告警列表') {
const modal = document.createElement('div');
modal.className = 'modal-container';
modal.innerHTML = `
<div class="modal-content" style="width: 85%; height: 85%;">
<button class="modal-close">✕</button>
<h3 style="color: #00e4ff; margin-bottom: 20px; text-align: center; font-size: 18px;">${title}</h3>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; padding: 10px; background: rgba(0,228,255,0.1); border-radius: 6px;">
<div style="color: #fff; font-size: 14px;">
共 <span style="color: #00e4ff; font-weight: bold;">${alerts.length}</span> 条告警记录
</div>
<div style="display: flex; gap: 10px;">
<button onclick="exportAlerts()" style="background: rgba(0,228,255,0.2); border: 1px solid #00e4ff; color: #00e4ff; padding: 5px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;">📊 导出</button>
<button onclick="refreshAlerts()" style="background: rgba(0,228,255,0.2); border: 1px solid #00e4ff; color: #00e4ff; padding: 5px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;">🔄 刷新</button>
</div>
</div>
<div style="height: calc(100% - 100px); overflow-y: auto; border: 1px solid rgba(0,228,255,0.2); border-radius: 6px;">
<table style="width: 100%; border-collapse: collapse; color: #fff; font-size: 13px;">
<thead style="position: sticky; top: 0; z-index: 10;">
<tr style="background: rgba(0,228,255,0.3); border-bottom: 2px solid #00e4ff;">
<th style="padding: 12px 8px; text-align: left; font-weight: bold; border-right: 1px solid rgba(0,228,255,0.2);">时间</th>
<th style="padding: 12px 8px; text-align: left; font-weight: bold; border-right: 1px solid rgba(0,228,255,0.2);">类型</th>
<th style="padding: 12px 8px; text-align: left; font-weight: bold; border-right: 1px solid rgba(0,228,255,0.2);">级别</th>
<th style="padding: 12px 8px; text-align: left; font-weight: bold; border-right: 1px solid rgba(0,228,255,0.2);">状态</th>
<th style="padding: 12px 8px; text-align: left; font-weight: bold; border-right: 1px solid rgba(0,228,255,0.2);">用户</th>
<th style="padding: 12px 8px; text-align: left; font-weight: bold; border-right: 1px solid rgba(0,228,255,0.2);">部门</th>
<th style="padding: 12px 8px; text-align: left; font-weight: bold; border-right: 1px solid rgba(0,228,255,0.2);">设备</th>
<th style="padding: 12px 8px; text-align: left; font-weight: bold;">描述</th>
</tr>
</thead>
<tbody>
${alerts.map((alert, index) => `
<tr style="border-bottom: 1px solid rgba(255,255,255,0.1); transition: all 0.2s ease; ${alert.severity_level === 'critical' ? 'background: rgba(255,68,68,0.15);' : index % 2 === 0 ? 'background: rgba(0,21,41,0.3);' : 'background: rgba(0,21,41,0.1);'}"
onmouseover="this.style.background='rgba(0,228,255,0.1)'"
onmouseout="this.style.background='${alert.severity_level === 'critical' ? 'rgba(255,68,68,0.15)' : index % 2 === 0 ? 'rgba(0,21,41,0.3)' : 'rgba(0,21,41,0.1)'}'">
<td style="padding: 10px 8px; font-size: 11px; border-right: 1px solid rgba(255,255,255,0.05);">
<div style="color: #7ecfff;">${alert.alert_timestamp.split(' ')[0]}</div>
<div style="color: #fff; font-size: 10px;">${alert.alert_timestamp.split(' ')[1]}</div>
</td>
<td style="padding: 10px 8px; border-right: 1px solid rgba(255,255,255,0.05);">
<span style="background: ${getAlertTypeColor(alert.alert_type)}; padding: 3px 8px; border-radius: 12px; font-size: 10px; font-weight: bold; color: #000;">
${translateAlertType(alert.alert_type)}
</span>
</td>
<td style="padding: 10px 8px; border-right: 1px solid rgba(255,255,255,0.05);">
<span style="background: ${alert.severity_level === 'critical' ? '#ff4444' : '#ffbb00'}; padding: 3px 8px; border-radius: 12px; font-size: 10px; font-weight: bold; color: #000;">
${alert.severity_level === 'critical' ? '🔴 严重' : '🟡 中等'}
</span>
</td>
<td style="padding: 10px 8px; border-right: 1px solid rgba(255,255,255,0.05);">
<span style="background: ${alert.alert_status === 'pending' ? '#ff4444' : '#00e4ff'}; padding: 3px 8px; border-radius: 12px; font-size: 10px; font-weight: bold; color: ${alert.alert_status === 'pending' ? '#fff' : '#000'};">
${alert.alert_status === 'pending' ? '⏳ 待处理' : '✅ 已处理'}
</span>
</td>
<td style="padding: 10px 8px; font-size: 12px; border-right: 1px solid rgba(255,255,255,0.05);">
<div style="color: #00e4ff; font-weight: bold;">${alert.user_name}</div>
<div style="color: #7ecfff; font-size: 10px;">ID: ${alert.user_id}</div>
</td>
<td style="padding: 10px 8px; font-size: 12px; border-right: 1px solid rgba(255,255,255,0.05);">
<div style="color: #fff;">${alert.dept_name}</div>
<div style="color: #7ecfff; font-size: 10px;">部门ID: ${alert.dept_id}</div>
</td>
<td style="padding: 10px 8px; font-size: 11px; border-right: 1px solid rgba(255,255,255,0.05);">
<div style="color: #ffbb00; font-family: monospace;">${alert.device_sn}</div>
${alert.health_id ? `<div style="color: #7ecfff; font-size: 10px;">健康ID: ${alert.health_id}</div>` : ''}
</td>
<td style="padding: 10px 8px; font-size: 11px; max-width: 200px; word-wrap: break-word; line-height: 1.4;">
<div style="color: #fff;">${alert.alert_desc || '无详细描述'}</div>
${alert.alert_status === 'pending' ? `
<button onclick="handleAlert('${alert.alert_id}')"
style="margin-top: 5px; background: #ff4444; color: #fff; border: none; padding: 3px 8px; border-radius: 4px; cursor: pointer; font-size: 10px; transition: all 0.2s ease;"
onmouseover="this.style.background='#ff6666'"
onmouseout="this.style.background='#ff4444'">
🚨 立即处理
</button>
` : ''}
</td>
</tr>
`).join('')}
</tbody>
</table>
${alerts.length === 0 ? `
<div style="text-align: center; padding: 40px; color: #7ecfff;">
<div style="font-size: 48px; margin-bottom: 10px;">📭</div>
<div style="font-size: 16px;">暂无告警记录</div>
<div style="font-size: 12px; margin-top: 5px; color: rgba(255,255,255,0.5);">系统运行正常</div>
</div>
` : ''}
</div>
</div>
`;
document.body.appendChild(modal);
// 关闭事件
modal.querySelector('.modal-close').onclick = () => modal.remove();
modal.onclick = (e) => {
if (e.target === modal) modal.remove();
};
// 添加键盘事件
document.addEventListener('keydown', function escHandler(e) {
if (e.key === 'Escape') {
modal.remove();
document.removeEventListener('keydown', escHandler);
}
});
}
// 导出告警数据
function exportAlerts() {
// 这里可以实现导出功能
showCustomAlert('导出功能开发中...', null);
}
// 刷新告警数据
function refreshAlerts() {
refreshData();
showCustomAlert('告警数据已刷新', null);
}
// 获取告警类型颜色
function getAlertTypeColor(type) {
const colors = {
'temperature': '#ff4444',
'stress': '#ff8800',
'heart_rate': '#00e4ff',
'blood_pressure': '#ffbb00',
'blood_oxygen': '#ff6666'
};
return colors[type] || '#00e4ff';
}
// 修改设备管理图表初始化函数
function initDeviceChart(data) {
const statsContainer = document.getElementById('statsChart');
if (!statsContainer) return;
const statsChart = echarts.init(statsContainer);
const deviceInfo = data.device_info || {};
const totalDevices = deviceInfo.totalDevices || 0;
document.getElementById('totalWatchDevices').textContent = totalDevices;
// 添加点击事件监听器
statsContainer.onclick = () => {
const legendData = {
'部门分布': deviceInfo.departmentDeviceCount || {},
'充电状态': deviceInfo.deviceChargingCount || {},
'设备状态': deviceInfo.deviceStatusCount || {},
'系统版本': deviceInfo.deviceSystemVersionCount || {},
'佩戴状态': deviceInfo.deviceWearableCount || {}
};
showFullLegend(legendData);
};
// 处理数据为堆叠格式
const categories = ['部门分布', '充电状态', '设备状态', '系统版本', '佩戴状态'];
// 处理部门设备数据
const departmentData = Object.entries(deviceInfo.departmentDeviceCount || {}).map(([name, value]) => ({
name,
value
}));
// 处理充电状态数据
const chargingData = Object.entries(deviceInfo.deviceChargingCount || {}).map(([status, value]) => ({
name: status === 'NOT_CHARGING' ? '未充电' : '充电中',
value
}));
// 处理设备状态数据
const statusData = Object.entries(deviceInfo.deviceStatusCount || {}).map(([status, value]) => ({
name: status === 'ACTIVE' ? '活跃' : '非活跃',
value
}));
// 处理系统版本数据
const versionData = Object.entries(deviceInfo.deviceSystemVersionCount || {}).map(([version, value]) => ({
name: version,
value
}));
// 处理佩戴状态数据
const wearableData = Object.entries(deviceInfo.deviceWearableCount || {}).map(([status, value]) => ({
name: status === 'WORN' ? '已佩戴' : '未佩戴',
value
}));
// 创建系列数据
const series = [];
// 部门分布系列
departmentData.forEach((item, index) => {
series.push({
name: item.name,
type: 'bar',
stack: '部门分布',
emphasis: {
focus: 'series'
},
data: [item.value, 0, 0, 0, 0]
});
});
// 充电状态系列
chargingData.forEach((item, index) => {
series.push({
name: item.name,
type: 'bar',
stack: '充电状态',
emphasis: {
focus: 'series'
},
data: [0, item.value, 0, 0, 0]
});
});
// 设备状态系列
statusData.forEach((item, index) => {
series.push({
name: item.name,
type: 'bar',
stack: '设备状态',
emphasis: {
focus: 'series'
},
data: [0, 0, item.value, 0, 0]
});
});
// 系统版本系列
versionData.forEach((item, index) => {
series.push({
name: item.name,
type: 'bar',
stack: '系统版本',
emphasis: {
focus: 'series'
},
data: [0, 0, 0, item.value, 0]
});
});
// 佩戴状态系列
wearableData.forEach((item, index) => {
series.push({
name: item.name,
type: 'bar',
stack: '佩戴状态',
emphasis: {
focus: 'series'
},
data: [0, 0, 0, 0, item.value]
});
});
const statsOption = {
title: {
text: '设备状态统计',
textStyle: {
color: '#fff',
fontSize: 14
},
left: 'center',
top: 10
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
formatter: function(params) {
const stackName = params[0].axisValue;
let result = `${stackName}<br/>`;
params.forEach(param => {
if (param.value > 0) {
result += `${param.seriesName}: ${param.value}<br/>`;
}
});
return result;
}
},
legend: {
textStyle: {
color: '#fff'
},
top: 35,
type: 'scroll',
orient: 'horizontal'
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
top: '25%',
containLabel: true
},
xAxis: {
type: 'category',
data: categories,
axisLabel: {
color: '#fff',
interval: 0,
rotate: 30
}
},
yAxis: {
type: 'value',
axisLabel: {
color: '#fff'
},
splitLine: {
lineStyle: {
color: 'rgba(255,255,255,0.1)'
}
}
},
series: series
};
statsChart.setOption(statsOption);
return statsChart;
}
// 修改趋势分析图表,增加预测数据
function updateTrendChart(data) {
const trendContainer = document.getElementById('trendChart');
if (!trendContainer) return;
const trendChart = echarts.init(trendContainer);
const trendOption = {
title: {
text: '健康指标趋势及预测',
textStyle: {
color: '#fff',
fontSize: 16
}
},
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(0,21,41,0.9)',
borderColor: 'rgba(0,228,255,0.2)',
textStyle: { color: '#fff' }
},
legend: {
data: ['心率', '血压', '压力指数', '距离', '卡路里', '步数', '预测值'],
textStyle: { color: '#fff' },
top: 30
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00', '预测'],
axisLabel: { color: '#fff' },
axisLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }
},
yAxis: {
type: 'value',
axisLabel: { color: '#fff' },
splitLine: {
lineStyle: {
color: 'rgba(255,255,255,0.1)',
type: 'dashed'
}
}
},
series: [
{
name: '心率',
type: 'line',
smooth: true,
data: [75, 72, 78, 85, 80, 75, 77],
itemStyle: { color: '#ff4444' }
},
{
name: '血压',
type: 'line',
smooth: true,
data: [120, 118, 122, 125, 121, 119, 120],
itemStyle: { color: '#00e4ff' }
},
{
name: '压力指数',
type: 'line',
smooth: true,
data: [45, 48, 52, 55, 50, 47, 49],
itemStyle: { color: '#ffbb00' }
},
{
name: '距离',
type: 'line',
smooth: true,
data: [2.1, 2.3, 2.8, 3.2, 3.5, 3.8, 4.0],
itemStyle: { color: '#00ff9d' }
},
{
name: '卡路里',
type: 'line',
smooth: true,
data: [150, 180, 220, 280, 320, 350, 380],
itemStyle: { color: '#ff7777' }
},
{
name: '步数',
type: 'line',
smooth: true,
data: [2000, 2500, 3000, 3800, 4200, 4500, 4800],
itemStyle: { color: '#7777ff' }
}
]
};
// 为每个系列添加预测区域
trendOption.series.forEach(series => {
const lastIndex = series.data.length - 1;
series.markArea = {
itemStyle: {
color: 'rgba(0, 228, 255, 0.1)'
},
data: [[{
xAxis: trendOption.xAxis.data[lastIndex - 1]
}, {
xAxis: trendOption.xAxis.data[lastIndex]
}]]
};
});
trendChart.setOption(trendOption);
return trendChart;
}
// 人员管理面板交互函数
function showPersonnelDetails() {
showCustomAlert('人员详情功能:显示完整的人员管理统计信息');
}
function filterByDepartment() {
showCustomAlert('部门筛选功能:按部门查看人员分布详情');
}
function filterByOnlineStatus() {
showCustomAlert('在线状态筛选功能:显示在线/离线人员列表');
}
function filterByDeviceStatus() {
showCustomAlert('设备状态筛选功能:显示已绑定/未绑定设备人员');
}
function filterByAlertStatus() {
showCustomAlert('告警状态筛选功能:显示有告警的人员列表');
}
// 显示部门详情
function showDepartmentDetails(deptName, userCount) {
const modal = document.createElement('div');
modal.className = 'modal-container';
modal.innerHTML = `
<div class="modal-content" style="width: 60%; height: 70%;">
<button class="modal-close">✕</button>
<h3 style="color: #00e4ff; margin-bottom: 20px; text-align: center; font-size: 18px;">📊 ${deptName} 部门详情</h3>
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; padding: 20px;">
<div style="background: rgba(0,228,255,0.1); padding: 20px; border-radius: 8px; text-align: center;">
<div style="color: #00e4ff; font-size: 24px; font-weight: bold;">${userCount}</div>
<div style="color: #fff; margin-top: 5px;">总人数</div>
</div>
<div style="background: rgba(0,255,157,0.1); padding: 20px; border-radius: 8px; text-align: center;">
<div style="color: #00ff9d; font-size: 24px; font-weight: bold;">${Math.floor(userCount * 0.8)}</div>
<div style="color: #fff; margin-top: 5px;">在线人数</div>
</div>
<div style="background: rgba(255,187,0,0.1); padding: 20px; border-radius: 8px; text-align: center;">
<div style="color: #ffbb00; font-size: 24px; font-weight: bold;">${Math.floor(userCount * 0.9)}</div>
<div style="color: #fff; margin-top: 5px;">设备绑定</div>
</div>
</div>
<div style="text-align: center; margin-top: 20px; color: #7ecfff;">
点击可查看该部门的详细人员列表和设备状态
</div>
</div>
`;
document.body.appendChild(modal);
// 关闭事件
modal.querySelector('.modal-close').onclick = () => modal.remove();
modal.onclick = (e) => {
if (e.target === modal) modal.remove();
};
}
// 显示状态详情
function showStatusDetails(statusName, statusValue) {
const statusMap = {
'在线': '当前在线的用户列表',
'离线': '当前离线的用户列表',
'绑定': '已绑定设备的用户',
'未绑定': '未绑定设备的用户',
'告警': '当前有告警的用户',
'正常': '状态正常的用户'
};
const modal = document.createElement('div');
modal.className = 'modal-container';
modal.innerHTML = `
<div class="modal-content" style="width: 70%; height: 80%;">
<button class="modal-close">✕</button>
<h3 style="color: #00e4ff; margin-bottom: 20px; text-align: center; font-size: 18px;">📋 ${statusName}用户详情</h3>
<div style="background: rgba(0,228,255,0.1); padding: 15px; border-radius: 8px; margin-bottom: 20px; text-align: center;">
<div style="color: #00e4ff; font-size: 28px; font-weight: bold;">${statusValue}</div>
<div style="color: #fff; margin-top: 5px;">${statusMap[statusName] || '用户统计'}</div>
</div>
<div style="color: #7ecfff; text-align: center; padding: 40px;">
<div style="font-size: 48px; margin-bottom: 15px;">👥</div>
<div style="font-size: 16px;">详细用户列表功能开发中...</div>
<div style="font-size: 12px; margin-top: 10px; color: rgba(255,255,255,0.5);">将显示具体的用户信息、设备状态和健康数据</div>
</div>
</div>
`;
document.body.appendChild(modal);
// 关闭事件
modal.querySelector('.modal-close').onclick = () => modal.remove();
modal.onclick = (e) => {
if (e.target === modal) modal.remove();
};
}
</script>
</script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
$(document).ready(function() {
// 获取部门数据
$.ajax({
url: `/get_departments?orgId={{ customerId }}`,
method: 'GET',
success: function(response) {
console.log('response', response);
if (response.success && response.data) {
updateDepartmentSelect(response.data);
} else {
console.error('Invalid response format:', response);
}
},
error: function(error) {
console.error('Failed to fetch departments:', error);
}
});
// 部门选择变化时更新用户列表
$('#deptSelect').change(function() {
const selectedDeptId = $(this).val();
currentDept=selectedDeptId
console.log('currentDept',currentDept);
updateUsers(selectedDeptId);
});
// 用户选择变化时刷新地图
$('#userSelect').change(function() {
currentUser=$(this).val()
//console.log('currentUser',currentUser);
// 调用 initializeMap 刷新数据
//initializeMap(selectedDeptId, selectedUserId);
updateMapData(lastTotalInfo);
});
// 更新部门选择框
function updateDepartmentSelect(departments) {
const deptSelect = document.getElementById('deptSelect');
if (!deptSelect) return;
// 清空现有选项
deptSelect.innerHTML = '<option value="">选择部门</option>';
// 递归添加部门选项
function addDepartmentOptions(departments, level = 0) {
departments.forEach(dept => {
const option = document.createElement('option');
option.value = dept.id;
const indent = ' '.repeat(level);
option.textContent = indent + dept.name;
deptSelect.appendChild(option);
if (dept.children && dept.children.length > 0) {
addDepartmentOptions(dept.children, level + 1);
}
});
}
addDepartmentOptions(departments);
deptSelect.style.fontFamily = 'monospace';
}
// 更新用户选择框
async function updateUsers(deptId) {
try {
const userSelect = document.getElementById('userSelect');
if (!userSelect) return;
// 清空现有选项
userSelect.innerHTML = '<option value="">选择用户</option>';
if (!deptId) return;
const response = await fetch(`/fetch_users?orgId=${deptId}`);
const data = await response.json();
console.log('updateUsers.data', data);
if (data) {
data.forEach(user => {
const option = document.createElement('option');
option.value = user.id;
let displayText = user.user_name;
// 如果有职位信息,添加到显示文本中
if (user.position) {
displayText += ` (${user.position})`;
}
option.textContent = displayText;
userSelect.appendChild(option);
});
const option = document.createElement('option');
option.value = 'all';
option.textContent = '全部用户';
userSelect.appendChild(option);
}
} catch (error) {
console.error('获取用户列表失败:', error);
}
}
// 添加样式
const style = document.createElement('style');
style.textContent = `
.filter-select {
font-family: monospace;
white-space: pre;
width: 100%;
padding: 8px;
margin-bottom: 10px;
background: rgba(0, 0, 0, 0.3);
color: #fff;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
.filter-select option {
font-family: monospace;
white-space: pre;
background: #1a1a1a;
color: #fff;
}
.filter-panel {
position: absolute;
top: 20px;
right: 20px;
z-index: 1000;
background: rgba(0, 21, 41, 0.9);
backdrop-filter: blur(8px);
padding: 15px;
border-radius: 8px;
width: 280px;
border: 1px solid rgba(0, 228, 255, 0.2);
box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
}
.filter-container {
display: flex;
flex-direction: column;
gap: 10px;
}
.filter-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid rgba(0, 228, 255, 0.2);
}
.filter-header span {
color: #00e4ff;
font-size: 14px;
}
.filter-toggle {
background: transparent;
border: none;
color: #00e4ff;
cursor: pointer;
padding: 5px;
transition: all 0.3s ease;
}
.filter-toggle:hover {
transform: scale(1.1);
}
`;
document.head.appendChild(style);
});
</script>
<div id="infoPanel" class="info-panel" style="display: none;">
<div class="info-panel-header">
<div class="info-panel-title">
<i class="fas fa-info-circle"></i>
<span>实时信息</span>
</div>
<button class="info-panel-close" onclick="toggleInfoPanel()">×</button>
</div>
<div class="info-panel-content" id="infoPanelContent">
<!-- 内容将通过JavaScript动态填充 -->
</div>
</div>
<script>
// 全局变量
let infoPanelVisible = false;
let lastTotalInfo = null;
// 切换信息面板显示状态
function toggleInfoPanel() {
const panel = document.getElementById('infoPanel');
infoPanelVisible = !infoPanelVisible;
panel.style.display = infoPanelVisible ? 'block' : 'none';
}
// 添加键盘快捷键切换信息面板
document.addEventListener('keydown', (e) => {
if (e.key === 'i' && e.ctrlKey) {
toggleInfoPanel();
}
});
function openInfoPanelWithAlertId(alertId) {
infoPanelVisible = true;
document.getElementById('infoPanel').style.display = 'block';
updateInfoPanel(lastTotalInfo, alertId);
}
function showCustomMapInfo(f){
removeCustomMapInfo();
const d=f.properties,toStr=x=>x===undefined||x===null?'':String(x);
const get=(...k)=>k.map(x=>d[x]).find(x=>x!==undefined&&x!==null&&x!=='')||'-';
// 判断是否为告警点:有alert_id/alertId且有alert_type/alertType,且type不是health
const isAlert=!!(get('alert_id','alertId')&&get('alert_type','alertType')&&d.type!=='health');
console.log('showCustomMapInfo.d',d);
console.log('showCustomMapInfo.isAlert',isAlert);
const level=get('severity_level','severityLevel');
const levelColor=level==='critical'?'#ff4d4f':level==='high'?'#ffbb00':'#ffe066';
const avatarUrl=d.avatar||'/static/images/avatar-tech.svg';
const div=document.createElement('div');
div.className='custom-map-info';
div.style.cssText='position:absolute;z-index:9999;min-width:360px;max-width:420px;background:rgba(10,24,48,0.98);border:1.5px solid #00e4ff;border-radius:16px;box-shadow:0 0 24px #00e4ff44;padding:22px 28px 18px 28px;color:#fff;top:120px;left:50%;transform:translateX(-50%);font-size:15px;font-family:Roboto,Arial,sans-serif;backdrop-filter:blur(6px);';
// 获取位置信息
const longitude = get('longitude');
const latitude = get('latitude');
if(longitude && latitude){
reverseGeocode(longitude, latitude)
.then(address => {
const locationInfo = document.getElementById('locationInfo');
if(locationInfo){
locationInfo.textContent = address || '未知位置';
}
})
.catch(error => {
console.error('获取位置信息失败:', error);
const locationInfo = document.getElementById('locationInfo');
if(locationInfo){
locationInfo.textContent = '获取位置信息失败';
}
});
}
if(isAlert){
// 告警点内容
div.innerHTML=`
<div style="display:flex;align-items:center;gap:18px;margin-bottom:12px;">
<img src="${avatarUrl}" style="width:56px;height:56px;border-radius:50%;border:2px solid #00e4ff;box-shadow:0 0 8px #00e4ff44;object-fit:cover;background:#001529;">
<div>
<div style="font-size:18px;font-weight:700;letter-spacing:1px;">${get('dept_name','deptName')}</div>
<div style="font-size:16px;color:#00e4ff;font-weight:500;margin-top:2px;">${get('user_name','userName')}</div>
</div>
</div>
<div style="display:flex;gap:18px;flex-wrap:wrap;margin-bottom:8px;">
<div><span style="color:#7ecfff;">告警类别:</span><span style="color:${getAlertTypeColor(get('alert_type','alertType','-'))};font-weight:700;">${translateAlertType(get('alert_type','alertType','-'))}</span></div>
<div><span style="color:#7ecfff;">级别:</span><span style="color:${getAlertSeverityColor(level||'-')};font-weight:700;">${translateAlertSeverity(level||'-')}</span></div>
<div><span style="color:#7ecfff;">状态:</span><span style="color:${getAlertStatusColor(get('alert_status','status','-'))};font-weight:700;">${translateAlertStatus(get('alert_status','status','-'))}</span></div>
</div>
<div style="margin-bottom:12px;">
<span style="color:#7ecfff;">健康信息:</span>
<a href="javascript:void(0)" onclick="showHealthProfile('${get('health_id','healthId')}')" style="color:#00e4ff;text-decoration:underline;font-family:monospace;font-size:15px;">${get('health_id','healthId')}</a>
</div>
<div style="margin-bottom:8px;">
<span style="color:#7ecfff;">位置信息:</span><span id="locationInfo">正在获取...</span>
</div>
<div style="margin-bottom:8px;">
<span style="color:#7ecfff;">告警时间:</span>${get('alert_timestamp','timestamp','-')}
</div>
<div style="display:flex;gap:18px;align-items:center;">
<button onclick="handleAlert('${get('alert_id','alertId')}')" style="padding:7px 22px;background:${levelColor};color:#001529;border:none;border-radius:6px;cursor:pointer;font-weight:700;box-shadow:0 2px 8px ${levelColor}44;transition:.2s;">一键处理</button>
<span style="flex:1"></span>
<span style="color:#00e4ff;cursor:pointer;font-size:22px;font-weight:700;" onclick="removeCustomMapInfo()">×</span>
</div>
`;
document.body.appendChild(div);
} else {
// 健康点内容
div.innerHTML=`
<div style="display:flex;align-items:center;gap:18px;margin-bottom:12px;">
<img src="${avatarUrl}" style="width:56px;height:56px;border-radius:50%;border:2px solid #00e4ff;box-shadow:0 0 8px #00e4ff44;object-fit:cover;background:#001529;">
<div>
<div style="font-size:18px;font-weight:700;letter-spacing:1px;">${get('dept_name','deptName')}</div>
<div style="font-size:16px;color:#00e4ff;font-weight:500;margin-top:2px;">${get('user_name','userName')}</div>
</div>
</div>
<div style="margin-bottom:8px;">
<span style="color:#7ecfff;">心率:</span>${get('heartRate','heart_rate')}
<span style="color:#7ecfff;margin-left:18px;">血压:</span>${get('pressureHigh','pressure_high')}/${get('pressureLow','pressure_low')} mmHg
</div>
<div style="margin-bottom:8px;">
<span style="color:#7ecfff;">血氧:</span>${get('bloodOxygen','blood_oxygen')}
<span style="color:#7ecfff;margin-left:18px;">体温:</span>${get('temperature','temp')} ℃
</div>
<div style="margin-bottom:8px;">
<span style="color:#7ecfff;">步数:</span>${get('step','steps')} 步
<span style="color:#7ecfff;margin-left:18px;">卡路里:</span>${get('calorie','calories')} kcal
<span style="color:#7ecfff;margin-left:18px;">距离:</span>${get('distance','distance')} 米
</div>
<div style="margin-bottom:8px;">
<span style="color:#7ecfff;">压力:</span>${get('stress','pressure')}
<span style="color:#7ecfff;margin-left:18px;">睡眠:</span>${get('sleepData','scientificSleepData')}
</div>
<div style="margin-bottom:8px;">
<span style="color:#7ecfff;">位置信息:</span><span id="locationInfo">正在获取...</span>
</div>
<div style="margin-bottom:8px;">
<span style="color:#7ecfff;">采集时间:</span>${get('timestamp')}
</div>
<div style="display:flex;gap:18px;align-items:center;">
<span style="flex:1"></span>
<span style="color:#00e4ff;cursor:pointer;font-size:22px;font-weight:700;" onclick="removeCustomMapInfo()">×</span>
</div>
`;
document.body.appendChild(div);
// 获取位置信息
const longitude = get('longitude');
const latitude = get('latitude');
if(longitude && latitude){
reverseGeocode(longitude, latitude)
.then(address => {
const locationInfo = document.getElementById('locationInfo');
if(locationInfo){
locationInfo.textContent = address || '未知位置';
}
})
.catch(error => {
console.error('获取位置信息失败:', error);
const locationInfo = document.getElementById('locationInfo');
if(locationInfo){
locationInfo.textContent = '获取位置信息失败';
}
});
}
}
}
function showHealthProfile(healthId){
fetch(`/fetchHealthDataById?id=${healthId}`).then(r=>r.json()).then(j=>{
if(!j.success||!j.data)return showCustomAlert('无健康数据');
const d=j.data,g=(...k)=>k.map(x=>d[x]).find(x=>x!==undefined&&x!==null&&x!=='')||'-';
let sleepStr=g('sleepData','scientificSleepData'),sleep='-';
try{
if(typeof sleepStr==='string'&&sleepStr.startsWith('{')){
const o=JSON.parse(sleepStr),arr=o.data||[];
if(arr.length){
const s=arr[0],fmt=m=>m?`${Math.floor(m/60)}h${m%60}m`:'-';
sleep=`总:${fmt(s.total)} 深:${fmt(s.deep)} 浅:${fmt(s.light)} 醒:${fmt(s.awake)}`;
}
}
}catch(e){sleep='-';}
const html=`
<div style="font-size:20px;font-weight:700;color:#00e4ff;margin-bottom:12px;text-align:center;">健康数据</div>
<div style="display:grid;grid-template-columns:100px 1fr;row-gap:8px;column-gap:10px;">
<span>心率</span><span style="text-align:right;font-weight:600;color:#7ecfff;">${g('heartRate','heart_rate')||'-'} <span style="color:#888;font-weight:400;">bpm</span></span>
<span>血压</span><span style="text-align:right;">${(g('pressureHigh','pressure_high')||'-')+'/'+(g('pressureLow','pressure_low')||'-')} <span style="color:#888;font-weight:400;">mmHg</span></span>
<span>血氧</span><span style="text-align:right;">${g('bloodOxygen','blood_oxygen')||'-'} <span style="color:#888;font-weight:400;">%</span></span>
<span>体温</span><span style="text-align:right;">${g('temperature','temp')||'-'} <span style="color:#888;font-weight:400;">℃</span></span>
<span>步数</span><span style="text-align:right;">${g('step','steps')||'-'} <span style="color:#888;font-weight:400;">步</span></span>
<span>距离</span><span style="text-align:right;">${g('distance','distance')||'-'} <span style="color:#888;font-weight:400;">米</span></span>
<span>卡路里</span><span style="text-align:right;">${g('calorie','calories')||'-'} <span style="color:#888;font-weight:400;">kcal</span></span>
<span>压力</span><span style="text-align:right;">${g('stress','pressure')||'-'} <span style="color:#888;font-weight:400;">分</span></span>
<span>睡眠</span><span style="text-align:right;">${sleep}</span>
<span>采集时间</span><span style="text-align:right;">${g('timestamp')||'-'}</span>
</div>
`;
const m=document.createElement('div');
m.style.cssText='position:fixed;top:0;left:0;width:100vw;height:100vh;background:rgba(0,21,41,0.7);z-index:10000;display:flex;align-items:center;justify-content:center;';
m.innerHTML=`<div style="background:rgba(10,24,48,0.98);border-radius:14px;box-shadow:0 0 24px #00e4ff44;padding:32px 38px;min-width:320px;max-width:420px;color:#fff;position:relative;">
<span style="position:absolute;right:18px;top:12px;cursor:pointer;font-size:22px;color:#00e4ff;" onclick="this.parentNode.parentNode.remove()">×</span>
${html}
</div>`;
document.body.appendChild(m);
}).catch(()=>showCustomAlert('获取健康数据失败'));
}
function removeCustomMapInfo(){const old=document.querySelector('.custom-map-info');if(old)old.remove();}
function filterData(data){
//console.log('filterData.data',currentDept,currentUser);
//console.log('filterData.data.healthData',data.healthData);
const toStr=x=>x===undefined||x===null?'':String(x);
const dept=toStr(currentDept),user=toStr(currentUser);
const alerts=(data.alert_info?.alerts||[]).filter(a=>
(!dept||[a.dept_id,a.deptId].some(v=>toStr(v)===dept))&&
(!user||[a.user_id,a.userId].some(v=>toStr(v)===user))&&
(['pending','1'].includes(toStr(a.alert_status||a.status)))
);
const healths=(data.health_data?.healthData||[]).filter(h=>
(!dept||[h.dept_id,h.deptId].some(v=>toStr(v)===dept))&&
(!user||[h.user_id,h.userId].some(v=>toStr(v)===user))
);
//console.log('filterData.alerts',alerts);
//console.log('filterData.healths',healths);
return {alerts,healths};
}
window.updateMapData = function(data){
//console.log('updateMapData.data',data);
if(!data||!map||!loca)return;
const {alerts,healths}=filterData(data);
const f=[];
// 处理告警数据
alerts.forEach(a=>{
if((a.longitude||a.longitude===0)&&(a.latitude||a.latitude===0)){
f.push({
type:'Feature',
geometry:{type:'Point',coordinates:[+a.longitude,+a.latitude]},
properties:{
...a,
alert_id: a.alert_id,
alert_type: a.alert_type,
alert_status: a.alert_status,
severity_level: a.severity_level,
dept_name: a.dept_name,
user_name: a.user_name,
health_id: a.health_id,
device_sn: a.device_sn,
alert_timestamp: a.alert_timestamp,
type: 'alert'
}
});
}
});
// 处理健康数据
healths.forEach(h=>{
if((h.longitude||h.longitude===0)&&(h.latitude||h.latitude===0)){
f.push({
type:'Feature',
geometry:{type:'Point',coordinates:[+h.longitude,+h.latitude]},
properties:{
...h,
dept_name: h.deptName,
user_name: h.userName,
heart_rate: h.heartRate,
blood_oxygen: h.bloodOxygen,
temperature: h.temperature,
pressure_high: h.pressureHigh,
pressure_low: h.pressureLow,
step: h.step,
stress: h.stress,
device_sn: h.deviceSn,
timestamp: h.timestamp,
avatar: h.avatar,
sleepData: h.sleepData,
timestamp: h.timestamp,
type: 'health'
}
});
}
});
const geoJSON={type:'FeatureCollection',features:f};
const criticalAlerts={type:'FeatureCollection',features:geoJSON.features.filter(f=>f.properties.severity_level==='critical')};
const highAlerts={type:'FeatureCollection',features:geoJSON.features.filter(f=>f.properties.severity_level==='high'||f.properties.severity_level==='medium')};
const healthData={type:'FeatureCollection',features:geoJSON.features.filter(f=>f.properties.type==='health')};
breathRed.setSource(new Loca.GeoJSONSource({data:criticalAlerts}));
breathYellow.setSource(new Loca.GeoJSONSource({data:highAlerts}));
breathGreen.setSource(new Loca.GeoJSONSource({data:healthData}));
// 设置地图中心
let center=null;
const getCoord=f=>f&&f.geometry&&f.geometry.coordinates;
const findFirst=(arr,cond)=>{for(let i=0;i<arr.length;i++)if(cond(arr[i]))return arr[i];return null;};
const cAlert=findFirst(geoJSON.features,f=>f.properties.type==='alert'&&['critical','high','medium'].includes(f.properties.severity_level));
const cHealth=findFirst(geoJSON.features,f=>f.properties.type==='health');
if(cAlert)center=getCoord(cAlert);
else if(cHealth)center=getCoord(cHealth);
if(center&&center.length>=2)map.setCenter([center[0],center[1]]);
else if(navigator.geolocation){
navigator.geolocation.getCurrentPosition(pos=>{
map.setCenter([pos.coords.longitude,pos.coords.latitude]);
});
}
}
</script>
</body>
</html>