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