- 🐍 添加所有 Python 源码文件(*.py) - 📄 添加所有 HTML 模板文件(templates/) - 🗂️ 添加控制器、模型、测试等目录结构 - 📋 添加文档文件(*.md)和配置文件 - ⚙️ 更新 .gitignore 配置,只忽略日志和 SQL 文件 - 🚀 确保所有重要代码都被 Git 跟踪和版本控制 根据要求:ljwx-bigscreen/bigscreen/bigScreen 里面除了 log 和 sql 都需要提交
6409 lines
232 KiB
HTML
6409 lines
232 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>智能健康数据分析平台</title>
|
||
|
||
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
background: #001529;
|
||
color: #fff;
|
||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.container {
|
||
display: grid;
|
||
grid-template-columns: 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: 140px; /* 增加告警面板的图表最小高度:从120px增加到140px */
|
||
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: 80px !important;
|
||
max-height: 150px !important; /* 限制单个图表最大高度 */
|
||
overflow: hidden !important;
|
||
}
|
||
|
||
/* 特别限制告警类型图表高度 */
|
||
#alertTypeChart {
|
||
max-height: 120px !important; /* 更严格的高度限制 */
|
||
}
|
||
|
||
/* 告警面板网格布局优化 */
|
||
.alert-charts-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
grid-template-rows: 1fr 1fr;
|
||
gap: 8px; /* 减少间距适应更窄的宽度 */
|
||
height: calc(100% - 60px); /* 为顶部统计留出更多空间 */
|
||
min-height: 220px; /* 减少最小高度适应面板 */
|
||
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: 220px;
|
||
}
|
||
|
||
.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);
|
||
padding: 15px;
|
||
border-radius: 8px;
|
||
width: 280px;
|
||
border: 1px solid rgba(0, 228, 255, 0.2);
|
||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.filter-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
|
||
.filter-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 15px;
|
||
padding-bottom: 10px;
|
||
border-bottom: 1px solid rgba(0, 228, 255, 0.2);
|
||
}
|
||
|
||
.filter-header span {
|
||
color: #00e4ff;
|
||
font-size: 14px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.filter-select {
|
||
font-family: monospace;
|
||
white-space: pre;
|
||
width: 100%;
|
||
padding: 8px;
|
||
margin-bottom: 10px;
|
||
background: rgba(0, 0, 0, 0.3);
|
||
color: #fff;
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
border-radius: 4px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.filter-select:hover {
|
||
border-color: rgba(0, 228, 255, 0.4);
|
||
}
|
||
|
||
.filter-select:focus {
|
||
border-color: rgba(0, 228, 255, 0.6);
|
||
outline: none;
|
||
box-shadow: 0 0 5px rgba(0, 228, 255, 0.2);
|
||
}
|
||
|
||
.filter-select option {
|
||
font-family: monospace;
|
||
white-space: pre;
|
||
background: #1a1a1a;
|
||
color: #fff;
|
||
padding: 8px;
|
||
}
|
||
|
||
/* 添加美化的弹出框样式 */
|
||
.custom-alert {
|
||
position: fixed;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
background: rgba(0, 21, 41, 0.95);
|
||
border: 1px solid rgba(0, 228, 255, 0.3);
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
|
||
z-index: 10000;
|
||
min-width: 300px;
|
||
text-align: center;
|
||
animation: alertFadeIn 0.3s ease;
|
||
}
|
||
|
||
.custom-alert-content {
|
||
color: #fff;
|
||
font-size: 16px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.custom-alert-button {
|
||
background: rgba(0, 228, 255, 0.2);
|
||
border: 1px solid rgba(0, 228, 255, 0.4);
|
||
color: #00e4ff;
|
||
padding: 8px 20px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.custom-alert-button:hover {
|
||
background: rgba(0, 228, 255, 0.3);
|
||
border-color: rgba(0, 228, 255, 0.6);
|
||
}
|
||
|
||
@keyframes alertFadeIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: translate(-50%, -60%);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translate(-50%, -50%);
|
||
}
|
||
}
|
||
|
||
/* 添加遮罩层样式 */
|
||
.custom-alert-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
backdrop-filter: blur(2px);
|
||
z-index: 9999;
|
||
animation: overlayFadeIn 0.3s ease;
|
||
}
|
||
|
||
@keyframes overlayFadeIn {
|
||
from {
|
||
opacity: 0;
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
/* 现代科技风格信息面板 */
|
||
.info-panel {
|
||
position: fixed;
|
||
right: 20px;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
width: 360px;
|
||
background: rgba(13, 17, 38, 0.95);
|
||
border: 1px solid rgba(0, 234, 255, 0.3);
|
||
border-radius: 12px;
|
||
box-shadow: 0 0 20px rgba(0, 234, 255, 0.2);
|
||
backdrop-filter: blur(10px);
|
||
z-index: 1000;
|
||
transition: all 0.3s ease;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.info-panel-header {
|
||
padding: 15px 20px;
|
||
background: linear-gradient(90deg, rgba(0, 234, 255, 0.1), rgba(0, 234, 255, 0.05));
|
||
border-bottom: 1px solid rgba(0, 234, 255, 0.2);
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.info-panel-title {
|
||
color: #00eaff;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.info-panel-close {
|
||
background: none;
|
||
border: none;
|
||
color: #00eaff;
|
||
font-size: 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">
|
||
<div class="filter-header">
|
||
<span>人员筛选</span>
|
||
</div>
|
||
<div class="filter-container">
|
||
<select id="deptSelect" class="filter-select">
|
||
<option value="">选择部门</option>
|
||
</select>
|
||
<select id="userSelect" class="filter-select">
|
||
<option value="">选择用户</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<!-- 保留原有地图代码 -->
|
||
</div>
|
||
|
||
<!-- 消息面板 -->
|
||
<div class="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="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
|
||
<script>
|
||
window.difyChatbotConfig = {
|
||
token: 'CdVhQIms3fZkQuTW',
|
||
baseUrl: 'http://192.168.1.83',
|
||
systemVariables: {
|
||
// user_id: 'YOU CAN DEFINE USER ID HERE',
|
||
// conversation_id: 'YOU CAN DEFINE CONVERSATION ID HERE, IT MUST BE A VALID UUID',
|
||
},
|
||
}
|
||
</script>
|
||
<script
|
||
src="http://192.168.1.83/embed.min.js"
|
||
id="CdVhQIms3fZkQuTW"
|
||
defer>
|
||
</script>
|
||
<style>
|
||
#dify-chatbot-bubble-button {
|
||
background-color: #1C64F2 !important;
|
||
}
|
||
#dify-chatbot-bubble-window {
|
||
width: 24rem !important;
|
||
height: 40rem !important;
|
||
}
|
||
</style>
|
||
<script>
|
||
// 在全局作用域声明图表变量
|
||
let globalCharts = null;
|
||
let currentDept = '', currentUser = '';
|
||
const ALERT_TYPE_MAP = {
|
||
heart_rate: '心率',
|
||
blood_pressure: '血压',
|
||
stress: '压力',
|
||
blood_oxygen: '血氧',
|
||
temperature: '体温',
|
||
one_key_alarm: '一键报警',
|
||
fall_down: '跌倒',
|
||
sleep: '睡眠'
|
||
};
|
||
const ALERT_SEVERITY_MAP = {
|
||
critical: '严重',
|
||
high: '高',
|
||
medium: '中',
|
||
low: '低'
|
||
};
|
||
const ALERT_STATUS_MAP = {
|
||
pending: '待处理',
|
||
responded: '已响应',
|
||
resolved: '已解决'
|
||
};
|
||
function translateAlertType(type) {
|
||
return ALERT_TYPE_MAP[type] || type;
|
||
}
|
||
function translateAlertSeverity(severity) {
|
||
return ALERT_SEVERITY_MAP[severity] || severity;
|
||
}
|
||
function translateAlertStatus(status) {
|
||
return ALERT_STATUS_MAP[status] || status;
|
||
}
|
||
const ALERT_TYPE_COLOR = {
|
||
heart_rate: '#ff6b6b',
|
||
blood_pressure: '#ffb347',
|
||
stress: '#f7b731',
|
||
blood_oxygen: '#00e4ff',
|
||
temperature: '#ffd700',
|
||
one_key_alarm: '#ffe066',
|
||
fall_down: '#00cfff',
|
||
sleep: '#7ecfff'
|
||
};
|
||
const ALERT_SEVERITY_COLOR = {
|
||
critical: '#ff6b6b',
|
||
high: '#ffb347',
|
||
medium: '#f7b731',
|
||
low: '#00e4ff'
|
||
};
|
||
const ALERT_STATUS_COLOR = {
|
||
pending: '#ff6b6b',
|
||
responded: '#ffb347',
|
||
resolved: '#00e4ff'
|
||
};
|
||
function getAlertTypeColor(type) {
|
||
return ALERT_TYPE_COLOR[type] || '#7ecfff';
|
||
}
|
||
function getAlertSeverityColor(severity) {
|
||
return ALERT_SEVERITY_COLOR[severity] || '#7ecfff';
|
||
}
|
||
function getAlertStatusColor(status) {
|
||
return ALERT_STATUS_COLOR[status] || '#7ecfff';
|
||
}
|
||
|
||
// 添加日期格式化函数
|
||
function formatDate(date) {
|
||
const year = date.getFullYear();
|
||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||
const day = String(date.getDate()).padStart(2, '0');
|
||
return `${year}-${month}-${day}`;
|
||
}
|
||
|
||
// 修改initCharts函数
|
||
function initCharts() {
|
||
try {
|
||
let charts = {
|
||
healthScore: null,
|
||
stats: null,
|
||
trend: null,
|
||
alert: null,
|
||
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 healthScoreOption = {
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
formatter: function(params) {
|
||
return params[0].name + '<br/>' +
|
||
params[0].marker + params[0].seriesName + ':' + params[0].value;
|
||
}
|
||
},
|
||
radar: {
|
||
radius: '65%',
|
||
center: ['50%', '55%'],
|
||
indicator: [
|
||
{ name: `心率 ${factors.heartRate?.score || 0}分`, max: 100 },
|
||
{ name: `血氧 ${factors.bloodOxygen?.score || 0}分`, max: 100 },
|
||
{ name: `体温 ${factors.temperature?.score || 0}分`, max: 100 },
|
||
{ name: `步数 ${factors.step?.score || 0}分`, max: 100 },
|
||
{ name: `卡路里 ${factors.calorie?.score || 0}分`, max: 100 },
|
||
{ name: `收缩压 ${factors.pressureHigh?.score || 0}分`, max: 100 },
|
||
{ name: `舒张压 ${factors.pressureLow?.score || 0}分`, max: 100 },
|
||
{ name: `压力 ${factors.stress?.score || 0}分`, max: 100 }
|
||
],
|
||
name: {
|
||
textStyle: {
|
||
color: '#00e4ff',
|
||
fontSize: 12,
|
||
padding: [3, 5]
|
||
},
|
||
rich: {
|
||
value: {
|
||
color: '#00e4ff',
|
||
fontSize: 12,
|
||
fontWeight: 'normal'
|
||
}
|
||
}
|
||
},
|
||
splitArea: {
|
||
show: true,
|
||
areaStyle: {
|
||
color: ['rgba(0,228,255,0.1)', 'rgba(0,228,255,0.2)']
|
||
}
|
||
},
|
||
axisLine: {
|
||
lineStyle: {
|
||
color: 'rgba(0,228,255,0.5)'
|
||
}
|
||
},
|
||
splitLine: {
|
||
lineStyle: {
|
||
color: 'rgba(0,228,255,0.3)'
|
||
}
|
||
}
|
||
},
|
||
series: [{
|
||
name: '健康指标',
|
||
type: 'radar',
|
||
data: [{
|
||
value: [
|
||
factors.heartRate?.score || 0,
|
||
factors.bloodOxygen?.score || 0,
|
||
factors.temperature?.score || 0,
|
||
factors.step?.score || 0,
|
||
factors.calorie?.score || 0,
|
||
factors.pressureHigh?.score || 0,
|
||
factors.pressureLow?.score || 0,
|
||
factors.stress?.score || 0
|
||
],
|
||
name: '当前状态',
|
||
itemStyle: {
|
||
color: '#00e4ff'
|
||
},
|
||
areaStyle: {
|
||
color: 'rgba(0,228,255,0.4)'
|
||
}
|
||
}]
|
||
}]
|
||
};
|
||
charts.healthScore.setOption(healthScoreOption);
|
||
} else {
|
||
// 如果没有数据,显示0分
|
||
const totalScoreElement = document.querySelector('.total-score');
|
||
if (totalScoreElement) {
|
||
totalScoreElement.textContent = '总分:0';
|
||
}
|
||
|
||
const healthScoreOption = {
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
formatter: function(params) {
|
||
return params[0].name + '<br/>' +
|
||
params[0].marker + params[0].seriesName + ':' + params[0].value;
|
||
}
|
||
},
|
||
radar: {
|
||
radius: '65%',
|
||
center: ['50%', '55%'],
|
||
indicator: [
|
||
{ name: '心率 0分', max: 100 },
|
||
{ name: '血氧 0分', max: 100 },
|
||
{ name: '体温 0分', max: 100 },
|
||
{ name: '步数 0分', max: 100 },
|
||
{ name: '卡路里 0分', max: 100 },
|
||
{ name: '收缩压 0分', max: 100 },
|
||
{ name: '舒张压 0分', max: 100 },
|
||
{ name: '压力 0分', max: 100 }
|
||
],
|
||
name: {
|
||
textStyle: {
|
||
color: '#00e4ff',
|
||
fontSize: 12,
|
||
padding: [3, 5]
|
||
},
|
||
rich: {
|
||
value: {
|
||
color: '#00e4ff',
|
||
fontSize: 12,
|
||
fontWeight: 'normal'
|
||
}
|
||
}
|
||
},
|
||
splitArea: {
|
||
show: true,
|
||
areaStyle: {
|
||
color: ['rgba(0,228,255,0.1)', 'rgba(0,228,255,0.2)']
|
||
}
|
||
},
|
||
axisLine: {
|
||
lineStyle: {
|
||
color: 'rgba(0,228,255,0.5)'
|
||
}
|
||
},
|
||
splitLine: {
|
||
lineStyle: {
|
||
color: 'rgba(0,228,255,0.3)'
|
||
}
|
||
}
|
||
},
|
||
series: [{
|
||
name: '健康指标',
|
||
type: 'radar',
|
||
data: [{
|
||
value: [0, 0, 0, 0, 0, 0, 0, 0],
|
||
name: '当前状态',
|
||
itemStyle: {
|
||
color: '#00e4ff'
|
||
},
|
||
areaStyle: {
|
||
color: 'rgba(0,228,255,0.4)'
|
||
}
|
||
}]
|
||
}]
|
||
};
|
||
charts.healthScore.setOption(healthScoreOption);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error fetching health data:', error);
|
||
// 发生错误时也显示0分
|
||
const totalScoreElement = document.querySelector('.total-score');
|
||
if (totalScoreElement) {
|
||
totalScoreElement.textContent = '总分:0';
|
||
}
|
||
// 设置默认的0分图表
|
||
const healthScoreOption = {
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
formatter: function(params) {
|
||
return params[0].name + '<br/>' +
|
||
params[0].marker + params[0].seriesName + ':' + params[0].value;
|
||
}
|
||
},
|
||
radar: {
|
||
radius: '65%',
|
||
center: ['50%', '55%'],
|
||
indicator: [
|
||
{ name: '心率 0分', max: 100 },
|
||
{ name: '血氧 0分', max: 100 },
|
||
{ name: '体温 0分', max: 100 },
|
||
{ name: '步数 0分', max: 100 },
|
||
{ name: '卡路里 0分', max: 100 },
|
||
{ name: '收缩压 0分', max: 100 },
|
||
{ name: '舒张压 0分', max: 100 },
|
||
{ name: '压力 0分', max: 100 }
|
||
],
|
||
name: {
|
||
textStyle: {
|
||
color: '#00e4ff',
|
||
fontSize: 12,
|
||
padding: [3, 5]
|
||
},
|
||
rich: {
|
||
value: {
|
||
color: '#00e4ff',
|
||
fontSize: 12,
|
||
fontWeight: 'normal'
|
||
}
|
||
}
|
||
},
|
||
splitArea: {
|
||
show: true,
|
||
areaStyle: {
|
||
color: ['rgba(0,228,255,0.1)', 'rgba(0,228,255,0.2)']
|
||
}
|
||
},
|
||
axisLine: {
|
||
lineStyle: {
|
||
color: 'rgba(0,228,255,0.5)'
|
||
}
|
||
},
|
||
splitLine: {
|
||
lineStyle: {
|
||
color: 'rgba(0,228,255,0.3)'
|
||
}
|
||
}
|
||
},
|
||
series: [{
|
||
name: '健康指标',
|
||
type: 'radar',
|
||
data: [{
|
||
value: [0, 0, 0, 0, 0, 0, 0, 0],
|
||
name: '当前状态',
|
||
itemStyle: {
|
||
color: '#00e4ff'
|
||
},
|
||
areaStyle: {
|
||
color: 'rgba(0,228,255,0.4)'
|
||
}
|
||
}]
|
||
}]
|
||
};
|
||
charts.healthScore.setOption(healthScoreOption);
|
||
});
|
||
}
|
||
|
||
// 检查并初始化数据统计图表
|
||
const statsContainer = document.getElementById('statsChart');
|
||
if (statsContainer) {
|
||
charts.stats = echarts.init(statsContainer);
|
||
const statsOption = {
|
||
tooltip: {
|
||
trigger: 'item'
|
||
},
|
||
legend: {
|
||
orient: 'vertical',
|
||
right: 10,
|
||
top: 'center',
|
||
textStyle: {
|
||
color: '#fff'
|
||
}
|
||
},
|
||
series: [{
|
||
name: '设备状态',
|
||
type: 'pie',
|
||
radius: ['40%', '70%'],
|
||
center: ['40%', '50%'],
|
||
avoidLabelOverlap: true,
|
||
itemStyle: {
|
||
borderColor: '#fff',
|
||
borderWidth: 1
|
||
},
|
||
label: {
|
||
show: true,
|
||
color: '#fff'
|
||
},
|
||
data: [
|
||
{ value: 35, name: '在线' },
|
||
{ value: 10, name: '离线' },
|
||
{ value: 15, name: '充电中' },
|
||
{ value: 20, name: '佩戴中' }
|
||
],
|
||
color: ['#00e4ff', '#ff4444', '#ffbb00', '#00ff9d']
|
||
}]
|
||
};
|
||
charts.stats.setOption(statsOption);
|
||
}
|
||
|
||
|
||
|
||
// 检查并初始化趋势分析图表
|
||
|
||
|
||
// 检查并初始化预警信息图表
|
||
const alertContainer = document.getElementById('alertList');
|
||
if (alertContainer) {
|
||
charts.alert = echarts.init(alertContainer);
|
||
const alertOption = {
|
||
tooltip: {
|
||
trigger: 'item',
|
||
backgroundColor: 'rgba(0,21,41,0.9)',
|
||
borderColor: 'rgba(0,228,255,0.2)',
|
||
textStyle: {
|
||
color: '#fff'
|
||
}
|
||
},
|
||
grid: {
|
||
left: '3%',
|
||
right: '4%',
|
||
bottom: '3%',
|
||
containLabel: true
|
||
},
|
||
xAxis: {
|
||
show: false
|
||
},
|
||
yAxis: {
|
||
type: 'category',
|
||
data: ['心率', '血氧', '体温', '步数', '卡路里', '距离'],
|
||
axisLabel: {
|
||
color: '#fff',
|
||
fontSize: 12
|
||
},
|
||
axisLine: {
|
||
show: false
|
||
},
|
||
axisTick: {
|
||
show: false
|
||
}
|
||
},
|
||
series: [{
|
||
type: 'bar',
|
||
data: [
|
||
{value: 8.02, itemStyle: {color: '#00e4ff'}},
|
||
{value: 8.09, itemStyle: {color: '#00e4ff'}},
|
||
{value: 2.87, itemStyle: {color: '#3498db'}},
|
||
{value: 0.85, itemStyle: {color: '#f39c12'}},
|
||
{value: 80.44, itemStyle: {color: '#00e4ff'}},
|
||
{value: 2.56, itemStyle: {color: '#e74c3c'}}
|
||
],
|
||
barWidth: '60%',
|
||
label: {
|
||
show: true,
|
||
position: 'right',
|
||
color: '#fff',
|
||
formatter: '{c}%'
|
||
}
|
||
}]
|
||
};
|
||
charts.alert.setOption(alertOption);
|
||
}
|
||
|
||
// 添加窗口resize事件监听
|
||
window.addEventListener('resize', () => {
|
||
Object.values(charts).forEach(chart => {
|
||
if (chart) {
|
||
chart.resize();
|
||
}
|
||
});
|
||
});
|
||
|
||
// 初始化消息统计图表
|
||
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(); // 存储图表实例
|
||
refreshData(); // 初始加载数据
|
||
|
||
// 每分钟刷新一次数据
|
||
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 src="https://webapi.amap.com/maps?v=2.0&key=7de0c6b90cb13571ce931ca75a66c6e7"></script>
|
||
<script src="https://webapi.amap.com/loca?v=2.0.0&key=7de0c6b90cb13571ce931ca75a66c6e7"></script>
|
||
<script src="/static/js/map-optimizer.js"></script> <!-- 地图优化模块 -->
|
||
<script>
|
||
// Declare infoWindow globally
|
||
// let infoWindow; // 移除
|
||
let geoLevelF,geoLevelE,geo,map,loca,breathGreen,breathRed,breathYellow;//变量合并
|
||
|
||
async function getFirstCoordinates(map, deptId, userId) {
|
||
try {
|
||
console.log('getFirstCoordinates.deptId',deptId);
|
||
console.log('getFirstCoordinates.userId',userId);
|
||
const response = await fetch(`./generateAlertJson?customerId=${deptId}&userId=${userId}`);
|
||
console.log('getFirstCoordinates.response',response);
|
||
const result = await response.json();
|
||
if (result.features && result.features.length > 0) {
|
||
const coordinates = result.features[0].geometry.coordinates;
|
||
map.setCenter(coordinates);
|
||
console.log('getFirstCoordinates.coordinates',coordinates);
|
||
return coordinates;
|
||
} else {
|
||
const response1 = await fetch(`./generateHealthJson?customerId=${deptId}&userId=${userId}`);
|
||
const result1 = await response1.json();
|
||
if (result1.features && result1.features.length > 0) {
|
||
const coordinates = result1.features[0].geometry.coordinates;
|
||
map.setCenter(coordinates);
|
||
console.log('getFirstCoordinates.coordinates',coordinates);
|
||
return coordinates;
|
||
} else {
|
||
return [114.01508952, 22.54036796, 0];
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Error accessing coordinates:', error);
|
||
return [114.01508952, 22.54036796, 0]; // 默认坐标
|
||
}
|
||
}
|
||
|
||
async function updateGeoJSONSources(deptId, userId) {
|
||
try {
|
||
console.log('Updating GeoJSON sources...', { deptId, userId });
|
||
|
||
// 构建URL参数
|
||
const params = new URLSearchParams({
|
||
customerId: deptId || '1',
|
||
userId: userId || '-1'
|
||
}).toString();
|
||
|
||
// 获取数据
|
||
console.log('updateGeoJSONSources.params',`./generateHealthJson?${params}`);
|
||
const [criticalData, highData, healthData] = await Promise.all([
|
||
fetch(`./generateAlertJson?${params}&severityLevel=critical`).then(res => res.json()),
|
||
fetch(`./generateAlertJson?${params}&severityLevel=high`).then(res => res.json()),
|
||
fetch(`./generateHealthJson?${params}`).then(res => res.json())
|
||
]);
|
||
|
||
// 创建新的数据源
|
||
const newGeoLevelF = new Loca.GeoJSONSource({
|
||
data: criticalData
|
||
});
|
||
|
||
const newGeoLevelE = new Loca.GeoJSONSource({
|
||
data: highData
|
||
});
|
||
|
||
const newGeo = new Loca.GeoJSONSource({
|
||
data: healthData
|
||
});
|
||
|
||
// 更新图层数据源
|
||
breathRed.setSource(newGeoLevelF);
|
||
breathYellow.setSource(newGeoLevelE);
|
||
breathGreen.setSource(newGeo);
|
||
|
||
// 更新地图中心点
|
||
if (healthData.features && healthData.features.length > 0) {
|
||
const coordinates = healthData.features[0].geometry.coordinates;
|
||
console.log('updateGeoJSONSources.coordinates',coordinates);
|
||
map.setCenter(coordinates);
|
||
}
|
||
|
||
// 重新启动动画
|
||
loca.animate.start();
|
||
|
||
console.log('GeoJSON sources updated successfully');
|
||
|
||
} catch (error) {
|
||
console.error('Error updating GeoJSON sources:', error);
|
||
}
|
||
}
|
||
|
||
|
||
function initializeMap(deptId, userId) {
|
||
//console.log(AMap); // 如果打印 `undefined`,说明 AMap 没有正确加载
|
||
//console.log(Loca); // 如果打印 `undefined`,说明 Loca 没有正确加载
|
||
//console.log('initializeMap.deptId',deptId);
|
||
//console.log('initializeMap.userId',userId);
|
||
//console.log('initializeMap.severityLevel',`./generateAlertJson?customerId=${deptId}&userId=${userId}&severityLevel=critical`);
|
||
|
||
geoLevelF = new Loca.GeoJSONSource({
|
||
url: `./generateAlertJson?customerId=${deptId}&userId=${userId}&severityLevel=critical`
|
||
});
|
||
|
||
|
||
// 黄色告警点
|
||
geoLevelE = new Loca.GeoJSONSource({
|
||
url: `./generateAlertJson?customerId=${deptId}&userId=${userId}&severityLevel=high`
|
||
});
|
||
|
||
// 绿色健康点
|
||
geo = new Loca.GeoJSONSource({
|
||
url: `./generateHealthJson?customerId=${deptId}&userId=${userId}`
|
||
});
|
||
|
||
console.log('geo',geo);
|
||
|
||
|
||
let current_coordinates = {
|
||
|
||
'longitude': 114.01508952,
|
||
'latitude': 22.58036796,
|
||
'altitude': 0
|
||
}
|
||
|
||
//console.log('current_coordinates',current_coordinates);
|
||
|
||
map = window.map = new AMap.Map('map-container', {
|
||
zoom: 17,
|
||
center: [current_coordinates['longitude'],current_coordinates['latitude']],
|
||
pitch: 45,
|
||
showLabel: false,
|
||
mapStyle: 'amap://styles/blue',
|
||
viewMode: '3D',
|
||
});
|
||
|
||
//const api_coordinates = getFirstCoordinates(map,deptId,userId);
|
||
|
||
|
||
loca = new Loca.Container({
|
||
map,
|
||
});
|
||
|
||
breathGreen = new Loca.ScatterLayer({
|
||
loca,
|
||
zIndex: 113,
|
||
opacity: 1,
|
||
visible: true,
|
||
zooms: [2, 22],
|
||
});
|
||
|
||
breathGreen.setSource(geo);
|
||
//console.log("BreathGreen GeoJSON data:", breathGreen);
|
||
breathGreen.setStyle({
|
||
unit: 'meter',
|
||
color: 'rgb(39, 207, 14)',
|
||
size: [10, 10],
|
||
borderWidth: 0,
|
||
exture: 'https://a.amap.com/Loca/static/loca-v2/demos/images/breath_green.png',
|
||
//texture: 'http://localhost:5001/static/images/avarta.png',
|
||
duration: 500,
|
||
animate: true,
|
||
});
|
||
|
||
// 添加到地图中
|
||
loca.add(breathGreen);
|
||
|
||
// 创建红色呼吸点图层
|
||
breathRed = new Loca.ScatterLayer({
|
||
loca,
|
||
zIndex: 113,
|
||
opacity: 1,
|
||
visible: true,
|
||
zooms: [2, 22],
|
||
});
|
||
//console.log('Health data:',geoLevelF);
|
||
|
||
breathRed.setSource(geoLevelF);
|
||
//console.log("BreathRed GeoJSON data:", breathRed);
|
||
breathRed.setStyle({
|
||
unit: 'meter',
|
||
size: [60, 60],
|
||
borderWidth: 0,
|
||
texture: 'https://a.amap.com/Loca/static/loca-v2/demos/images/breath_red.png',
|
||
duration: 500,
|
||
animate: true,
|
||
});
|
||
|
||
// 添加到地图中
|
||
loca.add(breathRed);
|
||
|
||
// 添加点击事件弹出信息框
|
||
map.on('click',e=>{const p=e.pixel.toArray();let f=breathRed.queryFeature(p)||breathYellow.queryFeature(p)||breathGreen.queryFeature(p);if(f&&f.coordinates){showCustomMapInfo(f)}else{removeCustomMapInfo()}});//优先级修正
|
||
|
||
|
||
|
||
breathYellow = new Loca.ScatterLayer({
|
||
loca,
|
||
zIndex: 112,
|
||
opacity: 1,
|
||
visible: true,
|
||
zooms: [2, 22],
|
||
});
|
||
breathYellow.setSource(geoLevelE);
|
||
//console.log("BreathYellow GeoJSON data:", breathYellow);
|
||
breathYellow.setStyle({
|
||
unit: 'meter',
|
||
size: [50, 50],
|
||
borderWidth: 0,
|
||
texture: 'https://a.amap.com/Loca/static/loca-v2/demos/images/breath_yellow.png',
|
||
duration: 1000,
|
||
animate: true,
|
||
});
|
||
loca.add(breathYellow);
|
||
|
||
|
||
|
||
|
||
|
||
//console.log("GeoJSON data for BreathRed:", geoLevelF);
|
||
|
||
//console.log("Map center:", map.getCenter());
|
||
|
||
|
||
// 启动渲染动画
|
||
loca.animate.start();
|
||
|
||
// 添加点击事件,显示信息框
|
||
// var infoWindow = new AMap.InfoWindow({
|
||
// offset: new AMap.Pixel(0, -30),
|
||
// });
|
||
|
||
|
||
|
||
// var dat = new Loca.Dat();
|
||
// dat.addLayer(breathGreen, '绿色');
|
||
// dat.addLayer(breathRed, '红色');
|
||
// dat.addLayer(breathYellow, '黄色');
|
||
|
||
|
||
// Function to fetch weather data
|
||
|
||
|
||
|
||
|
||
}
|
||
|
||
initializeMap('{{ customerId }}','-1');
|
||
|
||
|
||
|
||
async function reverseGeocode(lng, lat) {
|
||
try {
|
||
const response = await fetch(`https://restapi.amap.com/v3/geocode/regeo?key=d45f28e481665db5b1145a5aa989e68a&location=${lng},${lat}`);
|
||
const data = await response.json();
|
||
|
||
if (data.status === '1') {
|
||
const address = data.regeocode.formatted_address;
|
||
console.log('Address:', address);
|
||
return address;
|
||
} else {
|
||
console.error('Failed to reverse geocode:', data.info);
|
||
return null;
|
||
}
|
||
} catch (error) {
|
||
console.error('Error during reverse geocoding:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
|
||
function handleAlert(alertId) {
|
||
fetch(`./dealAlert?alertId=${alertId}`)
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
console.log('Alert processed:', data);
|
||
showCustomAlert(data.message, () => {
|
||
location.reload();
|
||
});
|
||
} else {
|
||
console.error('Failed to process alert:', data.message);
|
||
showCustomAlert('处理失败,请重试', null);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error processing alert:', error);
|
||
showCustomAlert('处理过程中发生错误,请重试', null);
|
||
});
|
||
}
|
||
|
||
// 添加自定义弹出框函数
|
||
function showCustomAlert(message, callback) {
|
||
// 创建遮罩层
|
||
const overlay = document.createElement('div');
|
||
overlay.className = 'custom-alert-overlay';
|
||
document.body.appendChild(overlay);
|
||
|
||
// 创建弹出框
|
||
const alertBox = document.createElement('div');
|
||
alertBox.className = 'custom-alert';
|
||
alertBox.innerHTML = `
|
||
<div class="custom-alert-content">${message}</div>
|
||
<button class="custom-alert-button">确定</button>
|
||
`;
|
||
document.body.appendChild(alertBox);
|
||
|
||
// 添加确定按钮点击事件
|
||
const confirmButton = alertBox.querySelector('.custom-alert-button');
|
||
confirmButton.onclick = () => {
|
||
document.body.removeChild(overlay);
|
||
document.body.removeChild(alertBox);
|
||
if (callback) callback();
|
||
};
|
||
}
|
||
|
||
|
||
function mapAlertStatus(alertStatus) {
|
||
switch (alertStatus) {
|
||
case 'responded':
|
||
return '已处理';
|
||
case 'pending':
|
||
return '未处理';
|
||
case 'closed':
|
||
return '已关闭';
|
||
default:
|
||
return '未知';
|
||
}
|
||
}
|
||
|
||
// 添加在其他 JavaScript 代码之前
|
||
function toggleFilter() {
|
||
const filterPanel = document.querySelector('.filter-panel');
|
||
const filterContainer = document.querySelector('.filter-container');
|
||
const toggleButton = document.querySelector('.filter-toggle i');
|
||
|
||
console.log('toggleFilter', toggleButton);
|
||
|
||
if (filterPanel.classList.contains('collapsed')) {
|
||
// 展开面板
|
||
filterPanel.classList.remove('collapsed');
|
||
toggleButton.classList.remove('fa-search');
|
||
toggleButton.classList.add('fa-times');
|
||
filterContainer.style.display = 'flex';
|
||
} else {
|
||
// 收起面板
|
||
filterPanel.classList.add('collapsed');
|
||
toggleButton.classList.remove('fa-times');
|
||
toggleButton.classList.add('fa-search');
|
||
filterContainer.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// 添加点击外部关闭筛选面板的功能
|
||
document.addEventListener('click', function(event) {
|
||
const filterPanel = document.querySelector('.filter-panel');
|
||
const filterToggle = document.querySelector('.filter-toggle');
|
||
|
||
// 如果点击的不是筛选面板内的元素且面板是展开的
|
||
if (!filterPanel.contains(event.target) &&
|
||
!filterToggle.contains(event.target) &&
|
||
!filterPanel.classList.contains('collapsed')) {
|
||
toggleFilter();
|
||
}
|
||
});
|
||
|
||
// 阻止筛选面板内的点击事件冒泡
|
||
document.querySelector('.filter-panel').addEventListener('click', function(event) {
|
||
event.stopPropagation();
|
||
});
|
||
|
||
// 初始化人员管理面板
|
||
function initPersonnelManagementPanel(data) {
|
||
// 更新总览数据
|
||
document.getElementById('totalUsers').innerText = data.user_info.totalUsers;
|
||
document.getElementById('totalBindDevices').innerText = data.user_info.totalDevices;
|
||
|
||
// 初始化部门分布图表
|
||
initDepartmentDistribution(data);
|
||
}
|
||
|
||
// 修改初始化部门分布图表函数 - 专业版
|
||
function initDepartmentDistribution(data) {
|
||
console.log('initDepartmentDistribution 开始执行,数据:', data); // 调试信息
|
||
|
||
const userInfo = data.user_info || {};
|
||
|
||
// 更新统计卡片数据
|
||
const totalUsers = userInfo.totalUsers || 0;
|
||
const totalDevices = userInfo.totalDevices || 0;
|
||
const departmentCount = userInfo.departmentCount || {};
|
||
const activeDeptCount = Object.keys(departmentCount).length;
|
||
|
||
// 模拟在线用户数据(实际应从后端获取)
|
||
const onlineUsers = Math.floor(totalUsers * 0.75); // 假设75%在线
|
||
const onlineRate = totalUsers > 0 ? ((onlineUsers / totalUsers) * 100).toFixed(1) : 0;
|
||
const alertUsers = Math.floor(totalUsers * 0.1); // 假设10%有告警
|
||
|
||
// 更新统计数据
|
||
document.getElementById('totalUsers').textContent = totalUsers;
|
||
document.getElementById('totalBindDevices').textContent = totalDevices;
|
||
document.getElementById('onlineRate').textContent = onlineRate + '%';
|
||
document.getElementById('activeDeptCount').textContent = activeDeptCount;
|
||
document.getElementById('onlineUsers').textContent = onlineUsers;
|
||
document.getElementById('boundDevices').textContent = totalDevices;
|
||
document.getElementById('alertUsers').textContent = alertUsers;
|
||
|
||
// 1. 部门分布图表 - 环形图
|
||
const departmentChart = echarts.init(document.getElementById('departmentDistribution'));
|
||
|
||
// 处理部门数据
|
||
const departmentData = Object.entries(departmentCount)
|
||
.map(([name, value]) => ({ name, value }))
|
||
.sort((a, b) => b.value - a.value);
|
||
|
||
// 如果没有数据,显示默认状态
|
||
const hasDeptData = departmentData.length > 0 && departmentData.some(d => d.value > 0);
|
||
const displayDeptData = hasDeptData ? departmentData : [
|
||
{ name: '暂无部门', value: 1 }
|
||
];
|
||
|
||
const deptColors = ['#00e4ff', '#00a8ff', '#0080ff', '#48cae4', '#90e0ef', '#a8dadc', '#457b9d'];
|
||
|
||
const departmentOption = {
|
||
tooltip: {
|
||
trigger: 'item',
|
||
backgroundColor: 'rgba(0,21,41,0.95)',
|
||
borderColor: '#00e4ff',
|
||
borderWidth: 1,
|
||
textStyle: { color: '#fff', fontSize: 11 },
|
||
formatter: function(params) {
|
||
if (!hasDeptData) return '暂无数据';
|
||
return `${params.name}<br/>人数: ${params.value}人<br/>占比: ${params.percent}%`;
|
||
}
|
||
},
|
||
series: [{
|
||
type: 'pie',
|
||
radius: ['40%', '75%'],
|
||
center: ['50%', '60%'],
|
||
avoidLabelOverlap: true,
|
||
itemStyle: {
|
||
borderColor: 'rgba(0,21,41,0.8)',
|
||
borderWidth: 2,
|
||
shadowBlur: 8,
|
||
shadowColor: 'rgba(0,228,255,0.3)'
|
||
},
|
||
label: {
|
||
show: true,
|
||
position: 'outside',
|
||
color: '#fff',
|
||
fontSize: 9,
|
||
formatter: function(params) {
|
||
if (!hasDeptData) return '';
|
||
return params.value > 0 ? `${params.name}\n${params.value}人` : '';
|
||
},
|
||
lineHeight: 10
|
||
},
|
||
labelLine: {
|
||
show: true,
|
||
length: 6,
|
||
length2: 4,
|
||
lineStyle: { color: 'rgba(255,255,255,0.5)', width: 1 }
|
||
},
|
||
emphasis: {
|
||
itemStyle: {
|
||
shadowBlur: 15,
|
||
shadowOffsetX: 0,
|
||
shadowColor: 'rgba(0,228,255,0.6)',
|
||
scale: 1.05
|
||
}
|
||
},
|
||
data: displayDeptData.map((item, index) => ({
|
||
...item,
|
||
itemStyle: { color: deptColors[index % deptColors.length] }
|
||
}))
|
||
}]
|
||
};
|
||
departmentChart.setOption(departmentOption);
|
||
|
||
// 2. 用户状态图表 - 柱状图
|
||
const statusChart = echarts.init(document.getElementById('userStatusChart'));
|
||
|
||
// 模拟状态数据
|
||
const statusData = {
|
||
online: onlineUsers,
|
||
offline: totalUsers - onlineUsers,
|
||
bound: totalDevices,
|
||
unbound: totalUsers - totalDevices,
|
||
alert: alertUsers,
|
||
normal: totalUsers - alertUsers
|
||
};
|
||
|
||
const statusOption = {
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
backgroundColor: 'rgba(0,21,41,0.95)',
|
||
borderColor: '#00e4ff',
|
||
textStyle: { color: '#fff', fontSize: 11 },
|
||
formatter: function(params) {
|
||
if (!params || !params[0]) return '';
|
||
const data = params[0];
|
||
return `${data.name}: ${data.value}人`;
|
||
}
|
||
},
|
||
grid: {
|
||
top: 25,
|
||
left: 25,
|
||
right: 15,
|
||
bottom: 20,
|
||
containLabel: true
|
||
},
|
||
xAxis: {
|
||
type: 'category',
|
||
data: ['在线', '离线', '绑定', '未绑定', '告警', '正常'],
|
||
axisLabel: {
|
||
color: '#7ecfff',
|
||
fontSize: 9,
|
||
interval: 0,
|
||
rotate: 0
|
||
},
|
||
axisLine: { lineStyle: { color: 'rgba(126,207,255,0.3)' } },
|
||
axisTick: { show: false }
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
axisLabel: { color: '#7ecfff', fontSize: 9 },
|
||
splitLine: { lineStyle: { color: 'rgba(126,207,255,0.1)', type: 'dashed' } },
|
||
axisLine: { show: false }
|
||
},
|
||
series: [{
|
||
type: 'bar',
|
||
data: [
|
||
{ value: statusData.online, itemStyle: { color: '#00ff9d' } },
|
||
{ value: statusData.offline, itemStyle: { color: '#666' } },
|
||
{ value: statusData.bound, itemStyle: { color: '#ffbb00' } },
|
||
{ value: statusData.unbound, itemStyle: { color: '#ff8800' } },
|
||
{ value: statusData.alert, itemStyle: { color: '#ff4444' } },
|
||
{ value: statusData.normal, itemStyle: { color: '#00e4ff' } }
|
||
],
|
||
barWidth: '60%',
|
||
label: {
|
||
show: true,
|
||
position: 'top',
|
||
color: '#fff',
|
||
fontSize: 9,
|
||
formatter: '{c}'
|
||
},
|
||
emphasis: {
|
||
itemStyle: {
|
||
shadowBlur: 10,
|
||
shadowColor: 'rgba(0,228,255,0.5)'
|
||
}
|
||
}
|
||
}]
|
||
};
|
||
statusChart.setOption(statusOption);
|
||
|
||
// 自适应大小
|
||
const resizeCharts = () => {
|
||
try {
|
||
departmentChart && departmentChart.resize();
|
||
statusChart && statusChart.resize();
|
||
} catch (e) {
|
||
console.warn('人员管理图表resize失败:', e);
|
||
}
|
||
};
|
||
|
||
window.addEventListener('resize', resizeCharts);
|
||
|
||
// 延迟执行确保图表正确渲染
|
||
setTimeout(() => {
|
||
resizeCharts();
|
||
console.log('人员管理图表初始化完成');
|
||
}, 200);
|
||
|
||
// 图表点击交互
|
||
departmentChart.on('click', function(params) {
|
||
if (hasDeptData && params.data && params.data.value > 0) {
|
||
showDepartmentDetails(params.data.name, params.data.value);
|
||
}
|
||
});
|
||
|
||
statusChart.on('click', function(params) {
|
||
const statusName = params.name;
|
||
const statusValue = params.value;
|
||
showStatusDetails(statusName, statusValue);
|
||
});
|
||
|
||
return { departmentChart, statusChart };
|
||
}
|
||
|
||
function getPastDateStr(daysAgo) {
|
||
const d = new Date();
|
||
d.setDate(d.getDate() - daysAgo);
|
||
return d.toISOString().slice(0,10);
|
||
}
|
||
|
||
function loadBaselineTrendChart(orgId) {
|
||
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();
|
||
});
|
||
}
|
||
|
||
// 渲染健康图表
|
||
function renderHealthChart(result) {
|
||
const {dates, metrics, health_summary} = result;
|
||
|
||
// 更新健康统计数据
|
||
if (health_summary) {
|
||
document.getElementById('healthScore').textContent = health_summary.overall_score || 0;
|
||
document.getElementById('normalCount').textContent = health_summary.normal_indicators || 0;
|
||
document.getElementById('riskCount').textContent = health_summary.risk_indicators || 0;
|
||
}
|
||
|
||
// 健康趋势图
|
||
const trendChart = echarts.init(document.getElementById('trendChart'));
|
||
|
||
// 核心健康指标
|
||
const mainMetrics = ['心率', '血氧', '体温', '压力', '睡眠'];
|
||
const metricColors = {
|
||
'心率': '#ff6b6b',
|
||
'血氧': '#00ff9d',
|
||
'体温': '#ffbb00',
|
||
'压力': '#ff9500',
|
||
'睡眠': '#7ecfff'
|
||
};
|
||
const series = [];
|
||
|
||
if (metrics && metrics.length > 0) {
|
||
mainMetrics.forEach(metricName => {
|
||
const metric = metrics.find(m => m.name === metricName);
|
||
if (metric && metric.values) {
|
||
series.push({
|
||
name: metric.name,
|
||
type: 'line',
|
||
data: metric.values,
|
||
smooth: true,
|
||
symbol: 'circle',
|
||
symbolSize: 4,
|
||
lineStyle: {
|
||
width: 2,
|
||
color: metricColors[metric.name] || '#00e4ff'
|
||
},
|
||
itemStyle: {
|
||
color: metricColors[metric.name] || '#00e4ff'
|
||
}
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
const trendOption = {
|
||
backgroundColor: 'transparent',
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
backgroundColor: 'rgba(10,24,48,0.98)',
|
||
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,
|
||
textStyle: { color: '#fff', fontSize: 10 }
|
||
},
|
||
grid: {
|
||
top: 35,
|
||
left: 30,
|
||
right: 20,
|
||
bottom: 25,
|
||
containLabel: true
|
||
},
|
||
xAxis: {
|
||
type: 'category',
|
||
data: dates,
|
||
axisLabel: {
|
||
color: '#7ecfff',
|
||
fontSize: 9
|
||
},
|
||
axisLine: { lineStyle: { color: 'rgba(0,228,255,0.2)' } }
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
axisLabel: {
|
||
color: '#7ecfff',
|
||
fontSize: 10
|
||
},
|
||
splitLine: {
|
||
lineStyle: {
|
||
color: 'rgba(0,228,255,0.1)',
|
||
type: 'dashed'
|
||
}
|
||
}
|
||
},
|
||
series
|
||
};
|
||
|
||
trendChart.setOption(trendOption);
|
||
|
||
// 图表点击事件
|
||
trendChart.on('click', function(params) {
|
||
console.log('点击了健康趋势图:', params);
|
||
});
|
||
|
||
// 自适应大小
|
||
window.addEventListener('resize', () => {
|
||
trendChart.resize();
|
||
});
|
||
}
|
||
|
||
// 获取指标单位
|
||
function getMetricUnit(metricName) {
|
||
const units = {
|
||
'心率': 'bpm',
|
||
'血氧': '%',
|
||
'体温': '°C',
|
||
'压力': '',
|
||
'睡眠': 'h'
|
||
};
|
||
return units[metricName] || '';
|
||
}
|
||
|
||
// 显示默认健康数据 - 增强版
|
||
function showDefaultHealthData() {
|
||
// 更新统计数据
|
||
document.getElementById('healthScore').textContent = '85';
|
||
document.getElementById('normalCount').textContent = '6';
|
||
document.getElementById('riskCount').textContent = '2';
|
||
|
||
// 显示默认趋势图 - 7天数据,5个指标
|
||
const trendChart = echarts.init(document.getElementById('trendChart'));
|
||
|
||
// 生成最近7天的日期
|
||
const defaultDates = [];
|
||
const today = new Date();
|
||
for (let i = 6; i >= 0; i--) {
|
||
const date = new Date(today);
|
||
date.setDate(date.getDate() - i);
|
||
defaultDates.push((date.getMonth() + 1).toString().padStart(2, '0') + '-' + date.getDate().toString().padStart(2, '0'));
|
||
}
|
||
|
||
const defaultOption = {
|
||
backgroundColor: 'transparent',
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
backgroundColor: 'rgba(10,24,48,0.98)',
|
||
borderColor: '#00e4ff',
|
||
borderWidth: 1,
|
||
textStyle: { color: '#fff', fontSize: 10 },
|
||
formatter: function(params) {
|
||
let result = params[0].name + '<br/>';
|
||
params.forEach(param => {
|
||
result += param.marker + param.seriesName + ': ' + param.value;
|
||
if (param.seriesName === '体温') result += '°C';
|
||
else if (param.seriesName === '血氧') result += '%';
|
||
else if (param.seriesName === '心率') result += 'bpm';
|
||
else if (param.seriesName === '睡眠') result += 'h';
|
||
result += '<br/>';
|
||
});
|
||
return result;
|
||
}
|
||
},
|
||
legend: {
|
||
show: true,
|
||
top: 5,
|
||
textStyle: { color: '#fff', fontSize: 10 },
|
||
itemWidth: 12,
|
||
itemHeight: 8
|
||
},
|
||
grid: {
|
||
top: 35,
|
||
left: 35,
|
||
right: 20,
|
||
bottom: 25,
|
||
containLabel: true
|
||
},
|
||
xAxis: {
|
||
type: 'category',
|
||
data: defaultDates,
|
||
axisLabel: { color: '#7ecfff', fontSize: 9 },
|
||
axisLine: { lineStyle: { color: 'rgba(0,228,255,0.2)' } }
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
axisLabel: { color: '#7ecfff', fontSize: 10 },
|
||
splitLine: {
|
||
lineStyle: {
|
||
color: 'rgba(0,228,255,0.1)',
|
||
type: 'dashed'
|
||
}
|
||
}
|
||
},
|
||
series: [
|
||
{
|
||
name: '心率',
|
||
type: 'line',
|
||
data: [72, 75, 78, 74, 76, 73, 77],
|
||
smooth: true,
|
||
lineStyle: { color: '#ff6b6b', width: 2 },
|
||
itemStyle: { color: '#ff6b6b' },
|
||
symbolSize: 4
|
||
},
|
||
{
|
||
name: '血氧',
|
||
type: 'line',
|
||
data: [98, 97, 99, 98, 97, 98, 99],
|
||
smooth: true,
|
||
lineStyle: { color: '#00ff9d', width: 2 },
|
||
itemStyle: { color: '#00ff9d' },
|
||
symbolSize: 4
|
||
},
|
||
{
|
||
name: '体温',
|
||
type: 'line',
|
||
data: [36.5, 36.7, 36.4, 36.6, 36.8, 36.5, 36.6],
|
||
smooth: true,
|
||
lineStyle: { color: '#ffbb00', width: 2 },
|
||
itemStyle: { color: '#ffbb00' },
|
||
symbolSize: 4
|
||
},
|
||
{
|
||
name: '压力',
|
||
type: 'line',
|
||
data: [45, 52, 38, 48, 55, 42, 47],
|
||
smooth: true,
|
||
lineStyle: { color: '#ff9500', width: 2 },
|
||
itemStyle: { color: '#ff9500' },
|
||
symbolSize: 4
|
||
},
|
||
{
|
||
name: '睡眠',
|
||
type: 'line',
|
||
data: [7.5, 8.2, 6.8, 7.8, 8.0, 7.2, 7.6],
|
||
smooth: true,
|
||
lineStyle: { color: '#7ecfff', width: 2 },
|
||
itemStyle: { color: '#7ecfff' },
|
||
symbolSize: 4
|
||
}
|
||
]
|
||
};
|
||
|
||
trendChart.setOption(defaultOption);
|
||
|
||
// 自适应大小
|
||
window.addEventListener('resize', () => {
|
||
trendChart.resize();
|
||
});
|
||
}
|
||
|
||
// 修改数据刷新函数
|
||
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);
|
||
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(lastTotalInfo);
|
||
|
||
// 刷新所有图表
|
||
if (globalCharts) {
|
||
// 更新健康评分图表
|
||
if (globalCharts.healthScore) {
|
||
// 获取日期范围
|
||
const today = new Date();
|
||
const yesterday = new Date(today);
|
||
yesterday.setDate(yesterday.getDate() - 7);
|
||
|
||
const startDate = formatDate(yesterday);
|
||
const endDate = formatDate(today);
|
||
|
||
fetch(`/health_data/score?orgId=${customerId}&startDate=${startDate}&endDate=${endDate}`)
|
||
.then(response => response.json())
|
||
.then(result => {
|
||
if (result.success && result.data && result.data.healthScores) {
|
||
const factors = result.data.healthScores.factors;
|
||
console.log('factors',factors);
|
||
|
||
// 更新总分显示
|
||
const totalScoreElement = document.querySelector('.total-score');
|
||
if (totalScoreElement) {
|
||
totalScoreElement.textContent = `总分:${result.data.summary.overallScore}`;
|
||
}
|
||
|
||
const healthScoreOption = {
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
formatter: function(params) {
|
||
return params[0].name + '<br/>' +
|
||
params[0].marker + params[0].seriesName + ':' + params[0].value;
|
||
}
|
||
},
|
||
radar: {
|
||
radius: '65%',
|
||
center: ['50%', '55%'],
|
||
indicator: [
|
||
{ name: `心率 ${factors.heartRate?.score || 0}分`, max: 100 },
|
||
{ name: `血氧 ${factors.bloodOxygen?.score || 0}分`, max: 100 },
|
||
{ name: `体温 ${factors.temperature?.score || 0}分`, max: 100 },
|
||
{ name: `步数 ${factors.step?.score || 0}分`, max: 100 },
|
||
{ name: `卡路里 ${factors.calorie?.score || 0}分`, max: 100 },
|
||
{ name: `收缩压 ${factors.pressureHigh?.score || 0}分`, max: 100 },
|
||
{ name: `舒张压 ${factors.pressureLow?.score || 0}分`, max: 100 },
|
||
{ name: `压力 ${factors.stress?.score || 0}分`, max: 100 }
|
||
],
|
||
name: {
|
||
textStyle: {
|
||
color: '#00e4ff',
|
||
fontSize: 12,
|
||
padding: [3, 5]
|
||
},
|
||
rich: {
|
||
value: {
|
||
color: '#00e4ff',
|
||
fontSize: 12,
|
||
fontWeight: 'normal'
|
||
}
|
||
}
|
||
},
|
||
splitArea: {
|
||
show: true,
|
||
areaStyle: {
|
||
color: ['rgba(0,228,255,0.1)', 'rgba(0,228,255,0.2)']
|
||
}
|
||
},
|
||
axisLine: {
|
||
lineStyle: {
|
||
color: 'rgba(0,228,255,0.5)'
|
||
}
|
||
},
|
||
splitLine: {
|
||
lineStyle: {
|
||
color: 'rgba(0,228,255,0.3)'
|
||
}
|
||
}
|
||
},
|
||
series: [{
|
||
name: '健康指标',
|
||
type: 'radar',
|
||
data: [{
|
||
value: [
|
||
factors.heartRate?.score || 0,
|
||
factors.bloodOxygen?.score || 0,
|
||
factors.temperature?.score || 0,
|
||
factors.step?.score || 0,
|
||
factors.calorie?.score || 0,
|
||
factors.pressureHigh?.score || 0,
|
||
factors.pressureLow?.score || 0,
|
||
factors.stress?.score || 0
|
||
],
|
||
name: '当前状态',
|
||
itemStyle: {
|
||
color: '#00e4ff'
|
||
},
|
||
areaStyle: {
|
||
color: 'rgba(0,228,255,0.4)'
|
||
}
|
||
}]
|
||
}]
|
||
};
|
||
globalCharts.healthScore.setOption(healthScoreOption);
|
||
} else {
|
||
// 如果没有数据,显示0分
|
||
const totalScoreElement = document.querySelector('.total-score');
|
||
if (totalScoreElement) {
|
||
totalScoreElement.textContent = '总分:0';
|
||
}
|
||
|
||
const healthScoreOption = {
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
formatter: function(params) {
|
||
return params[0].name + '<br/>' +
|
||
params[0].marker + params[0].seriesName + ':' + params[0].value;
|
||
}
|
||
},
|
||
radar: {
|
||
radius: '65%',
|
||
center: ['50%', '55%'],
|
||
indicator: [
|
||
{ name: '心率 0分', max: 100 },
|
||
{ name: '血氧 0分', max: 100 },
|
||
{ name: '体温 0分', max: 100 },
|
||
{ name: '步数 0分', max: 100 },
|
||
{ name: '卡路里 0分', max: 100 },
|
||
{ name: '收缩压 0分', max: 100 },
|
||
{ name: '舒张压 0分', max: 100 },
|
||
{ name: '压力 0分', max: 100 }
|
||
],
|
||
name: {
|
||
textStyle: {
|
||
color: '#00e4ff',
|
||
fontSize: 12,
|
||
padding: [3, 5]
|
||
},
|
||
rich: {
|
||
value: {
|
||
color: '#00e4ff',
|
||
fontSize: 12,
|
||
fontWeight: 'normal'
|
||
}
|
||
}
|
||
},
|
||
splitArea: {
|
||
show: true,
|
||
areaStyle: {
|
||
color: ['rgba(0,228,255,0.1)', 'rgba(0,228,255,0.2)']
|
||
}
|
||
},
|
||
axisLine: {
|
||
lineStyle: {
|
||
color: 'rgba(0,228,255,0.5)'
|
||
}
|
||
},
|
||
splitLine: {
|
||
lineStyle: {
|
||
color: 'rgba(0,228,255,0.3)'
|
||
}
|
||
}
|
||
},
|
||
series: [{
|
||
name: '健康指标',
|
||
type: 'radar',
|
||
data: [{
|
||
value: [0, 0, 0, 0, 0, 0, 0, 0],
|
||
name: '当前状态',
|
||
itemStyle: {
|
||
color: '#00e4ff'
|
||
},
|
||
areaStyle: {
|
||
color: 'rgba(0,228,255,0.4)'
|
||
}
|
||
}]
|
||
}]
|
||
};
|
||
globalCharts.healthScore.setOption(healthScoreOption);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error fetching health data:', error);
|
||
// 发生错误时也显示0分
|
||
const totalScoreElement = document.querySelector('.total-score');
|
||
if (totalScoreElement) {
|
||
totalScoreElement.textContent = '总分:0';
|
||
}
|
||
// 设置默认的0分图表
|
||
const healthScoreOption = {
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
formatter: function(params) {
|
||
return params[0].name + '<br/>' +
|
||
params[0].marker + params[0].seriesName + ':' + params[0].value;
|
||
}
|
||
},
|
||
radar: {
|
||
radius: '65%',
|
||
center: ['50%', '55%'],
|
||
indicator: [
|
||
{ name: '心率 0分', max: 100 },
|
||
{ name: '血氧 0分', max: 100 },
|
||
{ name: '体温 0分', max: 100 },
|
||
{ name: '步数 0分', max: 100 },
|
||
{ name: '卡路里 0分', max: 100 },
|
||
{ name: '收缩压 0分', max: 100 },
|
||
{ name: '舒张压 0分', max: 100 },
|
||
{ name: '压力 0分', max: 100 }
|
||
],
|
||
name: {
|
||
textStyle: {
|
||
color: '#00e4ff',
|
||
fontSize: 12,
|
||
padding: [3, 5]
|
||
},
|
||
rich: {
|
||
value: {
|
||
color: '#00e4ff',
|
||
fontSize: 12,
|
||
fontWeight: 'normal'
|
||
}
|
||
}
|
||
},
|
||
splitArea: {
|
||
show: true,
|
||
areaStyle: {
|
||
color: ['rgba(0,228,255,0.1)', 'rgba(0,228,255,0.2)']
|
||
}
|
||
},
|
||
axisLine: {
|
||
lineStyle: {
|
||
color: 'rgba(0,228,255,0.5)'
|
||
}
|
||
},
|
||
splitLine: {
|
||
lineStyle: {
|
||
color: 'rgba(0,228,255,0.3)'
|
||
}
|
||
}
|
||
},
|
||
series: [{
|
||
name: '健康指标',
|
||
type: 'radar',
|
||
data: [{
|
||
value: [0, 0, 0, 0, 0, 0, 0, 0],
|
||
name: '当前状态',
|
||
itemStyle: {
|
||
color: '#00e4ff'
|
||
},
|
||
areaStyle: {
|
||
color: 'rgba(0,228,255,0.4)'
|
||
}
|
||
}]
|
||
}]
|
||
};
|
||
globalCharts.healthScore.setOption(healthScoreOption);
|
||
});
|
||
}
|
||
}
|
||
|
||
// 更新人员管理面板
|
||
initPersonnelManagementPanel(data);
|
||
|
||
// 更新告警信息图表
|
||
initAlertChart(data);
|
||
|
||
// 更新设备管理图表
|
||
initDeviceChart(data);
|
||
|
||
// 更新消息列表
|
||
initMessageList(data);
|
||
|
||
// 为各个面板添加点击事件
|
||
setupPanelClickEvents(customerId);
|
||
}
|
||
})
|
||
.catch(error => console.error('Error fetching data:', error));
|
||
}
|
||
|
||
// 将面板点击事件设置抽取为单独的函数
|
||
function setupPanelClickEvents(customerId) {
|
||
// 1. 告警信息面板
|
||
const alertPanel = document.querySelector('.panel:has(#alertList)');
|
||
if (alertPanel) {
|
||
alertPanel.style.cursor = 'pointer';
|
||
alertPanel.onclick = function() {
|
||
createModalWindow(`/alert_view.html?customerId=${customerId}`);
|
||
};
|
||
}
|
||
|
||
// 2. 设备管理面板
|
||
const devicePanel = document.querySelector('.panel:has(#statsChart)');
|
||
if (devicePanel) {
|
||
devicePanel.style.cursor = 'pointer';
|
||
devicePanel.onclick = function() {
|
||
createModalWindow(`/device_view.html?customerId=${customerId}`);
|
||
};
|
||
}
|
||
|
||
// 3. 消息信息面板
|
||
const messagePanel = document.querySelector('.panel:has(#messageList)');
|
||
if (messagePanel) {
|
||
messagePanel.style.cursor = 'pointer';
|
||
messagePanel.onclick = function() {
|
||
createModalWindow(`/message_view.html?customerId=${customerId}`);
|
||
};
|
||
}
|
||
|
||
// 4. 趋势分析面板
|
||
const trendPanel = document.querySelector('.panel:has(#trendChart)');
|
||
if (trendPanel) {
|
||
trendPanel.style.cursor = 'pointer';
|
||
trendPanel.onclick = function() {
|
||
createModalWindow(`/health_main?customerId=${customerId}`);
|
||
};
|
||
}
|
||
|
||
// 5. 人员管理面板
|
||
const personnelPanel = document.querySelector('.panel:has(#departmentDistribution)');
|
||
if (personnelPanel) {
|
||
personnelPanel.style.cursor = 'pointer';
|
||
personnelPanel.onclick = function() {
|
||
createModalWindow(`/user_view.html?customerId=${customerId}`);
|
||
};
|
||
}
|
||
|
||
// 6. 健康评分面板
|
||
const scorePanel = document.querySelector('.panel:has(#healthScoreChart)');
|
||
if (scorePanel) {
|
||
scorePanel.style.cursor = 'pointer';
|
||
scorePanel.onclick = function() {
|
||
createModalWindow(`/user_health_data_analysis.html?customerId=${customerId}`);
|
||
};
|
||
}
|
||
}
|
||
|
||
// 修改初始化调用
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
setTimeout(() => {
|
||
globalCharts = initCharts(); // 存储图表实例
|
||
refreshData(); // 初始加载数据
|
||
// 每分钟刷新一次数据
|
||
setInterval(refreshData, 60000);
|
||
}, 100);
|
||
});
|
||
|
||
// 添加通用的创建模态窗口函数
|
||
function createModalWindow(url) {
|
||
const modalContainer = document.createElement('div');
|
||
modalContainer.className = 'modal-container';
|
||
modalContainer.innerHTML = `
|
||
<div class="modal-content">
|
||
<button class="modal-close">✖</button>
|
||
<div class="modal-header">
|
||
<div class="filter-controls">
|
||
<div class="select-group">
|
||
<select id="modalDeptSelect" class="modal-select">
|
||
<option value="">选择部门</option>
|
||
</select>
|
||
<select id="modalUserSelect" class="modal-select">
|
||
<option value="">选择用户</option>
|
||
</select>
|
||
</div>
|
||
<div class="date-picker" style="display: none;">
|
||
<!-- 预留时间选择器位置 -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<iframe src="${url}" class="user-view-iframe"></iframe>
|
||
</div>
|
||
`;
|
||
|
||
document.body.appendChild(modalContainer);
|
||
|
||
// 添加样式
|
||
const style = document.createElement('style');
|
||
style.textContent = `
|
||
.modal-header {
|
||
position: absolute;
|
||
top: 10px;
|
||
right: 50px;
|
||
z-index: 2;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 20px;
|
||
}
|
||
|
||
.filter-controls {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 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 = 6; // 最多显示6种类型
|
||
if (displayTypes.length > MAX_DISPLAY_TYPES) {
|
||
// 按数值排序,取前6个
|
||
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 > 4 ? value.substring(0, 4) + '...' : value;
|
||
}
|
||
},
|
||
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 > 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 trendChart = echarts.init(document.getElementById('alertTrendChart'));
|
||
|
||
// 处理时间数据,按小时统计
|
||
const hourlyData = {};
|
||
const now = new Date();
|
||
|
||
// 初始化24小时数据
|
||
for (let i = 0; i < 24; i++) {
|
||
hourlyData[i] = 0;
|
||
}
|
||
|
||
// 统计告警数据
|
||
if (alerts && alerts.length > 0) {
|
||
alerts.forEach(alert => {
|
||
try {
|
||
let alertTime;
|
||
if (alert.alert_timestamp) {
|
||
// 尝试解析时间戳
|
||
alertTime = new Date(alert.alert_timestamp);
|
||
if (isNaN(alertTime.getTime())) {
|
||
// 如果解析失败,尝试其他格式
|
||
alertTime = new Date(alert.alert_timestamp.replace(/-/g, '/'));
|
||
}
|
||
} else if (alert.timestamp) {
|
||
alertTime = new Date(alert.timestamp);
|
||
} else {
|
||
alertTime = now; // 默认当前时间
|
||
}
|
||
|
||
if (!isNaN(alertTime.getTime())) {
|
||
const hour = alertTime.getHours();
|
||
hourlyData[hour] = (hourlyData[hour] || 0) + 1;
|
||
}
|
||
} catch (e) {
|
||
console.warn('解析告警时间失败:', alert, e);
|
||
}
|
||
});
|
||
}
|
||
|
||
const hours = Array.from({length: 24}, (_, i) => i);
|
||
const hourlyValues = hours.map(h => hourlyData[h] || 0);
|
||
const maxValue = Math.max(...hourlyValues, 1);
|
||
|
||
const trendOption = {
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
backgroundColor: 'rgba(0,21,41,0.95)',
|
||
borderColor: '#00e4ff',
|
||
textStyle: { color: '#fff', fontSize: 11 },
|
||
formatter: function(params) {
|
||
if (!params || !params[0]) return '';
|
||
const data = params[0];
|
||
return `${data.name}:00<br/>告警数量: ${data.value}次`;
|
||
}
|
||
},
|
||
grid: { top: 25, left: 25, right: 15, bottom: 20 },
|
||
xAxis: {
|
||
type: 'category',
|
||
data: hours.map(h => h.toString().padStart(2, '0')),
|
||
axisLabel: {
|
||
color: '#7ecfff',
|
||
fontSize: 9,
|
||
interval: 3
|
||
},
|
||
axisLine: { lineStyle: { color: 'rgba(126,207,255,0.3)' } },
|
||
axisTick: { show: false }
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
max: Math.max(maxValue + 1, 3),
|
||
axisLabel: { color: '#7ecfff', fontSize: 9 },
|
||
splitLine: { lineStyle: { color: 'rgba(126,207,255,0.1)', type: 'dashed' } },
|
||
axisLine: { show: false }
|
||
},
|
||
series: [{
|
||
type: 'line',
|
||
data: hourlyValues,
|
||
smooth: true,
|
||
lineStyle: {
|
||
color: '#00e4ff',
|
||
width: 2,
|
||
shadowColor: 'rgba(0,228,255,0.3)',
|
||
shadowBlur: 5
|
||
},
|
||
itemStyle: {
|
||
color: '#00e4ff',
|
||
borderColor: '#fff',
|
||
borderWidth: 1
|
||
},
|
||
areaStyle: {
|
||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||
{ offset: 0, color: 'rgba(0,228,255,0.4)' },
|
||
{ offset: 1, color: 'rgba(0,228,255,0.05)' }
|
||
])
|
||
},
|
||
symbol: 'circle',
|
||
symbolSize: function(value) {
|
||
return value > 0 ? 4 : 2;
|
||
},
|
||
emphasis: {
|
||
itemStyle: {
|
||
color: '#fff',
|
||
borderColor: '#00e4ff',
|
||
borderWidth: 2,
|
||
shadowColor: 'rgba(0,228,255,0.6)',
|
||
shadowBlur: 8
|
||
},
|
||
scale: 1.2
|
||
}
|
||
}]
|
||
};
|
||
trendChart.setOption(trendOption);
|
||
|
||
// 添加动画样式
|
||
const style = document.createElement('style');
|
||
style.textContent = `
|
||
@keyframes pulse {
|
||
0%, 100% { transform: scale(1); opacity: 1; }
|
||
50% { transform: scale(1.05); opacity: 0.8; }
|
||
}
|
||
|
||
.alert-stat-item {
|
||
display: flex;
|
||
align-items: baseline;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.alert-stat-item:hover {
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
#alertBadge {
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
#alertBadge:hover {
|
||
transform: scale(1.1);
|
||
box-shadow: 0 0 10px rgba(0,228,255,0.5);
|
||
}
|
||
`;
|
||
document.head.appendChild(style);
|
||
|
||
// 自适应大小 - 延迟执行确保DOM已渲染
|
||
setTimeout(() => {
|
||
const resizeCharts = () => {
|
||
try {
|
||
typeChart && typeChart.resize();
|
||
levelChart && levelChart.resize();
|
||
statusChart && statusChart.resize();
|
||
trendChart && trendChart.resize();
|
||
} catch (e) {
|
||
console.warn('图表resize失败:', e);
|
||
}
|
||
};
|
||
|
||
resizeCharts(); // 立即执行一次
|
||
window.addEventListener('resize', resizeCharts);
|
||
}, 100);
|
||
|
||
// 添加点击事件显示详细告警列表
|
||
badge.onclick = () => showAlertDetails(alerts);
|
||
|
||
// 图表点击交互
|
||
typeChart.on('click', function(params) {
|
||
if (hasTypeData && params.dataIndex < alertTypes.length) {
|
||
const alertType = alertTypes[params.dataIndex];
|
||
const filteredAlerts = alerts.filter(alert => alert.alert_type === alertType);
|
||
showAlertDetails(filteredAlerts, `${translateAlertType(alertType)}告警详情`);
|
||
}
|
||
});
|
||
|
||
// 确保图表正确渲染 - 延迟执行
|
||
setTimeout(() => {
|
||
try {
|
||
typeChart.resize();
|
||
levelChart.resize();
|
||
statusChart.resize();
|
||
trendChart.resize();
|
||
console.log('告警图表初始化完成');
|
||
} catch (e) {
|
||
console.warn('告警图表初始化失败:', e);
|
||
}
|
||
}, 200);
|
||
|
||
return { typeChart, levelChart, statusChart, trendChart };
|
||
}
|
||
|
||
// 显示详细告警列表
|
||
function showAlertDetails(alerts, title = '📋 详细告警列表') {
|
||
const modal = document.createElement('div');
|
||
modal.className = 'modal-container';
|
||
modal.innerHTML = `
|
||
<div class="modal-content" style="width: 85%; height: 85%;">
|
||
<button class="modal-close">✕</button>
|
||
<h3 style="color: #00e4ff; margin-bottom: 20px; text-align: center; font-size: 18px;">${title}</h3>
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; padding: 10px; background: rgba(0,228,255,0.1); border-radius: 6px;">
|
||
<div style="color: #fff; font-size: 14px;">
|
||
共 <span style="color: #00e4ff; font-weight: bold;">${alerts.length}</span> 条告警记录
|
||
</div>
|
||
<div style="display: flex; gap: 10px;">
|
||
<button onclick="exportAlerts()" style="background: rgba(0,228,255,0.2); border: 1px solid #00e4ff; color: #00e4ff; padding: 5px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;">📊 导出</button>
|
||
<button onclick="refreshAlerts()" style="background: rgba(0,228,255,0.2); border: 1px solid #00e4ff; color: #00e4ff; padding: 5px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;">🔄 刷新</button>
|
||
</div>
|
||
</div>
|
||
<div style="height: calc(100% - 100px); overflow-y: auto; border: 1px solid rgba(0,228,255,0.2); border-radius: 6px;">
|
||
<table style="width: 100%; border-collapse: collapse; color: #fff; font-size: 13px;">
|
||
<thead style="position: sticky; top: 0; z-index: 10;">
|
||
<tr style="background: rgba(0,228,255,0.3); border-bottom: 2px solid #00e4ff;">
|
||
<th style="padding: 12px 8px; text-align: left; font-weight: bold; border-right: 1px solid rgba(0,228,255,0.2);">时间</th>
|
||
<th style="padding: 12px 8px; text-align: left; font-weight: bold; border-right: 1px solid rgba(0,228,255,0.2);">类型</th>
|
||
<th style="padding: 12px 8px; text-align: left; font-weight: bold; border-right: 1px solid rgba(0,228,255,0.2);">级别</th>
|
||
<th style="padding: 12px 8px; text-align: left; font-weight: bold; border-right: 1px solid rgba(0,228,255,0.2);">状态</th>
|
||
<th style="padding: 12px 8px; text-align: left; font-weight: bold; border-right: 1px solid rgba(0,228,255,0.2);">用户</th>
|
||
<th style="padding: 12px 8px; text-align: left; font-weight: bold; border-right: 1px solid rgba(0,228,255,0.2);">部门</th>
|
||
<th style="padding: 12px 8px; text-align: left; font-weight: bold; border-right: 1px solid rgba(0,228,255,0.2);">设备</th>
|
||
<th style="padding: 12px 8px; text-align: left; font-weight: bold;">描述</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${alerts.map((alert, index) => `
|
||
<tr style="border-bottom: 1px solid rgba(255,255,255,0.1); transition: all 0.2s ease; ${alert.severity_level === 'critical' ? 'background: rgba(255,68,68,0.15);' : index % 2 === 0 ? 'background: rgba(0,21,41,0.3);' : 'background: rgba(0,21,41,0.1);'}"
|
||
onmouseover="this.style.background='rgba(0,228,255,0.1)'"
|
||
onmouseout="this.style.background='${alert.severity_level === 'critical' ? 'rgba(255,68,68,0.15)' : index % 2 === 0 ? 'rgba(0,21,41,0.3)' : 'rgba(0,21,41,0.1)'}'">
|
||
<td style="padding: 10px 8px; font-size: 11px; border-right: 1px solid rgba(255,255,255,0.05);">
|
||
<div style="color: #7ecfff;">${alert.alert_timestamp.split(' ')[0]}</div>
|
||
<div style="color: #fff; font-size: 10px;">${alert.alert_timestamp.split(' ')[1]}</div>
|
||
</td>
|
||
<td style="padding: 10px 8px; border-right: 1px solid rgba(255,255,255,0.05);">
|
||
<span style="background: ${getAlertTypeColor(alert.alert_type)}; padding: 3px 8px; border-radius: 12px; font-size: 10px; font-weight: bold; color: #000;">
|
||
${translateAlertType(alert.alert_type)}
|
||
</span>
|
||
</td>
|
||
<td style="padding: 10px 8px; border-right: 1px solid rgba(255,255,255,0.05);">
|
||
<span style="background: ${alert.severity_level === 'critical' ? '#ff4444' : '#ffbb00'}; padding: 3px 8px; border-radius: 12px; font-size: 10px; font-weight: bold; color: #000;">
|
||
${alert.severity_level === 'critical' ? '🔴 严重' : '🟡 中等'}
|
||
</span>
|
||
</td>
|
||
<td style="padding: 10px 8px; border-right: 1px solid rgba(255,255,255,0.05);">
|
||
<span style="background: ${alert.alert_status === 'pending' ? '#ff4444' : '#00e4ff'}; padding: 3px 8px; border-radius: 12px; font-size: 10px; font-weight: bold; color: ${alert.alert_status === 'pending' ? '#fff' : '#000'};">
|
||
${alert.alert_status === 'pending' ? '⏳ 待处理' : '✅ 已处理'}
|
||
</span>
|
||
</td>
|
||
<td style="padding: 10px 8px; font-size: 12px; border-right: 1px solid rgba(255,255,255,0.05);">
|
||
<div style="color: #00e4ff; font-weight: bold;">${alert.user_name}</div>
|
||
<div style="color: #7ecfff; font-size: 10px;">ID: ${alert.user_id}</div>
|
||
</td>
|
||
<td style="padding: 10px 8px; font-size: 12px; border-right: 1px solid rgba(255,255,255,0.05);">
|
||
<div style="color: #fff;">${alert.dept_name}</div>
|
||
<div style="color: #7ecfff; font-size: 10px;">部门ID: ${alert.dept_id}</div>
|
||
</td>
|
||
<td style="padding: 10px 8px; font-size: 11px; border-right: 1px solid rgba(255,255,255,0.05);">
|
||
<div style="color: #ffbb00; font-family: monospace;">${alert.device_sn}</div>
|
||
${alert.health_id ? `<div style="color: #7ecfff; font-size: 10px;">健康ID: ${alert.health_id}</div>` : ''}
|
||
</td>
|
||
<td style="padding: 10px 8px; font-size: 11px; max-width: 200px; word-wrap: break-word; line-height: 1.4;">
|
||
<div style="color: #fff;">${alert.alert_desc || '无详细描述'}</div>
|
||
${alert.alert_status === 'pending' ? `
|
||
<button onclick="handleAlert('${alert.alert_id}')"
|
||
style="margin-top: 5px; background: #ff4444; color: #fff; border: none; padding: 3px 8px; border-radius: 4px; cursor: pointer; font-size: 10px; transition: all 0.2s ease;"
|
||
onmouseover="this.style.background='#ff6666'"
|
||
onmouseout="this.style.background='#ff4444'">
|
||
🚨 立即处理
|
||
</button>
|
||
` : ''}
|
||
</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
${alerts.length === 0 ? `
|
||
<div style="text-align: center; padding: 40px; color: #7ecfff;">
|
||
<div style="font-size: 48px; margin-bottom: 10px;">📭</div>
|
||
<div style="font-size: 16px;">暂无告警记录</div>
|
||
<div style="font-size: 12px; margin-top: 5px; color: rgba(255,255,255,0.5);">系统运行正常</div>
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
document.body.appendChild(modal);
|
||
|
||
// 关闭事件
|
||
modal.querySelector('.modal-close').onclick = () => modal.remove();
|
||
modal.onclick = (e) => {
|
||
if (e.target === modal) modal.remove();
|
||
};
|
||
|
||
// 添加键盘事件
|
||
document.addEventListener('keydown', function escHandler(e) {
|
||
if (e.key === 'Escape') {
|
||
modal.remove();
|
||
document.removeEventListener('keydown', escHandler);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 导出告警数据
|
||
function exportAlerts() {
|
||
// 这里可以实现导出功能
|
||
showCustomAlert('导出功能开发中...', null);
|
||
}
|
||
|
||
// 刷新告警数据
|
||
function refreshAlerts() {
|
||
refreshData();
|
||
showCustomAlert('告警数据已刷新', null);
|
||
}
|
||
|
||
// 获取告警类型颜色
|
||
function getAlertTypeColor(type) {
|
||
const colors = {
|
||
'temperature': '#ff4444',
|
||
'stress': '#ff8800',
|
||
'heart_rate': '#00e4ff',
|
||
'blood_pressure': '#ffbb00',
|
||
'blood_oxygen': '#ff6666'
|
||
};
|
||
return colors[type] || '#00e4ff';
|
||
}
|
||
|
||
// 修改设备管理图表初始化函数
|
||
function initDeviceChart(data) {
|
||
const statsContainer = document.getElementById('statsChart');
|
||
if (!statsContainer) return;
|
||
|
||
const statsChart = echarts.init(statsContainer);
|
||
|
||
const deviceInfo = data.device_info || {};
|
||
const totalDevices = deviceInfo.totalDevices || 0;
|
||
document.getElementById('totalWatchDevices').textContent = totalDevices;
|
||
|
||
|
||
|
||
// 添加点击事件监听器
|
||
statsContainer.onclick = () => {
|
||
const legendData = {
|
||
'部门分布': deviceInfo.departmentDeviceCount || {},
|
||
'充电状态': deviceInfo.deviceChargingCount || {},
|
||
'设备状态': deviceInfo.deviceStatusCount || {},
|
||
'系统版本': deviceInfo.deviceSystemVersionCount || {},
|
||
'佩戴状态': deviceInfo.deviceWearableCount || {}
|
||
};
|
||
showFullLegend(legendData);
|
||
};
|
||
|
||
// 处理数据为堆叠格式
|
||
const categories = ['部门分布', '充电状态', '设备状态', '系统版本', '佩戴状态'];
|
||
|
||
// 处理部门设备数据
|
||
const departmentData = Object.entries(deviceInfo.departmentDeviceCount || {}).map(([name, value]) => ({
|
||
name,
|
||
value
|
||
}));
|
||
|
||
// 处理充电状态数据
|
||
const chargingData = Object.entries(deviceInfo.deviceChargingCount || {}).map(([status, value]) => ({
|
||
name: status === 'NOT_CHARGING' ? '未充电' : '充电中',
|
||
value
|
||
}));
|
||
|
||
// 处理设备状态数据
|
||
const statusData = Object.entries(deviceInfo.deviceStatusCount || {}).map(([status, value]) => ({
|
||
name: status === 'ACTIVE' ? '活跃' : '非活跃',
|
||
value
|
||
}));
|
||
|
||
// 处理系统版本数据
|
||
const versionData = Object.entries(deviceInfo.deviceSystemVersionCount || {}).map(([version, value]) => ({
|
||
name: version,
|
||
value
|
||
}));
|
||
|
||
// 处理佩戴状态数据
|
||
const wearableData = Object.entries(deviceInfo.deviceWearableCount || {}).map(([status, value]) => ({
|
||
name: status === 'WORN' ? '已佩戴' : '未佩戴',
|
||
value
|
||
}));
|
||
|
||
// 创建系列数据
|
||
const series = [];
|
||
|
||
// 部门分布系列
|
||
departmentData.forEach((item, index) => {
|
||
series.push({
|
||
name: item.name,
|
||
type: 'bar',
|
||
stack: '部门分布',
|
||
emphasis: {
|
||
focus: 'series'
|
||
},
|
||
data: [item.value, 0, 0, 0, 0]
|
||
});
|
||
});
|
||
|
||
// 充电状态系列
|
||
chargingData.forEach((item, index) => {
|
||
series.push({
|
||
name: item.name,
|
||
type: 'bar',
|
||
stack: '充电状态',
|
||
emphasis: {
|
||
focus: 'series'
|
||
},
|
||
data: [0, item.value, 0, 0, 0]
|
||
});
|
||
});
|
||
|
||
// 设备状态系列
|
||
statusData.forEach((item, index) => {
|
||
series.push({
|
||
name: item.name,
|
||
type: 'bar',
|
||
stack: '设备状态',
|
||
emphasis: {
|
||
focus: 'series'
|
||
},
|
||
data: [0, 0, item.value, 0, 0]
|
||
});
|
||
});
|
||
|
||
// 系统版本系列
|
||
versionData.forEach((item, index) => {
|
||
series.push({
|
||
name: item.name,
|
||
type: 'bar',
|
||
stack: '系统版本',
|
||
emphasis: {
|
||
focus: 'series'
|
||
},
|
||
data: [0, 0, 0, item.value, 0]
|
||
});
|
||
});
|
||
|
||
// 佩戴状态系列
|
||
wearableData.forEach((item, index) => {
|
||
series.push({
|
||
name: item.name,
|
||
type: 'bar',
|
||
stack: '佩戴状态',
|
||
emphasis: {
|
||
focus: 'series'
|
||
},
|
||
data: [0, 0, 0, 0, item.value]
|
||
});
|
||
});
|
||
|
||
const statsOption = {
|
||
title: {
|
||
text: '设备状态统计',
|
||
textStyle: {
|
||
color: '#fff',
|
||
fontSize: 14
|
||
},
|
||
left: 'center',
|
||
top: 10
|
||
},
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
axisPointer: {
|
||
type: 'shadow'
|
||
},
|
||
formatter: function(params) {
|
||
const stackName = params[0].axisValue;
|
||
let result = `${stackName}<br/>`;
|
||
params.forEach(param => {
|
||
if (param.value > 0) {
|
||
result += `${param.seriesName}: ${param.value}<br/>`;
|
||
}
|
||
});
|
||
return result;
|
||
}
|
||
},
|
||
legend: {
|
||
textStyle: {
|
||
color: '#fff'
|
||
},
|
||
top: 35,
|
||
type: 'scroll',
|
||
orient: 'horizontal'
|
||
},
|
||
grid: {
|
||
left: '3%',
|
||
right: '4%',
|
||
bottom: '3%',
|
||
top: '25%',
|
||
containLabel: true
|
||
},
|
||
xAxis: {
|
||
type: 'category',
|
||
data: categories,
|
||
axisLabel: {
|
||
color: '#fff',
|
||
interval: 0,
|
||
rotate: 30
|
||
}
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
axisLabel: {
|
||
color: '#fff'
|
||
},
|
||
splitLine: {
|
||
lineStyle: {
|
||
color: 'rgba(255,255,255,0.1)'
|
||
}
|
||
}
|
||
},
|
||
series: series
|
||
};
|
||
|
||
statsChart.setOption(statsOption);
|
||
return statsChart;
|
||
}
|
||
|
||
// 修改趋势分析图表,增加预测数据
|
||
function updateTrendChart(data) {
|
||
const trendContainer = document.getElementById('trendChart');
|
||
if (!trendContainer) return;
|
||
|
||
const trendChart = echarts.init(trendContainer);
|
||
|
||
const trendOption = {
|
||
title: {
|
||
text: '健康指标趋势及预测',
|
||
textStyle: {
|
||
color: '#fff',
|
||
fontSize: 16
|
||
}
|
||
},
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
backgroundColor: 'rgba(0,21,41,0.9)',
|
||
borderColor: 'rgba(0,228,255,0.2)',
|
||
textStyle: { color: '#fff' }
|
||
},
|
||
legend: {
|
||
data: ['心率', '血压', '压力指数', '距离', '卡路里', '步数', '预测值'],
|
||
textStyle: { color: '#fff' },
|
||
top: 30
|
||
},
|
||
grid: {
|
||
left: '3%',
|
||
right: '4%',
|
||
bottom: '3%',
|
||
containLabel: true
|
||
},
|
||
xAxis: {
|
||
type: 'category',
|
||
boundaryGap: false,
|
||
data: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00', '预测'],
|
||
axisLabel: { color: '#fff' },
|
||
axisLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
axisLabel: { color: '#fff' },
|
||
splitLine: {
|
||
lineStyle: {
|
||
color: 'rgba(255,255,255,0.1)',
|
||
type: 'dashed'
|
||
}
|
||
}
|
||
},
|
||
series: [
|
||
{
|
||
name: '心率',
|
||
type: 'line',
|
||
smooth: true,
|
||
data: [75, 72, 78, 85, 80, 75, 77],
|
||
itemStyle: { color: '#ff4444' }
|
||
},
|
||
{
|
||
name: '血压',
|
||
type: 'line',
|
||
smooth: true,
|
||
data: [120, 118, 122, 125, 121, 119, 120],
|
||
itemStyle: { color: '#00e4ff' }
|
||
},
|
||
{
|
||
name: '压力指数',
|
||
type: 'line',
|
||
smooth: true,
|
||
data: [45, 48, 52, 55, 50, 47, 49],
|
||
itemStyle: { color: '#ffbb00' }
|
||
},
|
||
{
|
||
name: '距离',
|
||
type: 'line',
|
||
smooth: true,
|
||
data: [2.1, 2.3, 2.8, 3.2, 3.5, 3.8, 4.0],
|
||
itemStyle: { color: '#00ff9d' }
|
||
},
|
||
{
|
||
name: '卡路里',
|
||
type: 'line',
|
||
smooth: true,
|
||
data: [150, 180, 220, 280, 320, 350, 380],
|
||
itemStyle: { color: '#ff7777' }
|
||
},
|
||
{
|
||
name: '步数',
|
||
type: 'line',
|
||
smooth: true,
|
||
data: [2000, 2500, 3000, 3800, 4200, 4500, 4800],
|
||
itemStyle: { color: '#7777ff' }
|
||
}
|
||
]
|
||
};
|
||
|
||
// 为每个系列添加预测区域
|
||
trendOption.series.forEach(series => {
|
||
const lastIndex = series.data.length - 1;
|
||
series.markArea = {
|
||
itemStyle: {
|
||
color: 'rgba(0, 228, 255, 0.1)'
|
||
},
|
||
data: [[{
|
||
xAxis: trendOption.xAxis.data[lastIndex - 1]
|
||
}, {
|
||
xAxis: trendOption.xAxis.data[lastIndex]
|
||
}]]
|
||
};
|
||
});
|
||
|
||
trendChart.setOption(trendOption);
|
||
return trendChart;
|
||
}
|
||
|
||
// 人员管理面板交互函数
|
||
function showPersonnelDetails() {
|
||
showCustomAlert('人员详情功能:显示完整的人员管理统计信息');
|
||
}
|
||
|
||
function filterByDepartment() {
|
||
showCustomAlert('部门筛选功能:按部门查看人员分布详情');
|
||
}
|
||
|
||
function filterByOnlineStatus() {
|
||
showCustomAlert('在线状态筛选功能:显示在线/离线人员列表');
|
||
}
|
||
|
||
function filterByDeviceStatus() {
|
||
showCustomAlert('设备状态筛选功能:显示已绑定/未绑定设备人员');
|
||
}
|
||
|
||
function filterByAlertStatus() {
|
||
showCustomAlert('告警状态筛选功能:显示有告警的人员列表');
|
||
}
|
||
|
||
// 显示部门详情
|
||
function showDepartmentDetails(deptName, userCount) {
|
||
const modal = document.createElement('div');
|
||
modal.className = 'modal-container';
|
||
modal.innerHTML = `
|
||
<div class="modal-content" style="width: 60%; height: 70%;">
|
||
<button class="modal-close">✕</button>
|
||
<h3 style="color: #00e4ff; margin-bottom: 20px; text-align: center; font-size: 18px;">📊 ${deptName} 部门详情</h3>
|
||
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; padding: 20px;">
|
||
<div style="background: rgba(0,228,255,0.1); padding: 20px; border-radius: 8px; text-align: center;">
|
||
<div style="color: #00e4ff; font-size: 24px; font-weight: bold;">${userCount}</div>
|
||
<div style="color: #fff; margin-top: 5px;">总人数</div>
|
||
</div>
|
||
<div style="background: rgba(0,255,157,0.1); padding: 20px; border-radius: 8px; text-align: center;">
|
||
<div style="color: #00ff9d; font-size: 24px; font-weight: bold;">${Math.floor(userCount * 0.8)}</div>
|
||
<div style="color: #fff; margin-top: 5px;">在线人数</div>
|
||
</div>
|
||
<div style="background: rgba(255,187,0,0.1); padding: 20px; border-radius: 8px; text-align: center;">
|
||
<div style="color: #ffbb00; font-size: 24px; font-weight: bold;">${Math.floor(userCount * 0.9)}</div>
|
||
<div style="color: #fff; margin-top: 5px;">设备绑定</div>
|
||
</div>
|
||
</div>
|
||
<div style="text-align: center; margin-top: 20px; color: #7ecfff;">
|
||
点击可查看该部门的详细人员列表和设备状态
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
document.body.appendChild(modal);
|
||
|
||
// 关闭事件
|
||
modal.querySelector('.modal-close').onclick = () => modal.remove();
|
||
modal.onclick = (e) => {
|
||
if (e.target === modal) modal.remove();
|
||
};
|
||
}
|
||
|
||
// 显示状态详情
|
||
function showStatusDetails(statusName, statusValue) {
|
||
const statusMap = {
|
||
'在线': '当前在线的用户列表',
|
||
'离线': '当前离线的用户列表',
|
||
'绑定': '已绑定设备的用户',
|
||
'未绑定': '未绑定设备的用户',
|
||
'告警': '当前有告警的用户',
|
||
'正常': '状态正常的用户'
|
||
};
|
||
|
||
const modal = document.createElement('div');
|
||
modal.className = 'modal-container';
|
||
modal.innerHTML = `
|
||
<div class="modal-content" style="width: 70%; height: 80%;">
|
||
<button class="modal-close">✕</button>
|
||
<h3 style="color: #00e4ff; margin-bottom: 20px; text-align: center; font-size: 18px;">📋 ${statusName}用户详情</h3>
|
||
<div style="background: rgba(0,228,255,0.1); padding: 15px; border-radius: 8px; margin-bottom: 20px; text-align: center;">
|
||
<div style="color: #00e4ff; font-size: 28px; font-weight: bold;">${statusValue}</div>
|
||
<div style="color: #fff; margin-top: 5px;">${statusMap[statusName] || '用户统计'}</div>
|
||
</div>
|
||
<div style="color: #7ecfff; text-align: center; padding: 40px;">
|
||
<div style="font-size: 48px; margin-bottom: 15px;">👥</div>
|
||
<div style="font-size: 16px;">详细用户列表功能开发中...</div>
|
||
<div style="font-size: 12px; margin-top: 10px; color: rgba(255,255,255,0.5);">将显示具体的用户信息、设备状态和健康数据</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
document.body.appendChild(modal);
|
||
|
||
// 关闭事件
|
||
modal.querySelector('.modal-close').onclick = () => modal.remove();
|
||
modal.onclick = (e) => {
|
||
if (e.target === modal) modal.remove();
|
||
};
|
||
}
|
||
|
||
</script>
|
||
|
||
|
||
|
||
</script>
|
||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||
<script>
|
||
$(document).ready(function() {
|
||
// 获取部门数据
|
||
$.ajax({
|
||
url: `/get_departments?orgId={{ customerId }}`,
|
||
method: 'GET',
|
||
success: function(response) {
|
||
console.log('response', response);
|
||
if (response.success && response.data) {
|
||
updateDepartmentSelect(response.data);
|
||
} else {
|
||
console.error('Invalid response format:', response);
|
||
}
|
||
},
|
||
error: function(error) {
|
||
console.error('Failed to fetch departments:', error);
|
||
}
|
||
});
|
||
|
||
// 部门选择变化时更新用户列表
|
||
$('#deptSelect').change(function() {
|
||
const selectedDeptId = $(this).val();
|
||
currentDept=selectedDeptId
|
||
console.log('currentDept',currentDept);
|
||
updateUsers(selectedDeptId);
|
||
});
|
||
|
||
// 用户选择变化时刷新地图
|
||
$('#userSelect').change(function() {
|
||
currentUser=$(this).val()
|
||
//console.log('currentUser',currentUser);
|
||
// 调用 initializeMap 刷新数据
|
||
//initializeMap(selectedDeptId, selectedUserId);
|
||
updateMapData(lastTotalInfo);
|
||
});
|
||
|
||
|
||
|
||
// 更新部门选择框
|
||
function updateDepartmentSelect(departments) {
|
||
const deptSelect = document.getElementById('deptSelect');
|
||
if (!deptSelect) return;
|
||
|
||
// 清空现有选项
|
||
deptSelect.innerHTML = '<option value="">选择部门</option>';
|
||
|
||
// 递归添加部门选项
|
||
function addDepartmentOptions(departments, level = 0) {
|
||
departments.forEach(dept => {
|
||
const option = document.createElement('option');
|
||
option.value = dept.id;
|
||
const indent = ' '.repeat(level);
|
||
option.textContent = indent + dept.name;
|
||
deptSelect.appendChild(option);
|
||
|
||
if (dept.children && dept.children.length > 0) {
|
||
addDepartmentOptions(dept.children, level + 1);
|
||
}
|
||
});
|
||
}
|
||
|
||
addDepartmentOptions(departments);
|
||
deptSelect.style.fontFamily = 'monospace';
|
||
}
|
||
|
||
// 更新用户选择框
|
||
async function updateUsers(deptId) {
|
||
try {
|
||
const userSelect = document.getElementById('userSelect');
|
||
if (!userSelect) return;
|
||
|
||
// 清空现有选项
|
||
userSelect.innerHTML = '<option value="">选择用户</option>';
|
||
|
||
if (!deptId) return;
|
||
|
||
const response = await fetch(`/fetch_users?orgId=${deptId}`);
|
||
const data = await response.json();
|
||
console.log('updateUsers.data', data);
|
||
|
||
if (data) {
|
||
data.forEach(user => {
|
||
const option = document.createElement('option');
|
||
option.value = user.id;
|
||
let displayText = user.user_name;
|
||
// 如果有职位信息,添加到显示文本中
|
||
if (user.position) {
|
||
displayText += ` (${user.position})`;
|
||
}
|
||
option.textContent = displayText;
|
||
userSelect.appendChild(option);
|
||
});
|
||
const option = document.createElement('option');
|
||
option.value = 'all';
|
||
option.textContent = '全部用户';
|
||
userSelect.appendChild(option);
|
||
}
|
||
} catch (error) {
|
||
console.error('获取用户列表失败:', error);
|
||
}
|
||
}
|
||
|
||
// 添加样式
|
||
const style = document.createElement('style');
|
||
style.textContent = `
|
||
.filter-select {
|
||
font-family: monospace;
|
||
white-space: pre;
|
||
width: 100%;
|
||
padding: 8px;
|
||
margin-bottom: 10px;
|
||
background: rgba(0, 0, 0, 0.3);
|
||
color: #fff;
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.filter-select option {
|
||
font-family: monospace;
|
||
white-space: pre;
|
||
background: #1a1a1a;
|
||
color: #fff;
|
||
}
|
||
|
||
.filter-panel {
|
||
position: absolute;
|
||
top: 20px;
|
||
right: 20px;
|
||
z-index: 1000;
|
||
background: rgba(0, 21, 41, 0.9);
|
||
backdrop-filter: blur(8px);
|
||
padding: 15px;
|
||
border-radius: 8px;
|
||
width: 280px;
|
||
border: 1px solid rgba(0, 228, 255, 0.2);
|
||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.filter-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
|
||
.filter-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 15px;
|
||
padding-bottom: 10px;
|
||
border-bottom: 1px solid rgba(0, 228, 255, 0.2);
|
||
}
|
||
|
||
.filter-header span {
|
||
color: #00e4ff;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.filter-toggle {
|
||
background: transparent;
|
||
border: none;
|
||
color: #00e4ff;
|
||
cursor: pointer;
|
||
padding: 5px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.filter-toggle:hover {
|
||
transform: scale(1.1);
|
||
}
|
||
`;
|
||
document.head.appendChild(style);
|
||
});
|
||
</script>
|
||
|
||
<div id="infoPanel" class="info-panel" style="display: none;">
|
||
<div class="info-panel-header">
|
||
<div class="info-panel-title">
|
||
<i class="fas fa-info-circle"></i>
|
||
<span>实时信息</span>
|
||
</div>
|
||
<button class="info-panel-close" onclick="toggleInfoPanel()">×</button>
|
||
</div>
|
||
<div class="info-panel-content" id="infoPanelContent">
|
||
<!-- 内容将通过JavaScript动态填充 -->
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// 全局变量
|
||
let infoPanelVisible = false;
|
||
let lastTotalInfo = null;
|
||
|
||
// 切换信息面板显示状态
|
||
function toggleInfoPanel() {
|
||
const panel = document.getElementById('infoPanel');
|
||
infoPanelVisible = !infoPanelVisible;
|
||
panel.style.display = infoPanelVisible ? 'block' : 'none';
|
||
}
|
||
|
||
// 添加键盘快捷键切换信息面板
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'i' && e.ctrlKey) {
|
||
toggleInfoPanel();
|
||
}
|
||
});
|
||
|
||
function openInfoPanelWithAlertId(alertId) {
|
||
infoPanelVisible = true;
|
||
document.getElementById('infoPanel').style.display = 'block';
|
||
updateInfoPanel(lastTotalInfo, alertId);
|
||
}
|
||
function showCustomMapInfo(f){
|
||
removeCustomMapInfo();
|
||
const d=f.properties,toStr=x=>x===undefined||x===null?'':String(x);
|
||
const get=(...k)=>k.map(x=>d[x]).find(x=>x!==undefined&&x!==null&&x!=='')||'-';
|
||
// 判断是否为告警点:有alert_id/alertId且有alert_type/alertType,且type不是health
|
||
const isAlert=!!(get('alert_id','alertId')&&get('alert_type','alertType')&&d.type!=='health');
|
||
console.log('showCustomMapInfo.d',d);
|
||
console.log('showCustomMapInfo.isAlert',isAlert);
|
||
const level=get('severity_level','severityLevel');
|
||
const levelColor=level==='critical'?'#ff4d4f':level==='high'?'#ffbb00':'#ffe066';
|
||
const avatarUrl=d.avatar||'/static/images/avatar-tech.svg';
|
||
const div=document.createElement('div');
|
||
div.className='custom-map-info';
|
||
div.style.cssText='position:absolute;z-index:9999;min-width:360px;max-width:420px;background:rgba(10,24,48,0.98);border:1.5px solid #00e4ff;border-radius:16px;box-shadow:0 0 24px #00e4ff44;padding:22px 28px 18px 28px;color:#fff;top:120px;left:50%;transform:translateX(-50%);font-size:15px;font-family:Roboto,Arial,sans-serif;backdrop-filter:blur(6px);';
|
||
// 获取位置信息
|
||
const longitude = get('longitude');
|
||
const latitude = get('latitude');
|
||
if(longitude && latitude){
|
||
reverseGeocode(longitude, latitude)
|
||
.then(address => {
|
||
const locationInfo = document.getElementById('locationInfo');
|
||
if(locationInfo){
|
||
locationInfo.textContent = address || '未知位置';
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('获取位置信息失败:', error);
|
||
const locationInfo = document.getElementById('locationInfo');
|
||
if(locationInfo){
|
||
locationInfo.textContent = '获取位置信息失败';
|
||
}
|
||
});
|
||
}
|
||
if(isAlert){
|
||
|
||
// 告警点内容
|
||
div.innerHTML=`
|
||
<div style="display:flex;align-items:center;gap:18px;margin-bottom:12px;">
|
||
<img src="${avatarUrl}" style="width:56px;height:56px;border-radius:50%;border:2px solid #00e4ff;box-shadow:0 0 8px #00e4ff44;object-fit:cover;background:#001529;">
|
||
<div>
|
||
<div style="font-size:18px;font-weight:700;letter-spacing:1px;">${get('dept_name','deptName')}</div>
|
||
<div style="font-size:16px;color:#00e4ff;font-weight:500;margin-top:2px;">${get('user_name','userName')}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display:flex;gap:18px;flex-wrap:wrap;margin-bottom:8px;">
|
||
<div><span style="color:#7ecfff;">告警类别:</span><span style="color:${getAlertTypeColor(get('alert_type','alertType','-'))};font-weight:700;">${translateAlertType(get('alert_type','alertType','-'))}</span></div>
|
||
<div><span style="color:#7ecfff;">级别:</span><span style="color:${getAlertSeverityColor(level||'-')};font-weight:700;">${translateAlertSeverity(level||'-')}</span></div>
|
||
<div><span style="color:#7ecfff;">状态:</span><span style="color:${getAlertStatusColor(get('alert_status','status','-'))};font-weight:700;">${translateAlertStatus(get('alert_status','status','-'))}</span></div>
|
||
</div>
|
||
<div style="margin-bottom:12px;">
|
||
<span style="color:#7ecfff;">健康信息:</span>
|
||
<a href="javascript:void(0)" onclick="showHealthProfile('${get('health_id','healthId')}')" style="color:#00e4ff;text-decoration:underline;font-family:monospace;font-size:15px;">${get('health_id','healthId')}</a>
|
||
</div>
|
||
<div style="margin-bottom:8px;">
|
||
<span style="color:#7ecfff;">位置信息:</span><span id="locationInfo">正在获取...</span>
|
||
</div>
|
||
<div style="margin-bottom:8px;">
|
||
<span style="color:#7ecfff;">告警时间:</span>${get('alert_timestamp','timestamp','-')}
|
||
</div>
|
||
|
||
<div style="display:flex;gap:18px;align-items:center;">
|
||
<button onclick="handleAlert('${get('alert_id','alertId')}')" style="padding:7px 22px;background:${levelColor};color:#001529;border:none;border-radius:6px;cursor:pointer;font-weight:700;box-shadow:0 2px 8px ${levelColor}44;transition:.2s;">一键处理</button>
|
||
<span style="flex:1"></span>
|
||
<span style="color:#00e4ff;cursor:pointer;font-size:22px;font-weight:700;" onclick="removeCustomMapInfo()">×</span>
|
||
</div>
|
||
`;
|
||
document.body.appendChild(div);
|
||
|
||
|
||
} else {
|
||
// 健康点内容
|
||
div.innerHTML=`
|
||
<div style="display:flex;align-items:center;gap:18px;margin-bottom:12px;">
|
||
<img src="${avatarUrl}" style="width:56px;height:56px;border-radius:50%;border:2px solid #00e4ff;box-shadow:0 0 8px #00e4ff44;object-fit:cover;background:#001529;">
|
||
<div>
|
||
<div style="font-size:18px;font-weight:700;letter-spacing:1px;">${get('dept_name','deptName')}</div>
|
||
<div style="font-size:16px;color:#00e4ff;font-weight:500;margin-top:2px;">${get('user_name','userName')}</div>
|
||
</div>
|
||
</div>
|
||
<div style="margin-bottom:8px;">
|
||
<span style="color:#7ecfff;">心率:</span>${get('heartRate','heart_rate')}
|
||
<span style="color:#7ecfff;margin-left:18px;">血压:</span>${get('pressureHigh','pressure_high')}/${get('pressureLow','pressure_low')} mmHg
|
||
</div>
|
||
<div style="margin-bottom:8px;">
|
||
<span style="color:#7ecfff;">血氧:</span>${get('bloodOxygen','blood_oxygen')}
|
||
<span style="color:#7ecfff;margin-left:18px;">体温:</span>${get('temperature','temp')} ℃
|
||
</div>
|
||
<div style="margin-bottom:8px;">
|
||
<span style="color:#7ecfff;">步数:</span>${get('step','steps')} 步
|
||
<span style="color:#7ecfff;margin-left:18px;">卡路里:</span>${get('calorie','calories')} kcal
|
||
<span style="color:#7ecfff;margin-left:18px;">距离:</span>${get('distance','distance')} 米
|
||
</div>
|
||
<div style="margin-bottom:8px;">
|
||
<span style="color:#7ecfff;">压力:</span>${get('stress','pressure')}
|
||
<span style="color:#7ecfff;margin-left:18px;">睡眠:</span>${get('sleepData','scientificSleepData')}
|
||
</div>
|
||
<div style="margin-bottom:8px;">
|
||
<span style="color:#7ecfff;">位置信息:</span><span id="locationInfo">正在获取...</span>
|
||
</div>
|
||
<div style="margin-bottom:8px;">
|
||
<span style="color:#7ecfff;">采集时间:</span>${get('timestamp')}
|
||
</div>
|
||
<div style="display:flex;gap:18px;align-items:center;">
|
||
<span style="flex:1"></span>
|
||
<span style="color:#00e4ff;cursor:pointer;font-size:22px;font-weight:700;" onclick="removeCustomMapInfo()">×</span>
|
||
</div>
|
||
`;
|
||
document.body.appendChild(div);
|
||
|
||
// 获取位置信息
|
||
const longitude = get('longitude');
|
||
const latitude = get('latitude');
|
||
if(longitude && latitude){
|
||
reverseGeocode(longitude, latitude)
|
||
.then(address => {
|
||
const locationInfo = document.getElementById('locationInfo');
|
||
if(locationInfo){
|
||
locationInfo.textContent = address || '未知位置';
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('获取位置信息失败:', error);
|
||
const locationInfo = document.getElementById('locationInfo');
|
||
if(locationInfo){
|
||
locationInfo.textContent = '获取位置信息失败';
|
||
}
|
||
});
|
||
}
|
||
}
|
||
}
|
||
function showHealthProfile(healthId){
|
||
fetch(`/fetchHealthDataById?id=${healthId}`).then(r=>r.json()).then(j=>{
|
||
if(!j.success||!j.data)return showCustomAlert('无健康数据');
|
||
const d=j.data,g=(...k)=>k.map(x=>d[x]).find(x=>x!==undefined&&x!==null&&x!=='')||'-';
|
||
let sleepStr=g('sleepData','scientificSleepData'),sleep='-';
|
||
try{
|
||
if(typeof sleepStr==='string'&&sleepStr.startsWith('{')){
|
||
const o=JSON.parse(sleepStr),arr=o.data||[];
|
||
if(arr.length){
|
||
const s=arr[0],fmt=m=>m?`${Math.floor(m/60)}h${m%60}m`:'-';
|
||
sleep=`总:${fmt(s.total)} 深:${fmt(s.deep)} 浅:${fmt(s.light)} 醒:${fmt(s.awake)}`;
|
||
}
|
||
}
|
||
}catch(e){sleep='-';}
|
||
const html=`
|
||
<div style="font-size:20px;font-weight:700;color:#00e4ff;margin-bottom:12px;text-align:center;">健康数据</div>
|
||
<div style="display:grid;grid-template-columns:100px 1fr;row-gap:8px;column-gap:10px;">
|
||
<span>心率</span><span style="text-align:right;font-weight:600;color:#7ecfff;">${g('heartRate','heart_rate')||'-'} <span style="color:#888;font-weight:400;">bpm</span></span>
|
||
<span>血压</span><span style="text-align:right;">${(g('pressureHigh','pressure_high')||'-')+'/'+(g('pressureLow','pressure_low')||'-')} <span style="color:#888;font-weight:400;">mmHg</span></span>
|
||
<span>血氧</span><span style="text-align:right;">${g('bloodOxygen','blood_oxygen')||'-'} <span style="color:#888;font-weight:400;">%</span></span>
|
||
<span>体温</span><span style="text-align:right;">${g('temperature','temp')||'-'} <span style="color:#888;font-weight:400;">℃</span></span>
|
||
<span>步数</span><span style="text-align:right;">${g('step','steps')||'-'} <span style="color:#888;font-weight:400;">步</span></span>
|
||
<span>距离</span><span style="text-align:right;">${g('distance','distance')||'-'} <span style="color:#888;font-weight:400;">米</span></span>
|
||
<span>卡路里</span><span style="text-align:right;">${g('calorie','calories')||'-'} <span style="color:#888;font-weight:400;">kcal</span></span>
|
||
<span>压力</span><span style="text-align:right;">${g('stress','pressure')||'-'} <span style="color:#888;font-weight:400;">分</span></span>
|
||
<span>睡眠</span><span style="text-align:right;">${sleep}</span>
|
||
<span>采集时间</span><span style="text-align:right;">${g('timestamp')||'-'}</span>
|
||
</div>
|
||
`;
|
||
const m=document.createElement('div');
|
||
m.style.cssText='position:fixed;top:0;left:0;width:100vw;height:100vh;background:rgba(0,21,41,0.7);z-index:10000;display:flex;align-items:center;justify-content:center;';
|
||
m.innerHTML=`<div style="background:rgba(10,24,48,0.98);border-radius:14px;box-shadow:0 0 24px #00e4ff44;padding:32px 38px;min-width:320px;max-width:420px;color:#fff;position:relative;">
|
||
<span style="position:absolute;right:18px;top:12px;cursor:pointer;font-size:22px;color:#00e4ff;" onclick="this.parentNode.parentNode.remove()">×</span>
|
||
${html}
|
||
</div>`;
|
||
document.body.appendChild(m);
|
||
}).catch(()=>showCustomAlert('获取健康数据失败'));
|
||
}
|
||
|
||
function removeCustomMapInfo(){const old=document.querySelector('.custom-map-info');if(old)old.remove();}
|
||
|
||
function filterData(data){
|
||
//console.log('filterData.data',currentDept,currentUser);
|
||
|
||
//console.log('filterData.data.healthData',data.healthData);
|
||
const toStr=x=>x===undefined||x===null?'':String(x);
|
||
const dept=toStr(currentDept),user=toStr(currentUser);
|
||
const alerts=(data.alert_info?.alerts||[]).filter(a=>
|
||
(!dept||[a.dept_id,a.deptId].some(v=>toStr(v)===dept))&&
|
||
(!user||[a.user_id,a.userId].some(v=>toStr(v)===user))&&
|
||
(['pending','1'].includes(toStr(a.alert_status||a.status)))
|
||
);
|
||
const healths=(data.health_data?.healthData||[]).filter(h=>
|
||
(!dept||[h.dept_id,h.deptId].some(v=>toStr(v)===dept))&&
|
||
(!user||[h.user_id,h.userId].some(v=>toStr(v)===user))
|
||
);
|
||
//console.log('filterData.alerts',alerts);
|
||
//console.log('filterData.healths',healths);
|
||
return {alerts,healths};
|
||
}
|
||
|
||
window.updateMapData = function(data){
|
||
//console.log('updateMapData.data',data);
|
||
if(!data||!map||!loca)return;
|
||
const {alerts,healths}=filterData(data);
|
||
const f=[];
|
||
// 处理告警数据
|
||
alerts.forEach(a=>{
|
||
if((a.longitude||a.longitude===0)&&(a.latitude||a.latitude===0)){
|
||
f.push({
|
||
type:'Feature',
|
||
geometry:{type:'Point',coordinates:[+a.longitude,+a.latitude]},
|
||
properties:{
|
||
...a,
|
||
alert_id: a.alert_id,
|
||
alert_type: a.alert_type,
|
||
alert_status: a.alert_status,
|
||
severity_level: a.severity_level,
|
||
dept_name: a.dept_name,
|
||
user_name: a.user_name,
|
||
health_id: a.health_id,
|
||
device_sn: a.device_sn,
|
||
alert_timestamp: a.alert_timestamp,
|
||
type: 'alert'
|
||
}
|
||
});
|
||
}
|
||
});
|
||
// 处理健康数据
|
||
healths.forEach(h=>{
|
||
if((h.longitude||h.longitude===0)&&(h.latitude||h.latitude===0)){
|
||
f.push({
|
||
type:'Feature',
|
||
geometry:{type:'Point',coordinates:[+h.longitude,+h.latitude]},
|
||
properties:{
|
||
...h,
|
||
dept_name: h.deptName,
|
||
user_name: h.userName,
|
||
heart_rate: h.heartRate,
|
||
blood_oxygen: h.bloodOxygen,
|
||
temperature: h.temperature,
|
||
pressure_high: h.pressureHigh,
|
||
pressure_low: h.pressureLow,
|
||
step: h.step,
|
||
stress: h.stress,
|
||
device_sn: h.deviceSn,
|
||
timestamp: h.timestamp,
|
||
avatar: h.avatar,
|
||
sleepData: h.sleepData,
|
||
timestamp: h.timestamp,
|
||
type: 'health'
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
const geoJSON={type:'FeatureCollection',features:f};
|
||
const criticalAlerts={type:'FeatureCollection',features:geoJSON.features.filter(f=>f.properties.severity_level==='critical')};
|
||
const highAlerts={type:'FeatureCollection',features:geoJSON.features.filter(f=>f.properties.severity_level==='high'||f.properties.severity_level==='medium')};
|
||
const healthData={type:'FeatureCollection',features:geoJSON.features.filter(f=>f.properties.type==='health')};
|
||
|
||
breathRed.setSource(new Loca.GeoJSONSource({data:criticalAlerts}));
|
||
breathYellow.setSource(new Loca.GeoJSONSource({data:highAlerts}));
|
||
breathGreen.setSource(new Loca.GeoJSONSource({data:healthData}));
|
||
|
||
// 设置地图中心
|
||
let center=null;
|
||
const getCoord=f=>f&&f.geometry&&f.geometry.coordinates;
|
||
const findFirst=(arr,cond)=>{for(let i=0;i<arr.length;i++)if(cond(arr[i]))return arr[i];return null;};
|
||
const cAlert=findFirst(geoJSON.features,f=>f.properties.type==='alert'&&['critical','high','medium'].includes(f.properties.severity_level));
|
||
const cHealth=findFirst(geoJSON.features,f=>f.properties.type==='health');
|
||
if(cAlert)center=getCoord(cAlert);
|
||
else if(cHealth)center=getCoord(cHealth);
|
||
if(center&¢er.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 showDefaultHealthData() {
|
||
// 更新统计数据
|
||
document.getElementById('healthScore').textContent = '85';
|
||
document.getElementById('normalCount').textContent = '6';
|
||
document.getElementById('riskCount').textContent = '2';
|
||
|
||
// 显示默认趋势图
|
||
const trendChart = echarts.init(document.getElementById('trendChart'));
|
||
|
||
const defaultDates = ['05-20', '05-21', '05-22', '05-23', '05-24', '05-25', '05-26'];
|
||
const defaultOption = {
|
||
backgroundColor: 'transparent',
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
backgroundColor: 'rgba(10,24,48,0.98)',
|
||
borderColor: '#00e4ff',
|
||
textStyle: { color: '#fff', fontSize: 10 }
|
||
},
|
||
legend: {
|
||
show: true,
|
||
top: 5,
|
||
textStyle: { color: '#fff', fontSize: 10 }
|
||
},
|
||
grid: {
|
||
top: 35,
|
||
left: 30,
|
||
right: 20,
|
||
bottom: 25,
|
||
containLabel: true
|
||
},
|
||
xAxis: {
|
||
type: 'category',
|
||
data: defaultDates,
|
||
axisLabel: { color: '#7ecfff', fontSize: 9 },
|
||
axisLine: { lineStyle: { color: 'rgba(0,228,255,0.2)' } }
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
axisLabel: { color: '#7ecfff', fontSize: 10 },
|
||
splitLine: {
|
||
lineStyle: {
|
||
color: 'rgba(0,228,255,0.1)',
|
||
type: 'dashed'
|
||
}
|
||
}
|
||
},
|
||
series: [
|
||
{
|
||
name: '心率',
|
||
type: 'line',
|
||
data: [72, 75, 78, 74, 76, 73, 77],
|
||
smooth: true,
|
||
lineStyle: { color: '#ff4444' },
|
||
itemStyle: { color: '#ff4444' }
|
||
},
|
||
{
|
||
name: '血氧',
|
||
type: 'line',
|
||
data: [98, 97, 99, 98, 97, 98, 99],
|
||
smooth: true,
|
||
lineStyle: { color: '#00ff9d' },
|
||
itemStyle: { color: '#00ff9d' }
|
||
},
|
||
{
|
||
name: '体温',
|
||
type: 'line',
|
||
data: [36.5, 36.7, 36.4, 36.6, 36.8, 36.5, 36.6],
|
||
smooth: true,
|
||
lineStyle: { color: '#ffbb00' },
|
||
itemStyle: { color: '#ffbb00' }
|
||
}
|
||
]
|
||
};
|
||
|
||
trendChart.setOption(defaultOption);
|
||
}
|
||
|
||
// 交互功能函数
|
||
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 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];
|
||
|
||
// 设置当前日期
|
||
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);
|
||
|
||
// 更新数据显示
|
||
document.getElementById('healthDataCount').textContent = formatNumber(data.healthData);
|
||
document.getElementById('pendingAlerts').textContent = formatNumber(data.pendingAlerts);
|
||
document.getElementById('activeDevices').textContent = formatNumber(data.activeDevices);
|
||
document.getElementById('unreadMessages').textContent = formatNumber(data.unreadMessages);
|
||
|
||
// 更新系统状态
|
||
updateSystemStatus(data.summary);
|
||
|
||
// 计算并显示趋势(模拟数据,实际应该从历史数据计算)
|
||
updateTrends(data);
|
||
|
||
// 添加数据更新动画
|
||
animateStatCards();
|
||
}
|
||
})
|
||
.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) {
|
||
// 模拟趋势数据(实际应该从历史数据计算)
|
||
const trends = {
|
||
health: Math.random() > 0.5 ? '+' + (Math.random() * 20).toFixed(1) + '%' : '-' + (Math.random() * 10).toFixed(1) + '%',
|
||
alert: Math.random() > 0.3 ? '+' + (Math.random() * 15).toFixed(1) + '%' : '-' + (Math.random() * 25).toFixed(1) + '%',
|
||
device: Math.random() > 0.7 ? '+' + (Math.random() * 10).toFixed(1) + '%' : '-' + (Math.random() * 5).toFixed(1) + '%',
|
||
message: Math.random() > 0.4 ? '+' + (Math.random() * 30).toFixed(1) + '%' : '-' + (Math.random() * 20).toFixed(1) + '%'
|
||
};
|
||
|
||
// 更新趋势显示
|
||
updateTrendElement('healthTrend', trends.health);
|
||
updateTrendElement('alertTrend', trends.alert);
|
||
updateTrendElement('deviceTrend', trends.device);
|
||
updateTrendElement('messageTrend', trends.message);
|
||
}
|
||
|
||
// 更新单个趋势元素
|
||
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>
|
||
|