Files
ljwx-health/ljwx-bigscreen/bigscreen/bigScreen/templates/bigscreen_main_backup_with_fixes.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

7694 lines
292 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>
<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>
<style>
/* 告警动画效果 */
@keyframes alertPulse {
0%, 100% { box-shadow: 0 20px 60px rgba(255,68,68,0.3), 0 0 30px rgba(255,68,68,0.2); }
50% { box-shadow: 0 25px 80px rgba(255,68,68,0.5), 0 0 50px rgba(255,68,68,0.4); }
}
@keyframes alertBlink {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.6; transform: scale(1.2); }
}
@keyframes levelGlow {
0% { opacity: 0.8; }
100% { opacity: 1; box-shadow: 0 0 20px currentColor; }
}
@keyframes scanLine {
0% { left: -100%; }
100% { left: 100%; }
}
@keyframes avatarGlow {
0% { box-shadow: 0 0 20px rgba(255,68,68,0.6); }
100% { box-shadow: 0 0 30px rgba(255,68,68,0.9), 0 0 60px rgba(255,68,68,0.3); }
}
@keyframes iconPulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.1); }
}
/* 健康点动画效果 */
@keyframes healthGlow {
0%, 100% { box-shadow: 0 25px 80px rgba(0,228,255,0.3), 0 0 40px rgba(0,228,255,0.2); }
50% { box-shadow: 0 30px 100px rgba(0,228,255,0.5), 0 0 60px rgba(0,228,255,0.4); }
}
@keyframes healthPulse {
0% { opacity: 0.8; }
100% { opacity: 1; box-shadow: 0 0 25px currentColor; }
}
@keyframes healthBeat {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.1); }
}
@keyframes healthAvatarGlow {
0%, 100% { box-shadow: 0 0 25px rgba(0,228,255,0.6); }
50% { box-shadow: 0 0 35px rgba(0,228,255,0.9), 0 0 70px rgba(0,228,255,0.3); }
}
@keyframes healthIconFloat {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
@keyframes heartbeatLine {
0%, 100% { transform: translateX(-10px); opacity: 0; }
50% { transform: translateX(46px); opacity: 1; }
}
@keyframes bgShift {
0%, 100% { transform: translateX(0) translateY(0); }
50% { transform: translateX(10px) translateY(-10px); }
}
/* 通用动画 */
@keyframes slideIn {
from { transform: translateY(30px) scale(0.9); opacity: 0; }
to { transform: translateY(0) scale(1); opacity: 1; }
}
* {
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: 22% 1fr 22%; /* 调整为比例分配:左22% 中间自适应 右22% */
grid-gap: 16px; /* 减少间距:从20px减少到16px */
padding: 0 16px 16px 16px; /* 减少内边距:从20px减少到16px */
height: calc(100vh - 76px); /* 优化高度计算:60px标题+16px底边距 */
max-height: calc(100vh - 76px); /* 限制最大高度 */
overflow: hidden;
}
.panel {
background: rgba(0, 21, 41, 0.7);
border: 1px solid rgba(0, 228, 255, 0.2);
border-radius: 4px;
padding: 10px; /* 减少内边距:从12px减少到10px */
margin: 0;
display: flex;
flex-direction: column;
}
.side-container {
display: flex;
flex-direction: column;
gap: 12px; /* 减少间距:从15px减少到12px */
height: 100%;
overflow: hidden; /* 防止内容溢出 */
}
.panel:nth-child(1) {
height: 30%; /* 统计信息面板:从20%增加到30% */
min-height: 150px; /* 增加最小高度 */
max-height: 250px; /* 增加最大高度 */
}
.panel:nth-child(2) {
height: 30%; /* 人员管理面板:从35%减少到30% */
min-height: 220px; /* 调整最小高度 */
max-height: none; /* 调整最大高度 */
}
.panel:nth-child(3) {
flex: 1; /* 使用flex: 1填充剩余空间而不是固定高度 */
min-height: 220px; /* 调整最小高度 */
max-height: none; /* 允许自适应但不超过容器 */
overflow: hidden; /* 防止内容溢出 */
}
/* 右侧面板高度分配 */
.side-container:last-child .panel:nth-child(1) {
height: 30%; /* 手表管理面板:保持30% */
min-height: 220px; /* 保持最小高度 */
max-height: 280px; /* 保持最大高度 */
}
.side-container:last-child .panel:nth-child(2) {
height: 34%; /* 健康数据分析面板:从32%增加到34% */
min-height: 200px; /* 保持最小高度 */
max-height: 320px; /* 保持最大高度 */
}
.side-container:last-child .panel:nth-child(3) {
flex: 1; /* 使用flex: 1填充剩余空间确保底部对齐 */
min-height: 200px; /* 调整最小高度 */
max-height: none; /* 保持无最大高度限制 */
overflow: hidden; /* 防止内容溢出 */
}
.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 {
flex: 1; /* 使用flex: 1自动填充剩余空间 */
background: rgba(0, 21, 41, 0.7);
border: 1px solid rgba(0, 228, 255, 0.2);
border-radius: 4px;
padding: 10px;
margin-bottom: 10px;
min-height: 300px; /* 设置最小高度确保地图有足够显示空间 */
}
/* 数据卡片样式 */
.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: 120px; /* 增加最小高度确保内容显示 */
}
/* 统计信息面板的图表容器特殊样式 */
.panel:nth-child(1) .chart-container {
min-height: 100px; /* 统计信息面板的图表最小高度:从80px增加到100px */
height: calc(100% - 60px); /* 为统计数据留出更多空间 */
}
/* 告警面板的图表容器特殊样式 */
.panel:nth-child(3) .chart-container {
min-height: 160px; /* 增加告警面板的图表最小高度:为告警类型卡片提供更多空间 */
height: calc(100% - 20px);
}
/* 右侧面板图表容器优化 */
.side-container:last-child .panel:nth-child(1) .chart-container {
min-height: 120px; /* 手表管理面板图表最小高度 */
height: calc(100% - 40px);
}
.side-container:last-child .panel:nth-child(2) .chart-container {
min-height: 140px; /* 健康数据分析面板图表最小高度 */
height: calc(100% - 40px);
}
.side-container:last-child .panel:nth-child(3) .chart-container {
min-height: 150px; /* 健康评分面板最小高度:从120px增加到150px */
height: calc(100% - 40px);
max-height: none; /* 移除最大高度限制 */
}
/* 告警图表专用样式 */
#alertTypeChart, #alertLevelChart, #alertStatusChart, #alertTrendChart {
width: 100% !important;
height: 100% !important;
min-height: 90px !important; /* 增加最小高度:从80px到90px */
max-height: 180px !important; /* 增加单个图表最大高度:从150px增加到180px,给告警类型更多显示空间 */
overflow: hidden !important;
}
/* 特别限制告警类型图表高度 */
#alertTypeChart {
max-height: 160px !important; /* 增加告警类型图表高度:从120px增加到160px,当类型增多时有更好的显示效果 */
}
/* 告警面板网格布局优化 */
.alert-charts-grid {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
gap: 8px; /* 减少间距适应更窄的宽度 */
height: calc(100% - 55px); /* 为顶部统计留出空间,给图表区域更多空间 */
min-height: 250px; /* 增加最小高度:保证告警类型卡片有足够显示空间 */
max-height: none; /* 限制最大高度防止变形 */
overflow: hidden; /* 防止内容溢出 */
}
/* 响应式调整 */
@media screen and (min-width: 1920px) {
.container {
grid-template-columns: 20% 1fr 20%; /* 超宽屏:减少侧边栏宽度,给中间更多空间 */
}
}
@media screen and (max-width: 1600px) {
.container {
grid-template-columns: 24% 1fr 24%; /* 中等屏幕:稍微增加侧边栏宽度 */
}
}
@media screen and (max-width: 1200px) {
.container {
grid-template-columns: 26% 1fr 26%; /* 小屏幕:进一步增加侧边栏宽度确保内容显示 */
}
}
@media screen and (max-width: 1024px) {
.container {
grid-template-columns: 28% 1fr 28%; /* 更小屏幕:最大化侧边栏宽度 */
grid-gap: 12px; /* 减少间距 */
}
/* 优化小屏幕下的面板内容 */
.panel-header {
font-size: 14px;
margin-bottom: 8px;
}
.alert-charts-grid {
gap: 4px; /* 进一步减少间距 */
min-height: 250px; /* 保持与主屏幕一致的最小高度 */
}
.chart-container {
min-height: 100px;
}
.side-container:last-child .panel:nth-child(3) .chart-container {
min-height: 100px;
}
}
/* 在已有的 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: 18px;
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: 18px;
}
.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: calc(100% - 30px);
overflow-y: auto;
padding: 5px;
}
.message-item {
background: rgba(0,228,255,0.1);
border-radius: 4px;
padding: 8px;
margin-bottom: 6px;
border-left: 3px solid #00e4ff;
font-size: 12px;
line-height: 1.4;
}
.message-time {
color: rgba(255,255,255,0.6);
font-size: 10px;
float: right;
}
.message-content {
color: #fff;
margin-top: 2px;
}
.device-stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px; /* 减少间距 */
margin-bottom: 8px; /* 减少底部边距 */
height: 50px; /* 减少固定高度 */
}
.device-stat-item {
background: rgba(0, 228, 255, 0.1);
border-radius: 4px;
padding: 4px; /* 减少内边距 */
text-align: center;
border-left: 3px solid #00e4ff;
transition: all 0.3s ease;
position: relative;
}
.device-stat-item:hover {
background: rgba(0, 228, 255, 0.15);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 228, 255, 0.2);
}
.stat-label {
color: rgba(255, 255, 255, 0.8);
font-size: 9px; /* 减少字体大小 */
display: block;
margin-bottom: 1px; /* 减少边距 */
}
.stat-value {
color: #00e4ff;
font-size: 14px; /* 减少字体大小 */
font-weight: bold;
display: inline-block;
}
.stat-unit {
color: rgba(255, 255, 255, 0.6);
font-size: 8px;
margin-left: 2px;
}
.stats-date {
color: rgba(255, 255, 255, 0.6);
font-size: 12px;
font-weight: normal;
}
.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: 18px;
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);
border: 1px solid rgba(0, 228, 255, 0.2);
box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
transition: all 0.3s ease;
overflow: hidden;
border-radius: 50%; /* 收缩时圆形 */
width: 36px; height: 36px; /* 极致收缩 */
}
.filter-panel.expanded {
border-radius: 8px; /* 展开时方形圆角 */
width: 320px; height: auto;
padding: 15px;
}
.filter-toggle {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: transparent;
border: none;
width: 32px;
height: 32px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: #00e4ff;
font-size: 16px;
transition: all 0.3s ease;
z-index: 10;
}
.filter-panel.expanded .filter-toggle {
position: absolute;
top: 15px;
right: 15px;
transform: none;
background: rgba(0, 228, 255, 0.2);
border: 1px solid rgba(0, 228, 255, 0.4);
border-radius: 50%;
width: 30px;
height: 30px;
font-size: 14px;
}
.filter-toggle:hover {
color: #fff;
transform: translate(-50%, -50%) scale(1.1);
}
.filter-panel.expanded .filter-toggle:hover {
background: rgba(0, 228, 255, 0.3);
border-color: rgba(0, 228, 255, 0.6);
transform: scale(1.1);
}
.filter-content {
opacity: 0;
transform: translateY(-10px);
transition: all 0.3s ease;
pointer-events: none;
}
.filter-panel.expanded .filter-content {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
.filter-container {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 40px; /* 为toggle按钮留空间 */
}
.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-search {
width: 100%;
padding: 8px 12px;
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;
font-size: 12px;
}
.filter-search:hover {
border-color: rgba(0, 228, 255, 0.4);
}
.filter-search:focus {
border-color: rgba(0, 228, 255, 0.6);
outline: none;
box-shadow: 0 0 5px rgba(0, 228, 255, 0.2);
}
.filter-search::placeholder {
color: rgba(255, 255, 255, 0.5);
}
.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: 18px;
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;
}
/* 统计信息面板样式优化 */
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
margin-bottom: 10px;
height: auto;
}
.stat-card {
background: linear-gradient(135deg, rgba(0, 228, 255, 0.1), rgba(0, 228, 255, 0.05));
border-radius: 6px;
padding: 8px;
border-left: 3px solid #00e4ff;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
cursor: pointer;
}
.stat-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.1), transparent);
transform: translateX(-100%);
transition: transform 0.6s;
}
.stat-card:hover::before {
transform: translateX(100%);
}
.stat-card:hover {
background: linear-gradient(135deg, rgba(0, 228, 255, 0.15), rgba(0, 228, 255, 0.08));
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(0, 228, 255, 0.3);
border-left-color: #38f9d7;
}
.stat-card.health-data { border-left-color: #00e4ff; }
.stat-card.alert-data { border-left-color: #ff6b6b; }
.stat-card.device-data { border-left-color: #00ff9d; }
.stat-card.message-data { border-left-color: #ffbb00; }
.stat-icon {
font-size: 16px;
position: absolute;
top: 6px;
right: 8px;
opacity: 0.7;
}
.stat-info {
display: flex;
flex-direction: column;
gap: 2px;
}
.stat-label {
color: rgba(255, 255, 255, 0.8);
font-size: 9px;
font-weight: 500;
}
.stat-value-container {
display: flex;
align-items: baseline;
gap: 2px;
}
.stat-value {
color: #00e4ff;
font-size: 16px;
font-weight: bold;
font-family: 'Courier New', monospace;
}
.stat-unit {
color: rgba(255, 255, 255, 0.6);
font-size: 8px;
margin-left: 2px;
}
.stat-trend {
color: #00ff9d;
font-size: 8px;
font-weight: 500;
}
.stat-trend.negative {
color: #ff6b6b;
}
.system-status {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
background: rgba(0, 21, 41, 0.6);
border-radius: 4px;
border: 1px solid rgba(0, 228, 255, 0.2);
margin-top: 8px;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
animation: pulse 2s infinite;
}
.status-indicator.normal { background: #00ff9d; }
.status-indicator.warning { background: #ffbb00; }
.status-indicator.critical { background: #ff6b6b; }
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
.status-text {
color: rgba(255, 255, 255, 0.9);
font-size: 10px;
flex: 1;
}
.health-score {
color: #00e4ff;
font-size: 12px;
font-weight: bold;
}
.stats-date {
color: rgba(255, 255, 255, 0.6);
font-size: 10px;
font-weight: normal;
}
/* 告警类型图表特殊优化 - 防止类型过多时变形 */
.alert-type-container {
max-height: 120px;
overflow-y: auto;
overflow-x: hidden;
}
/* 当告警类型超过5个时的特殊处理 */
.alert-type-scroll {
max-height: 100px;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: rgba(0, 228, 255, 0.3) transparent;
}
.alert-type-scroll::-webkit-scrollbar {
width: 4px;
}
.alert-type-scroll::-webkit-scrollbar-track {
background: rgba(0, 228, 255, 0.1);
}
.alert-type-scroll::-webkit-scrollbar-thumb {
background: rgba(0, 228, 255, 0.3);
border-radius: 2px;
}
/* 消息面板优化样式 - 统一定义 */
.message-panel {
height: 240px; /* 使用固定高度240px确保底部对齐 */
background: rgba(0, 21, 41, 0.7);
border: 1px solid rgba(0, 228, 255, 0.2);
border-radius: 4px;
padding: 8px; /* 减少内边距:从10px减少到8px */
margin-bottom: 0; /* 移除底部间距确保对齐 */
min-height: 220px; /* 保持最小高度 */
max-height: 280px; /* 限制最大高度 */
overflow: hidden; /* 防止内容溢出 */
display: flex;
flex-direction: column;
flex-shrink: 0; /* 防止缩放 */
}
/* 消息面板标题 */
.message-panel .panel-header {
flex-shrink: 0; /* 标题不缩放 */
margin-bottom: 8px;
height: 24px; /* 固定标题高度 */
}
/* 消息面板内容区域 */
.message-panel-content {
flex: 1; /* 占用剩余空间 */
display: flex;
gap: 10px;
min-height: 0; /* 允许内容缩放 */
width: 100%;
box-sizing: border-box;
}
/* 消息列表区域 */
.message-list-container {
flex: 0 0 60%; /* 固定60%宽度 */
display: flex;
flex-direction: column;
min-height: 0;
}
/* 消息统计区域 */
.message-stats-container {
flex: 0 0 38%; /* 固定38%宽度,留2%间距 */
display: flex;
flex-direction: column;
min-height: 0;
}
/* 消息列表样式 */
.message-list {
flex: 1;
overflow-y: auto;
padding: 5px;
background: rgba(0,21,41,0.3);
border-radius: 4px;
border: 1px solid rgba(0,228,255,0.1);
min-height: 60px; /* 确保最小显示高度 */
}
/* 消息项样式 */
.message-item {
padding: 4px 6px;
margin-bottom: 3px;
background: rgba(0,228,255,0.05);
border-radius: 3px;
border-left: 2px solid #00e4ff;
font-size: 10px;
line-height: 1.3;
color: rgba(255,255,255,0.9);
cursor: pointer;
transition: all 0.2s ease;
}
.message-item:hover {
background: rgba(0,228,255,0.1);
border-left-color: #38f9d7;
}
/* 消息统计图表容器 */
.message-stats-chart {
flex: 1;
background: rgba(0,21,41,0.3);
border-radius: 4px;
border: 1px solid rgba(0,228,255,0.1);
position: relative;
min-height: 60px;
}
/* 消息面板额外优化 - 防止变形 */
.message-panel-content {
width: 100%;
box-sizing: border-box;
}
.message-list-container,
.message-stats-container {
box-sizing: border-box;
overflow: hidden;
}
/* 消息统计概览样式优化 */
.message-stats-overview {
display: flex;
justify-content: space-around;
padding: 6px 3px;
border-bottom: 1px solid rgba(0,228,255,0.1);
flex-shrink: 0;
height: 36px;
box-sizing: border-box;
}
.message-stats-overview > div {
text-align: center;
flex: 1;
}
/* 消息统计图表区域 */
#messageStatsChart {
height: calc(100% - 36px) !important;
width: 100% !important;
min-height: 40px !important;
}
/* 手表管理面板样式 */
.watch-management {
background: linear-gradient(135deg, rgba(0, 21, 41, 0.9), rgba(0, 91, 171, 0.1));
}
.watch-stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
margin-bottom: 12px;
}
.watch-stat-card {
background: rgba(0, 228, 255, 0.1);
border-radius: 4px;
padding: 8px;
text-align: center;
border-left: 3px solid #00e4ff;
transition: all 0.3s ease;
cursor: pointer;
}
.watch-stat-card:hover {
background: rgba(0, 228, 255, 0.2);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 228, 255, 0.3);
}
.watch-stat-value {
color: #00e4ff;
font-size: 18px;
font-weight: bold;
margin-bottom: 2px;
}
.watch-stat-label {
color: rgba(255, 255, 255, 0.8);
font-size: 10px;
}
/* 健康数据分析面板样式 */
.health-analysis {
background: linear-gradient(135deg, rgba(0, 21, 41, 0.9), rgba(0, 255, 157, 0.05));
}
.health-overview-card {
background: linear-gradient(135deg, rgba(0, 228, 255, 0.15), rgba(0, 255, 157, 0.1));
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
border: 1px solid rgba(0, 228, 255, 0.3);
position: relative;
overflow: hidden;
}
.health-overview-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.05), transparent);
transform: translateX(-100%);
transition: transform 0.6s;
}
.health-overview-card:hover::before {
transform: translateX(100%);
}
.health-metrics {
display: flex;
justify-content: space-between;
align-items: center;
}
.health-metric {
text-align: center;
flex: 1;
}
.health-metric-value {
color: #00e4ff;
font-size: 18px;
font-weight: bold;
margin-bottom: 4px;
}
.health-metric-label {
color: rgba(255, 255, 255, 0.9);
font-size: 11px;
}
/* 健康评分面板样式 */
.health-score-panel {
background: linear-gradient(135deg, rgba(0, 21, 41, 0.9), rgba(255, 187, 0, 0.05));
}
.score-display {
display: flex;
align-items: center;
justify-content: space-between;
background: rgba(0, 228, 255, 0.1);
border-radius: 6px;
padding: 10px;
margin-bottom: 10px;
border-left: 4px solid #ffbb00;
}
.score-main {
display: flex;
align-items: center;
gap: 12px;
}
.score-circle {
width: 50px;
height: 50px;
border-radius: 50%;
background: conic-gradient(#00e4ff 0deg, #00e4ff 320deg, rgba(255,255,255,0.2) 320deg);
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.score-circle::before {
content: '';
width: 36px;
height: 36px;
border-radius: 50%;
background: rgba(0, 21, 41, 0.9);
position: absolute;
}
.score-number {
color: #00e4ff;
font-size: 16px;
font-weight: bold;
z-index: 1;
}
.score-info {
flex: 1;
}
.score-title {
color: #fff;
font-size: 14px;
margin-bottom: 4px;
}
.score-status {
color: #00ff9d;
font-size: 12px;
}
/* 消息面板额外优化 - 防止变形 */
</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="stats-date" id="statsDate"></span>
</div>
<div class="panel-content">
<div class="stats-grid">
<div class="stat-card health-data">
<div class="stat-icon">📊</div>
<div class="stat-info">
<span class="stat-label">健康数据</span>
<div class="stat-value-container">
<span class="stat-value" id="healthDataCount">0</span>
<span class="stat-unit">条</span>
</div>
<div class="stat-trend" id="healthTrend">+0%</div>
</div>
</div>
<div class="stat-card alert-data">
<div class="stat-icon">⚠️</div>
<div class="stat-info">
<span class="stat-label">待处理告警</span>
<div class="stat-value-container">
<span class="stat-value" id="pendingAlerts">0</span>
<span class="stat-unit">条</span>
</div>
<div class="stat-trend" id="alertTrend">+0%</div>
</div>
</div>
<div class="stat-card device-data">
<div class="stat-icon">📱</div>
<div class="stat-info">
<span class="stat-label">活跃设备</span>
<div class="stat-value-container">
<span class="stat-value" id="activeDevices">0</span>
<span class="stat-unit">台</span>
</div>
<div class="stat-trend" id="deviceTrend">+0%</div>
</div>
</div>
<div class="stat-card message-data">
<div class="stat-icon">💬</div>
<div class="stat-info">
<span class="stat-label">未读消息</span>
<div class="stat-value-container">
<span class="stat-value" id="unreadMessages">0</span>
<span class="stat-unit">条</span>
</div>
<div class="stat-trend" id="messageTrend">+0%</div>
</div>
</div>
</div>
<div class="system-status" id="systemStatus">
<div class="status-indicator normal" id="statusIndicator"></div>
<span class="status-text" id="statusText">系统正常</span>
<span class="health-score" id="systemHealthScore">100</span>
</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: 8px; 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 style="display: flex; flex-direction: column; height: 100%; width: 100%;">
<!-- 地图容器 -->
<div class="map-container" id="map-container">
<!-- 筛选面板 -->
<div class="filter-panel" id="filterPanel">
<div class="filter-toggle" id="filterToggle">🔍</div>
<div class="filter-content">
<div class="filter-header">
<span>人员筛选</span>
</div>
<div class="filter-container">
<input type="text" id="searchInput" class="filter-search" placeholder="🔍 模糊搜索用户...">
<select id="deptSelect" class="filter-select">
<option value="">选择部门</option>
</select>
<select id="userSelect" class="filter-select">
<option value="">选择用户</option>
</select>
</div>
</div>
</div>
<!-- 保留原有地图代码 -->
</div>
<!-- 消息面板 -->
<div class="message-panel">
<div class="panel-header">
<span>消息信息</span>
<span class="message-count" id="messageCount">0</span>
</div>
<!-- 消息面板内容 -->
<div class="message-panel-content">
<!-- 左侧消息列表 -->
<div class="message-list-container">
<div style="color: rgba(255,255,255,0.8); font-size: 11px; margin-bottom: 5px; padding-left: 5px;">最新消息</div>
<div class="message-list" id="messageList"></div>
</div>
<!-- 右侧统计图表 -->
<div class="message-stats-container">
<div style="color: rgba(255,255,255,0.8); font-size: 11px; margin-bottom: 5px; padding-left: 5px;">消息统计</div>
<div class="message-stats-chart">
<!-- 消息统计概览 -->
<div class="message-stats-overview">
<div>
<div style="color: #00e4ff; font-size: 12px; font-weight: bold;" id="todayMessages">0</div>
<div style="color: rgba(255,255,255,0.6); font-size: 8px;">今日</div>
</div>
<div>
<div style="color: #ffbb00; font-size: 12px; font-weight: bold;" id="unreadMessages">0</div>
<div style="color: rgba(255,255,255,0.6); font-size: 8px;">未读</div>
</div>
<div>
<div style="color: #00ff9d; font-size: 12px; font-weight: bold;" id="urgentMessages">0</div>
<div style="color: rgba(255,255,255,0.6); font-size: 8px;">紧急</div>
</div>
</div>
<!-- 消息类型分布图 -->
<div id="messageStatsChart"></div>
</div>
</div>
</div>
</div>
</div>
<!-- 右侧面板 -->
<div class="side-container">
<div class="panel watch-management">
<div class="panel-header">
<span>📱 手表管理</span>
<span class="label">总数: <span class="number" id="totalWatchDevices">35</span></span>
</div>
<div class="panel-content">
<!-- 图表区域 -->
<div id="statsChart" class="chart-container"></div>
</div>
</div>
<div class="panel health-analysis">
<div class="panel-header">
<span>💗 健康数据分析</span>
<span style="color: rgba(255,255,255,0.6); font-size: 12px;">最近7天</span>
</div>
<div class="panel-content">
<!-- 健康趋势图 -->
<div id="trendChart" style="background: rgba(0,21,41,0.4); border: 1px solid rgba(0,228,255,0.3); border-radius: 6px; position: relative; height: 100%; 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: 8px; left: 12px; color: #00e4ff; font-size: 13px; font-weight: bold; z-index: 10;">📊 健康趋势分析</div>
</div>
</div>
</div>
<div class="panel health-score-panel">
<div class="panel-header">
<span>🏆 健康评分</span>
<span class="total-score">总分:88.9</span>
</div>
<div class="panel-content">
<!-- 评分显示区域 -->
<div class="score-display">
<div class="score-main">
<div class="score-circle">
<div class="score-number">89</div>
</div>
<div class="score-info">
<div class="score-title">综合健康评分</div>
<div class="score-status">良好状态</div>
</div>
</div>
<div style="color: rgba(255,255,255,0.6); font-size: 12px; cursor: pointer;" onclick="showScoreDetails()">
详情 →
</div>
</div>
<!-- 图表区域 -->
<div id="healthScoreChart" class="chart-container"></div>
</div>
</div>
</div>
</div>
<script src="{{ url_for('static', filename='libs/echarts.min.js') }}"></script>
<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() {
// 注意:告警信息panel和设备信息panel不在此处初始化图表
// 而是在refreshData时通过initAlertChart和initDeviceChart来初始化
// 这样确保初始化时和数据更新时使用相同的图表配置,避免显示不一致
try {
let charts = {
healthScore: null,
stats: null,
trend: null,
alert: null,
messageStats: 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 scoreNumberElement = document.querySelector('.score-number');
if (scoreNumberElement) {
scoreNumberElement.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) {
// 创建默认的设备统计结构,与initDeviceChart相同风格但显示默认数据
statsContainer.innerHTML = `
<div style="position: relative; height: 100%; padding: 8px;">
<!-- 设备状态总览 -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; padding: 6px 10px; background: rgba(0,228,255,0.1); border-radius: 6px; border-left: 4px solid #00e4ff;">
<div style="display: flex; align-items: center; gap: 12px;">
<div class="device-stat-item">
<span style="color: #00ff9d; font-size: 18px; font-weight: bold;" id="onlineDevices">0</span>
<span style="color: #fff; font-size: 12px; margin-left: 4px;">在线</span>
</div>
<div class="device-stat-item">
<span style="color: #ffbb00; font-size: 16px; font-weight: bold;" id="offlineDevices">0</span>
<span style="color: #fff; font-size: 12px; margin-left: 4px;">离线</span>
</div>
<div class="device-stat-item">
<span style="color: #ff6666; font-size: 14px; font-weight: bold;" id="errorDevices">0</span>
<span style="color: #fff; font-size: 12px; margin-left: 4px;">故障</span>
</div>
</div>
<div style="background: rgba(0,255,157,0.3); padding: 4px 8px; border-radius: 12px;" id="deviceStatusBadge">
<span style="color: #00ff9d; font-size: 11px; font-weight: bold;">✅ 正常</span>
</div>
</div>
<!-- 图表区域 -->
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 6px; height: calc(100% - 45px);">
<div id="deviceStatusChart" 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="deviceTypeChart" 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="deviceDeptChart" 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="deviceTrendChart" 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>
</div>
`;
// 延时初始化默认图表
setTimeout(() => {
if (document.getElementById('deviceStatusChart')) {
// 设备状态分布图
const statusChart = echarts.init(document.getElementById('deviceStatusChart'));
statusChart.setOption({
tooltip: { trigger: 'item', backgroundColor: 'rgba(0,21,41,0.95)', borderColor: '#00e4ff', textStyle: { color: '#fff', fontSize: 11 } },
series: [{ type: 'pie', radius: ['35%', '65%'], center: ['50%', '55%'], data: [
{ name: '暂无数据', value: 1, itemStyle: { color: '#666' } }
], label: { show: false } }]
});
// 设备类型分布图
const typeChart = echarts.init(document.getElementById('deviceTypeChart'));
typeChart.setOption({
tooltip: { trigger: 'item', backgroundColor: 'rgba(0,21,41,0.95)', borderColor: '#00e4ff', textStyle: { color: '#fff', fontSize: 11 } },
series: [{ type: 'pie', radius: ['35%', '65%'], center: ['50%', '55%'], data: [
{ name: '暂无数据', value: 1, itemStyle: { color: '#666' } }
], label: { show: false } }]
});
// 部门分布图
const deptChart = echarts.init(document.getElementById('deviceDeptChart'));
deptChart.setOption({
tooltip: { trigger: 'axis', backgroundColor: 'rgba(0,21,41,0.95)', borderColor: '#00e4ff', textStyle: { color: '#fff', fontSize: 11 } },
grid: { top: 25, left: 50, right: 15, bottom: 15 },
xAxis: { type: 'value', axisLabel: { color: '#7ecfff', fontSize: 9 }, splitLine: { lineStyle: { color: 'rgba(126,207,255,0.1)' } }, axisLine: { show: false }, max: 1 },
yAxis: { type: 'category', data: ['暂无数据'], axisLabel: { color: '#fff', fontSize: 9 }, axisLine: { show: false }, axisTick: { show: false } },
series: [{ type: 'bar', data: [0], barWidth: '65%', itemStyle: { color: '#00e4ff88' } }]
});
// 使用趋势图
const trendChart = echarts.init(document.getElementById('deviceTrendChart'));
const days = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
trendChart.setOption({
tooltip: { trigger: 'axis', backgroundColor: 'rgba(0,21,41,0.95)', borderColor: '#00e4ff', textStyle: { color: '#fff', fontSize: 11 } },
grid: { top: 25, left: 30, right: 15, bottom: 25 },
xAxis: { type: 'category', data: days, axisLabel: { color: '#7ecfff', fontSize: 8 }, axisLine: { show: false }, axisTick: { show: false } },
yAxis: { type: 'value', axisLabel: { color: '#7ecfff', fontSize: 8 }, splitLine: { lineStyle: { color: 'rgba(126,207,255,0.1)' } }, axisLine: { show: false } },
series: [{ type: 'line', data: [0, 0, 0, 0, 0, 0, 0], smooth: true, lineStyle: { color: '#00e4ff', width: 2 }, itemStyle: { color: '#00e4ff' }, symbol: 'circle', symbolSize: 4 }]
});
}
}, 200);
charts.stats = null; // 将在initDeviceChart中重新设置
}
// 检查并初始化预警信息图表
const alertContainer = document.getElementById('alertList');
if (alertContainer) {
// 创建默认的告警信息结构,与initAlertChart相同风格但显示默认数据
alertContainer.innerHTML = `
<div style="position: relative; height: 100%; padding: 8px;">
<!-- 告警状态总览 -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; padding: 6px 10px; background: rgba(0,228,255,0.1); border-radius: 6px; border-left: 4px solid #00e4ff;">
<div style="display: flex; align-items: center; gap: 12px;">
<div class="alert-stat-item">
<span style="color: #ff4444; font-size: 18px; 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: 16px; 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: 14px; font-weight: bold;" id="pendingCount">0</span>
<span style="color: #fff; font-size: 12px; margin-left: 4px;">待处理</span>
</div>
</div>
<div style="background: rgba(0,255,157,0.3); padding: 4px 8px; border-radius: 12px;" id="alertBadge">
<span style="color: #00ff9d; font-size: 11px; font-weight: bold;">✅ 正常</span>
</div>
</div>
<!-- 图表区域 -->
<div class="alert-charts-grid" style="display: grid; grid-template-columns: 1fr 1fr; gap: 6px; height: calc(100% - 45px);">
<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>
`;
// 延时初始化默认图表
setTimeout(() => {
if (document.getElementById('alertTypeChart')) {
// 告警类型分布图
const typeChart = echarts.init(document.getElementById('alertTypeChart'));
typeChart.setOption({
tooltip: { trigger: 'axis', backgroundColor: 'rgba(0,21,41,0.95)', borderColor: '#00e4ff', textStyle: { color: '#fff', fontSize: 11 } },
grid: { top: 25, left: 50, right: 15, bottom: 15 },
xAxis: { type: 'value', axisLabel: { color: '#7ecfff', fontSize: 9 }, splitLine: { lineStyle: { color: 'rgba(126,207,255,0.1)' } }, axisLine: { show: false }, max: 1 },
yAxis: { type: 'category', data: ['暂无数据'], axisLabel: { color: '#fff', fontSize: 9 }, axisLine: { show: false }, axisTick: { show: false } },
series: [{ type: 'bar', data: [0], barWidth: '65%', itemStyle: { color: '#00e4ff88' } }]
});
// 告警级别分布图
const levelChart = echarts.init(document.getElementById('alertLevelChart'));
levelChart.setOption({
tooltip: { trigger: 'item', backgroundColor: 'rgba(0,21,41,0.95)', borderColor: '#00e4ff', textStyle: { color: '#fff', fontSize: 11 } },
series: [{ type: 'pie', radius: ['35%', '65%'], center: ['50%', '55%'], data: [
{ name: '暂无数据', value: 1, itemStyle: { color: '#666' } }
], label: { show: false } }]
});
// 处理状态图
const statusChart = echarts.init(document.getElementById('alertStatusChart'));
statusChart.setOption({
tooltip: { trigger: 'item', backgroundColor: 'rgba(0,21,41,0.95)', borderColor: '#00e4ff', textStyle: { color: '#fff', fontSize: 11 } },
series: [{ type: 'pie', radius: ['35%', '65%'], center: ['50%', '55%'], data: [
{ name: '暂无数据', value: 1, itemStyle: { color: '#666' } }
], label: { show: false } }]
});
// 24小时趋势图
const trendChart = echarts.init(document.getElementById('alertTrendChart'));
const hours = ['00', '04', '08', '12', '16', '20'];
trendChart.setOption({
tooltip: { trigger: 'axis', backgroundColor: 'rgba(0,21,41,0.95)', borderColor: '#00e4ff', textStyle: { color: '#fff', fontSize: 11 } },
grid: { top: 25, left: 30, right: 15, bottom: 25 },
xAxis: { type: 'category', data: hours, axisLabel: { color: '#7ecfff', fontSize: 8 }, axisLine: { show: false }, axisTick: { show: false } },
yAxis: { type: 'value', axisLabel: { color: '#7ecfff', fontSize: 8 }, splitLine: { lineStyle: { color: 'rgba(126,207,255,0.1)' } }, axisLine: { show: false } },
series: [{ type: 'line', data: [0, 0, 0, 0, 0, 0], smooth: true, lineStyle: { color: '#00e4ff', width: 2 }, itemStyle: { color: '#00e4ff' }, symbol: 'circle', symbolSize: 4 }]
});
}
}, 200);
charts.alert = null; // 将在initAlertChart中重新设置
}
// 添加窗口resize事件监听
window.addEventListener('resize', () => {
Object.values(charts).forEach(chart => {
// 检查是否为ECharts实例并且有resize方法
if (chart &&
chart.resize &&
typeof chart.resize === 'function' &&
chart.getOption) { // ECharts实例通常有getOption方法
try {
chart.resize();
} catch (error) {
console.warn('图表resize失败:', error);
}
}
});
});
// 初始化消息统计图表
const messageStatsContainer = document.getElementById('messageStatsChart');
if (messageStatsContainer) {
charts.messageStats = echarts.init(messageStatsContainer);
// 消息类型颜色定义(与message_view.html保持一致)
const messageTypeColors = {
'announcement': '#1890ff', // 蓝色 - 公告
'notification': '#52c41a', // 绿色 - 通知
'job': '#722ed1', // 紫色 - 作业指导
'task': '#fa8c16', // 橙色 - 任务管理
'warning': '#f5222d' // 红色 - 告警
};
// 初始化消息统计图表
const messageStatsOption = {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
show: false
},
series: [{
name: '消息类型',
type: 'pie',
radius: ['30%', '70%'],
center: ['50%', '60%'],
avoidLabelOverlap: false,
label: {
show: false
},
emphasis: {
label: {
show: true,
fontSize: '10',
fontWeight: 'bold',
color: '#fff'
}
},
labelLine: {
show: false
},
data: [
{value: 0, name: '公告', itemStyle: {color: messageTypeColors.announcement}},
{value: 0, name: '工作指引', itemStyle: {color: messageTypeColors.job}},
{value: 0, name: '通知', itemStyle: {color: messageTypeColors.notification}},
{value: 0, name: '任务管理', itemStyle: {color: messageTypeColors.task}},
{value: 0, name: '告警', itemStyle: {color: messageTypeColors.warning}}
]
}]
};
charts.messageStats.setOption(messageStatsOption);
}
// 返回图表实例对象,以便其他地方可能需要使用
return charts;
} catch (error) {
console.error('Error initializing charts:', error);
}
}
// 修改初始化调用
document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
globalCharts = initCharts(); // 存储图表实例
// 🗺️ 智能地图初始化 - 支持在线/离线模式
console.log('🗺️ 开始智能地图初始化...');
try {
initializeMap('{{ customerId }}', '-1');
console.log('✅ 地图初始化调用完成');
} catch (error) {
console.error('❌ 地图初始化失败:', error);
}
// 等待地图初始化完成后再刷新数据
setTimeout(() => {
refreshData(); // 初始加载数据
}, 500); // 给地图初始化留500ms时间
// 每分钟刷新一次数据
setInterval(refreshData, 60000);
}, 100);
});
// 修改消息列表初始化
function initMessageList(data) {
const messageList = document.getElementById('messageList');
const messageCount = document.getElementById('messageCount');
if (!messageList || !messageCount) return;
// 消息类型颜色定义(参考message_view.html)
const messageTypeColors = {
'announcement': '#1890ff', // 蓝色 - 公告
'notification': '#52c41a', // 绿色 - 通知
'job': '#722ed1', // 紫色 - 作业指导
'task': '#fa8c16', // 橙色 - 任务管理
'warning': '#f5222d' // 红色 - 告警
};
const typeMap = {
'announcement': '公告',
'job': '工作指引',
'notification': '通知',
'task': '任务管理',
'warning': '告警'
};
// 从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;
// 统计消息数据
const today = new Date().toDateString();
const todayMessages = messages.filter(msg => new Date(msg.received_time).toDateString() === today).length;
const unreadMessages = messages.filter(msg => msg.message_status === 'pending' || msg.message_status === '1').length;
const urgentMessages = messages.filter(msg => msg.priority === 'high' || msg.priority === 'urgent').length;
// 更新统计显示
document.getElementById('todayMessages').textContent = todayMessages;
document.getElementById('unreadMessages').textContent = unreadMessages;
document.getElementById('urgentMessages').textContent = urgentMessages;
// 统计消息类型分布 - 使用正确的类型映射
const messageTypes = {
'公告': 0,
'工作指引': 0,
'通知': 0,
'任务管理': 0,
'告警': 0
};
messages.forEach(msg => {
const type = msg.message_type || 'notification';
const typeName = typeMap[type] || '通知';
if (messageTypes.hasOwnProperty(typeName)) {
messageTypes[typeName]++;
}
});
// 更新消息统计图表
if (globalCharts && globalCharts.messageStats) {
const chartData = [
{value: messageTypes['公告'], name: '公告', itemStyle: {color: messageTypeColors.announcement}},
{value: messageTypes['工作指引'], name: '工作指引', itemStyle: {color: messageTypeColors.job}},
{value: messageTypes['通知'], name: '通知', itemStyle: {color: messageTypeColors.notification}},
{value: messageTypes['任务管理'], name: '任务管理', itemStyle: {color: messageTypeColors.task}},
{value: messageTypes['告警'], name: '告警', itemStyle: {color: messageTypeColors.warning}}
];
globalCharts.messageStats.setOption({
series: [{
data: chartData
}]
});
}
// 清空现有消息
messageList.innerHTML = '';
// 创建消息容器
const messageContainer = document.createElement('div');
messageContainer.className = 'message-container';
// 添加所有消息
pendingMessages.forEach(message => {
const msgType = message.message_type || 'notification';
const msgColor = messageTypeColors[msgType] || messageTypeColors.notification;
const msgTypeName = typeMap[msgType] || '通知';
const messageElement = document.createElement('div');
messageElement.className = 'message-item';
messageElement.innerHTML = `
<div class="message-header">
<span style="color: ${msgColor}; font-weight: bold;">[${msgTypeName}] ${message.department_name || '未知部门'}-${message.user_name || '系统'}</span>
<span class="message-time">${message.received_time || new Date().toLocaleString()}</span>
</div>
<div class="message-content" style="border-left: 3px solid ${msgColor}; padding-left: 6px;">${message.message || '无消息内容'}</div>
`;
messageContainer.appendChild(messageElement);
});
// 如果没有消息,显示提示信息
if (pendingMessages.length === 0) {
messageList.innerHTML = '<div class="no-messages">暂无待处理消息</div>';
return;
}
// 将消息容器添加到列表中
messageList.appendChild(messageContainer);
// 添加样式 - 移除重复样式防止冲突
const existingStyle = document.getElementById('message-scroll-styles');
if (existingStyle) {
existingStyle.remove();
}
const style = document.createElement('style');
style.id = 'message-scroll-styles';
style.textContent = `
#messageList {
height: calc(100% - 5px);
overflow: hidden;
position: relative;
background: linear-gradient(135deg, rgba(0, 42, 74, 0.8), rgba(0, 74, 114, 0.6));
border-radius: 8px;
border: 1px solid rgba(0, 228, 255, 0.25);
box-shadow: inset 0 1px 3px rgba(0, 228, 255, 0.1);
}
.message-container {
display: flex;
flex-direction: column;
width: 100%;
padding: 8px;
}
.message-item {
background: linear-gradient(135deg, rgba(0, 62, 94, 0.8), rgba(0, 94, 134, 0.6));
border: 1px solid rgba(0, 228, 255, 0.2);
border-radius: 6px;
padding: 8px 12px;
margin-bottom: 6px;
position: relative;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15), 0 0 15px rgba(0, 228, 255, 0.05);
}
.message-item::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
border-radius: 2px 0 0 2px;
transition: all 0.3s ease;
}
.message-item.type-announcement::before { background: linear-gradient(135deg, #4A90E2, #357ABD); }
.message-item.type-notification::before { background: linear-gradient(135deg, #7ED321, #5BA317); }
.message-item.type-job::before { background: linear-gradient(135deg, #9013FE, #6A1B9A); }
.message-item.type-task::before { background: linear-gradient(135deg, #FF9500, #E6830F); }
.message-item.type-warning::before { background: linear-gradient(135deg, #F5A623, #E8940F); }
.message-item:hover {
transform: translateX(2px);
border-color: rgba(64, 169, 255, 0.4);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3), 0 0 20px rgba(64, 169, 255, 0.1);
}
.message-item:last-child {
margin-bottom: 0;
}
.message-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
.message-type-badge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 12px;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.5px;
text-transform: uppercase;
backdrop-filter: blur(5px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.message-type-badge.announcement {
background: linear-gradient(135deg, rgba(74, 144, 226, 0.2), rgba(53, 122, 189, 0.3));
color: #87CEEB;
border-color: rgba(74, 144, 226, 0.3);
}
.message-type-badge.notification {
background: linear-gradient(135deg, rgba(126, 211, 33, 0.2), rgba(91, 163, 23, 0.3));
color: #98FB98;
border-color: rgba(126, 211, 33, 0.3);
}
.message-type-badge.job {
background: linear-gradient(135deg, rgba(144, 19, 254, 0.2), rgba(106, 27, 154, 0.3));
color: #DDA0DD;
border-color: rgba(144, 19, 254, 0.3);
}
.message-type-badge.task {
background: linear-gradient(135deg, rgba(255, 149, 0, 0.2), rgba(230, 131, 15, 0.3));
color: #FFB84D;
border-color: rgba(255, 149, 0, 0.3);
}
.message-type-badge.warning {
background: linear-gradient(135deg, rgba(245, 166, 35, 0.2), rgba(232, 148, 15, 0.3));
color: #FFC04D;
border-color: rgba(245, 166, 35, 0.3);
}
.message-sender {
color: rgba(255, 255, 255, 0.9);
font-size: 11px;
font-weight: 500;
margin-left: 8px;
}
.message-time {
color: rgba(64, 169, 255, 0.8);
font-size: 9px;
font-weight: 400;
font-family: 'Consolas', 'Monaco', monospace;
background: rgba(64, 169, 255, 0.1);
padding: 2px 6px;
border-radius: 4px;
border: 1px solid rgba(64, 169, 255, 0.2);
}
.message-content {
color: rgba(255, 255, 255, 0.95);
font-size: 12px;
line-height: 1.4;
font-weight: 400;
margin-top: 2px;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
max-height: 32px;
}
.no-messages {
text-align: center;
padding: 30px 20px;
color: rgba(255, 255, 255, 0.4);
font-size: 14px;
background: linear-gradient(135deg, rgba(20, 45, 85, 0.3), rgba(30, 55, 95, 0.2));
border-radius: 8px;
border: 1px dashed rgba(64, 169, 255, 0.3);
margin: 20px;
}
/* 滚动动画 */
.message-container.scrolling {
animation: verticalScroll linear infinite;
}
@keyframes verticalScroll {
0% {
transform: translateY(0);
}
100% {
transform: translateY(-50%);
}
}
/* 添加呼吸光效 */
.message-item::after {
content: '';
position: absolute;
top: -1px;
left: -1px;
right: -1px;
bottom: -1px;
background: linear-gradient(45deg, transparent, rgba(64, 169, 255, 0.1), transparent);
border-radius: 6px;
opacity: 0;
animation: breathe 3s ease-in-out infinite;
pointer-events: none;
z-index: -1;
}
@keyframes breathe {
0%, 100% { opacity: 0; }
50% { opacity: 0.3; }
}
`;
document.head.appendChild(style);
// 实现滚动逻辑
setTimeout(() => {
const containerHeight = messageList.clientHeight;
const contentHeight = messageContainer.scrollHeight;
console.log('Container height:', containerHeight, 'Content height:', contentHeight);
if (contentHeight > containerHeight && pendingMessages.length > 3) {
// 复制内容实现无缝滚动
messageContainer.innerHTML += messageContainer.innerHTML;
// 计算滚动时间:每个消息显示3秒
const scrollDuration = pendingMessages.length * 3;
// 添加滚动类和设置动画时间
messageContainer.classList.add('scrolling');
messageContainer.style.animationDuration = `${scrollDuration}s`;
console.log('启动滚动动画,持续时间:', scrollDuration + 's');
// 鼠标悬停控制
messageList.addEventListener('mouseenter', () => {
messageContainer.style.animationPlayState = 'paused';
});
messageList.addEventListener('mouseleave', () => {
messageContainer.style.animationPlayState = 'running';
});
} else {
console.log('消息数量少,不启动滚动');
}
// 添加消息点击事件
addMessageClickEvents();
}, 500); // 延迟500ms确保DOM渲染完成
}
// 添加消息面板点击处理函数
function openMessagePanel() {
const urlParams = new URLSearchParams(window.location.search);
const customerId = urlParams.get('customerId') || '1';
createModalWindow('/message_view?customerId=' + customerId, '消息管理', '90%', '90%');
}
// 添加消息项点击事件
function addMessageClickEvents() {
const messageItems = document.querySelectorAll('.message-item');
messageItems.forEach(item => {
item.addEventListener('click', openMessagePanel);
item.style.cursor = 'pointer';
});
}
</script>
<script>
// Declare infoWindow globally
// let infoWindow; // 移除
let geoLevelF,geoLevelE,geo,map,loca,breathGreen,breathRed,breathYellow;//变量合并
// 地图初始化重试计数器和状态锁
let mapInitRetryCount = 0;
const MAX_RETRY_COUNT = 3;
let isMapInitializing = false;
function initializeMap(deptId, userId, retryCount = 0) {
// 检查是否正在初始化
if (isMapInitializing && retryCount === 0) {
console.warn('⚠️ 地图正在初始化中,跳过重复调用');
return;
}
// 检查重试次数
if (retryCount >= MAX_RETRY_COUNT) {
console.error('🚫 地图初始化重试次数超限,停止初始化');
isMapInitializing = false;
return;
}
isMapInitializing = true;
// 检查必要的库是否已加载
if (typeof AMap === 'undefined') {
console.error('AMap 库未加载,延迟重试...', `重试次数: ${retryCount + 1}`);
isMapInitializing = false; // 重置状态锁
setTimeout(() => initializeMap(deptId, userId, retryCount + 1), 1000);
return;
}
if (typeof Loca === 'undefined') {
console.error('Loca 库未加载,延迟重试...', `重试次数: ${retryCount + 1}`);
isMapInitializing = false; // 重置状态锁
setTimeout(() => initializeMap(deptId, userId, retryCount + 1), 1000);
return;
}
console.log('AMap和Loca库加载完成,开始初始化地图');
// 清理之前的实例,避免冲突
if (window.loca) {
try {
window.loca.destroy();
console.log('🧹 清理旧的Loca实例');
} catch (e) {
console.warn('清理旧Loca实例时出错:', e);
}
}
if (window.map) {
try {
window.map.destroy();
console.log('🧹 清理旧的地图实例');
} catch (e) {
console.warn('清理旧地图实例时出错:', e);
}
}
//console.log('initializeMap.deptId',deptId);
//console.log('initializeMap.userId',userId);
//console.log('initializeMap.severityLevel',`./generateAlertJson?customerId=${deptId}&userId=${userId}&severityLevel=critical`);
// v1.0.32 - 添加medium级别告警点支持
const dataUrls = {
critical: `./generateAlertJson?customerId=${deptId}&userId=${userId}&severityLevel=critical`,
high: `./generateAlertJson?customerId=${deptId}&userId=${userId}&severityLevel=high`,
medium: `./generateAlertJson?customerId=${deptId}&userId=${userId}&severityLevel=medium`, // 新增medium级别
health: `./generateHealthJson?customerId=${deptId}&userId=${userId}`
};
console.log('🔗 数据URL准备完成,等待地图加载');
let current_coordinates = {
'longitude': 114.01508952,
'latitude': 22.58036796,
'altitude': 0
}
//console.log('current_coordinates',current_coordinates);
console.log('🗺️ 开始创建地图实例');
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',
});
// 等待地图完全加载后再初始化Loca图层
map.on('complete', function() {
console.log('🎉 地图加载完成,开始完整的Loca初始化流程');
// 延迟执行,确保地图内部状态完全稳定
setTimeout(() => {
// 再次确保地图对象存在且有效
if (!map || !window.map || map !== window.map) {
console.error('❌ 地图对象不存在或无效,无法创建Loca容器');
isMapInitializing = false;
return;
}
// 检查地图容器DOM是否存在
const mapContainer = document.getElementById('map-container');
if (!mapContainer) {
console.error('❌ 地图容器DOM不存在');
isMapInitializing = false;
return;
}
try {
console.log('🔧 步骤1: 创建Loca数据源');
// v1.0.32 - 创建所有级别的数据源包括medium
geoLevelF = new Loca.GeoJSONSource({
url: dataUrls.critical
});
geoLevelE = new Loca.GeoJSONSource({
url: dataUrls.high
});
geoLevelM = new Loca.GeoJSONSource({
url: dataUrls.medium
});
geo = new Loca.GeoJSONSource({
url: dataUrls.health
});
console.log('✅ Loca数据源创建成功');
console.log('🔧 步骤2: 创建Loca容器');
loca = new Loca.Container({
map: window.map, // 使用window.map确保引用正确
});
// 确保Loca容器创建成功且有效
if (!loca || !loca.map) {
console.error('❌ Loca容器创建失败或map引用无效');
isMapInitializing = false;
return;
}
console.log('✅ Loca容器创建成功,开始创建图层');
} catch (error) {
console.error('❌ 创建Loca容器或数据源时出错:', error);
isMapInitializing = false;
return;
}
try {
console.log('🔧 步骤3: 开始创建图层');
// 验证Loca容器和地图的有效性
if (!loca || !loca.map || !window.map) {
console.error('❌ 图层创建前检查失败:Loca容器或地图无效');
isMapInitializing = false;
return;
}
console.log('🟢 创建绿色健康点图层');
breathGreen = new Loca.ScatterLayer({
loca,
zIndex: 113,
opacity: 1,
visible: true,
zooms: [2, 22],
});
// 安全设置数据源
if (geo) {
breathGreen.setSource(geo);
} else {
console.warn('绿色点数据源不存在,跳过设置');
}
breathGreen.setStyle({
unit: 'meter',
color: 'rgb(39, 207, 14)',
size: [10, 10],
borderWidth: 0,
texture: 'https://a.amap.com/Loca/static/loca-v2/demos/images/breath_green.png',
duration: 500,
animate: true,
});
// 安全添加到地图中
loca.add(breathGreen);
console.log('✅ 绿色健康点图层创建成功');
console.log('🔴 创建红色告警点图层');
breathRed = new Loca.ScatterLayer({
loca,
zIndex: 113,
opacity: 1,
visible: true,
zooms: [2, 22],
});
// 安全设置数据源
if (geoLevelF) {
breathRed.setSource(geoLevelF);
} else {
console.warn('红色点数据源不存在,跳过设置');
}
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);
console.log('✅ 红色告警点图层创建成功');
console.log('🟡 创建黄色告警点图层');
breathYellow = new Loca.ScatterLayer({
loca,
zIndex: 112,
opacity: 1,
visible: true,
zooms: [2, 22],
});
// 安全设置数据源
if (geoLevelE) {
breathYellow.setSource(geoLevelE);
} else {
console.warn('黄色点数据源不存在,跳过设置');
}
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('✅ 黄色告警点图层创建成功');
console.log('🟠 创建橙色medium告警点图层');
breathOrange = new Loca.ScatterLayer({
loca,
zIndex: 111,
opacity: 1,
visible: true,
zooms: [2, 22],
});
// 安全设置数据源
if (geoLevelM) {
breathOrange.setSource(geoLevelM);
} else {
console.warn('橙色点数据源不存在,跳过设置');
}
breathOrange.setStyle({
unit: 'meter',
size: [40, 40],
borderWidth: 0,
texture: 'https://a.amap.com/Loca/static/loca-v2/demos/images/breath_orange.png',
duration: 800,
animate: true,
});
// 安全添加到地图中
loca.add(breathOrange);
console.log('✅ 橙色medium告警点图层创建成功');
console.log('🔧 步骤4: 绑定交互事件和启动动画');
// v1.0.32 - 添加点击事件包含所有图层
if (window.map && breathRed && breathYellow && breathOrange && breathGreen) {
window.map.on('click',e=>{
const p=e.pixel.toArray();
let f=breathRed.queryFeature(p)||breathYellow.queryFeature(p)||breathOrange.queryFeature(p)||breathGreen.queryFeature(p);
if(f&&f.coordinates){showCustomMapInfo(f)}else{removeCustomMapInfo()}
});//优先级修正:红>黄>橙>绿
console.log('✅ 地图点击事件绑定成功(包含4个图层)');
} else {
console.warn('⚠️ 图层不完整,跳过点击事件绑定');
}
// 最终验证后启动渲染动画
if (loca && loca.animate) {
loca.animate.start();
console.log('✅ Loca渲染动画启动成功');
} else {
console.error('❌ Loca动画启动失败');
isMapInitializing = false;
return;
}
console.log('🎉 地图初始化流程完全成功!');
isMapInitializing = false; // 重置状态锁
} catch (error) {
console.error('❌ 创建Loca图层时出错:', error);
// 如果图层创建失败,尝试重新初始化
if (retryCount < MAX_RETRY_COUNT) {
console.log(`🔄 图层创建失败,3秒后重试... (重试次数: ${retryCount + 1})`);
isMapInitializing = false; // 重置状态锁以允许重试
setTimeout(() => initializeMap(deptId, userId, retryCount + 1), 3000);
} else {
console.error('🚫 图层创建重试次数超限,停止重试');
isMapInitializing = false; // 重置状态锁
}
}
}, 1000); // 延迟1000ms确保地图内部状态完全稳定
});
}
//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 '未知';
}
}
// 筛选面板伸缩功能
function toggleFilter() {
const p=document.getElementById('filterPanel'),t=document.getElementById('filterToggle');
if(p.classList.contains('expanded')){p.classList.remove('expanded');t.textContent='🔍';}
else{p.classList.add('expanded');t.textContent='✕';}
}
// 模糊搜索功能
let allUsers=[],filteredUsers=[];
function initSearchFilter() {
const searchInput=document.getElementById('searchInput'),userSelect=document.getElementById('userSelect'),deptSelect=document.getElementById('deptSelect');
searchInput.addEventListener('input',e=>{
const q=e.target.value.toLowerCase().trim();
if(!q){filteredUsers=allUsers;updateUserSelect();}
else{
filteredUsers=allUsers.filter(u=>u.user_name?.toLowerCase().includes(q)||u.dept_name?.toLowerCase().includes(q)||u.device_sn?.toLowerCase().includes(q));
updateUserSelect();
}
});
deptSelect.addEventListener('change',e=>{
const dept=e.target.value;
filteredUsers=dept?allUsers.filter(u=>u.dept_name===dept):allUsers;
updateUserSelect();
searchInput.value='';
});
}
function updateUserSelect() {
const s=document.getElementById('userSelect');
s.innerHTML='<option value="">选择用户</option>';
filteredUsers.forEach(u=>s.innerHTML+=`<option value="${u.user_id}">${u.user_name}${u.position?' - '+u.position:''}</option>`);
}
function setUsersData(users) {
allUsers=users||[];filteredUsers=allUsers;updateUserSelect();
}
// 添加点击外部关闭筛选面板的功能
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 loadHealthScoreChart(customerId) {
console.log('loadHealthScoreChart 开始执行,customerId:', customerId);
const endDate = getPastDateStr(0), startDate = getPastDateStr(6);
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);
});
}
}
function loadBaselineTrendChart(orgId) {
console.log('loadBaselineTrendChart 开始执行,orgId:', orgId);
const endDate = getPastDateStr(0), startDate = getPastDateStr(6);
fetch(`/health_data/chart/baseline?orgId=${orgId}&startDate=${startDate}&endDate=${endDate}`)
.then(r=>r.json())
.then(result=>{
console.log('健康数据接口返回:', result);
// 检查是否有数据,如果没有数据则生成baseline
if (!result || !result.dates || result.dates.length === 0) {
console.warn('baseline数据缺失,开始生成baseline');
return generateBaselineAndRetry(orgId, startDate, endDate);
}
renderHealthChart(result);
})
.catch(error => {
console.error('健康数据加载失败:', error);
// 尝试生成baseline后重试
generateBaselineAndRetry(orgId, startDate, endDate);
});
}
// 生成baseline并重试获取数据
function generateBaselineAndRetry(orgId, startDate, endDate) {
console.log('正在生成baseline数据...');
fetch('/api/baseline/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ target_date: endDate })
})
.then(r => r.json())
.then(generateResult => {
console.log('baseline生成结果:', generateResult);
if (generateResult.success) {
// 生成成功后重新获取数据
return fetch(`/health_data/chart/baseline?orgId=${orgId}&startDate=${startDate}&endDate=${endDate}`);
} else {
throw new Error('baseline生成失败: ' + generateResult.error);
}
})
.then(r => r.json())
.then(result => {
console.log('重新获取的健康数据:', result);
renderHealthChart(result);
})
.catch(error => {
console.error('生成baseline或重新获取数据失败:', error);
//showDefaultHealthData();
});
}
// 渲染健康图表 - 专业亮眼版本 v1.0.34
function renderHealthChart(result) {
const {dates, metrics, health_summary} = result;
console.log('🎯 renderHealthChart 接收数据:', {dates, metrics: metrics?.map(m=>({name:m.name,count:m.values?.length}))});
const trendChart = echarts.init(document.getElementById('trendChart'));
// 大屏科技风格健康指标配置 - 协调版 v1.0.34
const healthMetrics = {
'心率': { color: '#ff6b9d', icon: '💗', gradient: ['#ff6b9d', '#ff8fb3'], range: [60, 100] },
'血氧': { color: '#00e4ff', icon: '🫁', gradient: ['#00e4ff', '#5beeff'], range: [95, 100] },
'体温': { color: '#ffaa00', icon: '🌡️', gradient: ['#ffaa00', '#ffcc55'], range: [36, 38] },
'压力': { color: '#ff7700', icon: '😰', gradient: ['#ff7700', '#ff9944'], range: [0, 100] },
'睡眠': { color: '#7ecfff', icon: '😴', gradient: ['#7ecfff', '#a8d8ff'], range: [6, 10] }
};
const series = [];
// 处理现有数据
if (metrics?.length > 0) {
Object.keys(healthMetrics).forEach(metricName => {
const metric = metrics.find(m => m.name === metricName);
if (metric?.values?.length > 0) {
const config = healthMetrics[metricName];
series.push({
name: metricName,
type: 'line',
data: metric.values,
smooth: true,
symbol: 'circle',
symbolSize: 6,
lineStyle: {
width: 3,
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: config.gradient[0] },
{ offset: 1, color: config.gradient[1] }
]),
shadowColor: config.color + '40',
shadowBlur: 8
},
itemStyle: {
color: config.color,
borderColor: '#001529',
borderWidth: 1,
shadowColor: config.color,
shadowBlur: 6
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: config.color + '30' },
{ offset: 1, color: config.color + '05' }
])
},
emphasis: {
focus: 'series',
scale: 1.1
}
});
}
});
}
// 如果没有睡眠数据,生成模拟数据
const hasSleep = series.some(s => s.name === '睡眠');
if (!hasSleep && dates?.length > 0) {
console.log('⚠️ 未找到睡眠数据,生成模拟数据');
const sleepData = dates.map(() => (7 + Math.random() * 2).toFixed(1));
const sleepConfig = healthMetrics['睡眠'];
series.push({
name: '睡眠',
type: 'line',
data: sleepData,
smooth: true,
symbol: 'circle',
symbolSize: 6,
lineStyle: {
width: 3,
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: sleepConfig.gradient[0] },
{ offset: 1, color: sleepConfig.gradient[1] }
]),
shadowColor: sleepConfig.color + '40',
shadowBlur: 8
},
itemStyle: {
color: sleepConfig.color,
borderColor: '#001529',
borderWidth: 1,
shadowColor: sleepConfig.color,
shadowBlur: 6
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: sleepConfig.color + '30' },
{ offset: 1, color: sleepConfig.color + '05' }
])
}
});
}
console.log('📊 图表系列数据:', series.map(s=>({name:s.name,dataCount:s.data?.length})));
const trendOption = {
backgroundColor: 'transparent',
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(10,24,48,0.95)',
borderColor: '#00e4ff',
borderWidth: 1,
textStyle: { color: '#fff', fontSize: 10 },
formatter: function(params) {
let result = params[0].axisValue + '<br/>';
params.forEach(param => {
const unit = getMetricUnit(param.seriesName);
result += `${param.seriesName}: ${param.value}${unit}<br/>`;
});
return result;
}
},
legend: {
show: true,
top: 5,
right: 15,
textStyle: { color: '#7ecfff', fontSize: 10 },
itemGap: 12,
formatter: function(name) {
return name;
}
},
grid: {
top: 35,
left: 35,
right: 20,
bottom: 25,
containLabel: true,
backgroundColor: 'transparent'
},
xAxis: {
type: 'category',
data: dates || [],
axisLabel: {
color: '#7ecfff',
fontSize: 10,
fontWeight: 'bold',
interval: 0,
rotate: dates?.length > 5 ? 45 : 0
},
axisLine: {
lineStyle: { color: 'rgba(0,228,255,0.4)', width: 2 }
},
axisTick: {
lineStyle: { color: 'rgba(0,228,255,0.3)' }
}
},
yAxis: {
type: 'value',
axisLabel: {
color: '#7ecfff',
fontSize: 10,
fontWeight: 'bold'
},
splitLine: {
lineStyle: {
color: 'rgba(0,228,255,0.15)',
type: 'dashed',
width: 1
}
},
axisLine: {
lineStyle: { color: 'rgba(0,228,255,0.4)', width: 2 }
}
},
series,
animation: true,
animationDuration: 1500,
animationEasing: 'cubicOut'
};
trendChart.setOption(trendOption, true);
// 专业交互事件
trendChart.on('click', function(params) {
console.log('🔍 健康数据点击:', params);
const metric = params.seriesName;
const value = params.value;
const date = params.name;
showHealthDetailModal(metric, value, date);
});
// 悬停高亮效果
trendChart.on('mouseover', function(params) {
if (params.componentType === 'series') {
trendChart.dispatchAction({
type: 'highlight',
seriesIndex: params.seriesIndex
});
}
});
// 响应式调整
const resizeChart = () => {
if (trendChart && !trendChart.isDisposed()) {
trendChart.resize();
}
};
window.removeEventListener('resize', resizeChart);
window.addEventListener('resize', resizeChart);
}
// 显示健康数据详情弹窗
function showHealthDetailModal(metric, value, date) {
const healthMetrics = {
'心率': { color: '#ff6b9d', icon: '💗' },
'血氧': { color: '#00e4ff', icon: '🫁' },
'体温': { color: '#ffaa00', icon: '🌡️' },
'压力': { color: '#ff7700', icon: '😰' },
'睡眠': { color: '#7ecfff', icon: '😴' }
};
const icon = healthMetrics[metric]?.icon || '📊';
const color = healthMetrics[metric]?.color || '#00e4ff';
const unit = getMetricUnit(metric);
console.log(`${icon} ${metric}: ${value}${unit} (${date})`);
}
// 获取指标单位
function getMetricUnit(metricName) {
const units = {
'心率': 'bpm',
'血氧': '%',
'体温': '°C',
'压力': '',
'睡眠': 'h'
};
return units[metricName] || '';
}
// 修改数据刷新函数
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);
loadHealthScoreChart(customerId);
loadStatisticsData();
//loadMessages(); // 加载消息数据
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(data);
// 更新人员管理面板
initPersonnelManagementPanel(data);
// 更新告警信息图表
initAlertChart(data);
// 更新设备管理图表
initDeviceChart(data);
// 更新消息列表
initMessageList(data);
// 为各个面板添加点击事件
setupPanelClickEvents(customerId, data);
}
})
.catch(error => console.error('Error fetching data:', error));
}
// 将面板点击事件设置抽取为单独的函数
function setupPanelClickEvents(customerId, data) {
// 1. 告警信息面板
const alertPanel = document.querySelector('.panel:has(#alertList)');
if (alertPanel) {
alertPanel.style.cursor = 'pointer';
alertPanel.onclick = function() {
createModalWindow(`/alert_view.html?customerId=${customerId}`, data.alert_info || null, 'alertInfo');
};
}
// 2. 设备管理面板
const devicePanel = document.querySelector('.panel:has(#statsChart)');
if (devicePanel) {
devicePanel.style.cursor = 'pointer';
devicePanel.onclick = function() {
createModalWindow(`/device_view.html?customerId=${customerId}`, data.device_info || null, 'deviceInfo');
};
}
// 3. 消息信息面板
const messagePanel = document.querySelector('.panel:has(#messageList)');
if (messagePanel) {
messagePanel.style.cursor = 'pointer';
messagePanel.onclick = function() {
createModalWindow(`/message_view.html?customerId=${customerId}`, data.message_info || null, 'messageInfo');
};
}
// 4. 趋势分析面板
const trendPanel = document.querySelector('.panel:has(#trendChart)');
if (trendPanel) {
trendPanel.style.cursor = 'pointer';
trendPanel.onclick = function() {
createModalWindow(`/health_main?customerId=${customerId}`, data.health_data || null, 'healthInfo');
};
}
// 5. 人员管理面板
const personnelPanel = document.querySelector('.panel:has(#departmentDistribution)');
if (personnelPanel) {
personnelPanel.style.cursor = 'pointer';
personnelPanel.onclick = function() {
createModalWindow(`/user_view.html?customerId=${customerId}`, data.user_info || null, 'userInfo');
};
}
// 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}`, data.health_data || null, 'healthInfo');
};
}
}
// 修改初始化调用
document.addEventListener('DOMContentLoaded', () => {
// 初始化筛选面板功能
document.getElementById('filterToggle').addEventListener('click',toggleFilter);
initSearchFilter();
setTimeout(() => {
globalCharts = initCharts(); // 存储图表实例
// 🗺️ 地图初始化 - 修复地图未初始化问题
console.log('🗺️ 开始初始化地图...');
try {
initializeMap('{{ customerId }}', '-1');
console.log('✅ 地图初始化完成');
} catch (error) {
console.error('❌ 地图初始化失败:', error);
}
// 等待地图初始化完成后再刷新数据
setTimeout(() => {
refreshData(); // 初始加载数据
}, 500); // 给地图初始化留500ms时间
// 修改定时器
// 每分钟刷新一次数据
setInterval(refreshData, 60000);
}, 100);
});
// 添加通用的创建模态窗口函数
function createModalWindow(url, data, dataType) {
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);
// 获取iframe引用
const iframe = modalContainer.querySelector('iframe');
// 当iframe加载完成后传递数据
iframe.onload = function() {
if (data) {
try {
iframe.contentWindow.postMessage({
type: dataType,
data: data
}, '*');
} catch (e) {
console.warn('无法传递数据到iframe:', e);
}
}
};
// 添加样式
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: 12px;
}
.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: 18px;
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: 8px; padding: 6px 10px; background: rgba(0,228,255,0.1); border-radius: 6px; border-left: 4px solid #00e4ff;">
<div style="display: flex; align-items: center; gap: 12px;">
<div class="alert-stat-item">
<span style="color: #ff4444; font-size: 18px; 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: 16px; 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: 14px; 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);
let displayTypes = hasTypeData ? alertTypes : ['heart_rate', 'blood_pressure', 'temperature'];
let displayValues = hasTypeData ? alertValues : [0, 0, 0];
// 限制显示数量,防止图表变形,由于高度增加可以显示更多类型
const MAX_DISPLAY_TYPES = 8; // 从6增加到8种类型,充分利用增加的高度空间
if (displayTypes.length > MAX_DISPLAY_TYPES) {
// 按数值排序,取前8个
const sortedData = displayTypes.map((type, index) => ({
type: type,
value: displayValues[index]
})).sort((a, b) => b.value - a.value);
displayTypes = sortedData.slice(0, MAX_DISPLAY_TYPES).map(item => item.type);
displayValues = sortedData.slice(0, MAX_DISPLAY_TYPES).map(item => item.value);
// 如果有更多类型,将剩余的合并为"其他"
if (sortedData.length > MAX_DISPLAY_TYPES) {
const otherValue = sortedData.slice(MAX_DISPLAY_TYPES).reduce((sum, item) => sum + item.value, 0);
displayTypes.push('others');
displayValues.push(otherValue);
}
}
const typeColors = {
'temperature': '#ffd700',
'stress': '#ff8800',
'heart_rate': '#00e4ff',
'blood_pressure': '#ffbb00',
'blood_oxygen': '#ff6666',
'others': '#888888'
};
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;
const typeName = data.name === 'others' ? '其他类型' : translateAlertType(data.name);
return `${typeName}<br/>告警: ${data.value}次 (${percent}%)`;
}
},
grid: {
top: 25,
left: 50,
right: 15,
bottom: 15,
containLabel: true
},
xAxis: {
type: 'value',
axisLabel: { color: '#7ecfff', fontSize: 9 },
splitLine: { lineStyle: { color: 'rgba(126,207,255,0.1)' } },
axisLine: { show: false },
max: Math.max(...displayValues, 1)
},
yAxis: {
type: 'category',
data: displayTypes.map(t => t === 'others' ? '其他' : translateAlertType(t)),
axisLabel: {
color: '#fff',
fontSize: 9,
interval: 0, // 强制显示所有标签
formatter: function(value) {
return value.length > 5 ? value.substring(0, 5) + '...' : value; /* 从4增加到5,允许显示更多字符 */
}
},
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: displayTypes.length > 6 ? '40%' : displayTypes.length > 4 ? '50%' : '65%', // 动态调整条形宽度,适应更多类型
label: {
show: true,
position: 'right',
color: '#fff',
fontSize: 9,
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 alertTrendChart = 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
}
}]
};
alertTrendChart.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();
alertTrendChart && alertTrendChart.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();
alertTrendChart.resize();
console.log('告警图表初始化完成');
} catch (e) {
console.warn('告警图表初始化失败:', e);
}
}, 200);
return { typeChart, levelChart, statusChart, alertTrendChart };
}
// 显示详细告警列表
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;
// 检查是否已存在图表实例,如果存在则复用
let statsChart;
if (globalCharts && globalCharts.stats) {
statsChart = globalCharts.stats;
} else {
if (globalCharts) {
globalCharts.stats = statsChart;
}
}
const deviceInfo = data.device_info || {};
const totalDevices = deviceInfo.totalDevices || 0;
document.getElementById('totalWatchDevices').textContent = totalDevices;
// 计算各项统计数据
const activeDevices = deviceInfo.deviceStatusCount?.ACTIVE || 0;
const chargingDevices = deviceInfo.deviceChargingCount?.CHARGING || 0;
const faultDevices = deviceInfo.deviceStatusCount?.FAULT || 0;
const offlineDevices = deviceInfo.deviceStatusCount?.INACTIVE || 0;
// 计算在线率
const onlineRate = totalDevices > 0 ? ((activeDevices / totalDevices) * 100).toFixed(1) : 0;
// 只在首次创建时添加点击事件监听器
if (!statsContainer.hasClickListener) {
statsContainer.onclick = () => {
const legendData = {
'部门分布': deviceInfo.departmentDeviceCount || {},
'充电状态': deviceInfo.deviceChargingCount || {},
'设备状态': deviceInfo.deviceStatusCount || {},
'系统版本': deviceInfo.deviceSystemVersionCount || {},
'佩戴状态': deviceInfo.deviceWearableCount || {}
};
showFullLegend(legendData);
};
statsContainer.hasClickListener = true;
}
// 重新构建面板内容,删除中间卡片,只保留顶部总览和底部图表
if (!statsContainer.querySelector('.device-overview-header')) {
statsContainer.innerHTML = `
<div style="position: relative; height: 100%; padding: 8px;">
<!-- 顶部数据总览条 -->
<div class="device-overview-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; padding: 8px 15px; background: rgba(0,228,255,0.1); border-radius: 8px; border-left: 4px solid #00e4ff;">
<div style="display: flex; align-items: center; gap: 20px;">
<div class="overview-item">
<span style="color: #00e4ff; font-size: 24px; font-weight: bold;">${totalDevices}</span>
<span style="color: #fff; font-size: 14px; margin-left: 6px;">设备总数</span>
</div>
<div class="overview-item">
<span style="color: #52c41a; font-size: 20px; font-weight: bold;">${activeDevices}</span>
<span style="color: #fff; font-size: 14px; margin-left: 6px;">在线设备</span>
</div>
<div class="overview-item">
<span style="color: #faad14; font-size: 20px; font-weight: bold;">${onlineRate}%</span>
<span style="color: #fff; font-size: 14px; margin-left: 6px;">在线率</span>
</div>
</div>
<div style="color: #7ecfff; font-size: 12px; cursor: pointer;">详情 →</div>
</div>
<!-- 底部大图表区域 -->
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; height: calc(100% - 60px);">
<div id="deviceDeptChart" style="background: rgba(0,21,41,0.4); border: 1px solid rgba(0,228,255,0.3); border-radius: 8px; position: relative;">
<div style="position: absolute; top: 8px; left: 12px; color: #00e4ff; font-size: 14px; font-weight: bold; z-index: 10;">部门设备分布</div>
</div>
<div id="deviceStatusChart" style="background: rgba(0,21,41,0.4); border: 1px solid rgba(0,228,255,0.3); border-radius: 8px; position: relative;">
<div style="position: absolute; top: 8px; left: 12px; color: #00e4ff; font-size: 14px; font-weight: bold; z-index: 10;">设备状态统计</div>
</div>
</div>
</div>
`;
}
// 延时初始化图表
setTimeout(() => {
// 部门设备分布饼图(左侧大图表)
const deptChart = echarts.init(document.getElementById('deviceDeptChart'));
const deptData = Object.entries(deviceInfo.departmentDeviceCount || {}).map(([name, count]) => ({
name: name.length > 8 ? name.substring(0, 8) + '...' : name,
value: count,
itemStyle: { color: `hsl(${Math.random() * 360}, 60%, 50%)` }
}));
deptChart.setOption({
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(0,21,41,0.95)',
borderColor: '#00e4ff',
textStyle: { color: '#fff', fontSize: 12 },
formatter: '{b}: {c}台 ({d}%)'
},
legend: {
orient: 'vertical',
left: 'left',
top: 'center',
textStyle: { color: '#fff', fontSize: 10 },
itemWidth: 12,
itemHeight: 8
},
series: [{
type: 'pie',
radius: ['30%', '70%'],
center: ['70%', '50%'],
data: deptData.length > 0 ? deptData : [{ name: '暂无数据', value: 1, itemStyle: { color: '#666' } }],
label: { show: false },
labelLine: { show: false },
emphasis: {
itemStyle: {
shadowBlur: 15,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 228, 255, 0.6)'
}
}
}]
});
// 设备状态统计柱状图(右侧大图表)
const statusChart = echarts.init(document.getElementById('deviceStatusChart'));
const statusCategories = ['在线', '离线', '充电', '故障', '正常'];
const statusValues = [
activeDevices,
offlineDevices,
chargingDevices,
faultDevices,
totalDevices - faultDevices - offlineDevices
];
const statusColors = ['#52c41a', '#f5222d', '#1890ff', '#fa8c16', '#00e4ff'];
statusChart.setOption({
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(0,21,41,0.95)',
borderColor: '#00e4ff',
textStyle: { color: '#fff', fontSize: 12 },
formatter: function(params) {
return params[0].name + ': ' + params[0].value + '台';
}
},
grid: { top: 35, left: 40, right: 20, bottom: 30 },
xAxis: {
type: 'category',
data: statusCategories,
axisLabel: { color: '#7ecfff', fontSize: 10, rotate: 0 },
axisLine: { lineStyle: { color: 'rgba(126,207,255,0.3)' } },
axisTick: { show: false }
},
yAxis: {
type: 'value',
axisLabel: { color: '#7ecfff', fontSize: 10 },
splitLine: { lineStyle: { color: 'rgba(126,207,255,0.1)' } },
axisLine: { show: false }
},
series: [{
type: 'bar',
data: statusValues.map((value, index) => ({
value: value,
itemStyle: {
color: statusColors[index],
borderRadius: [4, 4, 0, 0]
}
})),
barWidth: '50%',
label: {
show: true,
position: 'top',
color: '#fff',
fontSize: 11,
formatter: '{c}'
}
}]
});
if (globalCharts) {
globalCharts.stats = { deptChart, statusChart };
}
}, 200);
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="{{ url_for('static', filename='libs/jquery.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>';
// 递归添加部门选项并收集所有用户数据
const allDeptUsers = [];
async function addDepartmentOptions(departments, level = 0) {
for (const dept of departments) {
const option = document.createElement('option');
option.value = dept.id;
const indent = ' '.repeat(level);
option.textContent = indent + dept.name;
deptSelect.appendChild(option);
// 获取该部门的用户数据并添加到全局数组
try {
const response = await fetch(`/fetch_users?orgId=${dept.id}`);
const users = await response.json();
if (users) {
users.forEach(user => {
allDeptUsers.push({
...user,
dept_name: dept.name,
dept_id: dept.id,
user_id: user.id
});
});
}
} catch (e) {
console.error('获取部门用户失败:', e);
}
if (dept.children && dept.children.length > 0) {
await addDepartmentOptions(dept.children, level + 1);
}
}
}
// 异步处理并设置用户数据
addDepartmentOptions(departments).then(() => {
setUsersData(allDeptUsers);
});
deptSelect.style.fontFamily = 'monospace';
}
// 更新用户选择框
async function updateUsers(deptId) {
if (deptId) {
// 如果选择了部门,过滤该部门的用户
filteredUsers = allUsers.filter(u => u.dept_id == deptId);
} else {
// 如果没有选择部门,显示所有用户
filteredUsers = allUsers;
}
updateUserSelect();
// 清空搜索框
const searchInput = document.getElementById('searchInput');
if (searchInput) searchInput.value = '';
}
// 添加样式
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;
}
`;
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);
}
// 面板显示状态管理,防止重复面板
let panelDisplaying = false;
function showCustomMapInfo(f){
// 防抖:如果正在显示面板,直接返回
if(panelDisplaying) {
console.log('面板正在显示中,跳过重复调用');
return;
}
panelDisplaying = true; // 标记面板正在显示
// 移除所有可能存在的面板,避免重复显示
removeCustomMapInfo(); // 移除地图信息面板
const existingHealthModal = document.querySelector('.health-modal-overlay');
if(existingHealthModal) {
existingHealthModal.remove(); // 移除健康详情面板
}
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="
background: linear-gradient(135deg, rgba(15,25,45,0.95) 0%, rgba(25,35,65,0.98) 50%, rgba(15,25,45,0.95) 100%);
border-radius: 20px;
border: 2px solid rgba(255,68,68,0.5);
box-shadow: 0 20px 60px rgba(255,68,68,0.3), 0 0 30px rgba(255,68,68,0.2), inset 0 1px 0 rgba(255,255,255,0.1);
padding: 24px;
color: #fff;
position: relative;
overflow: hidden;
animation: alertPulse 2s infinite, slideIn 0.5s ease-out;
min-width: 380px;
">
<!-- 背景动态效果 -->
<div style="
position: absolute; top: 0; left: -100%; width: 100%; height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,68,68,0.1), transparent);
animation: scanLine 3s infinite linear;
"></div>
<!-- 告警级别指示器 -->
<div style="
position: absolute; top: -1px; left: -1px; right: -1px; height: 4px;
background: linear-gradient(90deg, #ff4444, #ff6b6b, #ff4444);
border-radius: 20px 20px 0 0;
animation: levelGlow 1.5s infinite alternate;
"></div>
<!-- 头部信息 -->
<div style="display:flex;align-items:center;gap:20px;margin-bottom:20px;position:relative;z-index:2;">
<div style="position:relative;">
<img src="${avatarUrl}" style="
width:64px;height:64px;border-radius:50%;
border:3px solid #ff4444;
box-shadow:0 0 20px rgba(255,68,68,0.6), inset 0 0 10px rgba(255,68,68,0.2);
object-fit:cover;background:#001529;
animation: avatarGlow 2s infinite alternate;
">
<!-- 告警状态指示器 -->
<div style="
position:absolute;top:-2px;right:-2px;
width:20px;height:20px;border-radius:50%;
background: radial-gradient(circle, #ff4444 30%, transparent 70%);
border:2px solid #fff;
animation: alertBlink 1s infinite;
"></div>
</div>
<div style="flex:1;">
<div style="
font-size:20px;font-weight:700;letter-spacing:1.2px;
color:#fff;text-shadow:0 0 10px rgba(255,255,255,0.5);
margin-bottom:4px;
">${get('dept_name','deptName')}</div>
<div style="
font-size:18px;color:#00e4ff;font-weight:600;
text-shadow:0 0 8px rgba(0,228,255,0.6);
">${get('user_name','userName')}</div>
</div>
<!-- 告警图标 -->
<div style="
width:48px;height:48px;border-radius:12px;
background:linear-gradient(135deg, rgba(255,68,68,0.3) 0%, rgba(220,38,38,0.5) 100%);
display:flex;align-items:center;justify-content:center;
font-size:24px;
box-shadow:0 4px 15px rgba(255,68,68,0.4);
animation: iconPulse 1.5s infinite ease-in-out;
">⚠️</div>
</div>
<!-- 告警详情卡片组 -->
<div style="
display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));
gap:16px;margin-bottom:20px;
">
<!-- 告警类别卡片 -->
<div style="
background:rgba(0,21,41,0.6);border-radius:12px;padding:16px;
border:1px solid rgba(255,68,68,0.3);
transition:all 0.3s ease;
" onmouseover="this.style.transform='translateY(-4px)';this.style.boxShadow='0 8px 25px rgba(255,68,68,0.4)';"
onmouseout="this.style.transform='translateY(0)';this.style.boxShadow='none';">
<div style="color:#7ecfff;font-size:12px;margin-bottom:8px;text-transform:uppercase;letter-spacing:1px;">告警类别</div>
<div style="
color:${getAlertTypeColor(get('alert_type','alertType','-'))};
font-weight:700;font-size:16px;
text-shadow:0 0 8px currentColor;
">${translateAlertType(get('alert_type','alertType','-'))}</div>
</div>
<!-- 告警级别卡片 -->
<div style="
background:rgba(0,21,41,0.6);border-radius:12px;padding:16px;
border:1px solid rgba(255,187,0,0.3);
transition:all 0.3s ease;
" onmouseover="this.style.transform='translateY(-4px)';this.style.boxShadow='0 8px 25px rgba(255,187,0,0.4)';"
onmouseout="this.style.transform='translateY(0)';this.style.boxShadow='none';">
<div style="color:#7ecfff;font-size:12px;margin-bottom:8px;text-transform:uppercase;letter-spacing:1px;">告警级别</div>
<div style="
color:${getAlertSeverityColor(level||'-')};
font-weight:700;font-size:16px;
text-shadow:0 0 8px currentColor;
">${translateAlertSeverity(level||'-')}</div>
</div>
<!-- 告警状态卡片 -->
<div style="
background:rgba(0,21,41,0.6);border-radius:12px;padding:16px;
border:1px solid rgba(0,228,255,0.3);
transition:all 0.3s ease;
" onmouseover="this.style.transform='translateY(-4px)';this.style.boxShadow='0 8px 25px rgba(0,228,255,0.4)';"
onmouseout="this.style.transform='translateY(0)';this.style.boxShadow='none';">
<div style="color:#7ecfff;font-size:12px;margin-bottom:8px;text-transform:uppercase;letter-spacing:1px;">处理状态</div>
<div style="
color:${getAlertStatusColor(get('alert_status','status','-'))};
font-weight:700;font-size:16px;
text-shadow:0 0 8px currentColor;
">${translateAlertStatus(get('alert_status','status','-'))}</div>
</div>
</div>
<!-- 健康信息链接 -->
<div style="
background:linear-gradient(135deg, rgba(0,228,255,0.1) 0%, rgba(0,180,255,0.2) 100%);
border-radius:12px;padding:16px;margin-bottom:16px;
border:1px solid rgba(0,228,255,0.3);
position:relative;overflow:hidden;
">
<div style="color:#7ecfff;font-size:12px;margin-bottom:8px;text-transform:uppercase;letter-spacing:1px;">健康数据详情</div>
<a href="javascript:void(0)" onclick="showHealthProfile('${get('health_id','healthId')}')" style="
color:#00e4ff;text-decoration:none;font-family:monospace;font-size:16px;font-weight:600;
display:inline-flex;align-items:center;gap:8px;
padding:8px 16px;border-radius:8px;
background:rgba(0,228,255,0.1);border:1px solid rgba(0,228,255,0.3);
transition:all 0.3s ease;
text-shadow:0 0 10px rgba(0,228,255,0.5);
" onmouseover="this.style.background='rgba(0,228,255,0.2)';this.style.transform='scale(1.05)';"
onmouseout="this.style.background='rgba(0,228,255,0.1)';this.style.transform='scale(1)';">
<span>📊</span>${get('health_id','healthId')}
</a>
</div>
<!-- 位置和时间信息 -->
<div style="
display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:20px;
">
<div style="
background:rgba(0,21,41,0.4);border-radius:10px;padding:12px;
border:1px solid rgba(0,228,255,0.2);
">
<div style="color:#7ecfff;font-size:11px;margin-bottom:6px;text-transform:uppercase;">位置信息</div>
<div style="color:#fff;font-size:13px;" id="locationInfo">🌍 正在获取...</div>
</div>
<div style="
background:rgba(0,21,41,0.4);border-radius:10px;padding:12px;
border:1px solid rgba(0,228,255,0.2);
">
<div style="color:#7ecfff;font-size:11px;margin-bottom:6px;text-transform:uppercase;">告警时间</div>
<div style="color:#fff;font-size:13px;">⏰ ${get('alert_timestamp','timestamp','-')}</div>
</div>
</div>
<!-- 操作按钮区 -->
<div style="display:flex;gap:16px;align-items:center;position:relative;z-index:2;">
<button onclick="handleAlert('${get('alert_id','alertId')}')" style="
padding:12px 24px;
background:linear-gradient(135deg, ${levelColor} 0%, ${levelColor}dd 100%);
color:#001529;border:none;border-radius:10px;cursor:pointer;
font-weight:700;font-size:14px;
box-shadow:0 4px 15px ${levelColor}66, inset 0 1px 0 rgba(255,255,255,0.2);
transition:all 0.3s ease;
text-transform:uppercase;letter-spacing:1px;
" onmouseover="this.style.transform='translateY(-2px) scale(1.05)';this.style.boxShadow='0 8px 25px ${levelColor}88';"
onmouseout="this.style.transform='translateY(0) scale(1)';this.style.boxShadow='0 4px 15px ${levelColor}66';">
🚀 一键处理
</button>
<div style="flex:1;"></div>
<button onclick="removeCustomMapInfo()" style="
width:44px;height:44px;border-radius:50%;
background:rgba(0,228,255,0.1);border:2px solid rgba(0,228,255,0.3);
color:#00e4ff;cursor:pointer;display:flex;align-items:center;justify-content:center;
font-size:20px;font-weight:700;
transition:all 0.3s ease;
backdrop-filter:blur(10px);
" onmouseover="this.style.background='rgba(0,228,255,0.2)';this.style.transform='scale(1.1) rotate(90deg)';this.style.boxShadow='0 0 20px rgba(0,228,255,0.6)';"
onmouseout="this.style.background='rgba(0,228,255,0.1)';this.style.transform='scale(1) rotate(0deg)';this.style.boxShadow='none';">
✕
</button>
</div>
</div>
`;
document.body.appendChild(div);
} else {
// 健康点内容
// 健康点内容 - 科技健康风格
// 健康点内容 - 科技健康风格
div.innerHTML=`
<div style="
background: linear-gradient(135deg, rgba(10,24,48,0.95) 0%, rgba(15,35,65,0.98) 50%, rgba(10,24,48,0.95) 100%);
border-radius: 24px;
border: 2px solid rgba(0,228,255,0.4);
box-shadow: 0 25px 80px rgba(0,228,255,0.3), 0 0 40px rgba(0,228,255,0.2), inset 0 1px 0 rgba(255,255,255,0.1);
padding: 28px;
color: #fff;
position: relative;
overflow: hidden;
animation: healthGlow 3s infinite ease-in-out, slideIn 0.6s ease-out;
min-width: 420px;
">
<!-- 背景科技纹理 -->
<div style="
position: absolute; top: 0; left: 0; right: 0; bottom: 0;
background: radial-gradient(circle at 20% 20%, rgba(0,228,255,0.1) 0%, transparent 50%),
radial-gradient(circle at 80% 80%, rgba(0,180,255,0.1) 0%, transparent 50%);
animation: bgShift 6s infinite ease-in-out;
"></div>
<!-- 健康状态顶部指示条 -->
<div style="
position: absolute; top: -1px; left: -1px; right: -1px; height: 4px;
background: linear-gradient(90deg, #00ff88, #00e4ff, #00ff88);
border-radius: 24px 24px 0 0;
animation: healthPulse 2s infinite alternate;
"></div>
<!-- 头部用户信息 -->
<div style="display:flex;align-items:center;gap:20px;margin-bottom:24px;position:relative;z-index:2;">
<div style="position:relative;">
<img src="${avatarUrl}" style="
width:72px;height:72px;border-radius:50%;
border:3px solid #00e4ff;
box-shadow:0 0 25px rgba(0,228,255,0.6), inset 0 0 15px rgba(0,228,255,0.2);
object-fit:cover;background:#001529;
animation: healthAvatarGlow 3s infinite ease-in-out;
">
<!-- 健康状态指示器 -->
<div style="
position:absolute;bottom:-2px;right:-2px;
width:24px;height:24px;border-radius:50%;
background: radial-gradient(circle, #00ff88 40%, transparent 70%);
border:3px solid #fff;
animation: healthBeat 1.5s infinite ease-in-out;
">💚</div>
</div>
<div style="flex:1;">
<div style="
font-size:22px;font-weight:700;letter-spacing:1.2px;
color:#fff;text-shadow:0 0 15px rgba(255,255,255,0.5);
margin-bottom:6px;
">${get('dept_name','deptName')}</div>
<div style="
font-size:18px;color:#00e4ff;font-weight:600;
text-shadow:0 0 12px rgba(0,228,255,0.6);
">${get('user_name','userName')}</div>
</div>
<!-- 健康图标 -->
<div style="
width:56px;height:56px;border-radius:16px;
background:linear-gradient(135deg, rgba(0,255,136,0.3) 0%, rgba(0,228,255,0.5) 100%);
display:flex;align-items:center;justify-content:center;
font-size:28px;
box-shadow:0 6px 20px rgba(0,255,136,0.4);
animation: healthIconFloat 2s infinite ease-in-out;
">🏥</div>
</div>
<!-- 健康指标网格 -->
<div style="
display:grid;grid-template-columns:repeat(2,1fr);
gap:16px;margin-bottom:20px;
">
<!-- 心率血压卡片 -->
<div style="
background:rgba(0,21,41,0.6);border-radius:16px;padding:18px;
border:1px solid rgba(255,107,107,0.3);
transition:all 0.4s ease;position:relative;overflow:hidden;
" onmouseover="this.style.transform='translateY(-6px)';this.style.boxShadow='0 12px 35px rgba(255,107,107,0.4)';"
onmouseout="this.style.transform='translateY(0)';this.style.boxShadow='none';">
<div style="
display:flex;align-items:center;gap:12px;margin-bottom:12px;
">
<div style="
width:40px;height:40px;border-radius:10px;
background:linear-gradient(135deg, rgba(255,107,107,0.3) 0%, rgba(238,90,36,0.5) 100%);
display:flex;align-items:center;justify-content:center;font-size:20px;
">❤️</div>
<div>
<div style="color:#7ecfff;font-size:11px;text-transform:uppercase;letter-spacing:1px;">生命体征</div>
<div style="color:#fff;font-size:14px;font-weight:600;">心率 & 血压</div>
</div>
</div>
<div style="
display:flex;justify-content:space-between;align-items:center;
">
<div>
<div style="color:#ff6b6b;font-size:18px;font-weight:700;">${get('heartRate','heart_rate')} <span style="font-size:12px;color:#888;">bpm</span></div>
<div style="color:#7ecfff;font-size:14px;">${get('pressureHigh','pressure_high')}/${get('pressureLow','pressure_low')} mmHg</div>
</div>
<!-- 心率可视化 -->
<div style="
width:50px;height:30px;
background:linear-gradient(90deg, transparent, rgba(255,107,107,0.3), transparent);
border-radius:4px;position:relative;overflow:hidden;
">
<div style="
width:4px;height:100%;background:#ff6b6b;
animation: heartbeatLine 1.5s infinite ease-in-out;
box-shadow:0 0 10px #ff6b6b;
"></div>
</div>
</div>
</div>
<!-- 血氧体温卡片 -->
<div style="
background:rgba(0,21,41,0.6);border-radius:16px;padding:18px;
border:1px solid rgba(0,255,136,0.3);
transition:all 0.4s ease;position:relative;overflow:hidden;
" onmouseover="this.style.transform='translateY(-6px)';this.style.boxShadow='0 12px 35px rgba(0,255,136,0.4)';"
onmouseout="this.style.transform='translateY(0)';this.style.boxShadow='none';">
<div style="
display:flex;align-items:center;gap:12px;margin-bottom:12px;
">
<div style="
width:40px;height:40px;border-radius:10px;
background:linear-gradient(135deg, rgba(0,255,136,0.3) 0%, rgba(0,204,102,0.5) 100%);
display:flex;align-items:center;justify-content:center;font-size:20px;
">🫁</div>
<div>
<div style="color:#7ecfff;font-size:11px;text-transform:uppercase;letter-spacing:1px;">呼吸体温</div>
<div style="color:#fff;font-size:14px;font-weight:600;">血氧 & 体温</div>
</div>
</div>
<div style="
display:flex;justify-content:space-between;align-items:center;
">
<div>
<div style="color:#00ff88;font-size:18px;font-weight:700;">${get('bloodOxygen','blood_oxygen')} <span style="font-size:12px;color:#888;">%</span></div>
<div style="color:#7ecfff;font-size:14px;">${get('temperature','temp')} ℃</div>
</div>
<!-- 血氧环形进度 -->
<div style="position:relative;width:40px;height:40px;">
<svg width="40" height="40" style="transform:rotate(-90deg);">
<circle cx="20" cy="20" r="16" stroke="rgba(0,255,136,0.2)" stroke-width="3" fill="none"/>
<circle cx="20" cy="20" r="16" stroke="#00ff88" stroke-width="3" fill="none"
stroke-dasharray="100.48" stroke-dashoffset="${100.48 * (1 - (get('bloodOxygen','blood_oxygen')||95)/100)}"
style="transition:stroke-dashoffset 1s ease;"/>
</svg>
<div style="
position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
font-size:10px;font-weight:700;color:#00ff88;
">${get('bloodOxygen','blood_oxygen')||'-'}</div>
</div>
</div>
</div>
</div>
<!-- 运动数据横条 -->
<div style="
background:linear-gradient(135deg, rgba(138,43,226,0.2) 0%, rgba(75,0,130,0.3) 100%);
border-radius:16px;padding:20px;margin-bottom:20px;
border:1px solid rgba(138,43,226,0.4);
position:relative;overflow:hidden;
">
<div style="
display:flex;align-items:center;gap:16px;margin-bottom:16px;
">
<div style="
width:48px;height:48px;border-radius:12px;
background:linear-gradient(135deg, rgba(138,43,226,0.4) 0%, rgba(75,0,130,0.6) 100%);
display:flex;align-items:center;justify-content:center;font-size:24px;
">🏃</div>
<div>
<div style="color:#9d4edd;font-size:18px;font-weight:700;">运动数据统计</div>
<div style="color:#7ecfff;font-size:12px;">Activity & Fitness Metrics</div>
</div>
</div>
<div style="
display:grid;grid-template-columns:repeat(4,1fr);gap:16px;
">
<div style="text-align:center;padding:12px;border-radius:10px;background:rgba(0,0,0,0.3);">
<div style="color:#9d4edd;font-size:20px;font-weight:700;">${get('step','steps')||'-'}</div>
<div style="color:#7ecfff;font-size:10px;text-transform:uppercase;">步数</div>
</div>
<div style="text-align:center;padding:12px;border-radius:10px;background:rgba(0,0,0,0.3);">
<div style="color:#9d4edd;font-size:20px;font-weight:700;">${get('distance','distance')||'-'}</div>
<div style="color:#7ecfff;font-size:10px;text-transform:uppercase;">距离(米)</div>
</div>
<div style="text-align:center;padding:12px;border-radius:10px;background:rgba(0,0,0,0.3);">
<div style="color:#9d4edd;font-size:20px;font-weight:700;">${get('calorie','calories')||'-'}</div>
<div style="color:#7ecfff;font-size:10px;text-transform:uppercase;">卡路里</div>
</div>
<div style="text-align:center;padding:12px;border-radius:10px;background:rgba(0,0,0,0.3);">
<div style="color:#9d4edd;font-size:20px;font-weight:700;">${get('stress','pressure')||'-'}</div>
<div style="color:#7ecfff;font-size:10px;text-transform:uppercase;">压力值</div>
</div>
</div>
</div>
<!-- 位置和时间信息 -->
<div style="
display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:20px;
">
<div style="
background:rgba(0,21,41,0.5);border-radius:12px;padding:16px;
border:1px solid rgba(0,228,255,0.3);
">
<div style="color:#7ecfff;font-size:12px;margin-bottom:8px;text-transform:uppercase;letter-spacing:1px;">位置信息</div>
<div style="color:#fff;font-size:14px;display:flex;align-items:center;gap:8px;" id="locationInfo">
<span>🌍</span><span>正在获取位置...</span>
</div>
</div>
<div style="
background:rgba(0,21,41,0.5);border-radius:12px;padding:16px;
border:1px solid rgba(0,228,255,0.3);
">
<div style="color:#7ecfff;font-size:12px;margin-bottom:8px;text-transform:uppercase;letter-spacing:1px;">采集时间</div>
<div style="color:#fff;font-size:14px;display:flex;align-items:center;gap:8px;">
<span>⏰</span><span>${get('timestamp')}</span>
</div>
</div>
</div>
<!-- 关闭按钮 -->
<div style="display:flex;justify-content:flex-end;position:relative;z-index:2;">
<button onclick="removeCustomMapInfo()" style="
width:48px;height:48px;border-radius:50%;
background:rgba(0,228,255,0.1);border:2px solid rgba(0,228,255,0.3);
color:#00e4ff;cursor:pointer;display:flex;align-items:center;justify-content:center;
font-size:22px;font-weight:700;
transition:all 0.3s ease;
backdrop-filter:blur(15px);
" onmouseover="this.style.background='rgba(0,228,255,0.2)';this.style.transform='scale(1.15) rotate(90deg)';this.style.boxShadow='0 0 25px rgba(0,228,255,0.7)';"
onmouseout="this.style.background='rgba(0,228,255,0.1)';this.style.transform='scale(1) rotate(0deg)';this.style.boxShadow='none';">
✕
</button>
</div>
</div>
`;
document.body.appendChild(div);
// 延迟重置防抖标志,给面板渲染留出时间
setTimeout(() => {
panelDisplaying = false;
console.log('地图面板渲染完成,重置显示状态');
}, 300);
// 获取位置信息
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){
// 防抖:如果正在显示面板,直接返回
if(panelDisplaying) {
console.log('面板正在显示中,跳过重复调用');
return;
}
panelDisplaying = true; // 标记面板正在显示
// 移除所有可能存在的面板,避止重复显示
removeCustomMapInfo(); // 移除地图信息面板
const existingHealthModal = document.querySelector('.health-modal-overlay');
if(existingHealthModal) {
existingHealthModal.remove(); // 移除已存在的健康详情面板
}
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 heartRate = parseInt(g('heartRate','heart_rate')) || 0;
const bloodOxygen = parseInt(g('bloodOxygen','blood_oxygen')) || 0;
const temperature = parseFloat(g('temperature','temp')) || 0;
const steps = parseInt(g('step','steps')) || 0;
const getHealthScore = () => {
let score = 0;
if(heartRate >= 60 && heartRate <= 100) score += 25;
if(bloodOxygen >= 95) score += 25;
if(temperature >= 36.1 && temperature <= 37.2) score += 25;
if(steps >= 8000) score += 25;
return Math.min(score, 100);
};
const healthScore = getHealthScore();
const getScoreColor = (score) => {
if(score >= 80) return '#00ff88';
if(score >= 60) return '#ffbb00';
return '#ff4444';
};
const html=`
<div class="health-profile-header">
<div class="health-icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none">
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" fill="url(#heartGradient)"/>
<defs>
<linearGradient id="heartGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#ff6b6b;stop-opacity:1" />
<stop offset="100%" style="stop-color:#ee5a24;stop-opacity:1" />
</linearGradient>
</defs>
</svg>
</div>
<div class="health-title">
<div class="title-main">智能健康监测</div>
<div class="title-sub">Health Profile Analysis</div>
</div>
<div class="health-score-circle">
<svg width="60" height="60" class="score-ring">
<circle cx="30" cy="30" r="25" stroke="rgba(0,228,255,0.2)" stroke-width="3" fill="none"/>
<circle cx="30" cy="30" r="25" stroke="${getScoreColor(healthScore)}" stroke-width="3" fill="none"
stroke-dasharray="${2 * Math.PI * 25}" stroke-dashoffset="${2 * Math.PI * 25 * (1 - healthScore/100)}"
transform="rotate(-90 30 30)" class="score-progress"/>
</svg>
<div class="score-text">${healthScore}</div>
</div>
</div>
<div class="health-metrics-grid">
<div class="metric-card vital">
<div class="metric-icon">
<svg width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-width="2" d="M12 8V4m0 4V2m0 6v4m0 0v6m0-6h8m-8 0H4"/>
</svg>
</div>
<div class="metric-info">
<div class="metric-label">心率监测</div>
<div class="metric-value ${heartRate>=60&&heartRate<=100?'normal':'abnormal'}">${g('heartRate','heart_rate')||'-'} <span class="unit">bpm</span></div>
<div class="metric-bar">
<div class="bar-fill heart-rate" style="width:${Math.min((heartRate/120)*100,100)}%"></div>
</div>
</div>
</div>
<div class="metric-card pressure">
<div class="metric-icon">
<svg width="24" height="24" fill="none" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
<path stroke="currentColor" stroke-width="2" d="M12 8v8m-4-4h8"/>
</svg>
</div>
<div class="metric-info">
<div class="metric-label">血压监测</div>
<div class="metric-value">${(g('pressureHigh','pressure_high')||'-')+'/'+(g('pressureLow','pressure_low')||'-')} <span class="unit">mmHg</span></div>
<div class="metric-status ${parseInt(g('pressureHigh'))<140&&parseInt(g('pressureLow'))<90?'normal':'warning'}">
${parseInt(g('pressureHigh'))<140&&parseInt(g('pressureLow'))<90?'正常范围':'需要关注'}
</div>
</div>
</div>
<div class="metric-card oxygen">
<div class="metric-icon">
<svg width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-width="2" d="M12 2L2 12l10 10 10-10L12 2z"/>
<path stroke="currentColor" stroke-width="2" d="M12 8v8"/>
</svg>
</div>
<div class="metric-info">
<div class="metric-label">血氧饱和度</div>
<div class="metric-value ${bloodOxygen>=95?'normal':'abnormal'}">${g('bloodOxygen','blood_oxygen')||'-'} <span class="unit">%</span></div>
<div class="metric-bar">
<div class="bar-fill oxygen" style="width:${bloodOxygen}%"></div>
</div>
</div>
</div>
<div class="metric-card temperature">
<div class="metric-icon">
<svg width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-width="2" d="M14 3v7a3 3 0 1 1-6 0V3a3 3 0 1 1 6 0z"/>
<path stroke="currentColor" stroke-width="2" d="M11 14h2"/>
</svg>
</div>
<div class="metric-info">
<div class="metric-label">体温监测</div>
<div class="metric-value ${temperature>=36.1&&temperature<=37.2?'normal':'abnormal'}">${g('temperature','temp')||'-'} <span class="unit">℃</span></div>
<div class="temperature-indicator ${temperature>=36.1&&temperature<=37.2?'normal':'abnormal'}">
${temperature>=36.1&&temperature<=37.2?'体温正常':'异常体温'}
</div>
</div>
</div>
<div class="metric-card activity">
<div class="metric-icon">
<svg width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-width="2" d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.29 1.51 4.04 3 5.5"/>
<path stroke="currentColor" stroke-width="2" d="M12 5L8 21h8l-4-16z"/>
</svg>
</div>
<div class="metric-info">
<div class="metric-label">运动数据</div>
<div class="activity-stats">
<div class="activity-item">
<span class="activity-num">${g('step','steps')||'-'}</span>
<span class="activity-label">步数</span>
</div>
<div class="activity-item">
<span class="activity-num">${g('distance','distance')||'-'}</span>
<span class="activity-label">米</span>
</div>
<div class="activity-item">
<span class="activity-num">${g('calorie','calories')||'-'}</span>
<span class="activity-label">卡路里</span>
</div>
</div>
<div class="progress-ring-container">
<div class="progress-ring">
<div class="ring-progress" style="--progress:${Math.min((steps/10000)*100,100)}%"></div>
<div class="ring-text">${Math.min(Math.round((steps/10000)*100),100)}%</div>
</div>
</div>
</div>
</div>
<div class="metric-card stress">
<div class="metric-icon">
<svg width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-width="2" d="M12 2v20M2 12h20"/>
<circle cx="12" cy="12" r="8" stroke="currentColor" stroke-width="2"/>
</svg>
</div>
<div class="metric-info">
<div class="metric-label">压力指数</div>
<div class="metric-value">${g('stress','pressure')||'-'} <span class="unit">分</span></div>
<div class="stress-level ${parseInt(g('stress','pressure'))<=50?'low':parseInt(g('stress','pressure'))<=70?'medium':'high'}">
${parseInt(g('stress','pressure'))<=50?'压力较低':parseInt(g('stress','pressure'))<=70?'压力适中':'压力偏高'}
</div>
</div>
</div>
</div>
<div class="sleep-analysis">
<div class="sleep-header">
<div class="sleep-icon">
<svg width="20" height="20" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-width="2" d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
</svg>
</div>
<div class="sleep-title">睡眠分析</div>
</div>
<div class="sleep-content">${sleep}</div>
</div>
<div class="timestamp-info">
<div class="time-icon">⏰</div>
<div class="time-text">数据采集时间:${g('timestamp')||'-'}</div>
</div>
`;
const m=document.createElement('div');
m.className = 'health-modal-overlay';
m.innerHTML=`
<div class="health-modal-content">
<div class="modal-close" onclick="removeCustomMapInfo()">
<svg width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="currentColor" stroke-width="2" d="M18 6L6 18M6 6l12 12"/>
</svg>
</div>
${html}
</div>
`;
// 添加CSS样式
if(!document.querySelector('#health-profile-styles')){
const style = document.createElement('style');
style.id = 'health-profile-styles';
style.textContent = `
.health-modal-overlay {
position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
background: radial-gradient(ellipse at center, rgba(0,21,41,0.95) 0%, rgba(0,10,25,0.98) 100%);
z-index: 10000; display: flex; align-items: center; justify-content: center;
backdrop-filter: blur(8px); animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
.health-modal-content {
background: linear-gradient(135deg, rgba(10,24,48,0.95) 0%, rgba(15,35,65,0.98) 100%);
border-radius: 20px; box-shadow: 0 20px 60px rgba(0,228,255,0.3), inset 0 1px 0 rgba(255,255,255,0.1);
padding: 32px; min-width: 480px; max-width: 600px; color: #fff; position: relative;
border: 1px solid rgba(0,228,255,0.2); animation: slideUp 0.4s ease-out;
max-height: 90vh; overflow-y: auto;
}
@keyframes slideUp { from { transform: translateY(30px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
.modal-close {
position: absolute; right: 16px; top: 16px; cursor: pointer;
width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center;
background: rgba(0,228,255,0.1); border: 1px solid rgba(0,228,255,0.3);
color: #00e4ff; transition: all 0.3s ease;
}
.modal-close:hover { background: rgba(0,228,255,0.2); transform: scale(1.1); }
.health-profile-header {
display: flex; align-items: center; gap: 20px; margin-bottom: 32px;
padding: 20px; background: linear-gradient(90deg, rgba(0,228,255,0.05) 0%, rgba(0,180,255,0.1) 100%);
border-radius: 16px; border: 1px solid rgba(0,228,255,0.1);
}
.health-icon {
width: 64px; height: 64px; display: flex; align-items: center; justify-content: center;
background: radial-gradient(circle, rgba(255,107,107,0.2) 0%, rgba(238,90,36,0.3) 100%);
border-radius: 50%; animation: pulse 2s infinite;
}
@keyframes pulse { 0%, 100% { box-shadow: 0 0 0 0 rgba(255,107,107,0.7); } 50% { box-shadow: 0 0 0 20px rgba(255,107,107,0); } }
.health-title { flex: 1; }
.title-main { font-size: 24px; font-weight: 700; color: #00e4ff; margin-bottom: 4px; letter-spacing: 1px; }
.title-sub { font-size: 12px; color: #7ecfff; opacity: 0.8; text-transform: uppercase; letter-spacing: 2px; }
.health-score-circle {
position: relative; width: 60px; height: 60px;
}
.score-ring { width: 60px; height: 60px; }
.score-progress { transition: stroke-dashoffset 1s ease-out; }
.score-text {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
font-size: 18px; font-weight: 700; color: #00ff88;
}
.health-metrics-grid {
display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px; margin-bottom: 24px;
}
.metric-card {
background: linear-gradient(135deg, rgba(0,21,41,0.6) 0%, rgba(0,35,70,0.8) 100%);
border-radius: 16px; padding: 20px; border: 1px solid rgba(0,228,255,0.1);
transition: all 0.3s ease; position: relative; overflow: hidden;
}
.metric-card::before {
content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%;
background: linear-gradient(90deg, transparent, rgba(0,228,255,0.1), transparent);
transition: left 0.6s ease;
}
.metric-card:hover::before { left: 100%; }
.metric-card:hover { transform: translateY(-4px); box-shadow: 0 8px 32px rgba(0,228,255,0.2); }
.metric-card { display: flex; gap: 16px; align-items: flex-start; }
.metric-icon {
width: 48px; height: 48px; border-radius: 12px; display: flex; align-items: center; justify-content: center;
background: linear-gradient(135deg, rgba(0,228,255,0.2) 0%, rgba(0,180,255,0.3) 100%);
color: #00e4ff; flex-shrink: 0;
}
.metric-info { flex: 1; }
.metric-label { font-size: 12px; color: #7ecfff; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 1px; }
.metric-value { font-size: 20px; font-weight: 700; margin-bottom: 8px; }
.metric-value.normal { color: #00ff88; }
.metric-value.abnormal { color: #ff4444; }
.unit { font-size: 14px; color: #888; font-weight: 400; }
.metric-bar {
height: 4px; background: rgba(126,207,255,0.2); border-radius: 2px; overflow: hidden; margin-top: 8px;
}
.bar-fill {
height: 100%; border-radius: 2px; transition: width 1s ease-out;
}
.bar-fill.heart-rate { background: linear-gradient(90deg, #ff6b6b, #ee5a24); }
.bar-fill.oxygen { background: linear-gradient(90deg, #00ff88, #00cc66); }
.metric-status, .temperature-indicator, .stress-level {
font-size: 11px; padding: 4px 8px; border-radius: 12px; display: inline-block; margin-top: 4px;
}
.metric-status.normal, .temperature-indicator.normal, .stress-level.low {
background: rgba(0,255,136,0.2); color: #00ff88;
}
.metric-status.warning, .stress-level.medium {
background: rgba(255,187,0,0.2); color: #ffbb00;
}
.temperature-indicator.abnormal, .stress-level.high {
background: rgba(255,68,68,0.2); color: #ff4444;
}
.activity-stats {
display: flex; gap: 16px; margin-bottom: 12px;
}
.activity-item {
text-align: center;
}
.activity-num {
display: block; font-size: 16px; font-weight: 700; color: #00e4ff;
}
.activity-label {
font-size: 10px; color: #7ecfff; text-transform: uppercase;
}
.progress-ring-container {
display: flex; justify-content: center; margin-top: 12px;
}
.progress-ring {
position: relative; width: 40px; height: 40px;
}
.ring-progress {
width: 40px; height: 40px; border-radius: 50%;
background: conic-gradient(#00ff88 calc(var(--progress) * 1%), rgba(126,207,255,0.2) 0%);
display: flex; align-items: center; justify-content: center;
}
.ring-text {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
font-size: 10px; font-weight: 700; color: #00ff88;
}
.sleep-analysis {
background: linear-gradient(135deg, rgba(0,21,41,0.6) 0%, rgba(15,25,45,0.8) 100%);
border-radius: 16px; padding: 20px; margin-bottom: 20px;
border: 1px solid rgba(0,228,255,0.1);
}
.sleep-header {
display: flex; align-items: center; gap: 12px; margin-bottom: 12px;
}
.sleep-icon {
width: 32px; height: 32px; border-radius: 8px; display: flex; align-items: center; justify-content: center;
background: linear-gradient(135deg, rgba(138,43,226,0.2) 0%, rgba(75,0,130,0.3) 100%);
color: #9d4edd;
}
.sleep-title {
font-size: 16px; font-weight: 600; color: #00e4ff;
}
.sleep-content {
font-size: 14px; color: #fff; font-family: 'SF Mono', 'Monaco', 'Inconsolata', monospace;
background: rgba(0,0,0,0.3); padding: 12px; border-radius: 8px;
}
.timestamp-info {
display: flex; align-items: center; gap: 12px; padding: 16px;
background: rgba(0,228,255,0.05); border-radius: 12px;
border: 1px solid rgba(0,228,255,0.1);
}
.time-icon { font-size: 16px; }
.time-text { font-size: 13px; color: #7ecfff; }
`;
document.head.appendChild(style);
}
document.body.appendChild(m);
// 延迟重置防抖标志,给健康面板渲染留出时间
setTimeout(() => {
panelDisplaying = false;
console.log('健康面板渲染完成,重置显示状态');
}, 300);
}).catch(()=>{
showCustomAlert('获取健康数据失败');
// 获取数据失败时也要重置状态
panelDisplaying = false;
});
}
function removeCustomMapInfo(){
// 移除地图信息面板
const mapInfo=document.querySelector('.custom-map-info');
if(mapInfo)mapInfo.remove();
// 移除健康详情面板
const healthModal=document.querySelector('.health-modal-overlay');
if(healthModal)healthModal.remove();
// 重置面板显示状态,允许新的面板显示
panelDisplaying = false;
console.log('面板已关闭,重置显示状态');
}
function filterData(data){
//console.log('filterData.data',currentDept,currentUser);
//console.log('filterData.data.health_count || 0',data.health_count || 0);
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) {
console.warn('❌ 数据为空,无法更新地图');
return;
}
// 检查地图实例是否存在,如果不存在则重新初始化
if (!map || !loca) {
console.warn('🗺️ 地图未初始化,正在重新初始化...');
try {
// 重新初始化地图
const urlParams = new URLSearchParams(window.location.search);
const customerId = urlParams.get('customerId') || '{{ customerId }}' || '1';
initializeMap(customerId, '-1');
// 等待地图初始化完成再更新数据
setTimeout(() => {
window.updateMapData(data);
}, 1000);
return;
} catch (error) {
console.error('❌ 地图重新初始化失败:', error);
return;
}
}
// 检查地图图层是否存在
if (!breathRed || !breathYellow || !breathGreen) {
console.warn('🔧 地图图层未完全初始化,等待图层创建...');
setTimeout(() => {
window.updateMapData(data);
}, 500);
return;
}
const {alerts, healths} = filterData(data);
function isValidCoordinate(lng, lat) {
try {
const longitude = parseFloat(lng);
const latitude = parseFloat(lat);
return !isNaN(longitude) && !isNaN(latitude) &&
isFinite(longitude) && isFinite(latitude) &&
longitude >= -180 && longitude <= 180 &&
latitude >= -90 && latitude <= 90 &&
longitude !== 0 && latitude !== 0;
} catch (e) {
console.warn('坐标验证错误:', e);
return false;
}
}
const validHealths = healths.filter(h => {
const isValid = isValidCoordinate(h.longitude, h.latitude);
if (!isValid) {
console.log('无效健康点坐标被过滤:', h.userName, h.longitude, h.latitude);
}
return isValid;
});
const healthFeatures = validHealths.map(h => ({
type: 'Feature',
geometry: {type: 'Point', coordinates: [parseFloat(h.longitude), parseFloat(h.latitude)]},
properties: {
...h,
dept_name: h.deptName || '未知部门',
user_name: h.userName || '未知员工',
label: `${h.deptName || '未知部门'}-${h.userName || '未知员工'}`,
type: 'health'
}
}));
const validAlerts = alerts.filter(a => isValidCoordinate(a.longitude, a.latitude));
console.log('updateMapData.validAlerts',alerts);
const alertFeatures = validAlerts.map(a => ({
type: 'Feature',
geometry: {type: 'Point', coordinates: [parseFloat(a.longitude), parseFloat(a.latitude)]},
properties: {
...a,
label: `⚠️${a.dept_name || '未知部门'}-${a.user_name || '未知员工'}`,
type: 'alert'
}
}));
const criticalAlerts = {type: 'FeatureCollection', features: alertFeatures.filter(f => f.properties.severity_level === 'critical')};
const highAlerts = {type: 'FeatureCollection', features: alertFeatures.filter(f => f.properties.severity_level === 'high' || f.properties.severity_level === 'medium')};
const healthData = {type: 'FeatureCollection', features: healthFeatures};
console.log(`处理数据: ${healthFeatures.length}个有效健康点, ${alertFeatures.length}个有效告警点`);
try {
// 安全更新图层数据源
if (breathRed && typeof breathRed.setSource === 'function') {
breathRed.setSource(new Loca.GeoJSONSource({data: criticalAlerts}));
}
if (breathYellow && typeof breathYellow.setSource === 'function') {
breathYellow.setSource(new Loca.GeoJSONSource({data: highAlerts}));
}
if (breathGreen && typeof breathGreen.setSource === 'function') {
breathGreen.setSource(new Loca.GeoJSONSource({data: healthData}));
}
const allValidFeatures = [...alertFeatures, ...healthFeatures];
if (allValidFeatures.length > 0) {
const firstFeature = allValidFeatures[0];
const [lng, lat] = firstFeature.geometry.coordinates;
if (map && typeof map.setCenter === 'function') {
map.setCenter([lng, lat]);
console.log(`地图中心设置为: [${lng}, ${lat}]`);
}
} else if (map && typeof map.setCenter === 'function') {
map.setCenter([114.01508952, 22.54036796]);
}
// 重新启动动画
if (loca && loca.animate && typeof loca.animate.start === 'function') {
loca.animate.start();
}
console.log('✅ 地图更新完成');
} catch (error) {
console.error('❌ 地图更新失败:', error);
}
};
window.updateMapData1 = function(data){
//console.log('updateMapData.data',data);
if(!data||!map||!loca)return;
const {alerts,healths}=filterData(data);
console.log('updateMapData1.alerts',alerts);
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]);
});
}
}
// 健康数据统计计算函数
function calculateHealthStats(metrics) {
const stats = {
healthScore: 0,
normalCount: 0,
riskCount: 0,
avgHeartRate: 0,
avgBloodOxygen: 0,
avgTemperature: 0,
avgSteps: 0
};
if (!metrics || metrics.length === 0) return stats;
let totalScore = 0, validMetrics = 0;
metrics.forEach(metric => {
const values = metric.values.filter(v => v !== null && v !== undefined);
if (values.length === 0) return;
const avg = values.reduce((a, b) => a + b, 0) / values.length;
switch(metric.name) {
case '心率':
stats.avgHeartRate = Math.round(avg);
const heartScore = avg >= 60 && avg <= 100 ? 85 : (avg < 60 ? 70 : 60); // 正常范围评分
totalScore += heartScore;
if (heartScore >= 80) stats.normalCount++; else stats.riskCount++;
validMetrics++;
break;
case '血氧':
stats.avgBloodOxygen = Math.round(avg);
const oxygenScore = avg >= 95 ? 90 : (avg >= 90 ? 75 : 50);
totalScore += oxygenScore;
if (oxygenScore >= 80) stats.normalCount++; else stats.riskCount++;
validMetrics++;
break;
case '体温':
stats.avgTemperature = avg.toFixed(1);
const tempScore = avg >= 36.1 && avg <= 37.2 ? 88 : 65;
totalScore += tempScore;
if (tempScore >= 80) stats.normalCount++; else stats.riskCount++;
validMetrics++;
break;
case '步数':
stats.avgSteps = Math.round(avg);
const stepScore = avg >= 8000 ? 85 : (avg >= 5000 ? 70 : 55);
totalScore += stepScore;
if (stepScore >= 80) stats.normalCount++; else stats.riskCount++;
validMetrics++;
break;
}
});
stats.healthScore = validMetrics > 0 ? Math.round(totalScore / validMetrics) : 0;
return stats;
}
// 更新健康统计UI
function updateHealthStats(stats) {
try {
document.getElementById('healthScore').textContent = stats.healthScore;
document.getElementById('normalCount').textContent = stats.normalCount;
document.getElementById('riskCount').textContent = stats.riskCount;
document.getElementById('avgHeartRate').textContent = stats.avgHeartRate;
document.getElementById('avgBloodOxygen').textContent = stats.avgBloodOxygen + '%';
document.getElementById('avgTemperature').textContent = stats.avgTemperature + '°C';
document.getElementById('avgSteps').textContent = stats.avgSteps;
} catch (e) {
console.warn('更新健康统计UI失败:', e);
}
}
// 计算雷达图数据
function calculateRadarData(metrics) {
const indicators = [];
const values = [];
const metricMap = {
'心率': { max: 100, unit: 'bpm' },
'血氧': { max: 100, unit: '%' },
'体温': { max: 100, unit: '°C' },
'步数': { max: 100, unit: '步' },
'压力': { max: 100, unit: '级' },
'睡眠': { max: 100, unit: '小时' },
'卡路里': { max: 100, unit: 'kcal' },
'血压': { max: 100, unit: 'mmHg' }
};
metrics.forEach(metric => {
const config = metricMap[metric.name];
if (!config) return;
const validValues = metric.values.filter(v => v !== null && v !== undefined);
if (validValues.length === 0) return;
const avg = validValues.reduce((a, b) => a + b, 0) / validValues.length;
// 根据指标类型计算健康评分
let score = 0;
switch(metric.name) {
case '心率':
score = avg >= 60 && avg <= 100 ? 85 : (avg < 60 ? 70 : 60);
break;
case '血氧':
score = avg >= 95 ? 90 : (avg >= 90 ? 75 : 50);
break;
case '体温':
score = avg >= 36.1 && avg <= 37.2 ? 88 : 65;
break;
case '步数':
score = avg >= 8000 ? 85 : (avg >= 5000 ? 70 : 55);
break;
case '压力':
score = avg <= 3 ? 85 : (avg <= 5 ? 70 : 50);
break;
case '睡眠':
score = avg >= 7 ? 85 : (avg >= 6 ? 70 : 55);
break;
default:
score = Math.min(90, Math.max(50, 100 - (avg * 0.5))); // 默认计算
}
indicators.push({
name: metric.name,
max: 100,
min: 0
});
values.push(Math.round(score));
});
return { indicators, values };
}
function optimizeMapDisplay() {
// 根据缩放级别动态调整显示策略
map.on('zoomend', () => {
const zoom = map.getZoom();
if (zoom < 12) {
// 低缩放级别:隐藏文本,显示聚合点
textLayer.hide();
alertTextLayer.hide();
enableClustering();
} else if (zoom < 15) {
// 中等缩放级别:显示部分文本
textLayer.show();
alertTextLayer.show();
disableClustering();
setTextFilter(0.3); // 只显示30%的文本避免重叠
} else {
// 高缩放级别:显示所有标识
textLayer.show();
alertTextLayer.show();
disableClustering();
setTextFilter(1); // 显示所有文本
}
});
}
// 聚合显示实现
function enableClustering() {
// 使用Loca的聚合功能
const clusterLayer = new Loca.PointLayer({
loca,
zIndex: 115
});
clusterLayer.setSource(geo);
clusterLayer.setStyle({
unit: 'meter',
size: [20, 20],
color: (feature) => {
const count = feature.properties.count || 1;
return count > 10 ? '#ff4d4f' : count > 5 ? '#faad14' : '#52c41a';
},
text: {
field: 'count',
font: 'bold 12px Arial',
fillColor: '#fff'
}
});
loca.add(clusterLayer);
}
// 文本过滤显示
function setTextFilter(ratio) {
textLayer.setStyle({
filter: (feature, index) => {
return Math.random() < ratio; // 随机显示部分文本
}
});
}
// 交互功能函数
function showHealthDetails() {
const data = window.healthAnalysisData;
if (!data) {
alert('健康详情功能开发中...\n\n当前显示的是7天健康数据汇总');
return;
}
const { health_summary, risk_alerts } = data;
let message = `健康数据详情报告\n\n`;
message += `📊 综合评分: ${health_summary.overall_score}分\n`;
message += `✅ 正常指标: ${health_summary.normal_indicators}项\n`;
message += `⚠️ 风险指标: ${health_summary.risk_indicators}项\n`;
message += `👥 活跃用户: ${health_summary.active_users}/${health_summary.total_users}人\n\n`;
if (risk_alerts && risk_alerts.length > 0) {
message += `🚨 风险预警:\n`;
risk_alerts.slice(0, 3).forEach(alert => {
message += `• ${alert.message}\n`;
});
} else {
message += `✨ 暂无风险预警,整体健康状况良好`;
}
alert(message);
}
function filterByHeartRate() {
const data = window.healthAnalysisData;
if (!data) {
alert('心率筛选功能:显示心率异常的时间段和用户');
return;
}
const heartRateMetric = data.metrics.find(m => m.name === '心率');
if (heartRateMetric) {
const abnormalDays = heartRateMetric.daily_stats ?
heartRateMetric.daily_stats.filter(d => d.status === 'risk').length : 0;
alert(`心率分析结果\n\n平均心率: ${heartRateMetric.avg_value}bpm\n异常天数: ${abnormalDays}天\n正常范围: ${heartRateMetric.normal_range[0]}-${heartRateMetric.normal_range[1]}bpm`);
} else {
alert('暂无心率数据');
}
}
function filterByBloodOxygen() {
const data = window.healthAnalysisData;
if (!data) {
alert('血氧筛选功能:显示血氧偏低的时间段和用户');
return;
}
const oxygenMetric = data.metrics.find(m => m.name === '血氧');
if (oxygenMetric) {
const abnormalDays = oxygenMetric.daily_stats ?
oxygenMetric.daily_stats.filter(d => d.status === 'risk').length : 0;
alert(`血氧分析结果\n\n平均血氧: ${oxygenMetric.avg_value}%\n异常天数: ${abnormalDays}天\n正常范围: ${oxygenMetric.normal_range[0]}-${oxygenMetric.normal_range[1]}%`);
} else {
alert('暂无血氧数据');
}
}
function filterByTemperature() {
const data = window.healthAnalysisData;
if (!data) {
alert('体温筛选功能:显示体温异常的时间段和用户');
return;
}
const tempMetric = data.metrics.find(m => m.name === '体温');
if (tempMetric) {
const abnormalDays = tempMetric.daily_stats ?
tempMetric.daily_stats.filter(d => d.status === 'risk').length : 0;
alert(`体温分析结果\n\n平均体温: ${tempMetric.avg_value}°C\n异常天数: ${abnormalDays}天\n正常范围: ${tempMetric.normal_range[0]}-${tempMetric.normal_range[1]}°C`);
} else {
alert('暂无体温数据');
}
}
function filterBySteps() {
const data = window.healthAnalysisData;
if (!data) {
alert('步数筛选功能:显示运动量不足的用户');
return;
}
const stepsMetric = data.metrics.find(m => m.name === '步数');
if (stepsMetric) {
const lowActivityDays = stepsMetric.daily_stats ?
stepsMetric.daily_stats.filter(d => d.value && d.value < 5000).length : 0;
alert(`步数分析结果\n\n平均步数: ${stepsMetric.avg_value}步\n运动不足天数: ${lowActivityDays}天\n建议目标: ${stepsMetric.normal_range[0]}步以上`);
} else {
alert('暂无步数数据');
}
}
function showMetricDetails(metricName, value, date) {
const data = window.healthAnalysisData;
if (!data) {
alert(`指标详情\n\n指标: ${metricName}\n数值: ${value}\n日期: ${date}\n\n点击可查看该指标的详细分析和建议`);
return;
}
const metric = data.metrics.find(m => m.name === metricName);
if (metric) {
const dayData = metric.daily_stats ? metric.daily_stats.find(d => d.date === date) : null;
let message = `${metricName}详细信息\n\n`;
message += `📅 日期: ${date}\n`;
message += `📊 数值: ${value}${metric.unit}\n`;
message += `📈 7天平均: ${metric.avg_value}${metric.unit}\n`;
message += `📏 正常范围: ${metric.normal_range[0]}-${metric.normal_range[1]}${metric.unit}\n`;
if (dayData) {
message += `⭐ 健康评分: ${dayData.score}分\n`;
message += `🔍 状态: ${dayData.status === 'normal' ? '正常' : '需关注'}\n`;
}
message += `\n💡 建议: 保持规律监测,如有异常请及时就医`;
alert(message);
}
}
// 获取指标平均值的辅助函数
function getMetricAvg(metrics, metricName) {
const metric = metrics.find(m => m.name === metricName);
if (!metric || !metric.values) return 0;
const validValues = metric.values.filter(v => v !== null && v !== undefined);
return validValues.length > 0 ? Math.round(validValues.reduce((a,b) => a+b, 0) / validValues.length) : 0;
}
// 从后端metrics数据计算雷达图数据
function calculateRadarDataFromMetrics(metrics) {
const indicators = [];
const values = [];
// 选择主要健康指标
const mainMetrics = ['心率', '血氧', '体温', '步数', '压力', '收缩压'];
metrics.forEach(metric => {
if (mainMetrics.includes(metric.name) && metric.avg_value > 0) {
// 根据指标类型计算健康评分
let score = 0;
const avg = metric.avg_value;
const [min, max] = metric.normal_range;
if (avg >= min && avg <= max) {
score = 85 + (15 * (1 - Math.abs(avg - (min + max)/2) / ((max - min)/2)));
} else {
if (avg < min) {
score = Math.max(50, 85 - (min - avg) / min * 35);
} else {
score = Math.max(50, 85 - (avg - max) / max * 35);
}
}
indicators.push({
name: metric.name,
max: 100,
min: 0
});
values.push(Math.round(Math.min(100, Math.max(0, score))));
}
});
// 如果指标不足,添加默认指标
if (indicators.length < 4) {
const defaultIndicators = [
{ name: '心率', max: 100, min: 0 },
{ name: '血氧', max: 100, min: 0 },
{ name: '体温', max: 100, min: 0 },
{ name: '步数', max: 100, min: 0 }
];
const defaultValues = [75, 85, 80, 65];
for (let i = indicators.length; i < 4; i++) {
indicators.push(defaultIndicators[i]);
values.push(defaultValues[i]);
}
}
return { indicators, values };
}
// 更新交互功能函数
function showHealthDetails() {
const data = window.healthAnalysisData;
if (!data) {
alert('健康详情功能开发中...\n\n当前显示的是7天健康数据汇总');
return;
}
const { health_summary, risk_alerts } = data;
let message = `健康数据详情报告\n\n`;
message += `📊 综合评分: ${health_summary.overall_score}分\n`;
message += `✅ 正常指标: ${health_summary.normal_indicators}项\n`;
message += `⚠️ 风险指标: ${health_summary.risk_indicators}项\n`;
message += `👥 活跃用户: ${health_summary.active_users}/${health_summary.total_users}人\n\n`;
if (risk_alerts && risk_alerts.length > 0) {
message += `🚨 风险预警:\n`;
risk_alerts.slice(0, 3).forEach(alert => {
message += `• ${alert.message}\n`;
});
} else {
message += `✨ 暂无风险预警,整体健康状况良好`;
}
alert(message);
}
function filterByHeartRate() {
const data = window.healthAnalysisData;
if (!data) {
alert('心率筛选功能:显示心率异常的时间段和用户');
return;
}
const heartRateMetric = data.metrics.find(m => m.name === '心率');
if (heartRateMetric) {
const abnormalDays = heartRateMetric.daily_stats ?
heartRateMetric.daily_stats.filter(d => d.status === 'risk').length : 0;
alert(`心率分析结果\n\n平均心率: ${heartRateMetric.avg_value}bpm\n异常天数: ${abnormalDays}天\n正常范围: ${heartRateMetric.normal_range[0]}-${heartRateMetric.normal_range[1]}bpm`);
} else {
alert('暂无心率数据');
}
}
function filterByBloodOxygen() {
const data = window.healthAnalysisData;
if (!data) {
alert('血氧筛选功能:显示血氧偏低的时间段和用户');
return;
}
const oxygenMetric = data.metrics.find(m => m.name === '血氧');
if (oxygenMetric) {
const abnormalDays = oxygenMetric.daily_stats ?
oxygenMetric.daily_stats.filter(d => d.status === 'risk').length : 0;
alert(`血氧分析结果\n\n平均血氧: ${oxygenMetric.avg_value}%\n异常天数: ${abnormalDays}天\n正常范围: ${oxygenMetric.normal_range[0]}-${oxygenMetric.normal_range[1]}%`);
} else {
alert('暂无血氧数据');
}
}
function filterByTemperature() {
const data = window.healthAnalysisData;
if (!data) {
alert('体温筛选功能:显示体温异常的时间段和用户');
return;
}
const tempMetric = data.metrics.find(m => m.name === '体温');
if (tempMetric) {
const abnormalDays = tempMetric.daily_stats ?
tempMetric.daily_stats.filter(d => d.status === 'risk').length : 0;
alert(`体温分析结果\n\n平均体温: ${tempMetric.avg_value}°C\n异常天数: ${abnormalDays}天\n正常范围: ${tempMetric.normal_range[0]}-${tempMetric.normal_range[1]}°C`);
} else {
alert('暂无体温数据');
}
}
function filterBySteps() {
const data = window.healthAnalysisData;
if (!data) {
alert('步数筛选功能:显示运动量不足的用户');
return;
}
const stepsMetric = data.metrics.find(m => m.name === '步数');
if (stepsMetric) {
const lowActivityDays = stepsMetric.daily_stats ?
stepsMetric.daily_stats.filter(d => d.value && d.value < 5000).length : 0;
alert(`步数分析结果\n\n平均步数: ${stepsMetric.avg_value}步\n运动不足天数: ${lowActivityDays}天\n建议目标: ${stepsMetric.normal_range[0]}步以上`);
} else {
alert('暂无步数数据');
}
}
function showMetricDetails(metricName, value, date) {
const data = window.healthAnalysisData;
if (!data) {
alert(`指标详情\n\n指标: ${metricName}\n数值: ${value}\n日期: ${date}\n\n点击可查看该指标的详细分析和建议`);
return;
}
const metric = data.metrics.find(m => m.name === metricName);
if (metric) {
const dayData = metric.daily_stats ? metric.daily_stats.find(d => d.date === date) : null;
let message = `${metricName}详细信息\n\n`;
message += `📅 日期: ${date}\n`;
message += `📊 数值: ${value}${metric.unit}\n`;
message += `📈 7天平均: ${metric.avg_value}${metric.unit}\n`;
message += `📏 正常范围: ${metric.normal_range[0]}-${metric.normal_range[1]}${metric.unit}\n`;
if (dayData) {
message += `⭐ 健康评分: ${dayData.score}分\n`;
message += `🔍 状态: ${dayData.status === 'normal' ? '正常' : '需关注'}\n`;
}
message += `\n💡 建议: 保持规律监测,如有异常请及时就医`;
alert(message);
}
}
function showHealthRadarDetails(radarData) {
const data = window.healthAnalysisData;
const avgScore = radarData.values.reduce((a,b)=>a+b,0) / radarData.values.length;
let message = `健康雷达详情\n\n`;
message += `🎯 综合评分: ${avgScore.toFixed(1)}分\n\n`;
message += `📋 各项指标评分:\n`;
radarData.indicators.forEach((ind,i) => {
const score = radarData.values[i];
const status = score >= 80 ? '✅' : score >= 60 ? '⚠️' : '❌';
message += `${status} ${ind.name}: ${score}分\n`;
});
if (data && data.risk_alerts && data.risk_alerts.length > 0) {
message += `\n🚨 需要关注:\n`;
data.risk_alerts.slice(0, 2).forEach(alert => {
message += `• ${alert.metric}: ${alert.current_value}\n`;
});
}
alert(message);
}
function openMessagePanel() {
// 获取customerId参数
const urlParams = new URLSearchParams(window.location.search);
const customerId = urlParams.get('customerId') || '1';
// 打开消息详情页面
createModalWindow(`/message_view.html?customerId=${customerId}`);
}
// 获取统计数据
function loadStatisticsData() {
const urlParams = new URLSearchParams(window.location.search);
const customerId = urlParams.get('customerId') || '1';
//const today = new Date().toISOString().split('T')[0];
// 获取北京时间日期(UTC+8) - 修复时区问题
const today = new Date().toLocaleDateString('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit'
}).replace(/\//g, '-');
console.log("today", today);
// 设置当前日期
document.getElementById('statsDate').textContent = today;
// 获取统计概览数据
fetch(`/api/statistics/overview?orgId=${customerId}&date=${today}`)
.then(response => response.json())
.then(result => {
if (result.success) {
const data = result.data;
console.log("statistics", data);
// 修复字段映射 - 使用API实际返回的字段名并添加过滤逻辑
document.getElementById('healthDataCount').textContent = formatNumber(data.health_count || 0);
// 过滤并统计真实数据
let pendingAlerts = data.alert_count || 0;
let activeDevices = data.active_devices || 0;
let unreadMessages = data.message_count || 0;
// 如果有lastTotalInfo缓存数据,使用过滤逻辑进行验证
if (typeof lastTotalInfo !== 'undefined' && lastTotalInfo) {
console.log('🔍 使用lastTotalInfo过滤验证:', lastTotalInfo);
// 过滤pending状态的告警
if (lastTotalInfo.alert_info && lastTotalInfo.alert_info.alerts) {
const filteredPendingAlerts = lastTotalInfo.alert_info.alerts.filter(
alert => alert.alert_status === 'pending' || alert.status === 'pending'
);
console.log(`📊 过滤pending告警: ${filteredPendingAlerts.length}/${lastTotalInfo.alert_info.alerts.length}`);
}
// 过滤message_status=1的消息
if (lastTotalInfo.message_info && lastTotalInfo.message_info.messages) {
const filteredUnreadMessages = lastTotalInfo.message_info.messages.filter(
msg => msg.message_status === 1 || msg.message_status === '1'
);
console.log(`📊 过滤未读消息: ${filteredUnreadMessages.length}/${lastTotalInfo.message_info.messages.length}`);
}
// 过滤status=ACTIVE的设备
if (lastTotalInfo.device_info && lastTotalInfo.device_info.devices) {
const filteredActiveDevices = lastTotalInfo.device_info.devices.filter(
device => device.status === 'ACTIVE'
);
console.log(`📊 过滤活跃设备: ${filteredActiveDevices.length}/${lastTotalInfo.device_info.devices.length}`);
}
}
// 更新前端显示
document.getElementById('pendingAlerts').textContent = formatNumber(pendingAlerts);
document.getElementById('activeDevices').textContent = activeDevices;
document.getElementById('unreadMessages').textContent = formatNumber(unreadMessages);
// 模拟系统状态数据(因为API未返回此数据)
const mockSummary = {
systemStatus: data.alert_count > 20 ? 'warning' : (data.alert_count > 10 ? 'normal' : 'normal'),
healthScore: Math.max(100 - Math.floor(data.alert_count * 2), 60) // 基于告警数量计算健康分数
};
updateSystemStatus(mockSummary);
// 计算并显示趋势(基于当前数据生成模拟趋势)
const mockTrends = {
changes: {
healthDataChange: '+5%', // 健康数据增长
alertsChange: data.alert_count > 15 ? '+12%' : '+3%', // 告警变化
activeDevicesChange: '+2%', // 活跃设备变化
messagesChange: '+8%' // 消息变化
}
};
updateTrends(mockTrends);
// 添加数据更新动画
animateStatCards();
console.log('✅ 实时统计数据已更新:', {
health_count: data.health_count,
alert_count: data.alert_count,
active_devices: data.active_devices,
message_count: data.message_count
});
}
})
.catch(error => {
console.error('获取统计数据失败:', error);
// 显示错误状态
showErrorState();
});
}
// 格式化数字显示
function formatNumber(num) {
if (num >= 1000000) {
return (num / 1000000).toFixed(1) + 'M';
} else if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K';
}
return num.toString();
}
// 更新系统状态
function updateSystemStatus(summary) {
const indicator = document.getElementById('statusIndicator');
const statusText = document.getElementById('statusText');
const healthScore = document.getElementById('systemHealthScore');
// 移除所有状态类
indicator.className = 'status-indicator';
// 根据系统状态设置样式和文本
switch (summary.systemStatus) {
case 'normal':
indicator.classList.add('normal');
statusText.textContent = '系统正常';
break;
case 'warning':
indicator.classList.add('warning');
statusText.textContent = '系统警告';
break;
case 'critical':
indicator.classList.add('critical');
statusText.textContent = '系统异常';
break;
}
// 更新健康评分
healthScore.textContent = summary.healthScore;
// 根据评分设置颜色
if (summary.healthScore >= 80) {
healthScore.style.color = '#00ff9d';
} else if (summary.healthScore >= 60) {
healthScore.style.color = '#ffbb00';
} else {
healthScore.style.color = '#ff6b6b';
}
}
// 更新趋势显示
// 更新趋势显示
function updateTrends(data) {
// 使用接口返回的真实变化数据
if (data.changes) {
const trends = {
health: data.changes.healthDataChange || '0%',
alert: data.changes.alertsChange || '0%',
device: data.changes.activeDevicesChange || '0%',
message: data.changes.messagesChange || '0%'
};
// 更新趋势显示
updateTrendElement('healthTrend', trends.health);
updateTrendElement('alertTrend', trends.alert);
updateTrendElement('deviceTrend', trends.device);
updateTrendElement('messageTrend', trends.message);
console.log('趋势数据已更新:', trends);
console.log('昨天对比数据:', data.yesterday);
} else {
// 兜底:如果没有changes数据,显示无数据状态
updateTrendElement('healthTrend', '0%');
updateTrendElement('alertTrend', '0%');
updateTrendElement('deviceTrend', '0%');
updateTrendElement('messageTrend', '0%');
console.warn('接口未返回changes数据,使用默认值');
}
}
// 更新单个趋势元素
function updateTrendElement(elementId, trend) {
const element = document.getElementById(elementId);
element.textContent = trend;
element.className = 'stat-trend';
if (trend.startsWith('-')) {
element.classList.add('negative');
}
}
// 统计卡片动画
function animateStatCards() {
const cards = document.querySelectorAll('.stat-card');
cards.forEach((card, index) => {
setTimeout(() => {
card.style.transform = 'scale(1.05)';
setTimeout(() => {
card.style.transform = 'scale(1)';
}, 200);
}, index * 100);
});
}
// 显示错误状态
function showErrorState() {
const statusText = document.getElementById('statusText');
const indicator = document.getElementById('statusIndicator');
statusText.textContent = '数据获取失败';
indicator.className = 'status-indicator critical';
// 显示默认值
document.getElementById('healthDataCount').textContent = '--';
document.getElementById('pendingAlerts').textContent = '--';
document.getElementById('activeDevices').textContent = '--';
document.getElementById('unreadMessages').textContent = '--';
}
// 初始化统计概览图表
function initOverviewChart() {
const overviewContainer = document.getElementById('overviewChart');
if (overviewContainer) {
const overviewChart = echarts.init(overviewContainer);
const overviewOption = {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
show: false
},
series: [{
name: '数据概览',
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '50%'],
data: [
{value: 0, name: '健康数据', itemStyle: {color: '#00e4ff'}},
{value: 0, name: '告警数据', itemStyle: {color: '#ff6b6b'}},
{value: 0, name: '设备数据', itemStyle: {color: '#00ff9d'}},
{value: 0, name: '消息数据', itemStyle: {color: '#ffbb00'}}
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
},
label: {
show: false
},
labelLine: {
show: false
}
}]
};
overviewChart.setOption(overviewOption);
// 保存图表实例以便后续更新
window.overviewChart = overviewChart;
}
}
</script>
</body>
</html>