CI/CD Pipeline / build-push (ljwx-admin) (push) Blocked by required conditions
CI/CD Pipeline / build-push (ljwx-bigscreen) (push) Blocked by required conditions
CI/CD Pipeline / build-push (ljwx-boot) (push) Blocked by required conditions
CI/CD Pipeline / update-deploy (push) Blocked by required conditions
CI/CD Pipeline / security-scan (push) Successful in 54s
CI/CD Pipeline / test-build-boot (push) Successful in 5m15s
CI/CD Pipeline / build-pr (ljwx-admin) (push) Has been skipped
CI/CD Pipeline / build-pr (ljwx-bigscreen) (push) Has been skipped
CI/CD Pipeline / build-pr (ljwx-boot) (push) Has been skipped
CI/CD Pipeline / test-admin (push) Successful in 7m16s
CI/CD Pipeline / test-bigscreen (push) Failing after 29m38s
## 主要变更 ### 1. API 架构分析文档 - 创建 api-architecture-analysis.md:深度分析当前 API 架构混乱问题 - 发现核心问题:93% API 无版本号,命名规范混乱,功能重复 - 提供渐进式和激进式两种重构方案 ### 2. OpenAPI 契约维护文档 - 创建 openapi-maintenance-workflow.md:OpenAPI 维护流程规范 - 创建 v1-api-contract-validation.md:V1 API 契约验证报告 - 创建 v1-api-quick-reference.md:V1 API 快速参考指南 ### 3. ljwx-bigscreen API 迁移 - 修复 BigscreenApiV1Controller 中 userId 参数 required 问题 - 更新 ljwx-bigscreen 调用正确的 API 路径 - 修复 session key 不一致问题(customer_id vs customerId) - 添加 boot_api_client 统一 HTTP 客户端 ### 4. 日志和配置优化 - 添加 StaticFileFilter 过滤 304 日志 - 修复 SQLAlchemy ROLLBACK 警告 - 优化 ljwx-bigscreen 日志格式 ### 5. 数据库迁移 - 添加 V1.9.0 Flyway 迁移脚本(geofence alert 字段) - 创建本地数据库迁移指南 ### 6. Claude Code 配置 - 添加 context-guard.md 和 task-type-detector.md - 创建 QUICK_REFERENCE.md 决策流程图 - 优化 CLAUDE.md 文档管理规范 ## 下一步 - 启动激进式 API 重构 - 统一 ljwx-admin、ljwx-watch、ljwx-phone API - 维护 OpenAPI 契约 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
7181 lines
303 KiB
HTML
7181 lines
303 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>
|
||
|
||
<script src="{{ url_for('static', filename='js/map-optimizer.js') }}"></script>
|
||
|
||
<!-- 引入工具函数模块 -->
|
||
<script src="{{ url_for('static', filename='js/modules/utils.js') }}"></script>
|
||
|
||
<!-- 引入 API 客户端模块 -->
|
||
<script src="{{ url_for('static', filename='js/modules/api-client.js') }}"></script>
|
||
|
||
<!-- 引入应用初始化模块 -->
|
||
<script src="{{ url_for('static', filename='js/modules/app-initializer.js') }}"></script>
|
||
|
||
<!-- 引入 Alert Panel 模块 -->
|
||
<script src="{{ url_for('static', filename='js/modules/alert-panel.js') }}"></script>
|
||
|
||
<!-- ========== Phase 1: 核心模块 (V2.0 架构) ========== -->
|
||
<!-- 数据管理器 - 精细化数据刷新 -->
|
||
<script src="{{ url_for('static', filename='js/modules/core/data-manager.js') }}"></script>
|
||
|
||
<!-- 事件总线 - 全局事件通信 -->
|
||
<script src="{{ url_for('static', filename='js/modules/core/event-bus.js') }}"></script>
|
||
|
||
<!-- API 适配器 - 统一请求封装 -->
|
||
<script src="{{ url_for('static', filename='js/modules/core/api-adapter.js') }}"></script>
|
||
|
||
<!-- API Client V2.0 - 缓存、重试、错误处理 -->
|
||
<script src="{{ url_for('static', filename='js/modules/api-client-v2.js') }}"></script>
|
||
|
||
<!-- Base Panel - Panel 组件基类 -->
|
||
<script src="{{ url_for('static', filename='js/modules/core/base-panel.js') }}"></script>
|
||
|
||
<!-- 组件管理器 - 组件生命周期管理 -->
|
||
<script src="{{ url_for('static', filename='js/modules/core/component-manager.js') }}"></script>
|
||
|
||
<!-- ========== Phase 2: 业务模块 (V2.0 架构) ========== -->
|
||
<!-- 地图图层管理器 V2 -->
|
||
<script src="{{ url_for('static', filename='js/modules/business/map-layer.js') }}"></script>
|
||
|
||
<!-- Alert Panel V2 -->
|
||
<script src="{{ url_for('static', filename='js/modules/business/alert-panel-v2.js') }}"></script>
|
||
|
||
<!-- Device Panel V2 -->
|
||
<script src="{{ url_for('static', filename='js/modules/business/device-panel-v2.js') }}"></script>
|
||
|
||
<!-- Message Panel V2 -->
|
||
<script src="{{ url_for('static', filename='js/modules/business/message-panel-v2.js') }}"></script>
|
||
|
||
<!-- Health Cards -->
|
||
<script src="{{ url_for('static', filename='js/modules/business/health-cards.js') }}"></script>
|
||
|
||
<!-- Stats Panel -->
|
||
<script src="{{ url_for('static', filename='js/modules/business/stats-panel.js') }}"></script>
|
||
|
||
<!-- ========== Phase 3: Panel V3.0 重构 (基于 BasePanel) ========== -->
|
||
<!-- Alert Panel V3 - 继承 BasePanel -->
|
||
<script src="{{ url_for('static', filename='js/modules/business/alert-panel-v3.js') }}"></script>
|
||
|
||
<!-- Device Panel V3 - 继承 BasePanel -->
|
||
<script src="{{ url_for('static', filename='js/modules/business/device-panel-v3.js') }}"></script>
|
||
|
||
<!-- Health Cards V3 - 继承 BasePanel -->
|
||
<script src="{{ url_for('static', filename='js/modules/business/health-cards-v3.js') }}"></script>
|
||
|
||
<!-- Stats Panel V3 - 继承 BasePanel -->
|
||
<script src="{{ url_for('static', filename='js/modules/business/stats-panel-v3.js') }}"></script>
|
||
|
||
<!-- ========== Phase 4: Panel V4.0 (Flask 中间层 API) ========== -->
|
||
<!-- Stats Panel V4 - 统计概览 -->
|
||
<script src="{{ url_for('static', filename='js/modules/business/stats-panel-v4.js') }}"></script>
|
||
|
||
<!-- Personnel Panel V4 - 人员概览 -->
|
||
<script src="{{ url_for('static', filename='js/modules/business/personnel-panel-v4.js') }}"></script>
|
||
|
||
<!-- Alert Panel V4 - 告警列表 -->
|
||
<script src="{{ url_for('static', filename='js/modules/business/alert-panel-v4.js') }}"></script>
|
||
|
||
<!-- Device Panel V4 - 设备状态 -->
|
||
<script src="{{ url_for('static', filename='js/modules/business/device-panel-v4.js') }}"></script>
|
||
|
||
<!-- Message Panel V4 - 消息列表 -->
|
||
<script src="{{ url_for('static', filename='js/modules/business/message-panel-v4.js') }}"></script>
|
||
|
||
<!-- Health Score Panel V4 - 健康评分 -->
|
||
<script src="{{ url_for('static', filename='js/modules/business/health-score-panel-v4.js') }}"></script>
|
||
|
||
<!-- Info Panel V4 - 系统信息 -->
|
||
<script src="{{ url_for('static', filename='js/modules/business/info-panel-v4.js') }}"></script>
|
||
|
||
<!-- App Main - 应用主控制器 -->
|
||
<script src="{{ url_for('static', filename='js/app-main.js') }}"></script>
|
||
|
||
<!-- 引入主样式表 -->
|
||
<link rel="stylesheet" href="{{ url_for('static', filename='css/main-complete.css') }}">
|
||
|
||
<!-- 引入统一模态窗口组件 -->
|
||
<script src="/static/js/modal-window.js"></script>
|
||
</head>
|
||
<body>
|
||
<!-- 添加标题栏 -->
|
||
<div class="header-title">
|
||
<div class="header-decoration left"></div>
|
||
<h1>{{ BIGSCREEN_TITLE }}</h1>
|
||
<div class="version-info">{{ BIGSCREEN_VERSION }}</div>
|
||
<div class="header-decoration right"></div>
|
||
</div>
|
||
|
||
<div class="container">
|
||
<!-- 左侧面板 -->
|
||
<div class="side-container">
|
||
<div class="panel">
|
||
<div class="panel-header">
|
||
<span>实时统计</span>
|
||
<div class="stats-controls">
|
||
<input type="date" id="statsDatePicker" class="date-picker" value="">
|
||
<span class="compare-info" id="compareInfo">与昨日对比</span>
|
||
</div>
|
||
</div>
|
||
<div class="panel-content">
|
||
<div class="stats-grid">
|
||
<div class="stat-card health-data">
|
||
<div class="stat-icon">📊</div>
|
||
<div class="stat-info">
|
||
<span class="stat-label">健康数据</span>
|
||
<div class="stat-value-container">
|
||
<span class="stat-value" id="healthDataCount">0</span>
|
||
<span class="stat-unit">条</span>
|
||
</div>
|
||
<div class="stat-trend" id="healthTrend">+0%</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stat-card alert-data">
|
||
<div class="stat-icon">⚠️</div>
|
||
<div class="stat-info">
|
||
<span class="stat-label">待处理告警</span>
|
||
<div class="stat-value-container">
|
||
<span class="stat-value" id="pendingAlerts">0</span>
|
||
<span class="stat-unit">条</span>
|
||
</div>
|
||
<div class="stat-trend" id="alertTrend">+0%</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stat-card device-data">
|
||
<div class="stat-icon">📱</div>
|
||
<div class="stat-info">
|
||
<span class="stat-label">活跃设备</span>
|
||
<div class="stat-value-container">
|
||
<span class="stat-value" id="activeDevices">0</span>
|
||
<span class="stat-unit">台</span>
|
||
</div>
|
||
<div class="stat-trend" id="deviceTrend">+0%</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="stat-card message-data">
|
||
<div class="stat-icon">💬</div>
|
||
<div class="stat-info">
|
||
<span class="stat-label">未读消息</span>
|
||
<div class="stat-value-container">
|
||
<span class="stat-value" id="unreadMessages">0</span>
|
||
<span class="stat-unit">条</span>
|
||
</div>
|
||
<div class="stat-trend" id="messageTrend">+0%</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="system-status" id="systemStatus">
|
||
<div class="status-indicator normal" id="statusIndicator"></div>
|
||
<span class="status-text" id="statusText">系统正常</span>
|
||
<span class="health-score" id="systemHealthScore">100</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel personnel-management">
|
||
<div class="panel-header">
|
||
<h3>人员管理</h3>
|
||
</div>
|
||
<div class="panel-content">
|
||
<!-- 统计概览卡片 -->
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; padding: 8px 12px; background: rgba(0,228,255,0.1); border-radius: 6px; border-left: 4px solid #00e4ff;">
|
||
<div style="display: flex; align-items: center; gap: 20px;">
|
||
<div style="text-align: center;">
|
||
<div style="color: #00e4ff; font-size: 18px; font-weight: bold;" id="totalUsers">0</div>
|
||
<div style="color: rgba(255,255,255,0.8); font-size: 11px;">总人数</div>
|
||
</div>
|
||
<div style="text-align: center;">
|
||
<div style="color: #00ff9d; font-size: 18px; font-weight: bold;" id="totalBindDevices">0</div>
|
||
<div style="color: rgba(255,255,255,0.8); font-size: 11px;">已绑定设备</div>
|
||
</div>
|
||
<div style="text-align: center;">
|
||
<div style="color: #ffbb00; font-size: 18px; font-weight: bold;" id="onlineRate">0%</div>
|
||
<div style="color: rgba(255,255,255,0.8); font-size: 11px;">绑定率</div>
|
||
</div>
|
||
</div>
|
||
<div style="color: rgba(255,255,255,0.6); font-size: 12px; cursor: pointer;" onclick="showPersonnelDetails()">
|
||
详情 →
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 快速统计卡片 -->
|
||
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-bottom: 10px; height: 45px;">
|
||
<div style="background: rgba(0,228,255,0.1); border-radius: 4px; padding: 4px; text-align: center; border-left: 3px solid #00e4ff; cursor: pointer; transition: all 0.3s ease;" onclick="filterByDepartment()" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 12px rgba(0,228,255,0.3)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='none'">
|
||
<div style="color: #00e4ff; font-size: 14px; font-weight: bold;" id="activeOrgCount">0</div>
|
||
<div style="color: #fff; font-size: 9px;">活跃组织</div>
|
||
</div>
|
||
<div style="background: rgba(0,255,157,0.1); border-radius: 4px; padding: 4px; text-align: center; border-left: 3px solid #00ff9d; cursor: pointer; transition: all 0.3s ease;" onclick="filterByOnlineStatus()" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 12px rgba(0,255,157,0.3)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='none'">
|
||
<div style="color: #00ff9d; font-size: 14px; font-weight: bold;" id="onlineUsers">0</div>
|
||
<div style="color: #fff; font-size: 9px;">在线人员</div>
|
||
</div>
|
||
<div style="background: rgba(255,187,0,0.1); border-radius: 4px; padding: 4px; text-align: center; border-left: 3px solid #ffbb00; cursor: pointer; transition: all 0.3s ease;" onclick="filterByDeviceStatus()" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 12px rgba(255,187,0,0.3)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='none'">
|
||
<div style="color: #ffbb00; font-size: 14px; font-weight: bold;" id="boundDevices">0</div>
|
||
<div style="color: #fff; font-size: 9px;">绑定设备</div>
|
||
</div>
|
||
<div style="background: rgba(255,68,68,0.1); border-radius: 4px; padding: 4px; text-align: center; border-left: 3px solid #ff4444; cursor: pointer; transition: all 0.3s ease;" onclick="filterByAlertStatus()" onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 12px rgba(255,68,68,0.3)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='none'">
|
||
<div style="color: #ff4444; font-size: 14px; font-weight: bold;" id="alertUsers">0</div>
|
||
<div style="color: #fff; font-size: 9px;">告警人员</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 图表区域 -->
|
||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; height: calc(100% - 120px);">
|
||
<div id="departmentDistribution" style="background: rgba(0,21,41,0.4); border: 1px solid rgba(0,228,255,0.3); border-radius: 6px; position: relative; transition: all 0.3s ease;" onmouseover="this.style.borderColor='rgba(0,228,255,0.6)'; this.style.boxShadow='0 4px 15px rgba(0,228,255,0.2)'" onmouseout="this.style.borderColor='rgba(0,228,255,0.3)'; this.style.boxShadow='none'">
|
||
<div style="position: absolute; top: 5px; left: 8px; color: #00e4ff; font-size: 12px; font-weight: bold; z-index: 10;">部门分布</div>
|
||
</div>
|
||
<div id="userStatusChart" style="background: rgba(0,21,41,0.4); border: 1px solid rgba(0,228,255,0.3); border-radius: 6px; position: relative; transition: all 0.3s ease;" onmouseover="this.style.borderColor='rgba(0,228,255,0.6)'; this.style.boxShadow='0 4px 15px rgba(0,228,255,0.2)'" onmouseout="this.style.borderColor='rgba(0,228,255,0.3)'; this.style.boxShadow='none'">
|
||
<div style="position: absolute; top: 5px; left: 8px; color: #00e4ff; font-size: 12px; font-weight: bold; z-index: 10;">在线状态</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="panel">
|
||
<div class="panel-header">
|
||
<span>告警信息</span>
|
||
</div>
|
||
<div class="panel-content">
|
||
<div id="alertList" class="chart-container"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 中间地图区域 -->
|
||
<div style="display: flex; flex-direction: column; height: 100%; width: 100%;">
|
||
|
||
<!-- 租户信息卡片 -->
|
||
<div class="tenant-info-header">
|
||
<div class="tenant-card-compact">
|
||
<div class="tenant-header-compact">
|
||
<div class="tenant-avatar-small">
|
||
<img id="tenantLogo" src="/static/images/tenant-default.png" alt="租户Logo" onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
|
||
<div class="tenant-avatar-placeholder-small" style="display: none;">🏢</div>
|
||
</div>
|
||
<div class="tenant-info-compact">
|
||
<div class="tenant-name-compact" id="tenantName">{{ tenantName }}</div>
|
||
<div class="tenant-meta-compact">
|
||
<span class="tenant-id-compact">ID: <span id="tenantId">{{ customerId }}</span></span>
|
||
<span class="tenant-separator">•</span>
|
||
<span class="tenant-status-compact online">运行中</span>
|
||
{% if isSuperAdmin and not hideSelector %}
|
||
<span class="tenant-separator">•</span>
|
||
<select id="tenantSelector" class="tenant-selector" onchange="switchTenant(this.value)">
|
||
{% for tenant in tenantList %}
|
||
<option value="{{ tenant.id }}" {% if tenant.id == customerId %}selected{% endif %}>
|
||
{{ tenant.name }}
|
||
</option>
|
||
{% endfor %}
|
||
</select>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
<div class="tenant-stats-compact">
|
||
<div class="compact-stat">
|
||
<span class="compact-stat-label">组织</span>
|
||
<span class="compact-stat-value" id="tenantOrgCount">-</span>
|
||
</div>
|
||
<div class="compact-stat">
|
||
<span class="compact-stat-label">用户</span>
|
||
<span class="compact-stat-value" id="tenantUserCount">-</span>
|
||
</div>
|
||
<div class="compact-stat">
|
||
<span class="compact-stat-label">设备</span>
|
||
<span class="compact-stat-value" id="tenantDeviceCount">-</span>
|
||
</div>
|
||
<div class="compact-stat">
|
||
<span class="compact-stat-label">告警</span>
|
||
<span class="compact-stat-value" id="tenantAlertCount">-</span>
|
||
</div>
|
||
<div class="compact-stat">
|
||
<span class="compact-stat-label">消息</span>
|
||
<span class="compact-stat-value" id="tenantMessageCount">-</span>
|
||
</div>
|
||
<div class="compact-stat">
|
||
<span class="compact-stat-label">健康数据</span>
|
||
<span class="compact-stat-value" id="tenantHealthDataCount">-</span>
|
||
</div>
|
||
</div>
|
||
<div class="tenant-indicator-compact">
|
||
<div class="indicator-dot-small active"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 健康功能卡片组 -->
|
||
<div class="health-cards-container">
|
||
<!-- 健康预测卡片 -->
|
||
<div class="health-card" id="healthPredictionCard">
|
||
<div class="health-card-header">
|
||
<div class="health-card-icon">🔮</div>
|
||
<div class="health-card-title">健康预测</div>
|
||
</div>
|
||
<div class="health-card-content">
|
||
<div class="health-loading" id="predictionLoading">
|
||
<div class="loading-spinner-small"></div>
|
||
<span>正在分析健康预测...</span>
|
||
</div>
|
||
<div id="predictionItems" style="display: none;">
|
||
<!-- 健康预测内容将通过API动态加载 -->
|
||
</div>
|
||
<div class="prediction-item-small">
|
||
<div class="prediction-label-small">环境适应性</div>
|
||
<div class="prediction-content-small">
|
||
车间环境温度适宜,建议保持当前作业强度
|
||
<span class="prediction-risk-small">📈 理想</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 健康评分卡片 -->
|
||
<div class="health-card" id="healthScoreCard">
|
||
<div class="health-card-header">
|
||
<div class="health-card-icon">🏆</div>
|
||
<div class="health-card-title">健康评分</div>
|
||
</div>
|
||
<div class="health-card-content">
|
||
<div class="score-display-compact">
|
||
<div class="score-circle-compact">
|
||
<div class="score-number-compact" id="compactHealthScore">0</div>
|
||
</div>
|
||
<div class="score-status-compact" id="compactHealthStatus">正在计算...</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 健康建议卡片 -->
|
||
<div class="health-card" id="healthRecommendationCard">
|
||
<div class="health-card-header">
|
||
<div class="health-card-icon">💡</div>
|
||
<div class="health-card-title">健康建议</div>
|
||
</div>
|
||
<div class="health-card-content">
|
||
<div class="health-loading" id="recommendationLoading">
|
||
<div class="loading-spinner-small"></div>
|
||
<span>正在生成健康建议...</span>
|
||
</div>
|
||
<div id="recommendationItems" style="display: none;">
|
||
<!-- 健康建议内容将通过API动态加载 -->
|
||
</div>
|
||
<div class="recommendation-item-small">
|
||
<div class="recommendation-icon-small">😴</div>
|
||
<div class="recommendation-text-small">
|
||
保证7-8小时优质睡眠时间
|
||
<div class="recommendation-priority-small">日常</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 地图容器 -->
|
||
<div class="map-container" id="map-container">
|
||
<!-- 筛选面板 -->
|
||
<div class="filter-panel" id="filterPanel">
|
||
<div class="filter-toggle" id="filterToggle">🔍</div>
|
||
<div class="filter-content">
|
||
<div class="filter-header">
|
||
<span>人员筛选</span>
|
||
</div>
|
||
<div class="filter-container">
|
||
<input type="text" id="searchInput" class="filter-search" placeholder="🔍 模糊搜索用户...">
|
||
<select id="deptSelect" class="filter-select">
|
||
<option value="">选择部门</option>
|
||
</select>
|
||
<select id="userSelect" class="filter-select">
|
||
<option value="">选择用户</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- 保留原有地图代码 -->
|
||
</div>
|
||
|
||
<!-- 消息卡片 -->
|
||
<div class="message-card" onclick="openMessagePanel()" style="cursor: pointer;">
|
||
<div class="message-card-header">
|
||
<div class="message-card-title">
|
||
<div class="message-card-icon">📬</div>
|
||
<span>消息信息</span>
|
||
</div>
|
||
<div class="message-card-count" id="messageCount">0</div>
|
||
</div>
|
||
<div class="message-card-content" id="messageCardContent">
|
||
<div class="health-loading" id="messageLoading">
|
||
<div class="loading-spinner-small"></div>
|
||
<span>正在加载消息...</span>
|
||
</div>
|
||
<div id="messageItems" style="display: none;">
|
||
<!-- 消息列表将动态加载 -->
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧面板 -->
|
||
<div class="side-container">
|
||
<div class="panel watch-management">
|
||
<div class="panel-header">
|
||
<span>📱 手表管理</span>
|
||
<span class="label">总数: <span class="number" id="totalWatchDevices">0</span></span>
|
||
</div>
|
||
<div class="panel-content">
|
||
<!-- 图表区域 -->
|
||
<div id="statsChart" class="chart-container"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel health-analysis">
|
||
<div class="panel-header">
|
||
<span>💗 健康数据分析</span>
|
||
<span style="color: rgba(255,255,255,0.6); font-size: 12px;">最近7天</span>
|
||
</div>
|
||
<div class="panel-content">
|
||
<!-- 健康趋势图 -->
|
||
<div id="trendChart" style="background: rgba(0,21,41,0.4); border: 1px solid rgba(0,228,255,0.3); border-radius: 6px; position: relative; height: 100%; transition: all 0.3s ease;" onmouseover="this.style.borderColor='rgba(0,228,255,0.6)'; this.style.boxShadow='0 4px 15px rgba(0,228,255,0.2)'" onmouseout="this.style.borderColor='rgba(0,228,255,0.3)'; this.style.boxShadow='none'">
|
||
<div style="position: absolute; top: 8px; left: 12px; color: #00e4ff; font-size: 13px; font-weight: bold; z-index: 10;">📊 健康趋势分析</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel health-score-panel">
|
||
<div class="panel-header">
|
||
<span>🏆 健康评分</span>
|
||
<span class="total-score" id="totalHealthScore">总分:0</span>
|
||
</div>
|
||
<div class="panel-content">
|
||
<!-- 评分显示区域 -->
|
||
<div class="score-display">
|
||
<div class="score-main">
|
||
<div class="score-circle">
|
||
<div class="score-number" id="overallHealthScore">0</div>
|
||
</div>
|
||
<div class="score-info">
|
||
<div class="score-title">综合健康评分</div>
|
||
<div class="score-status">良好状态</div>
|
||
</div>
|
||
</div>
|
||
<div style="color: rgba(255,255,255,0.6); font-size: 12px; cursor: pointer;" onclick="showScoreDetails()">
|
||
详情 →
|
||
</div>
|
||
</div>
|
||
<!-- 图表区域 -->
|
||
<div id="healthScoreChart" class="chart-container"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="{{ url_for('static', filename='libs/echarts.min.js') }}"></script>
|
||
|
||
<script>
|
||
|
||
// 全局配置变量
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
let GLOBAL_CUSTOMER_ID = '{{ customerId }}'; // 从后端模板获取当前租户ID
|
||
let GLOBAL_ORG_ID = '{{ customerId }}'; // 组织ID(在大屏场景中,orgId 默认等于 customerId)
|
||
|
||
// 暴露到 window 全局作用域(供 Panel V3/V4 使用)
|
||
window.GLOBAL_CUSTOMER_ID = GLOBAL_CUSTOMER_ID;
|
||
window.GLOBAL_ORG_ID = GLOBAL_ORG_ID;
|
||
|
||
// 租户切换函数(仅超级管理员可用)
|
||
async function switchTenant(newCustomerId) {
|
||
if (!newCustomerId || newCustomerId === GLOBAL_CUSTOMER_ID) {
|
||
return;
|
||
}
|
||
|
||
console.log(`🔄 切换租户: ${GLOBAL_CUSTOMER_ID} -> ${newCustomerId}`);
|
||
|
||
try {
|
||
const response = await fetch('/api/switch-tenant', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({ customerId: newCustomerId })
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
console.log(`✅ 租户切换成功: ${result.data.tenantName}`);
|
||
// 更新全局变量
|
||
GLOBAL_CUSTOMER_ID = newCustomerId;
|
||
GLOBAL_ORG_ID = newCustomerId; // orgId 同步更新
|
||
window.GLOBAL_CUSTOMER_ID = newCustomerId;
|
||
window.GLOBAL_ORG_ID = newCustomerId;
|
||
|
||
// 刷新页面以加载新租户数据
|
||
window.location.reload();
|
||
} else {
|
||
console.error(`❌ 租户切换失败: ${result.message}`);
|
||
alert(`切换失败: ${result.message}`);
|
||
// 恢复原选择
|
||
document.getElementById('tenantSelector').value = GLOBAL_CUSTOMER_ID;
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ 租户切换请求失败:', error);
|
||
alert('切换失败,请稍后重试');
|
||
// 恢复原选择
|
||
document.getElementById('tenantSelector').value = GLOBAL_CUSTOMER_ID;
|
||
}
|
||
}
|
||
|
||
// 设置租户ID显示
|
||
// DOMContentLoaded 事件监听器已移至 app-initializer.js 模块 (Phase 7)
|
||
// 租户信息初始化已由 AppInitializer.initTenantInfo() 处理
|
||
|
||
// 在全局作用域声明图表变量
|
||
let globalCharts = null;
|
||
let currentDept = '', currentUser = '';
|
||
|
||
// 统一的API调用函数
|
||
// =============================================================================
|
||
// 统一API调用函数 - 已移至 api-client.js 模块 (Phase 5)
|
||
// =============================================================================
|
||
// API_MAPPINGS, API_CACHE, apiCall(), api 对象已移至 api-client.js 模块
|
||
// 通过 window.* 暴露到全局作用域,可直接使用
|
||
|
||
// =============================================================================
|
||
// Alert Panel 常量和辅助函数 - 已移至 alert-panel.js 模块 (Phase 8)
|
||
// =============================================================================
|
||
// ALERT_TYPE_MAP, ALERT_SEVERITY_MAP, ALERT_STATUS_MAP, ALERT_*_COLOR 常量
|
||
// translateAlertType, translateAlertSeverity, translateAlertStatus 翻译函数
|
||
// getAlertTypeColor, getAlertSeverityColor, getAlertStatusColor 颜色函数
|
||
// handleAlert, exportAlerts, refreshAlerts, filterByAlertStatus, mapAlertStatus 业务函数
|
||
// 通过 window.AlertPanel 和 window.* 暴露到全局作用域,可直接使用
|
||
//
|
||
// 注意:initAlertChart 和 showAlertDetails 函数因包含大量内联配置(~678行)
|
||
// 暂时保留在 main.html 中,待后续 Phase 进一步优化拆分
|
||
|
||
// formatDate() 函数已移至 utils.js 模块
|
||
|
||
// 修改initCharts函数
|
||
function initCharts() {
|
||
// 注意:告警信息panel和设备信息panel不在此处初始化图表
|
||
// 而是在refreshData时通过initAlertChart和initDeviceChart来初始化
|
||
// 这样确保初始化时和数据更新时使用相同的图表配置,避免显示不一致
|
||
try {
|
||
let charts = {
|
||
healthScore: null,
|
||
stats: null,
|
||
trend: null,
|
||
alert: null,
|
||
messageStats: null // 添加消息统计图表
|
||
};
|
||
|
||
// 检查并初始化健康评分图表
|
||
const healthScoreContainer = document.getElementById('healthScoreChart');
|
||
if (healthScoreContainer) {
|
||
charts.healthScore = echarts.init(healthScoreContainer);
|
||
|
||
// 获取日期范围
|
||
const today = new Date();
|
||
const yesterday = new Date(today);
|
||
yesterday.setDate(yesterday.getDate() - 7);
|
||
|
||
const startDate = formatDate(yesterday);
|
||
const endDate = formatDate(today);
|
||
|
||
// 调用接口获取健康评分数据 - 使用标准化API
|
||
api.health.getScore({
|
||
customerId: GLOBAL_CUSTOMER_ID,
|
||
startDate: startDate,
|
||
endDate: endDate,
|
||
includeFactors: true
|
||
})
|
||
.then(response => response.json())
|
||
.then(result => {
|
||
if (result.success && result.data && result.data.healthScores && result.data.healthScores.factors) {
|
||
const factors = result.data.healthScores.factors;
|
||
|
||
// 更新总分显示
|
||
const totalScoreElement = document.getElementById('totalHealthScore');
|
||
if (totalScoreElement) {
|
||
totalScoreElement.textContent = `总分:${result.data.summary.overallScore}`;
|
||
}
|
||
const scoreNumberElement = document.getElementById('overallHealthScore');
|
||
if (scoreNumberElement) {
|
||
scoreNumberElement.textContent = `${result.data.summary.overallScore}`;
|
||
}
|
||
|
||
|
||
// 兼容驼峰和下划线命名的辅助函数
|
||
function getFactorScore(factors, camelCase, snakeCase) {
|
||
return factors[camelCase]?.score || factors[snakeCase]?.score || 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: `心率 ${getFactorScore(factors, 'heartRate', 'heart_rate')}分`, max: 100 },
|
||
{ name: `血氧 ${getFactorScore(factors, 'bloodOxygen', 'blood_oxygen')}分`, max: 100 },
|
||
{ name: `体温 ${getFactorScore(factors, 'temperature', 'temperature')}分`, max: 100 },
|
||
{ name: `步数 ${getFactorScore(factors, 'step', 'step')}分`, max: 100 },
|
||
{ name: `卡路里 ${getFactorScore(factors, 'calorie', 'calorie')}分`, max: 100 },
|
||
{ name: `收缩压 ${getFactorScore(factors, 'pressureHigh', 'pressure_high')}分`, max: 100 },
|
||
{ name: `舒张压 ${getFactorScore(factors, 'pressureLow', 'pressure_low')}分`, max: 100 },
|
||
{ name: `压力 ${getFactorScore(factors, 'stress', 'stress')}分`, max: 100 },
|
||
{ name: `睡眠 ${getFactorScore(factors, 'sleep', 'sleep')}分`, 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: [
|
||
getFactorScore(factors, 'heartRate', 'heart_rate'),
|
||
getFactorScore(factors, 'bloodOxygen', 'blood_oxygen'),
|
||
getFactorScore(factors, 'temperature', 'temperature'),
|
||
getFactorScore(factors, 'step', 'step'),
|
||
getFactorScore(factors, 'calorie', 'calorie'),
|
||
getFactorScore(factors, 'pressureHigh', 'pressure_high'),
|
||
getFactorScore(factors, 'pressureLow', 'pressure_low'),
|
||
getFactorScore(factors, 'stress', 'stress'),
|
||
getFactorScore(factors, 'sleep', 'sleep')
|
||
],
|
||
name: '当前状态',
|
||
itemStyle: {
|
||
color: '#00e4ff'
|
||
},
|
||
areaStyle: {
|
||
color: 'rgba(0,228,255,0.4)'
|
||
}
|
||
}]
|
||
}]
|
||
};
|
||
charts.healthScore.setOption(healthScoreOption);
|
||
} else {
|
||
// 如果没有数据,显示0分
|
||
const totalScoreElement = document.getElementById('totalHealthScore');
|
||
if (totalScoreElement) {
|
||
totalScoreElement.textContent = '总分:0';
|
||
}
|
||
const scoreNumberElement = document.getElementById('overallHealthScore');
|
||
if (scoreNumberElement) {
|
||
scoreNumberElement.textContent = '0';
|
||
}
|
||
const scoreStatusElement = document.querySelector('.score-status');
|
||
if (scoreStatusElement) {
|
||
scoreStatusElement.textContent = `暂无数据`;
|
||
}
|
||
|
||
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: '睡眠 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, 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: '睡眠 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, 0],
|
||
name: '当前状态',
|
||
itemStyle: {
|
||
color: '#00e4ff'
|
||
},
|
||
areaStyle: {
|
||
color: 'rgba(0,228,255,0.4)'
|
||
}
|
||
}]
|
||
}]
|
||
};
|
||
charts.healthScore.setOption(healthScoreOption);
|
||
});
|
||
}
|
||
|
||
// 检查并初始化数据统计图表
|
||
// 检查并初始化数据统计图表
|
||
const statsContainer = document.getElementById('statsChart');
|
||
if (statsContainer) {
|
||
// 创建默认的设备统计结构,与initDeviceChart相同风格但显示默认数据
|
||
statsContainer.innerHTML = `
|
||
<div style="position: relative; height: 100%; padding: 8px;">
|
||
<!-- 设备状态总览 -->
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; padding: 6px 10px; background: rgba(0,228,255,0.1); border-radius: 6px; border-left: 4px solid #00e4ff;">
|
||
<div style="display: flex; align-items: center; gap: 12px;">
|
||
<div class="device-stat-item">
|
||
<span style="color: #00ff9d; font-size: 18px; font-weight: bold;" id="onlineDevices">0</span>
|
||
<span style="color: #fff; font-size: 12px; margin-left: 4px;">在线</span>
|
||
</div>
|
||
<div class="device-stat-item">
|
||
<span style="color: #ffbb00; font-size: 16px; font-weight: bold;" id="offlineDevices">0</span>
|
||
<span style="color: #fff; font-size: 12px; margin-left: 4px;">离线</span>
|
||
</div>
|
||
<div class="device-stat-item">
|
||
<span style="color: #ff6666; font-size: 14px; font-weight: bold;" id="errorDevices">0</span>
|
||
<span style="color: #fff; font-size: 12px; margin-left: 4px;">故障</span>
|
||
</div>
|
||
</div>
|
||
<div style="background: rgba(0,255,157,0.3); padding: 4px 8px; border-radius: 12px;" id="deviceStatusBadge">
|
||
<span style="color: #00ff9d; font-size: 11px; font-weight: bold;">✅ 正常</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 图表区域 -->
|
||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 6px; height: calc(100% - 45px);">
|
||
<div id="deviceStatusChart" style="background: rgba(0,21,41,0.4); border: 1px solid rgba(0,228,255,0.3); border-radius: 6px; position: relative;">
|
||
<div style="position: absolute; top: 5px; left: 8px; color: #00e4ff; font-size: 12px; font-weight: bold; z-index: 10;">设备状态分布</div>
|
||
</div>
|
||
<div id="deviceTypeChart" style="background: rgba(0,21,41,0.4); border: 1px solid rgba(0,228,255,0.3); border-radius: 6px; position: relative;">
|
||
<div style="position: absolute; top: 5px; left: 8px; color: #00e4ff; font-size: 12px; font-weight: bold; z-index: 10;">设备类型分布</div>
|
||
</div>
|
||
<div id="deviceDeptChart" style="background: rgba(0,21,41,0.4); border: 1px solid rgba(0,228,255,0.3); border-radius: 6px; position: relative;">
|
||
<div style="position: absolute; top: 5px; left: 8px; color: #00e4ff; font-size: 12px; font-weight: bold; z-index: 10;">部门分布</div>
|
||
</div>
|
||
<div id="deviceTrendChart" style="background: rgba(0,21,41,0.4); border: 1px solid rgba(0,228,255,0.3); border-radius: 6px; position: relative;">
|
||
<div style="position: absolute; top: 5px; left: 8px; color: #00e4ff; font-size: 12px; font-weight: bold; z-index: 10;">使用趋势</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// 延时初始化默认图表
|
||
setTimeout(() => {
|
||
if (document.getElementById('deviceStatusChart')) {
|
||
// 设备状态分布图
|
||
const statusChart = echarts.init(document.getElementById('deviceStatusChart'));
|
||
statusChart.setOption({
|
||
tooltip: { trigger: 'item', backgroundColor: 'rgba(0,21,41,0.95)', borderColor: '#00e4ff', textStyle: { color: '#fff', fontSize: 11 } },
|
||
series: [{ type: 'pie', radius: ['35%', '65%'], center: ['50%', '55%'], data: [
|
||
{ name: '暂无数据', value: 1, itemStyle: { color: '#666' } }
|
||
], label: { show: false } }]
|
||
});
|
||
|
||
// 设备类型分布图
|
||
const typeChart = echarts.init(document.getElementById('deviceTypeChart'));
|
||
typeChart.setOption({
|
||
tooltip: { trigger: 'item', backgroundColor: 'rgba(0,21,41,0.95)', borderColor: '#00e4ff', textStyle: { color: '#fff', fontSize: 11 } },
|
||
series: [{ type: 'pie', radius: ['35%', '65%'], center: ['50%', '55%'], data: [
|
||
{ name: '暂无数据', value: 1, itemStyle: { color: '#666' } }
|
||
], label: { show: false } }]
|
||
});
|
||
|
||
// 部门分布图
|
||
const deptChart = echarts.init(document.getElementById('deviceDeptChart'));
|
||
deptChart.setOption({
|
||
tooltip: { trigger: 'axis', backgroundColor: 'rgba(0,21,41,0.95)', borderColor: '#00e4ff', textStyle: { color: '#fff', fontSize: 11 } },
|
||
grid: { top: 25, left: 50, right: 15, bottom: 15 },
|
||
xAxis: { type: 'value', axisLabel: { color: '#7ecfff', fontSize: 9 }, splitLine: { lineStyle: { color: 'rgba(126,207,255,0.1)' } }, axisLine: { show: false }, max: 1 },
|
||
yAxis: { type: 'category', data: ['暂无数据'], axisLabel: { color: '#fff', fontSize: 9 }, axisLine: { show: false }, axisTick: { show: false } },
|
||
series: [{ type: 'bar', data: [0], barWidth: '65%', itemStyle: { color: '#00e4ff88' } }]
|
||
});
|
||
|
||
// 使用趋势图
|
||
const trendChart = echarts.init(document.getElementById('deviceTrendChart'));
|
||
const days = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
|
||
trendChart.setOption({
|
||
tooltip: { trigger: 'axis', backgroundColor: 'rgba(0,21,41,0.95)', borderColor: '#00e4ff', textStyle: { color: '#fff', fontSize: 11 } },
|
||
grid: { top: 25, left: 30, right: 15, bottom: 25 },
|
||
xAxis: { type: 'category', data: days, axisLabel: { color: '#7ecfff', fontSize: 8 }, axisLine: { show: false }, axisTick: { show: false } },
|
||
yAxis: { type: 'value', axisLabel: { color: '#7ecfff', fontSize: 8 }, splitLine: { lineStyle: { color: 'rgba(126,207,255,0.1)' } }, axisLine: { show: false } },
|
||
series: [{ type: 'line', data: [0, 0, 0, 0, 0, 0, 0], smooth: true, lineStyle: { color: '#00e4ff', width: 2 }, itemStyle: { color: '#00e4ff' }, symbol: 'circle', symbolSize: 4 }]
|
||
});
|
||
}
|
||
}, 200);
|
||
|
||
charts.stats = null; // 将在initDeviceChart中重新设置
|
||
}
|
||
|
||
// 检查并初始化预警信息图表
|
||
const alertContainer = document.getElementById('alertList');
|
||
if (alertContainer) {
|
||
// 创建默认的告警信息结构,与initAlertChart相同风格但显示默认数据
|
||
alertContainer.innerHTML = `
|
||
<div style="position: relative; height: 100%; padding: 8px;">
|
||
<!-- 告警状态总览 -->
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; padding: 6px 10px; background: rgba(0,228,255,0.1); border-radius: 6px; border-left: 4px solid #00e4ff;">
|
||
<div style="display: flex; align-items: center; gap: 12px;">
|
||
<div class="alert-stat-item">
|
||
<span style="color: #ff4444; font-size: 18px; font-weight: bold;" id="criticalCount">0</span>
|
||
<span style="color: #fff; font-size: 12px; margin-left: 4px;">严重</span>
|
||
</div>
|
||
<div class="alert-stat-item">
|
||
<span style="color: #ffbb00; font-size: 16px; font-weight: bold;" id="mediumCount">0</span>
|
||
<span style="color: #fff; font-size: 12px; margin-left: 4px;">中等</span>
|
||
</div>
|
||
<div class="alert-stat-item">
|
||
<span style="color: #ff6666; font-size: 14px; font-weight: bold;" id="pendingCount">0</span>
|
||
<span style="color: #fff; font-size: 12px; margin-left: 4px;">待处理</span>
|
||
</div>
|
||
</div>
|
||
<div style="background: rgba(0,255,157,0.3); padding: 4px 8px; border-radius: 12px;" id="alertBadge">
|
||
<span style="color: #00ff9d; font-size: 11px; font-weight: bold;">✅ 正常</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 图表区域 -->
|
||
<div class="alert-charts-grid" style="display: grid; grid-template-columns: 1fr 1fr; gap: 6px; height: calc(100% - 45px);">
|
||
<div id="alertTypeChart" style="background: rgba(0,21,41,0.4); border: 1px solid rgba(0,228,255,0.3); border-radius: 6px; position: relative;">
|
||
<div style="position: absolute; top: 5px; left: 8px; color: #00e4ff; font-size: 12px; font-weight: bold; z-index: 10;">告警类型分布</div>
|
||
</div>
|
||
<div id="alertLevelChart" style="background: rgba(0,21,41,0.4); border: 1px solid rgba(0,228,255,0.3); border-radius: 6px; position: relative;">
|
||
<div style="position: absolute; top: 5px; left: 8px; color: #00e4ff; font-size: 12px; font-weight: bold; z-index: 10;">严重程度分析</div>
|
||
</div>
|
||
<div id="alertStatusChart" style="background: rgba(0,21,41,0.4); border: 1px solid rgba(0,228,255,0.3); border-radius: 6px; position: relative;">
|
||
<div style="position: absolute; top: 5px; left: 8px; color: #00e4ff; font-size: 12px; font-weight: bold; z-index: 10;">处理状态</div>
|
||
</div>
|
||
<div id="alertTrendChart" style="background: rgba(0,21,41,0.4); border: 1px solid rgba(0,228,255,0.3); border-radius: 6px; position: relative;">
|
||
<div style="position: absolute; top: 5px; left: 8px; color: #00e4ff; font-size: 12px; font-weight: bold; z-index: 10;">24小时趋势</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// 延时初始化默认图表
|
||
setTimeout(() => {
|
||
if (document.getElementById('alertTypeChart')) {
|
||
// 告警类型分布图
|
||
const typeChart = echarts.init(document.getElementById('alertTypeChart'));
|
||
typeChart.setOption({
|
||
tooltip: { trigger: 'axis', backgroundColor: 'rgba(0,21,41,0.95)', borderColor: '#00e4ff', textStyle: { color: '#fff', fontSize: 11 } },
|
||
grid: { top: 25, left: 50, right: 15, bottom: 15 },
|
||
xAxis: { type: 'value', axisLabel: { color: '#7ecfff', fontSize: 9 }, splitLine: { lineStyle: { color: 'rgba(126,207,255,0.1)' } }, axisLine: { show: false }, max: 1 },
|
||
yAxis: { type: 'category', data: ['暂无数据'], axisLabel: { color: '#fff', fontSize: 9 }, axisLine: { show: false }, axisTick: { show: false } },
|
||
series: [{ type: 'bar', data: [0], barWidth: '65%', itemStyle: { color: '#00e4ff88' } }]
|
||
});
|
||
|
||
// 告警级别分布图
|
||
const levelChart = echarts.init(document.getElementById('alertLevelChart'));
|
||
levelChart.setOption({
|
||
tooltip: { trigger: 'item', backgroundColor: 'rgba(0,21,41,0.95)', borderColor: '#00e4ff', textStyle: { color: '#fff', fontSize: 11 } },
|
||
series: [{ type: 'pie', radius: ['35%', '65%'], center: ['50%', '55%'], data: [
|
||
{ name: '暂无数据', value: 1, itemStyle: { color: '#666' } }
|
||
], label: { show: false } }]
|
||
});
|
||
|
||
// 处理状态图
|
||
const statusChart = echarts.init(document.getElementById('alertStatusChart'));
|
||
statusChart.setOption({
|
||
tooltip: { trigger: 'item', backgroundColor: 'rgba(0,21,41,0.95)', borderColor: '#00e4ff', textStyle: { color: '#fff', fontSize: 11 } },
|
||
series: [{ type: 'pie', radius: ['35%', '65%'], center: ['50%', '55%'], data: [
|
||
{ name: '暂无数据', value: 1, itemStyle: { color: '#666' } }
|
||
], label: { show: false } }]
|
||
});
|
||
|
||
// 24小时趋势图
|
||
const trendChart = echarts.init(document.getElementById('alertTrendChart'));
|
||
const hours = ['00', '04', '08', '12', '16', '20'];
|
||
trendChart.setOption({
|
||
tooltip: { trigger: 'axis', backgroundColor: 'rgba(0,21,41,0.95)', borderColor: '#00e4ff', textStyle: { color: '#fff', fontSize: 11 } },
|
||
grid: { top: 25, left: 30, right: 15, bottom: 25 },
|
||
xAxis: { type: 'category', data: hours, axisLabel: { color: '#7ecfff', fontSize: 8 }, axisLine: { show: false }, axisTick: { show: false } },
|
||
yAxis: { type: 'value', axisLabel: { color: '#7ecfff', fontSize: 8 }, splitLine: { lineStyle: { color: 'rgba(126,207,255,0.1)' } }, axisLine: { show: false } },
|
||
series: [{ type: 'line', data: [0, 0, 0, 0, 0, 0], smooth: true, lineStyle: { color: '#00e4ff', width: 2 }, itemStyle: { color: '#00e4ff' }, symbol: 'circle', symbolSize: 4 }]
|
||
});
|
||
}
|
||
}, 200);
|
||
|
||
charts.alert = null; // 将在initAlertChart中重新设置
|
||
}
|
||
|
||
// 添加窗口resize事件监听
|
||
window.addEventListener('resize', () => {
|
||
Object.values(charts).forEach(chart => {
|
||
// 检查是否为ECharts实例并且有resize方法
|
||
if (chart &&
|
||
chart.resize &&
|
||
typeof chart.resize === 'function' &&
|
||
chart.getOption) { // ECharts实例通常有getOption方法
|
||
try {
|
||
chart.resize();
|
||
} catch (error) {
|
||
console.warn('图表resize失败:', error);
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
// 初始化消息统计图表
|
||
const messageStatsContainer = document.getElementById('messageStatsChart');
|
||
if (messageStatsContainer) {
|
||
charts.messageStats = echarts.init(messageStatsContainer);
|
||
|
||
// 消息类型颜色定义(与message_view.html保持一致)
|
||
const messageTypeColors = {
|
||
'announcement': '#1890ff', // 蓝色 - 公告
|
||
'notification': '#52c41a', // 绿色 - 通知
|
||
'job': '#722ed1', // 紫色 - 作业指导
|
||
'task': '#fa8c16', // 橙色 - 任务管理
|
||
'warning': '#f5222d' // 红色 - 告警
|
||
};
|
||
|
||
// 初始化消息统计图表
|
||
const messageStatsOption = {
|
||
tooltip: {
|
||
trigger: 'item',
|
||
formatter: '{a} <br/>{b}: {c} ({d}%)'
|
||
},
|
||
legend: {
|
||
show: false
|
||
},
|
||
series: [{
|
||
name: '消息类型',
|
||
type: 'pie',
|
||
radius: ['30%', '70%'],
|
||
center: ['50%', '60%'],
|
||
avoidLabelOverlap: false,
|
||
label: {
|
||
show: false
|
||
},
|
||
emphasis: {
|
||
label: {
|
||
show: true,
|
||
fontSize: '10',
|
||
fontWeight: 'bold',
|
||
color: '#fff'
|
||
}
|
||
},
|
||
labelLine: {
|
||
show: false
|
||
},
|
||
data: [
|
||
{value: 0, name: '公告', itemStyle: {color: messageTypeColors.announcement}},
|
||
{value: 0, name: '工作指引', itemStyle: {color: messageTypeColors.job}},
|
||
{value: 0, name: '通知', itemStyle: {color: messageTypeColors.notification}},
|
||
{value: 0, name: '任务管理', itemStyle: {color: messageTypeColors.task}},
|
||
{value: 0, name: '告警', itemStyle: {color: messageTypeColors.warning}}
|
||
]
|
||
}]
|
||
};
|
||
charts.messageStats.setOption(messageStatsOption);
|
||
}
|
||
|
||
// 返回图表实例对象,以便其他地方可能需要使用
|
||
return charts;
|
||
|
||
} catch (error) {
|
||
console.error('Error initializing charts:', error);
|
||
}
|
||
}
|
||
|
||
// DOMContentLoaded 事件监听器已移至 app-initializer.js 模块 (Phase 7)
|
||
// 图表初始化、数据刷新、定时器已由 AppInitializer.initChartsAndData() 处理
|
||
|
||
// 修改消息列表初始化
|
||
function initMessageList(data) {
|
||
const messageList = document.getElementById('messageList');
|
||
const messageCount = document.getElementById('messageCount');
|
||
|
||
if (!messageList || !messageCount) return;
|
||
|
||
// 消息类型颜色定义(参考message_view.html)
|
||
const messageTypeColors = {
|
||
'announcement': '#1890ff', // 蓝色 - 公告
|
||
'notification': '#52c41a', // 绿色 - 通知
|
||
'job': '#722ed1', // 紫色 - 作业指导
|
||
'task': '#fa8c16', // 橙色 - 任务管理
|
||
'warning': '#f5222d' // 红色 - 告警
|
||
};
|
||
|
||
const typeMap = {
|
||
'announcement': '公告',
|
||
'job': '工作指引',
|
||
'notification': '通知',
|
||
'task': '任务管理',
|
||
'warning': '告警'
|
||
};
|
||
|
||
// 从message_info中获取数据(安全访问)
|
||
//console.log('initMessageList.data', data);
|
||
const messageInfo = data.message_info || {};
|
||
const messages = messageInfo.messages || [];
|
||
// 过滤出状态为pending的消息
|
||
const pendingMessages = messages.filter(msg => msg.message_status === 'pending' || msg.message_status === '1');
|
||
|
||
// 更新消息计数
|
||
messageCount.textContent = pendingMessages.length;
|
||
|
||
// 统计消息数据
|
||
const today = new Date().toDateString();
|
||
const todayMessages = messages.filter(msg => new Date(msg.received_time).toDateString() === today).length;
|
||
const unreadMessages = messages.filter(msg => msg.message_status === 'pending' || msg.message_status === '1').length;
|
||
const urgentMessages = messages.filter(msg => msg.priority === 'high' || msg.priority === 'urgent').length;
|
||
|
||
// 更新统计显示
|
||
document.getElementById('todayMessages').textContent = todayMessages;
|
||
document.getElementById('messageUnreadCount').textContent = unreadMessages;
|
||
document.getElementById('urgentMessages').textContent = urgentMessages;
|
||
|
||
// 统计消息类型分布 - 使用正确的类型映射
|
||
const messageTypes = {
|
||
'公告': 0,
|
||
'工作指引': 0,
|
||
'通知': 0,
|
||
'任务管理': 0,
|
||
'告警': 0
|
||
};
|
||
|
||
messages.forEach(msg => {
|
||
const type = msg.message_type || 'notification';
|
||
const typeName = typeMap[type] || '通知';
|
||
if (messageTypes.hasOwnProperty(typeName)) {
|
||
messageTypes[typeName]++;
|
||
}
|
||
});
|
||
|
||
// 更新消息统计图表
|
||
if (globalCharts && globalCharts.messageStats) {
|
||
const chartData = [
|
||
{value: messageTypes['公告'], name: '公告', itemStyle: {color: messageTypeColors.announcement}},
|
||
{value: messageTypes['工作指引'], name: '工作指引', itemStyle: {color: messageTypeColors.job}},
|
||
{value: messageTypes['通知'], name: '通知', itemStyle: {color: messageTypeColors.notification}},
|
||
{value: messageTypes['任务管理'], name: '任务管理', itemStyle: {color: messageTypeColors.task}},
|
||
{value: messageTypes['告警'], name: '告警', itemStyle: {color: messageTypeColors.warning}}
|
||
];
|
||
|
||
globalCharts.messageStats.setOption({
|
||
series: [{
|
||
data: chartData
|
||
}]
|
||
});
|
||
}
|
||
|
||
// 清空现有消息
|
||
messageList.innerHTML = '';
|
||
|
||
// 创建消息容器
|
||
const messageContainer = document.createElement('div');
|
||
messageContainer.className = 'message-container';
|
||
|
||
// 添加所有消息
|
||
pendingMessages.forEach(message => {
|
||
const msgType = message.message_type || 'notification';
|
||
const msgColor = messageTypeColors[msgType] || messageTypeColors.notification;
|
||
const msgTypeName = typeMap[msgType] || '通知';
|
||
|
||
const messageElement = document.createElement('div');
|
||
messageElement.className = 'message-item';
|
||
messageElement.innerHTML = `
|
||
<div class="message-header">
|
||
<span style="color: ${msgColor}; font-weight: bold;">[${msgTypeName}] ${message.department_name || '未知部门'}-${message.user_name || '系统'}</span>
|
||
<span class="message-time">${message.received_time || new Date().toLocaleString()}</span>
|
||
</div>
|
||
<div class="message-content" style="border-left: 3px solid ${msgColor}; padding-left: 6px;">${message.message || '无消息内容'}</div>
|
||
`;
|
||
messageContainer.appendChild(messageElement);
|
||
});
|
||
|
||
// 如果没有消息,显示提示信息
|
||
if (pendingMessages.length === 0) {
|
||
messageList.innerHTML = '<div class="no-messages">暂无待处理消息</div>';
|
||
return;
|
||
}
|
||
|
||
// 将消息容器添加到列表中
|
||
messageList.appendChild(messageContainer);
|
||
|
||
// 添加样式 - 移除重复样式防止冲突
|
||
const existingStyle = document.getElementById('message-scroll-styles');
|
||
if (existingStyle) {
|
||
existingStyle.remove();
|
||
}
|
||
|
||
const style = document.createElement('style');
|
||
style.id = 'message-scroll-styles';
|
||
style.textContent = `
|
||
#messageList {
|
||
height: calc(100% - 5px);
|
||
overflow: hidden;
|
||
position: relative;
|
||
background: linear-gradient(135deg, rgba(0, 42, 74, 0.8), rgba(0, 74, 114, 0.6));
|
||
border-radius: 8px;
|
||
border: 1px solid rgba(0, 228, 255, 0.25);
|
||
box-shadow: inset 0 1px 3px rgba(0, 228, 255, 0.1);
|
||
}
|
||
|
||
.message-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
width: 100%;
|
||
padding: 8px;
|
||
}
|
||
|
||
.message-item {
|
||
background: linear-gradient(135deg, rgba(0, 62, 94, 0.8), rgba(0, 94, 134, 0.6));
|
||
border: 1px solid rgba(0, 228, 255, 0.2);
|
||
border-radius: 6px;
|
||
padding: 8px 12px;
|
||
margin-bottom: 6px;
|
||
position: relative;
|
||
transition: all 0.3s ease;
|
||
backdrop-filter: blur(10px);
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15), 0 0 15px rgba(0, 228, 255, 0.05);
|
||
}
|
||
|
||
.message-item::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
bottom: 0;
|
||
width: 4px;
|
||
border-radius: 2px 0 0 2px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.message-item.type-announcement::before { background: linear-gradient(135deg, #4A90E2, #357ABD); }
|
||
.message-item.type-notification::before { background: linear-gradient(135deg, #7ED321, #5BA317); }
|
||
.message-item.type-job::before { background: linear-gradient(135deg, #9013FE, #6A1B9A); }
|
||
.message-item.type-task::before { background: linear-gradient(135deg, #FF9500, #E6830F); }
|
||
.message-item.type-warning::before { background: linear-gradient(135deg, #F5A623, #E8940F); }
|
||
|
||
.message-item:hover {
|
||
transform: translateX(2px);
|
||
border-color: rgba(64, 169, 255, 0.4);
|
||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3), 0 0 20px rgba(64, 169, 255, 0.1);
|
||
}
|
||
|
||
.message-item:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.message-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.message-type-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
padding: 2px 8px;
|
||
border-radius: 12px;
|
||
font-size: 10px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.5px;
|
||
text-transform: uppercase;
|
||
backdrop-filter: blur(5px);
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.message-type-badge.announcement {
|
||
background: linear-gradient(135deg, rgba(74, 144, 226, 0.2), rgba(53, 122, 189, 0.3));
|
||
color: #87CEEB;
|
||
border-color: rgba(74, 144, 226, 0.3);
|
||
}
|
||
|
||
.message-type-badge.notification {
|
||
background: linear-gradient(135deg, rgba(126, 211, 33, 0.2), rgba(91, 163, 23, 0.3));
|
||
color: #98FB98;
|
||
border-color: rgba(126, 211, 33, 0.3);
|
||
}
|
||
|
||
.message-type-badge.job {
|
||
background: linear-gradient(135deg, rgba(144, 19, 254, 0.2), rgba(106, 27, 154, 0.3));
|
||
color: #DDA0DD;
|
||
border-color: rgba(144, 19, 254, 0.3);
|
||
}
|
||
|
||
.message-type-badge.task {
|
||
background: linear-gradient(135deg, rgba(255, 149, 0, 0.2), rgba(230, 131, 15, 0.3));
|
||
color: #FFB84D;
|
||
border-color: rgba(255, 149, 0, 0.3);
|
||
}
|
||
|
||
.message-type-badge.warning {
|
||
background: linear-gradient(135deg, rgba(245, 166, 35, 0.2), rgba(232, 148, 15, 0.3));
|
||
color: #FFC04D;
|
||
border-color: rgba(245, 166, 35, 0.3);
|
||
}
|
||
|
||
.message-sender {
|
||
color: rgba(255, 255, 255, 0.9);
|
||
font-size: 11px;
|
||
font-weight: 500;
|
||
margin-left: 8px;
|
||
}
|
||
|
||
.message-time {
|
||
color: rgba(64, 169, 255, 0.8);
|
||
font-size: 9px;
|
||
font-weight: 400;
|
||
font-family: 'Consolas', 'Monaco', monospace;
|
||
background: rgba(64, 169, 255, 0.1);
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
border: 1px solid rgba(64, 169, 255, 0.2);
|
||
}
|
||
|
||
.message-content {
|
||
color: rgba(255, 255, 255, 0.95);
|
||
font-size: 12px;
|
||
line-height: 1.4;
|
||
font-weight: 400;
|
||
margin-top: 2px;
|
||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 2;
|
||
-webkit-box-orient: vertical;
|
||
overflow: hidden;
|
||
max-height: 32px;
|
||
}
|
||
|
||
.no-messages {
|
||
text-align: center;
|
||
padding: 30px 20px;
|
||
color: rgba(255, 255, 255, 0.4);
|
||
font-size: 14px;
|
||
background: linear-gradient(135deg, rgba(20, 45, 85, 0.3), rgba(30, 55, 95, 0.2));
|
||
border-radius: 8px;
|
||
border: 1px dashed rgba(64, 169, 255, 0.3);
|
||
margin: 20px;
|
||
}
|
||
|
||
/* 滚动动画 */
|
||
.message-container.scrolling {
|
||
animation: verticalScroll linear infinite;
|
||
}
|
||
|
||
@keyframes verticalScroll {
|
||
0% {
|
||
transform: translateY(0);
|
||
}
|
||
100% {
|
||
transform: translateY(-50%);
|
||
}
|
||
}
|
||
|
||
/* 添加呼吸光效 */
|
||
.message-item::after {
|
||
content: '';
|
||
position: absolute;
|
||
top: -1px;
|
||
left: -1px;
|
||
right: -1px;
|
||
bottom: -1px;
|
||
background: linear-gradient(45deg, transparent, rgba(64, 169, 255, 0.1), transparent);
|
||
border-radius: 6px;
|
||
opacity: 0;
|
||
animation: breathe 3s ease-in-out infinite;
|
||
pointer-events: none;
|
||
z-index: -1;
|
||
}
|
||
|
||
@keyframes breathe {
|
||
0%, 100% { opacity: 0; }
|
||
50% { opacity: 0.3; }
|
||
}
|
||
`;
|
||
document.head.appendChild(style);
|
||
|
||
// 实现滚动逻辑
|
||
setTimeout(() => {
|
||
const containerHeight = messageList.clientHeight;
|
||
const contentHeight = messageContainer.scrollHeight;
|
||
|
||
console.log('Container height:', containerHeight, 'Content height:', contentHeight);
|
||
|
||
if (contentHeight > containerHeight && pendingMessages.length > 3) {
|
||
// 复制内容实现无缝滚动
|
||
messageContainer.innerHTML += messageContainer.innerHTML;
|
||
|
||
// 计算滚动时间:每个消息显示3秒
|
||
const scrollDuration = pendingMessages.length * 3;
|
||
|
||
// 添加滚动类和设置动画时间
|
||
messageContainer.classList.add('scrolling');
|
||
messageContainer.style.animationDuration = `${scrollDuration}s`;
|
||
|
||
console.log('启动滚动动画,持续时间:', scrollDuration + 's');
|
||
|
||
// 鼠标悬停控制
|
||
messageList.addEventListener('mouseenter', () => {
|
||
messageContainer.style.animationPlayState = 'paused';
|
||
});
|
||
|
||
messageList.addEventListener('mouseleave', () => {
|
||
messageContainer.style.animationPlayState = 'running';
|
||
});
|
||
} else {
|
||
console.log('消息数量少,不启动滚动');
|
||
}
|
||
|
||
// 添加消息点击事件
|
||
addMessageClickEvents();
|
||
}, 500); // 延迟500ms确保DOM渲染完成
|
||
}
|
||
|
||
// 添加消息面板点击处理函数 - 统一版本
|
||
function openMessagePanel() {
|
||
createModalWindow('/message_view.html?customerId=' + GLOBAL_CUSTOMER_ID, '消息管理', '90%', '90%');
|
||
}
|
||
|
||
// 消息项点击事件已移至面板级别,无需单独处理
|
||
function addMessageClickEvents() {
|
||
// 整个消息面板现在都可以点击,不需要单独为消息项添加点击事件
|
||
console.log('消息面板点击事件已在HTML中配置');
|
||
}
|
||
</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>
|
||
// Declare infoWindow globally
|
||
// let infoWindow; // 移除
|
||
let geoLevelF,geoLevelE,geo,map,loca,breathGreen,breathRed,breathYellow,breathOrange;//变量合并
|
||
|
||
|
||
|
||
// 地图初始化重试计数器和状态锁
|
||
let mapInitRetryCount = 0;
|
||
const MAX_RETRY_COUNT = 3;
|
||
let isMapInitializing = false;
|
||
|
||
function initializeMap(deptId, userId, retryCount = 0) {
|
||
// 检查是否正在初始化
|
||
if (isMapInitializing && retryCount === 0) {
|
||
console.warn('⚠️ 地图正在初始化中,跳过重复调用');
|
||
return;
|
||
}
|
||
|
||
// 检查重试次数
|
||
if (retryCount >= MAX_RETRY_COUNT) {
|
||
console.error('🚫 地图初始化重试次数超限,停止重试');
|
||
isMapInitializing = false;
|
||
return;
|
||
}
|
||
|
||
isMapInitializing = true;
|
||
|
||
// 检查必要的库是否已加载
|
||
if (typeof AMap === 'undefined') {
|
||
console.error('AMap 库未加载,延迟重试...', `重试次数: ${retryCount + 1}`);
|
||
isMapInitializing = false; // 重置状态锁
|
||
setTimeout(() => initializeMap(deptId, userId, retryCount + 1), 1000);
|
||
return;
|
||
}
|
||
if (typeof Loca === 'undefined') {
|
||
console.error('Loca 库未加载,延迟重试...', `重试次数: ${retryCount + 1}`);
|
||
isMapInitializing = false; // 重置状态锁
|
||
setTimeout(() => initializeMap(deptId, userId, retryCount + 1), 1000);
|
||
return;
|
||
}
|
||
|
||
console.log('AMap和Loca库加载完成,开始初始化地图');
|
||
|
||
// 清理之前的实例,避免冲突
|
||
if (window.loca) {
|
||
try {
|
||
window.loca.destroy();
|
||
console.log('🧹 清理旧的Loca实例');
|
||
} catch (e) {
|
||
console.warn('清理旧Loca实例时出错:', e);
|
||
}
|
||
}
|
||
if (window.map) {
|
||
try {
|
||
window.map.destroy();
|
||
console.log('🧹 清理旧的地图实例');
|
||
} catch (e) {
|
||
console.warn('清理旧地图实例时出错:', e);
|
||
}
|
||
}
|
||
|
||
//console.log('initializeMap.deptId',deptId);
|
||
//console.log('initializeMap.userId',userId);
|
||
//console.log('initializeMap.severityLevel',`./generateAlertJson?customerId=${deptId}&userId=${userId}&severityLevel=critical`);
|
||
|
||
// v1.0.32 - 添加medium级别告警点支持,优化URL构建逻辑
|
||
const baseParams = `customerId=${GLOBAL_CUSTOMER_ID}`;
|
||
const deptParam = (deptId && deptId !== '-1' && deptId !== 'all') ? `&deptId=${deptId}` : '';
|
||
const userParam = (userId && userId !== '-1' && userId !== 'all') ? `&userId=${userId}` : '';
|
||
|
||
const dataUrls = {
|
||
critical: `./generateAlertJson?${baseParams}${deptParam}${userParam}&severityLevel=critical`,
|
||
high: `./generateAlertJson?${baseParams}${deptParam}${userParam}&severityLevel=high`,
|
||
medium: `./generateAlertJson?${baseParams}${deptParam}${userParam}&severityLevel=medium`,
|
||
health: `./generateHealthJson?${baseParams}${deptParam}${userParam}`
|
||
};
|
||
|
||
console.log('🔗 数据URL准备完成,等待地图加载');
|
||
|
||
let current_coordinates = {
|
||
'longitude': 114.01508952,
|
||
'latitude': 22.58036796,
|
||
'altitude': 0
|
||
}
|
||
|
||
//console.log('current_coordinates',current_coordinates);
|
||
|
||
console.log('🗺️ 开始创建地图实例');
|
||
map = window.map = new AMap.Map('map-container', {
|
||
zoom: 17,
|
||
center: [current_coordinates['longitude'],current_coordinates['latitude']],
|
||
pitch: 45,
|
||
showLabel: false,
|
||
mapStyle: 'amap://styles/blue',
|
||
viewMode: '3D',
|
||
});
|
||
|
||
// 等待地图完全加载后再初始化Loca图层
|
||
map.on('complete', function() {
|
||
console.log('🎉 地图加载完成,开始完整的Loca初始化流程');
|
||
|
||
// 延迟执行,确保地图内部状态完全稳定
|
||
setTimeout(() => {
|
||
// 再次确保地图对象存在且有效
|
||
if (!map || !window.map || map !== window.map) {
|
||
console.error('❌ 地图对象不存在或无效,无法创建Loca容器');
|
||
isMapInitializing = false;
|
||
return;
|
||
}
|
||
|
||
// 检查地图容器DOM是否存在
|
||
const mapContainer = document.getElementById('map-container');
|
||
if (!mapContainer) {
|
||
console.error('❌ 地图容器DOM不存在');
|
||
isMapInitializing = false;
|
||
return;
|
||
}
|
||
|
||
try {
|
||
console.log('🔧 步骤1: 创建Loca数据源');
|
||
|
||
// v1.0.32 - 创建所有级别的数据源包括medium
|
||
geoLevelF = new Loca.GeoJSONSource({
|
||
url: dataUrls.critical
|
||
});
|
||
|
||
geoLevelE = new Loca.GeoJSONSource({
|
||
url: dataUrls.high
|
||
});
|
||
|
||
geoLevelM = new Loca.GeoJSONSource({
|
||
url: dataUrls.medium
|
||
});
|
||
|
||
geo = new Loca.GeoJSONSource({
|
||
url: dataUrls.health
|
||
});
|
||
|
||
console.log('✅ Loca数据源创建成功');
|
||
|
||
console.log('🔧 步骤2: 创建Loca容器');
|
||
|
||
loca = new Loca.Container({
|
||
map: window.map, // 使用window.map确保引用正确
|
||
});
|
||
|
||
// 确保Loca容器创建成功且有效
|
||
if (!loca || !loca.map) {
|
||
console.error('❌ Loca容器创建失败或map引用无效');
|
||
isMapInitializing = false;
|
||
return;
|
||
}
|
||
|
||
console.log('✅ Loca容器创建成功,开始创建图层');
|
||
|
||
} catch (error) {
|
||
console.error('❌ 创建Loca容器或数据源时出错:', error);
|
||
isMapInitializing = false;
|
||
return;
|
||
}
|
||
|
||
try {
|
||
console.log('🔧 步骤3: 开始创建图层');
|
||
|
||
// 验证Loca容器和地图的有效性
|
||
if (!loca || !loca.map || !window.map) {
|
||
console.error('❌ 图层创建前检查失败:Loca容器或地图无效');
|
||
isMapInitializing = false;
|
||
return;
|
||
}
|
||
|
||
console.log('🟢 创建绿色健康点图层');
|
||
breathGreen = new Loca.ScatterLayer({
|
||
loca,
|
||
zIndex: 113,
|
||
opacity: 1,
|
||
visible: true,
|
||
zooms: [2, 22],
|
||
});
|
||
|
||
// 安全设置数据源
|
||
if (geo) {
|
||
breathGreen.setSource(geo);
|
||
} else {
|
||
console.warn('绿色点数据源不存在,跳过设置');
|
||
}
|
||
|
||
breathGreen.setStyle({
|
||
unit: 'meter',
|
||
color: 'rgb(39, 207, 14)',
|
||
size: [10, 10],
|
||
borderWidth: 0,
|
||
exture: 'https://a.amap.com/Loca/static/loca-v2/demos/images/breath_green.png',
|
||
duration: 500,
|
||
animate: true,
|
||
});
|
||
|
||
// 安全添加到地图中
|
||
loca.add(breathGreen);
|
||
console.log('✅ 绿色健康点图层创建成功');
|
||
|
||
console.log('🔴 创建红色告警点图层');
|
||
breathRed = new Loca.ScatterLayer({
|
||
loca,
|
||
zIndex: 113,
|
||
opacity: 1,
|
||
visible: true,
|
||
zooms: [2, 22],
|
||
});
|
||
|
||
// 安全设置数据源
|
||
if (geoLevelF) {
|
||
breathRed.setSource(geoLevelF);
|
||
} else {
|
||
console.warn('红色点数据源不存在,跳过设置');
|
||
}
|
||
|
||
breathRed.setStyle({
|
||
unit: 'meter',
|
||
size: [60, 60],
|
||
borderWidth: 0,
|
||
texture: 'https://a.amap.com/Loca/static/loca-v2/demos/images/breath_red.png',
|
||
duration: 500,
|
||
animate: true,
|
||
});
|
||
|
||
// 安全添加到地图中
|
||
loca.add(breathRed);
|
||
console.log('✅ 红色告警点图层创建成功');
|
||
|
||
console.log('🟡 创建黄色告警点图层');
|
||
breathYellow = new Loca.ScatterLayer({
|
||
loca,
|
||
zIndex: 112,
|
||
opacity: 1,
|
||
visible: true,
|
||
zooms: [2, 22],
|
||
});
|
||
|
||
// 安全设置数据源
|
||
if (geoLevelE) {
|
||
breathYellow.setSource(geoLevelE);
|
||
} else {
|
||
console.warn('黄色点数据源不存在,跳过设置');
|
||
}
|
||
|
||
breathYellow.setStyle({
|
||
unit: 'meter',
|
||
size: [50, 50],
|
||
borderWidth: 0,
|
||
texture: 'https://a.amap.com/Loca/static/loca-v2/demos/images/breath_yellow.png',
|
||
duration: 1000,
|
||
animate: true,
|
||
});
|
||
|
||
// 安全添加到地图中
|
||
loca.add(breathYellow);
|
||
console.log('✅ 黄色告警点图层创建成功');
|
||
|
||
console.log('🟠 创建橙色medium告警点图层');
|
||
breathOrange = new Loca.ScatterLayer({
|
||
loca,
|
||
zIndex: 111,
|
||
opacity: 1,
|
||
visible: true,
|
||
zooms: [2, 22],
|
||
});
|
||
|
||
// 安全设置数据源
|
||
if (geoLevelM) {
|
||
breathOrange.setSource(geoLevelM);
|
||
} else {
|
||
console.warn('橙色点数据源不存在,跳过设置');
|
||
}
|
||
|
||
breathOrange.setStyle({
|
||
unit: 'meter',
|
||
size: [40, 40],
|
||
borderWidth: 0,
|
||
texture: 'https://a.amap.com/Loca/static/loca-v2/demos/images/breath_orange.png',
|
||
duration: 800,
|
||
animate: true,
|
||
});
|
||
|
||
// 安全添加到地图中
|
||
loca.add(breathOrange);
|
||
console.log('✅ 橙色medium告警点图层创建成功');
|
||
|
||
console.log('🔧 步骤4: 绑定交互事件和启动动画');
|
||
|
||
// v1.0.32 - 添加点击事件包含所有图层
|
||
if (window.map && breathRed && breathYellow && breathOrange && breathGreen) {
|
||
window.map.on('click',e=>{
|
||
const p=e.pixel.toArray();
|
||
let f=breathRed.queryFeature(p)||breathYellow.queryFeature(p)||breathOrange.queryFeature(p)||breathGreen.queryFeature(p);
|
||
if(f&&f.coordinates){showCustomMapInfo(f)}else{removeCustomMapInfo()}
|
||
});//优先级修正:红>黄>橙>绿
|
||
console.log('✅ 地图点击事件绑定成功(包含4个图层)');
|
||
} else {
|
||
console.warn('⚠️ 图层不完整,跳过点击事件绑定');
|
||
}
|
||
|
||
// 最终验证后启动渲染动画
|
||
if (loca && loca.animate) {
|
||
loca.animate.start();
|
||
console.log('✅ Loca渲染动画启动成功');
|
||
} else {
|
||
console.error('❌ Loca动画启动失败');
|
||
isMapInitializing = false;
|
||
return;
|
||
}
|
||
|
||
console.log('🎉 地图初始化流程完全成功!');
|
||
isMapInitializing = false; // 重置状态锁
|
||
|
||
} catch (error) {
|
||
console.error('❌ 创建Loca图层时出错:', error);
|
||
// 如果图层创建失败,尝试重新初始化
|
||
if (retryCount < MAX_RETRY_COUNT) {
|
||
console.log(`🔄 图层创建失败,3秒后重试... (重试次数: ${retryCount + 1})`);
|
||
isMapInitializing = false; // 重置状态锁以允许重试
|
||
setTimeout(() => initializeMap(deptId, userId, retryCount + 1), 3000);
|
||
} else {
|
||
console.error('🚫 图层创建重试次数超限,停止重试');
|
||
isMapInitializing = false; // 重置状态锁
|
||
}
|
||
}
|
||
|
||
}, 1000); // 延迟1000ms确保地图内部状态完全稳定
|
||
});
|
||
|
||
|
||
}
|
||
|
||
//initializeMap('{{ customerId }}','-1');
|
||
|
||
|
||
|
||
async function reverseGeocode(lng, lat) {
|
||
try {
|
||
const response = await fetch(`https://restapi.amap.com/v3/geocode/regeo?key=d45f28e481665db5b1145a5aa989e68a&location=${lng},${lat}`);
|
||
const data = await response.json();
|
||
|
||
if (data.status === '1') {
|
||
const address = data.regeocode.formatted_address;
|
||
console.log('Address:', address);
|
||
return address;
|
||
} else {
|
||
console.error('Failed to reverse geocode:', data.info);
|
||
return null;
|
||
}
|
||
} catch (error) {
|
||
console.error('Error during reverse geocoding:', error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
|
||
// handleAlert() 函数已移至 alert-panel.js 模块 (Phase 8)
|
||
|
||
// 添加自定义弹出框函数
|
||
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 (typeof callback === 'function') callback();
|
||
};
|
||
}
|
||
|
||
|
||
// mapAlertStatus() 函数已移至 alert-panel.js 模块 (Phase 8)
|
||
|
||
// 筛选面板伸缩功能
|
||
function toggleFilter() {
|
||
const p=document.getElementById('filterPanel'),t=document.getElementById('filterToggle');
|
||
if(p.classList.contains('expanded')){p.classList.remove('expanded');t.textContent='🔍';}
|
||
else{p.classList.add('expanded');t.textContent='✕';}
|
||
}
|
||
|
||
// 模糊搜索功能
|
||
let allUsers=[],filteredUsers=[];
|
||
function initSearchFilter() {
|
||
const searchInput=document.getElementById('searchInput'),userSelect=document.getElementById('userSelect'),deptSelect=document.getElementById('deptSelect');
|
||
|
||
searchInput.addEventListener('input',e=>{
|
||
const q=e.target.value.toLowerCase().trim();
|
||
if(!q){filteredUsers=allUsers;updateUserSelect();}
|
||
else{
|
||
filteredUsers=allUsers.filter(u=>u.user_name?.toLowerCase().includes(q)||u.dept_name?.toLowerCase().includes(q)||u.device_sn?.toLowerCase().includes(q));
|
||
updateUserSelect();
|
||
}
|
||
});
|
||
|
||
deptSelect.addEventListener('change',e=>{
|
||
const dept=e.target.value;
|
||
filteredUsers=dept?allUsers.filter(u=>u.dept_name===dept):allUsers;
|
||
updateUserSelect();
|
||
searchInput.value='';
|
||
});
|
||
}
|
||
|
||
function updateUserSelect() {
|
||
const s=document.getElementById('userSelect');
|
||
s.innerHTML='<option value="">选择用户</option>';
|
||
filteredUsers.forEach(u=>s.innerHTML+=`<option value="${u.user_id}">${u.user_name}${u.position?' - '+u.position:''}</option>`);
|
||
}
|
||
|
||
function setUsersData(users) {
|
||
allUsers=users||[];filteredUsers=allUsers;updateUserSelect();
|
||
}
|
||
|
||
// 添加点击外部关闭筛选面板的功能
|
||
// 全局 click 事件监听器已移至 app-initializer.js 模块 (Phase 7)
|
||
// 筛选面板点击事件由 AppInitializer.bindGlobalEvents() 处理
|
||
|
||
// 初始化人员管理面板
|
||
function initPersonnelManagementPanel(data) {
|
||
// 安全获取用户信息,支持多种数据结构
|
||
const userInfo = data.user_info || data.current || {};
|
||
const totalUsers = userInfo.totalUsers || userInfo.user_count || 0;
|
||
const totalDevices = userInfo.totalDevices || userInfo.device_count || 0;
|
||
|
||
// 更新总览数据(带安全检查)
|
||
const totalUsersEl = document.getElementById('totalUsers');
|
||
const totalDevicesEl = document.getElementById('totalBindDevices');
|
||
|
||
if (totalUsersEl) totalUsersEl.innerText = totalUsers;
|
||
if (totalDevicesEl) totalDevicesEl.innerText = 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 activeOrgCount = Object.keys(departmentCount).length;
|
||
|
||
// 模拟在线用户数据(实际应从后端获取)
|
||
const onlineUsers = Math.floor(totalUsers * 0.75); // 假设75%在线
|
||
const onlineRate = totalUsers > 0 ? ((totalDevices / 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('activeOrgCount').textContent = activeOrgCount;
|
||
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 };
|
||
}
|
||
|
||
// getPastDateStr() 函数已移至 utils.js 模块
|
||
|
||
function loadHealthScoreChart() {
|
||
console.log('loadHealthScoreChart 开始执行,使用全局customerId:', GLOBAL_CUSTOMER_ID);
|
||
|
||
const endDate = getPastDateStr(0), startDate = getPastDateStr(6);
|
||
|
||
if (globalCharts.healthScore) {
|
||
// 获取日期范围
|
||
const today = new Date();
|
||
const yesterday = new Date(today);
|
||
yesterday.setDate(yesterday.getDate() - 7);
|
||
|
||
const startDate = formatDate(yesterday);
|
||
const endDate = formatDate(today);
|
||
|
||
api.health.getScore({
|
||
orgId: GLOBAL_CUSTOMER_ID,
|
||
startDate: startDate,
|
||
endDate: endDate,
|
||
includeFactors: true
|
||
})
|
||
.then(response => response.json())
|
||
.then(result => {
|
||
if (result.success && result.data && result.data.healthScores && result.data.healthScores.factors) {
|
||
const factors = result.data.healthScores.factors;
|
||
console.log('factors',factors);
|
||
|
||
// 更新总分显示
|
||
const totalScoreElement = document.querySelector('.total-score');
|
||
if (totalScoreElement) {
|
||
totalScoreElement.textContent = `总分:${result.data.summary.overallScore}`;
|
||
}
|
||
|
||
// 兼容驼峰和下划线命名的辅助函数
|
||
function getFactorScore(factors, camelCase, snakeCase) {
|
||
return factors[camelCase]?.score || factors[snakeCase]?.score || 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: `心率 ${getFactorScore(factors, 'heartRate', 'heart_rate')}分`, max: 100 },
|
||
{ name: `血氧 ${getFactorScore(factors, 'bloodOxygen', 'blood_oxygen')}分`, max: 100 },
|
||
{ name: `体温 ${getFactorScore(factors, 'temperature', 'temperature')}分`, max: 100 },
|
||
{ name: `步数 ${getFactorScore(factors, 'step', 'step')}分`, max: 100 },
|
||
{ name: `卡路里 ${getFactorScore(factors, 'calorie', 'calorie')}分`, max: 100 },
|
||
{ name: `收缩压 ${getFactorScore(factors, 'pressureHigh', 'pressure_high')}分`, max: 100 },
|
||
{ name: `舒张压 ${getFactorScore(factors, 'pressureLow', 'pressure_low')}分`, max: 100 },
|
||
{ name: `压力 ${getFactorScore(factors, 'stress', 'stress')}分`, max: 100 },
|
||
{ name: `睡眠 ${getFactorScore(factors, 'sleep', 'sleep')}分`, 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: [
|
||
getFactorScore(factors, 'heartRate', 'heart_rate'),
|
||
getFactorScore(factors, 'bloodOxygen', 'blood_oxygen'),
|
||
getFactorScore(factors, 'temperature', 'temperature'),
|
||
getFactorScore(factors, 'step', 'step'),
|
||
getFactorScore(factors, 'calorie', 'calorie'),
|
||
getFactorScore(factors, 'pressureHigh', 'pressure_high'),
|
||
getFactorScore(factors, 'pressureLow', 'pressure_low'),
|
||
getFactorScore(factors, 'stress', 'stress'),
|
||
getFactorScore(factors, 'sleep', 'sleep')
|
||
],
|
||
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: '睡眠 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, 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: '睡眠 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, 0],
|
||
name: '当前状态',
|
||
itemStyle: {
|
||
color: '#00e4ff'
|
||
},
|
||
areaStyle: {
|
||
color: 'rgba(0,228,255,0.4)'
|
||
}
|
||
}]
|
||
}]
|
||
};
|
||
globalCharts.healthScore.setOption(healthScoreOption);
|
||
});
|
||
}
|
||
}
|
||
|
||
function loadBaselineTrendChart(orgId) {
|
||
console.log('loadBaselineTrendChart 开始执行,orgId:', orgId);
|
||
|
||
const endDate = getPastDateStr(0), startDate = getPastDateStr(6);
|
||
|
||
api.health.getBaseline(orgId, startDate, endDate)
|
||
.then(r=>r.json())
|
||
.then(result=>{
|
||
console.log('健康数据接口返回:', result);
|
||
|
||
// 检查是否有数据,如果没有数据则生成baseline
|
||
if (!result || !result.dates || result.dates.length === 0) {
|
||
console.warn('baseline数据缺失,开始生成baseline');
|
||
return generateBaselineAndRetry(orgId, startDate, endDate);
|
||
}
|
||
|
||
renderHealthChart(result);
|
||
})
|
||
.catch(error => {
|
||
console.error('健康数据加载失败:', error);
|
||
// 尝试生成baseline后重试
|
||
generateBaselineAndRetry(orgId, startDate, endDate);
|
||
});
|
||
}
|
||
|
||
// 生成baseline并重试获取数据
|
||
function generateBaselineAndRetry(orgId, startDate, endDate) {
|
||
console.log('正在生成baseline数据...');
|
||
|
||
api.health.generateBaseline(endDate)
|
||
.then(r => r.json())
|
||
.then(generateResult => {
|
||
console.log('baseline生成结果:', generateResult);
|
||
|
||
if (generateResult.success) {
|
||
// 生成成功后重新获取数据
|
||
return api.health.getBaseline(orgId, startDate, endDate);
|
||
} else {
|
||
throw new Error('baseline生成失败: ' + generateResult.error);
|
||
}
|
||
})
|
||
.then(r => r.json())
|
||
.then(result => {
|
||
console.log('重新获取的健康数据:', result);
|
||
renderHealthChart(result);
|
||
})
|
||
.catch(error => {
|
||
console.error('生成baseline或重新获取数据失败:', error);
|
||
//showDefaultHealthData();
|
||
});
|
||
}
|
||
|
||
// 渲染健康图表 - 专业亮眼版本 v1.0.34
|
||
function renderHealthChart(result) {
|
||
const {dates, metrics, health_summary} = result;
|
||
console.log('🎯 renderHealthChart 接收数据:', {dates, metrics: metrics?.map(m=>({name:m.name,count:m.values?.length}))});
|
||
|
||
const trendChart = echarts.init(document.getElementById('trendChart'));
|
||
|
||
// 大屏科技风格健康指标配置 - 协调版 v1.0.34
|
||
const healthMetrics = {
|
||
'心率': { color: '#ff6b9d', icon: '💗', gradient: ['#ff6b9d', '#ff8fb3'], range: [60, 100] },
|
||
'血氧': { color: '#00e4ff', icon: '🫁', gradient: ['#00e4ff', '#5beeff'], range: [95, 100] },
|
||
'体温': { color: '#ffaa00', icon: '🌡️', gradient: ['#ffaa00', '#ffcc55'], range: [36, 38] },
|
||
'压力': { color: '#ff7700', icon: '😰', gradient: ['#ff7700', '#ff9944'], range: [0, 100] },
|
||
'睡眠': { color: '#7ecfff', icon: '😴', gradient: ['#7ecfff', '#a8d8ff'], range: [6, 10] }
|
||
};
|
||
|
||
const series = [];
|
||
|
||
// 处理现有数据
|
||
if (metrics?.length > 0) {
|
||
Object.keys(healthMetrics).forEach(metricName => {
|
||
const metric = metrics.find(m => m.name === metricName);
|
||
if (metric?.values?.length > 0) {
|
||
const config = healthMetrics[metricName];
|
||
series.push({
|
||
name: metricName,
|
||
type: 'line',
|
||
data: metric.values,
|
||
smooth: true,
|
||
symbol: 'circle',
|
||
symbolSize: 6,
|
||
lineStyle: {
|
||
width: 3,
|
||
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||
{ offset: 0, color: config.gradient[0] },
|
||
{ offset: 1, color: config.gradient[1] }
|
||
]),
|
||
shadowColor: config.color + '40',
|
||
shadowBlur: 8
|
||
},
|
||
itemStyle: {
|
||
color: config.color,
|
||
borderColor: '#001529',
|
||
borderWidth: 1,
|
||
shadowColor: config.color,
|
||
shadowBlur: 6
|
||
},
|
||
areaStyle: {
|
||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||
{ offset: 0, color: config.color + '30' },
|
||
{ offset: 1, color: config.color + '05' }
|
||
])
|
||
},
|
||
emphasis: {
|
||
focus: 'series',
|
||
scale: 1.1
|
||
}
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
// 如果没有睡眠数据,生成模拟数据
|
||
const hasSleep = series.some(s => s.name === '睡眠');
|
||
if (!hasSleep && dates?.length > 0) {
|
||
console.log('⚠️ 未找到睡眠数据,生成模拟数据');
|
||
const sleepData = dates.map(() => (7 + Math.random() * 2).toFixed(1));
|
||
const sleepConfig = healthMetrics['睡眠'];
|
||
series.push({
|
||
name: '睡眠',
|
||
type: 'line',
|
||
data: sleepData,
|
||
smooth: true,
|
||
symbol: 'circle',
|
||
symbolSize: 6,
|
||
lineStyle: {
|
||
width: 3,
|
||
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||
{ offset: 0, color: sleepConfig.gradient[0] },
|
||
{ offset: 1, color: sleepConfig.gradient[1] }
|
||
]),
|
||
shadowColor: sleepConfig.color + '40',
|
||
shadowBlur: 8
|
||
},
|
||
itemStyle: {
|
||
color: sleepConfig.color,
|
||
borderColor: '#001529',
|
||
borderWidth: 1,
|
||
shadowColor: sleepConfig.color,
|
||
shadowBlur: 6
|
||
},
|
||
areaStyle: {
|
||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||
{ offset: 0, color: sleepConfig.color + '30' },
|
||
{ offset: 1, color: sleepConfig.color + '05' }
|
||
])
|
||
}
|
||
});
|
||
}
|
||
|
||
console.log('📊 图表系列数据:', series.map(s=>({name:s.name,dataCount:s.data?.length})));
|
||
|
||
const trendOption = {
|
||
backgroundColor: 'transparent',
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
backgroundColor: 'rgba(10,24,48,0.95)',
|
||
borderColor: '#00e4ff',
|
||
borderWidth: 1,
|
||
textStyle: { color: '#fff', fontSize: 10 },
|
||
formatter: function(params) {
|
||
let result = params[0].axisValue + '<br/>';
|
||
params.forEach(param => {
|
||
const unit = getMetricUnit(param.seriesName);
|
||
result += `${param.seriesName}: ${param.value}${unit}<br/>`;
|
||
});
|
||
return result;
|
||
}
|
||
},
|
||
legend: {
|
||
show: true,
|
||
top: 5,
|
||
right: 15,
|
||
textStyle: { color: '#7ecfff', fontSize: 10 },
|
||
itemGap: 12,
|
||
formatter: function(name) {
|
||
return name;
|
||
}
|
||
},
|
||
grid: {
|
||
top: 35,
|
||
left: 35,
|
||
right: 20,
|
||
bottom: 25,
|
||
containLabel: true,
|
||
backgroundColor: 'transparent'
|
||
},
|
||
xAxis: {
|
||
type: 'category',
|
||
data: dates || [],
|
||
axisLabel: {
|
||
color: '#7ecfff',
|
||
fontSize: 10,
|
||
fontWeight: 'bold',
|
||
interval: 0,
|
||
rotate: dates?.length > 5 ? 45 : 0
|
||
},
|
||
axisLine: {
|
||
lineStyle: { color: 'rgba(0,228,255,0.4)', width: 2 }
|
||
},
|
||
axisTick: {
|
||
lineStyle: { color: 'rgba(0,228,255,0.3)' }
|
||
}
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
axisLabel: {
|
||
color: '#7ecfff',
|
||
fontSize: 10,
|
||
fontWeight: 'bold'
|
||
},
|
||
splitLine: {
|
||
lineStyle: {
|
||
color: 'rgba(0,228,255,0.15)',
|
||
type: 'dashed',
|
||
width: 1
|
||
}
|
||
},
|
||
axisLine: {
|
||
lineStyle: { color: 'rgba(0,228,255,0.4)', width: 2 }
|
||
}
|
||
},
|
||
series,
|
||
animation: true,
|
||
animationDuration: 1500,
|
||
animationEasing: 'cubicOut'
|
||
};
|
||
|
||
trendChart.setOption(trendOption, true);
|
||
|
||
// 专业交互事件
|
||
trendChart.on('click', function(params) {
|
||
console.log('🔍 健康数据点击:', params);
|
||
const metric = params.seriesName;
|
||
const value = params.value;
|
||
const date = params.name;
|
||
showHealthDetailModal(metric, value, date);
|
||
});
|
||
|
||
// 悬停高亮效果
|
||
trendChart.on('mouseover', function(params) {
|
||
if (params.componentType === 'series') {
|
||
trendChart.dispatchAction({
|
||
type: 'highlight',
|
||
seriesIndex: params.seriesIndex
|
||
});
|
||
}
|
||
});
|
||
|
||
// 响应式调整
|
||
const resizeChart = () => {
|
||
if (trendChart && !trendChart.isDisposed()) {
|
||
trendChart.resize();
|
||
}
|
||
};
|
||
window.removeEventListener('resize', resizeChart);
|
||
window.addEventListener('resize', resizeChart);
|
||
}
|
||
|
||
// 显示健康数据详情弹窗
|
||
function showHealthDetailModal(metric, value, date) {
|
||
const healthMetrics = {
|
||
'心率': { color: '#ff6b9d', icon: '💗' },
|
||
'血氧': { color: '#00e4ff', icon: '🫁' },
|
||
'体温': { color: '#ffaa00', icon: '🌡️' },
|
||
'压力': { color: '#ff7700', icon: '😰' },
|
||
'睡眠': { color: '#7ecfff', icon: '😴' }
|
||
};
|
||
const icon = healthMetrics[metric]?.icon || '📊';
|
||
const color = healthMetrics[metric]?.color || '#00e4ff';
|
||
const unit = getMetricUnit(metric);
|
||
console.log(`${icon} ${metric}: ${value}${unit} (${date})`);
|
||
}
|
||
|
||
// 获取指标单位
|
||
function getMetricUnit(metricName) {
|
||
const units = {
|
||
'心率': 'bpm',
|
||
'血氧': '%',
|
||
'体温': '°C',
|
||
'压力': '',
|
||
'睡眠': 'h'
|
||
};
|
||
return units[metricName] || '';
|
||
}
|
||
|
||
|
||
// 🎯 生成Mock健康点数据
|
||
function addMockHealthPoints(data) {
|
||
console.log('🎯 添加Mock健康点到地图...');
|
||
|
||
// 确保health_data结构存在
|
||
if (!data.health_data) {
|
||
data.health_data = { healthData: [] };
|
||
}
|
||
if (!data.health_data.healthData) {
|
||
data.health_data.healthData = [];
|
||
}
|
||
|
||
// 定义几个深圳区域的Mock健康点(使用真实坐标,围绕默认中心114.015, 22.540)
|
||
const mockHealthPoints = [
|
||
{
|
||
user_id: 'mock_user_001',
|
||
user_name: '张伟',
|
||
dept_id: 'mock_dept_001',
|
||
dept_name: '智能制造车间A',
|
||
longitude: 114.01508952, // 深圳市民中心(地图默认中心)
|
||
latitude: 22.54036796,
|
||
health_score: 95, // 优秀 - 绿色
|
||
heart_rate: 75,
|
||
blood_oxygen: 98,
|
||
temperature: 36.5,
|
||
update_time: new Date().toISOString()
|
||
},
|
||
{
|
||
user_id: 'mock_user_002',
|
||
user_name: '李娜',
|
||
dept_id: 'mock_dept_001',
|
||
dept_name: '智能制造车间A',
|
||
longitude: 114.020523, // 深圳会展中心(东侧约500米)
|
||
latitude: 22.541876,
|
||
health_score: 72, // 良好 - 黄色
|
||
heart_rate: 82,
|
||
blood_oxygen: 96,
|
||
temperature: 36.8,
|
||
update_time: new Date().toISOString()
|
||
},
|
||
{
|
||
user_id: 'mock_user_003',
|
||
user_name: '王强',
|
||
dept_id: 'mock_dept_002',
|
||
dept_name: '质检部门B',
|
||
longitude: 114.009256, // 深圳图书馆(西侧约500米)
|
||
latitude: 22.538459,
|
||
health_score: 55, // 注意 - 橙色
|
||
heart_rate: 95,
|
||
blood_oxygen: 94,
|
||
temperature: 37.2,
|
||
update_time: new Date().toISOString()
|
||
},
|
||
{
|
||
user_id: 'mock_user_004',
|
||
user_name: '刘洋',
|
||
dept_id: 'mock_dept_002',
|
||
dept_name: '质检部门B',
|
||
longitude: 114.012345, // 莲花山公园(北侧约600米)
|
||
latitude: 22.546789,
|
||
health_score: 88, // 优秀 - 绿色
|
||
heart_rate: 70,
|
||
blood_oxygen: 99,
|
||
temperature: 36.3,
|
||
update_time: new Date().toISOString()
|
||
},
|
||
{
|
||
user_id: 'mock_user_005',
|
||
user_name: '陈明',
|
||
dept_id: 'mock_dept_003',
|
||
dept_name: '研发中心C',
|
||
longitude: 114.018765, // 中心公园(东南侧约400米)
|
||
latitude: 22.534567,
|
||
health_score: 35, // 异常 - 红色
|
||
heart_rate: 105,
|
||
blood_oxygen: 91,
|
||
temperature: 37.8,
|
||
update_time: new Date().toISOString()
|
||
},
|
||
{
|
||
user_id: 'mock_user_006',
|
||
user_name: '赵敏',
|
||
dept_id: 'mock_dept_003',
|
||
dept_name: '研发中心C',
|
||
longitude: 114.010987, // 少年宫(西北侧约300米)
|
||
latitude: 22.543210,
|
||
health_score: 92, // 优秀 - 绿色
|
||
heart_rate: 68,
|
||
blood_oxygen: 98,
|
||
temperature: 36.4,
|
||
update_time: new Date().toISOString()
|
||
}
|
||
];
|
||
|
||
// 将Mock数据添加到现有健康数据中
|
||
data.health_data.healthData.push(...mockHealthPoints);
|
||
|
||
console.log(`✅ 已添加 ${mockHealthPoints.length} 个Mock健康点,总计 ${data.health_data.healthData.length} 个健康点`);
|
||
console.log('📍 Mock健康点分布: 绿色×3, 黄色×1, 橙色×1, 红色×1');
|
||
|
||
// 不需要返回,直接修改了传入的对象
|
||
}
|
||
|
||
// 修改数据刷新函数
|
||
function refreshData() {
|
||
// 从 URL 获取 customerId 参数
|
||
console.log('使用全局customerId:', GLOBAL_CUSTOMER_ID);
|
||
loadBaselineTrendChart(GLOBAL_CUSTOMER_ID);
|
||
loadHealthScoreChart();
|
||
console.log('🔄 开始刷新数据,使用细粒度API调用...');
|
||
|
||
// 更新实时统计数据
|
||
updateRealtimeStats(GLOBAL_CUSTOMER_ID);
|
||
|
||
// 使用细粒度API替代get_total_info - 并行获取各模块数据
|
||
const dataPromises = {
|
||
// 保留原始的get_total_info调用以获取完整数据结构
|
||
totalInfo: api.data.getTotal(GLOBAL_CUSTOMER_ID).then(r => r.json()),
|
||
|
||
// 同时获取实时统计概览(用于补充数据)
|
||
statsOverview: api.statistics.getOverview(GLOBAL_CUSTOMER_ID, formatDate(new Date())).then(r => r.json()).catch(() => null),
|
||
|
||
// 获取细分数据(备用)
|
||
alerts: api.data.getAlerts(GLOBAL_CUSTOMER_ID, null, null, GLOBAL_CUSTOMER_ID).then(r => r.json()).catch(() => null),
|
||
messages: api.data.getMessages(GLOBAL_CUSTOMER_ID, null, null, GLOBAL_CUSTOMER_ID).then(r => r.json()).catch(() => null),
|
||
devices: api.data.getDevices(GLOBAL_CUSTOMER_ID, null, GLOBAL_CUSTOMER_ID).then(r => r.json()).catch(() => null)
|
||
};
|
||
|
||
// 并行处理所有数据请求
|
||
Promise.allSettled(Object.values(dataPromises))
|
||
.then(results => {
|
||
console.log('📊 数据获取结果:', results.map(r => r.status));
|
||
|
||
return Promise.all([
|
||
dataPromises.totalInfo,
|
||
dataPromises.statsOverview,
|
||
dataPromises.alerts,
|
||
dataPromises.messages,
|
||
dataPromises.devices
|
||
]);
|
||
})
|
||
.then(([totalResult, statsResult, alertsResult, messagesResult, devicesResult]) => {
|
||
console.log('✅ 所有数据加载完成');
|
||
|
||
if (totalResult && totalResult.success) {
|
||
const data = totalResult.data;
|
||
console.log('📋 主要数据来源: get_total_info', data);
|
||
|
||
// 如果有统计概览数据,进行数据融合
|
||
if (statsResult && statsResult.success) {
|
||
console.log('📊 补充数据来源: statistics/overview', statsResult.data);
|
||
// 融合统计数据
|
||
Object.assign(data, {
|
||
stats_overview: statsResult.data,
|
||
enhanced_health_count: statsResult.data.health_count || data.health_count,
|
||
enhanced_alert_count: statsResult.data.alert_count || data.alert_count
|
||
});
|
||
}
|
||
|
||
lastTotalInfo = data;
|
||
|
||
// 🎯 添加Mock健康点数据(直接修改data对象)
|
||
addMockHealthPoints(data);
|
||
|
||
updateMapData(data);
|
||
|
||
// 更新健康卡片数据
|
||
updateHealthCardsData(data, GLOBAL_CUSTOMER_ID);
|
||
|
||
// 更新人员管理面板
|
||
initPersonnelManagementPanel(data);
|
||
|
||
// 更新告警信息图表
|
||
initAlertChart(data);
|
||
|
||
// 更新设备管理图表
|
||
initDeviceChart(data);
|
||
|
||
// 更新消息列表
|
||
initMessageList(data);
|
||
|
||
// 为各个面板添加点击事件
|
||
setupPanelClickEvents(GLOBAL_CUSTOMER_ID, data);
|
||
|
||
console.log('✅ 界面更新完成');
|
||
} else {
|
||
console.error('❌ 主要数据获取失败:', totalResult);
|
||
|
||
// 降级处理:尝试使用统计概览数据
|
||
if (statsResult && statsResult.success) {
|
||
console.log('🔄 使用统计概览数据作为降级方案');
|
||
const fallbackData = {
|
||
...statsResult.data,
|
||
source: 'statistics_overview_fallback'
|
||
};
|
||
|
||
lastTotalInfo = fallbackData;
|
||
updateHealthCardsData(fallbackData, GLOBAL_CUSTOMER_ID);
|
||
console.log('✅ 降级数据加载完成');
|
||
} else {
|
||
console.error('❌ 所有数据源都失败');
|
||
showCustomAlert('数据加载失败,请稍后重试', 'error');
|
||
}
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('🔥 数据加载异常:', error);
|
||
showCustomAlert('系统异常,请稍后重试', 'error');
|
||
});
|
||
}
|
||
|
||
// 将面板点击事件设置抽取为单独的函数
|
||
function setupPanelClickEvents(customerId, data) {
|
||
// 0. 实时统计面板 - 新增点击事件
|
||
|
||
|
||
// 1. 告警信息面板
|
||
const alertPanel = document.querySelector('.panel:has(#alertList)');
|
||
if (alertPanel) {
|
||
alertPanel.style.cursor = 'pointer';
|
||
alertPanel.onclick = function() {
|
||
createModalWindow(`/alert_view.html?customerId=${customerId}`, data.alert_info || null, 'alertInfo');
|
||
};
|
||
}
|
||
|
||
// 2. 设备管理面板
|
||
const devicePanel = document.querySelector('.panel:has(#statsChart)');
|
||
if (devicePanel) {
|
||
devicePanel.style.cursor = 'pointer';
|
||
devicePanel.onclick = function() {
|
||
console.log('🔧 点击设备管理面板,跳转到设备视图 (简化模式)');
|
||
|
||
// 创建简化的模态窗口,不传递复杂数据,类似personal.html的方式
|
||
const modalContainer = document.createElement('div');
|
||
modalContainer.className = 'modal-container';
|
||
modalContainer.innerHTML = `
|
||
<div class="modal-content">
|
||
<button class="modal-close">✖</button>
|
||
<iframe src="/device_view.html?customerId=${customerId}" class="user-view-iframe"></iframe>
|
||
</div>
|
||
`;
|
||
|
||
document.body.appendChild(modalContainer);
|
||
|
||
// 添加简化样式
|
||
if (!document.getElementById('simpleModalStyles')) {
|
||
const style = document.createElement('style');
|
||
style.id = 'simpleModalStyles';
|
||
style.textContent = `
|
||
.modal-container {
|
||
position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7);
|
||
display: flex; justify-content: center; align-items: center; z-index: 9999; animation: fadeIn 0.3s ease;
|
||
}
|
||
.modal-content {
|
||
position: relative; width: 90%; height: 90%; background: rgba(0,21,41,0.95);
|
||
border: 1px solid rgba(0,228,255,0.3); border-radius: 8px; padding: 20px;
|
||
box-shadow: 0 0 20px rgba(0,228,255,0.2); animation: slideIn 0.3s ease;
|
||
}
|
||
.modal-close {
|
||
position: absolute; top: 10px; right: 10px; background: transparent; border: none;
|
||
color: #00e4ff; font-size: 18px; cursor: pointer; z-index: 1; padding: 5px 10px; transition: all 0.3s ease;
|
||
}
|
||
.modal-close:hover { transform: scale(1.1); color: #ff4444; }
|
||
.user-view-iframe { width: 100%; height: 100%; border: none; border-radius: 4px; background: rgba(1,19,38,0.8); }
|
||
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
||
@keyframes slideIn { from { transform: translateY(-20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
|
||
`;
|
||
document.head.appendChild(style);
|
||
}
|
||
|
||
// 关闭事件
|
||
modalContainer.querySelector('.modal-close').onclick = () => modalContainer.remove();
|
||
modalContainer.onclick = (e) => { if (e.target === modalContainer) modalContainer.remove(); };
|
||
|
||
console.log('✅ 简化设备视图模态窗口创建完成');
|
||
};
|
||
}
|
||
|
||
// 3. 消息信息面板 - 已在HTML中直接设置onclick,无需重复设置
|
||
const messagePanel = document.querySelector('.message-panel');
|
||
if (messagePanel) {
|
||
console.log('✅ 消息面板点击事件已在HTML中配置');
|
||
// 面板已通过HTML onclick属性配置,无需重复设置
|
||
}
|
||
|
||
// 4. 趋势分析面板
|
||
const trendPanel = document.querySelector('.panel:has(#trendChart)');
|
||
if (trendPanel) {
|
||
trendPanel.style.cursor = 'pointer';
|
||
trendPanel.onclick = function() {
|
||
createModalWindow(`/health_main?customerId=${customerId}`, data.health_data || null, 'healthInfo');
|
||
};
|
||
}
|
||
|
||
// 5. 人员管理面板
|
||
const personnelPanel = document.querySelector('.panel:has(#departmentDistribution)');
|
||
if (personnelPanel) {
|
||
personnelPanel.style.cursor = 'pointer';
|
||
personnelPanel.onclick = function() {
|
||
createModalWindow(`/user_view.html?customerId=${customerId}`, data.user_info || null, 'userInfo');
|
||
};
|
||
}
|
||
|
||
// 6. 健康评分面板
|
||
const scorePanel = document.querySelector('.panel:has(#healthScoreChart)');
|
||
if (scorePanel) {
|
||
scorePanel.style.cursor = 'pointer';
|
||
scorePanel.onclick = function() {
|
||
createModalWindow(`/user_health_data_analysis.html?customerId=${customerId}`, data.health_data || null, 'healthInfo');
|
||
};
|
||
}
|
||
}
|
||
|
||
// DOMContentLoaded 事件监听器已移至 app-initializer.js 模块 (Phase 7)
|
||
// 筛选面板、日期选择器、图表、地图、数据刷新、定时器已统一由 AppInitializer 处理
|
||
|
||
// 添加通用的创建模态窗口函数
|
||
// createModalWindow 函数已由统一模态窗口组件 modal-window.js 提供
|
||
// 对于需要特殊过滤器功能的场景,使用统一组件的 showFilters 选项
|
||
|
||
// 添加面板样式
|
||
const panelStyle = document.createElement('style');
|
||
panelStyle.textContent = `
|
||
.panel {
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.panel:hover {
|
||
border-color: rgba(0, 228, 255, 0.4);
|
||
box-shadow: 0 0 15px rgba(0, 228, 255, 0.2);
|
||
}
|
||
|
||
.modal-container {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.7);
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
z-index: 9999;
|
||
animation: fadeIn 0.3s ease;
|
||
}
|
||
|
||
.modal-content {
|
||
position: relative;
|
||
width: 90%;
|
||
height: 90%;
|
||
background: rgba(0, 21, 41, 0.95);
|
||
border: 1px solid rgba(0, 228, 255, 0.3);
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
box-shadow: 0 0 20px rgba(0, 228, 255, 0.2);
|
||
animation: slideIn 0.3s ease;
|
||
}
|
||
|
||
.modal-close {
|
||
position: absolute;
|
||
top: 10px;
|
||
right: 10px;
|
||
background: transparent;
|
||
border: none;
|
||
color: #00e4ff;
|
||
font-size: 18px;
|
||
cursor: pointer;
|
||
z-index: 1;
|
||
padding: 5px 10px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.modal-close:hover {
|
||
transform: scale(1.1);
|
||
color: #ff4444;
|
||
}
|
||
|
||
.user-view-iframe {
|
||
width: 100%;
|
||
height: 100%;
|
||
border: none;
|
||
border-radius: 4px;
|
||
background: rgba(1, 19, 38, 0.8);
|
||
}
|
||
|
||
@keyframes fadeIn {
|
||
from { opacity: 0; }
|
||
to { opacity: 1; }
|
||
}
|
||
|
||
@keyframes slideIn {
|
||
from {
|
||
transform: translateY(-20px);
|
||
opacity: 0;
|
||
}
|
||
to {
|
||
transform: translateY(0);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
`;
|
||
document.head.appendChild(panelStyle);
|
||
|
||
// 初始化告警信息图表 - 专业版
|
||
function initAlertChart(data) {
|
||
console.log('initAlertChart 开始执行,数据:', data); // 调试信息
|
||
|
||
const alertContainer = document.getElementById('alertList');
|
||
if (!alertContainer) {
|
||
console.warn('告警容器 #alertList 未找到');
|
||
return;
|
||
}
|
||
|
||
// 清空容器并创建专业布局
|
||
alertContainer.innerHTML = `
|
||
<div style="position: relative; height: 100%; padding: 8px;">
|
||
<!-- 告警状态总览 -->
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; padding: 6px 10px; background: rgba(0,228,255,0.1); border-radius: 6px; border-left: 4px solid #00e4ff;">
|
||
<div style="display: flex; align-items: center; gap: 12px;">
|
||
<div class="alert-stat-item">
|
||
<span style="color: #ff4444; font-size: 18px; font-weight: bold;" id="criticalCount">0</span>
|
||
<span style="color: #fff; font-size: 12px; margin-left: 4px;">严重</span>
|
||
</div>
|
||
<div class="alert-stat-item">
|
||
<span style="color: #ffbb00; font-size: 16px; font-weight: bold;" id="mediumCount">0</span>
|
||
<span style="color: #fff; font-size: 12px; margin-left: 4px;">中等</span>
|
||
</div>
|
||
<div class="alert-stat-item">
|
||
<span style="color: #ff6666; font-size: 14px; font-weight: bold;" id="pendingCount">0</span>
|
||
<span style="color: #fff; font-size: 12px; margin-left: 4px;">待处理</span>
|
||
</div>
|
||
</div>
|
||
<div style="background: rgba(255,68,68,0.2); padding: 4px 8px; border-radius: 12px; animation: pulse 2s infinite;" id="alertBadge">
|
||
<span style="color: #ff4444; font-size: 11px; font-weight: bold;">🚨 实时监控</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 图表区域 -->
|
||
<div class="alert-charts-grid">
|
||
<div id="alertTypeChart" style="background: rgba(0,21,41,0.4); border: 1px solid rgba(0,228,255,0.3); border-radius: 6px; position: relative;">
|
||
<div style="position: absolute; top: 5px; left: 8px; color: #00e4ff; font-size: 12px; font-weight: bold; z-index: 10;">告警类型分布</div>
|
||
</div>
|
||
<div id="alertLevelChart" style="background: rgba(0,21,41,0.4); border: 1px solid rgba(0,228,255,0.3); border-radius: 6px; position: relative;">
|
||
<div style="position: absolute; top: 5px; left: 8px; color: #00e4ff; font-size: 12px; font-weight: bold; z-index: 10;">严重程度分析</div>
|
||
</div>
|
||
<div id="alertStatusChart" style="background: rgba(0,21,41,0.4); border: 1px solid rgba(0,228,255,0.3); border-radius: 6px; position: relative;">
|
||
<div style="position: absolute; top: 5px; left: 8px; color: #00e4ff; font-size: 12px; font-weight: bold; z-index: 10;">处理状态</div>
|
||
</div>
|
||
<div id="alertTrendChart" style="background: rgba(0,21,41,0.4); border: 1px solid rgba(0,228,255,0.3); border-radius: 6px; position: relative;">
|
||
<div style="position: absolute; top: 5px; left: 8px; color: #00e4ff; font-size: 12px; font-weight: bold; z-index: 10;">24小时趋势</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
const alertInfo = data.alert_info || {};
|
||
const alerts = alertInfo.alerts || [];
|
||
|
||
console.log('告警信息:', alertInfo); // 调试信息
|
||
console.log('告警列表:', alerts); // 调试信息
|
||
|
||
// 更新统计数据
|
||
const criticalCount = alertInfo.alertLevelCount?.critical || 0;
|
||
const mediumCount = alertInfo.alertLevelCount?.medium || 0;
|
||
const pendingCount = alertInfo.alertStatusCount?.pending || 0;
|
||
|
||
document.getElementById('criticalCount').textContent = criticalCount;
|
||
document.getElementById('mediumCount').textContent = mediumCount;
|
||
document.getElementById('pendingCount').textContent = pendingCount;
|
||
|
||
// 更新告警徽章
|
||
const badge = document.getElementById('alertBadge');
|
||
if (criticalCount > 0) {
|
||
badge.innerHTML = '<span style="color: #ff4444; font-size: 11px; font-weight: bold;">🔴 严重告警</span>';
|
||
badge.style.background = 'rgba(255,68,68,0.3)';
|
||
} else if (pendingCount > 0) {
|
||
badge.innerHTML = '<span style="color: #ffbb00; font-size: 11px; font-weight: bold;">⚠️ 待处理</span>';
|
||
badge.style.background = 'rgba(255,187,0,0.3)';
|
||
} else {
|
||
badge.innerHTML = '<span style="color: #00ff9d; font-size: 11px; font-weight: bold;">✅ 正常</span>';
|
||
badge.style.background = 'rgba(0,255,157,0.3)';
|
||
}
|
||
|
||
// 1. 告警类型分布图 - 水平条形图(优化版)
|
||
const typeChart = echarts.init(document.getElementById('alertTypeChart'));
|
||
|
||
// 添加调试日志查看原始数据
|
||
console.log('原始告警类型数据:', alertInfo.alertTypeCount);
|
||
|
||
// 创建告警类型映射 - 将API返回的类型映射到标准类型
|
||
const alertTypeMapping = {
|
||
'WEAR_STATUS_CHANGED': 'fall_down',
|
||
'TEMPERATURE_ALERT': 'temperature',
|
||
'TEMP_ALERT': 'temperature',
|
||
'STRESS_ALERT': 'stress',
|
||
'SPO2_ALERT': 'blood_oxygen',
|
||
'SOS_EMERGENCY': 'one_key_alarm',
|
||
'HEART_RATE_ALERT': 'heart_rate',
|
||
'FALL_DOWN_ALERT': 'fall_down',
|
||
'BLOOD_PRESSURE_ALERT': 'blood_pressure',
|
||
'SLEEP_ALERT': 'sleep',
|
||
// 添加常见的缩略形式
|
||
'WEAR_': 'fall_down',
|
||
'TEMPE': 'temperature',
|
||
'STRES': 'stress',
|
||
'SPO2_': 'blood_oxygen',
|
||
'HEART': 'heart_rate',
|
||
'FALLD': 'fall_down'
|
||
};
|
||
|
||
// 处理和规范化告警类型数据
|
||
const rawAlertTypes = alertInfo.alertTypeCount || {};
|
||
const normalizedAlertTypes = {};
|
||
|
||
Object.keys(rawAlertTypes).forEach(rawType => {
|
||
let normalizedType = rawType;
|
||
|
||
// 直接匹配
|
||
if (alertTypeMapping[rawType]) {
|
||
normalizedType = alertTypeMapping[rawType];
|
||
} else {
|
||
// 部分匹配 - 检查原始类型是否包含映射中的任何前缀
|
||
for (const [prefix, mappedType] of Object.entries(alertTypeMapping)) {
|
||
if (rawType.startsWith(prefix)) {
|
||
normalizedType = mappedType;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 累积相同规范化类型的数量
|
||
if (normalizedAlertTypes[normalizedType]) {
|
||
normalizedAlertTypes[normalizedType] += rawAlertTypes[rawType];
|
||
} else {
|
||
normalizedAlertTypes[normalizedType] = rawAlertTypes[rawType];
|
||
}
|
||
});
|
||
|
||
console.log('规范化后的告警类型数据:', normalizedAlertTypes);
|
||
|
||
const alertTypes = Object.keys(normalizedAlertTypes);
|
||
const alertValues = Object.values(normalizedAlertTypes);
|
||
|
||
// 如果没有数据,显示默认数据
|
||
const hasTypeData = alertTypes.length > 0 && alertValues.some(v => v > 0);
|
||
let displayTypes = hasTypeData ? alertTypes : ['heart_rate', 'blood_pressure', 'temperature'];
|
||
let displayValues = hasTypeData ? alertValues : [0, 0, 0];
|
||
|
||
// 限制显示数量,防止图表变形,由于高度增加可以显示更多类型
|
||
const MAX_DISPLAY_TYPES = 8; // 从6增加到8种类型,充分利用增加的高度空间
|
||
if (displayTypes.length > MAX_DISPLAY_TYPES) {
|
||
// 按数值排序,取前8个
|
||
const sortedData = displayTypes.map((type, index) => ({
|
||
type: type,
|
||
value: displayValues[index]
|
||
})).sort((a, b) => b.value - a.value);
|
||
|
||
displayTypes = sortedData.slice(0, MAX_DISPLAY_TYPES).map(item => item.type);
|
||
displayValues = sortedData.slice(0, MAX_DISPLAY_TYPES).map(item => item.value);
|
||
|
||
// 如果有更多类型,将剩余的合并为"其他"
|
||
if (sortedData.length > MAX_DISPLAY_TYPES) {
|
||
const otherValue = sortedData.slice(MAX_DISPLAY_TYPES).reduce((sum, item) => sum + item.value, 0);
|
||
displayTypes.push('others');
|
||
displayValues.push(otherValue);
|
||
}
|
||
}
|
||
|
||
// 使用完整的告警类型颜色映射
|
||
const typeColors = {
|
||
'heart_rate': '#ff6b6b',
|
||
'blood_pressure': '#ffb347',
|
||
'stress': '#f7b731',
|
||
'blood_oxygen': '#00e4ff',
|
||
'temperature': '#ffd700',
|
||
'one_key_alarm': '#ffe066',
|
||
'fall_down': '#00cfff',
|
||
'sleep': '#7ecfff',
|
||
'others': '#888888'
|
||
};
|
||
|
||
const typeOption = {
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
backgroundColor: 'rgba(0,21,41,0.95)',
|
||
borderColor: '#00e4ff',
|
||
borderWidth: 1,
|
||
textStyle: { color: '#fff', fontSize: 11 },
|
||
formatter: function(params) {
|
||
if (!params || !params[0]) return '';
|
||
const data = params[0];
|
||
const total = displayValues.reduce((a, b) => a + b, 0);
|
||
const percent = total > 0 ? (data.value / total * 100).toFixed(1) : 0;
|
||
const typeName = data.name === 'others' ? '其他类型' : translateAlertType(data.name);
|
||
return `${typeName}<br/>告警: ${data.value}次 (${percent}%)`;
|
||
}
|
||
},
|
||
grid: {
|
||
top: 25,
|
||
left: 50,
|
||
right: 15,
|
||
bottom: 15,
|
||
containLabel: true
|
||
},
|
||
xAxis: {
|
||
type: 'value',
|
||
axisLabel: { color: '#7ecfff', fontSize: 9 },
|
||
splitLine: { lineStyle: { color: 'rgba(126,207,255,0.1)' } },
|
||
axisLine: { show: false },
|
||
max: Math.max(...displayValues, 1)
|
||
},
|
||
yAxis: {
|
||
type: 'category',
|
||
data: displayTypes.map(t => t === 'others' ? '其他' : translateAlertType(t)),
|
||
axisLabel: {
|
||
color: '#fff',
|
||
fontSize: 9,
|
||
interval: 0, // 强制显示所有标签
|
||
formatter: function(value) {
|
||
return value.length > 5 ? value.substring(0, 5) + '...' : value; /* 从4增加到5,允许显示更多字符 */
|
||
}
|
||
},
|
||
axisLine: { show: false },
|
||
axisTick: { show: false }
|
||
},
|
||
series: [{
|
||
type: 'bar',
|
||
data: displayTypes.map((type, index) => ({
|
||
value: displayValues[index],
|
||
itemStyle: {
|
||
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||
{ offset: 0, color: typeColors[type] || '#00e4ff' },
|
||
{ offset: 1, color: (typeColors[type] || '#00e4ff') + '88' }
|
||
])
|
||
}
|
||
})),
|
||
barWidth: displayTypes.length > 6 ? '40%' : displayTypes.length > 4 ? '50%' : '65%', // 动态调整条形宽度,适应更多类型
|
||
label: {
|
||
show: true,
|
||
position: 'right',
|
||
color: '#fff',
|
||
fontSize: 9,
|
||
formatter: '{c}'
|
||
}
|
||
}]
|
||
};
|
||
typeChart.setOption(typeOption);
|
||
|
||
// 2. 告警级别分布图 - 环形图
|
||
const levelChart = echarts.init(document.getElementById('alertLevelChart'));
|
||
const levelEntries = Object.entries(alertInfo.alertLevelCount || {});
|
||
const hasLevelData = levelEntries.length > 0 && levelEntries.some(([_, count]) => count > 0);
|
||
|
||
const levelData = hasLevelData ?
|
||
levelEntries.map(([level, count]) => ({
|
||
name: level === 'critical' ? '严重' : level === 'medium' ? '中等' : '轻微',
|
||
value: count,
|
||
itemStyle: {
|
||
color: level === 'critical' ? '#ff4444' : level === 'medium' ? '#ffbb00' : '#00e4ff'
|
||
}
|
||
})) :
|
||
[
|
||
{ name: '严重', value: 0, itemStyle: { color: '#ff4444' } },
|
||
{ name: '中等', value: 0, itemStyle: { color: '#ffbb00' } },
|
||
{ name: '正常', value: 1, itemStyle: { color: '#00e4ff' } }
|
||
];
|
||
|
||
const levelOption = {
|
||
tooltip: {
|
||
trigger: 'item',
|
||
backgroundColor: 'rgba(0,21,41,0.95)',
|
||
borderColor: '#00e4ff',
|
||
textStyle: { color: '#fff', fontSize: 11 },
|
||
formatter: function(params) {
|
||
return `${params.name}<br/>数量: ${params.value}次<br/>占比: ${params.percent}%`;
|
||
}
|
||
},
|
||
series: [{
|
||
type: 'pie',
|
||
radius: ['35%', '65%'],
|
||
center: ['50%', '55%'],
|
||
data: levelData,
|
||
label: {
|
||
show: true,
|
||
position: 'outside',
|
||
color: '#fff',
|
||
fontSize: 10,
|
||
formatter: function(params) {
|
||
return hasLevelData ? `${params.name}\n${params.value}次` : `${params.name}`;
|
||
},
|
||
lineHeight: 12
|
||
},
|
||
labelLine: {
|
||
show: true,
|
||
length: 8,
|
||
length2: 5,
|
||
lineStyle: { color: 'rgba(255,255,255,0.5)' }
|
||
},
|
||
emphasis: {
|
||
itemStyle: {
|
||
shadowBlur: 15,
|
||
shadowOffsetX: 0,
|
||
shadowColor: 'rgba(0, 0, 0, 0.8)',
|
||
scale: 1.05
|
||
}
|
||
}
|
||
}]
|
||
};
|
||
levelChart.setOption(levelOption);
|
||
|
||
// 3. 告警状态分布图 - 仪表盘样式
|
||
const statusChart = echarts.init(document.getElementById('alertStatusChart'));
|
||
const totalAlerts = (alertInfo.alertStatusCount?.pending || 0) + (alertInfo.alertStatusCount?.responded || 0);
|
||
const pendingPercent = totalAlerts > 0 ? ((alertInfo.alertStatusCount?.pending || 0) / totalAlerts * 100).toFixed(1) : 0;
|
||
|
||
const statusOption = {
|
||
tooltip: {
|
||
trigger: 'item',
|
||
backgroundColor: 'rgba(0,21,41,0.95)',
|
||
borderColor: '#00e4ff',
|
||
textStyle: { color: '#fff', fontSize: 11 }
|
||
},
|
||
series: [{
|
||
type: 'gauge',
|
||
radius: '75%',
|
||
center: ['50%', '55%'],
|
||
startAngle: 180,
|
||
endAngle: 0,
|
||
min: 0,
|
||
max: 100,
|
||
splitNumber: 4,
|
||
axisLine: {
|
||
lineStyle: {
|
||
width: 8,
|
||
color: [
|
||
[0.3, '#00e4ff'],
|
||
[0.7, '#ffbb00'],
|
||
[1, '#ff4444']
|
||
]
|
||
}
|
||
},
|
||
pointer: {
|
||
icon: 'circle',
|
||
length: '60%',
|
||
width: 4,
|
||
offsetCenter: [0, '5%'],
|
||
itemStyle: { color: '#fff' }
|
||
},
|
||
axisTick: { show: false },
|
||
splitLine: { show: false },
|
||
axisLabel: {
|
||
show: true,
|
||
distance: -25,
|
||
color: '#7ecfff',
|
||
fontSize: 9,
|
||
formatter: function(value) {
|
||
if (value === 0) return '正常';
|
||
if (value === 50) return '中等';
|
||
if (value === 100) return '严重';
|
||
return '';
|
||
}
|
||
},
|
||
detail: {
|
||
valueAnimation: true,
|
||
formatter: function(value) {
|
||
return `{value|${value}%}\n{name|待处理率}`;
|
||
},
|
||
rich: {
|
||
value: {
|
||
fontSize: 16,
|
||
fontWeight: 'bold',
|
||
color: pendingPercent > 70 ? '#ff4444' : pendingPercent > 30 ? '#ffbb00' : '#00e4ff'
|
||
},
|
||
name: {
|
||
fontSize: 10,
|
||
color: '#7ecfff',
|
||
padding: [5, 0, 0, 0]
|
||
}
|
||
},
|
||
offsetCenter: [0, '20%']
|
||
},
|
||
data: [{ value: pendingPercent }]
|
||
}]
|
||
};
|
||
statusChart.setOption(statusOption);
|
||
|
||
// 4. 24小时告警趋势图 - 修复数据处理
|
||
const alertTrendChart = echarts.init(document.getElementById('alertTrendChart'));
|
||
|
||
// 处理时间数据,按小时统计
|
||
const hourlyData = {};
|
||
const now = new Date();
|
||
|
||
// 初始化24小时数据
|
||
for (let i = 0; i < 24; i++) {
|
||
hourlyData[i] = 0;
|
||
}
|
||
|
||
// 统计告警数据
|
||
if (alerts && alerts.length > 0) {
|
||
alerts.forEach(alert => {
|
||
try {
|
||
let alertTime;
|
||
if (alert.alert_timestamp) {
|
||
// 尝试解析时间戳
|
||
alertTime = new Date(alert.alert_timestamp);
|
||
if (isNaN(alertTime.getTime())) {
|
||
// 如果解析失败,尝试其他格式
|
||
alertTime = new Date(alert.alert_timestamp.replace(/-/g, '/'));
|
||
}
|
||
} else if (alert.timestamp) {
|
||
alertTime = new Date(alert.timestamp);
|
||
} else {
|
||
alertTime = now; // 默认当前时间
|
||
}
|
||
|
||
if (!isNaN(alertTime.getTime())) {
|
||
const hour = alertTime.getHours();
|
||
hourlyData[hour] = (hourlyData[hour] || 0) + 1;
|
||
}
|
||
} catch (e) {
|
||
console.warn('解析告警时间失败:', alert, e);
|
||
}
|
||
});
|
||
}
|
||
|
||
const hours = Array.from({length: 24}, (_, i) => i);
|
||
const hourlyValues = hours.map(h => hourlyData[h] || 0);
|
||
const maxValue = Math.max(...hourlyValues, 1);
|
||
|
||
const trendOption = {
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
backgroundColor: 'rgba(0,21,41,0.95)',
|
||
borderColor: '#00e4ff',
|
||
textStyle: { color: '#fff', fontSize: 11 },
|
||
formatter: function(params) {
|
||
if (!params || !params[0]) return '';
|
||
const data = params[0];
|
||
return `${data.name}:00<br/>告警数量: ${data.value}次`;
|
||
}
|
||
},
|
||
grid: { top: 25, left: 25, right: 15, bottom: 20 },
|
||
xAxis: {
|
||
type: 'category',
|
||
data: hours.map(h => h.toString().padStart(2, '0')),
|
||
axisLabel: {
|
||
color: '#7ecfff',
|
||
fontSize: 9,
|
||
interval: 3
|
||
},
|
||
axisLine: { lineStyle: { color: 'rgba(126,207,255,0.3)' } },
|
||
axisTick: { show: false }
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
max: Math.max(maxValue + 1, 3),
|
||
axisLabel: { color: '#7ecfff', fontSize: 9 },
|
||
splitLine: { lineStyle: { color: 'rgba(126,207,255,0.1)', type: 'dashed' } },
|
||
axisLine: { show: false }
|
||
},
|
||
series: [{
|
||
type: 'line',
|
||
data: hourlyValues,
|
||
smooth: true,
|
||
lineStyle: {
|
||
color: '#00e4ff',
|
||
width: 2,
|
||
shadowColor: 'rgba(0,228,255,0.3)',
|
||
shadowBlur: 5
|
||
},
|
||
itemStyle: {
|
||
color: '#00e4ff',
|
||
borderColor: '#fff',
|
||
borderWidth: 1
|
||
},
|
||
areaStyle: {
|
||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||
{ offset: 0, color: 'rgba(0,228,255,0.4)' },
|
||
{ offset: 1, color: 'rgba(0,228,255,0.05)' }
|
||
])
|
||
},
|
||
symbol: 'circle',
|
||
symbolSize: function(value) {
|
||
return value > 0 ? 4 : 2;
|
||
},
|
||
emphasis: {
|
||
itemStyle: {
|
||
color: '#fff',
|
||
borderColor: '#00e4ff',
|
||
borderWidth: 2,
|
||
shadowColor: 'rgba(0,228,255,0.6)',
|
||
shadowBlur: 8
|
||
},
|
||
scale: 1.2
|
||
}
|
||
}]
|
||
};
|
||
alertTrendChart.setOption(trendOption);
|
||
|
||
// 添加动画样式
|
||
const style = document.createElement('style');
|
||
style.textContent = `
|
||
@keyframes pulse {
|
||
0%, 100% { transform: scale(1); opacity: 1; }
|
||
50% { transform: scale(1.05); opacity: 0.8; }
|
||
}
|
||
|
||
.alert-stat-item {
|
||
display: flex;
|
||
align-items: baseline;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.alert-stat-item:hover {
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
#alertBadge {
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
#alertBadge:hover {
|
||
transform: scale(1.1);
|
||
box-shadow: 0 0 10px rgba(0,228,255,0.5);
|
||
}
|
||
`;
|
||
document.head.appendChild(style);
|
||
|
||
// 自适应大小 - 延迟执行确保DOM已渲染
|
||
setTimeout(() => {
|
||
const resizeCharts = () => {
|
||
try {
|
||
typeChart && typeChart.resize();
|
||
levelChart && levelChart.resize();
|
||
statusChart && statusChart.resize();
|
||
alertTrendChart && alertTrendChart.resize();
|
||
} catch (e) {
|
||
console.warn('图表resize失败:', e);
|
||
}
|
||
};
|
||
|
||
resizeCharts(); // 立即执行一次
|
||
window.addEventListener('resize', resizeCharts);
|
||
}, 100);
|
||
|
||
// 添加点击事件显示详细告警列表
|
||
badge.onclick = () => showAlertDetails(alerts);
|
||
|
||
// 图表点击交互
|
||
typeChart.on('click', function(params) {
|
||
if (hasTypeData && params.dataIndex < alertTypes.length) {
|
||
const alertType = alertTypes[params.dataIndex];
|
||
const filteredAlerts = alerts.filter(alert => alert.alert_type === alertType);
|
||
showAlertDetails(filteredAlerts, `${translateAlertType(alertType)}告警详情`);
|
||
}
|
||
});
|
||
|
||
// 确保图表正确渲染 - 延迟执行
|
||
setTimeout(() => {
|
||
try {
|
||
typeChart.resize();
|
||
levelChart.resize();
|
||
statusChart.resize();
|
||
alertTrendChart.resize();
|
||
console.log('告警图表初始化完成');
|
||
} catch (e) {
|
||
console.warn('告警图表初始化失败:', e);
|
||
}
|
||
}, 200);
|
||
|
||
return { typeChart, levelChart, statusChart, alertTrendChart };
|
||
}
|
||
|
||
// 显示详细告警列表
|
||
function showAlertDetails(alerts, title = '📋 详细告警列表') {
|
||
const modal = document.createElement('div');
|
||
modal.className = 'modal-container';
|
||
modal.innerHTML = `
|
||
<div class="modal-content" style="width: 85%; height: 85%;">
|
||
<button class="modal-close">✕</button>
|
||
<h3 style="color: #00e4ff; margin-bottom: 20px; text-align: center; font-size: 18px;">${title}</h3>
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; padding: 10px; background: rgba(0,228,255,0.1); border-radius: 6px;">
|
||
<div style="color: #fff; font-size: 14px;">
|
||
共 <span style="color: #00e4ff; font-weight: bold;">${alerts.length}</span> 条告警记录
|
||
</div>
|
||
<div style="display: flex; gap: 10px;">
|
||
<button onclick="exportAlerts()" style="background: rgba(0,228,255,0.2); border: 1px solid #00e4ff; color: #00e4ff; padding: 5px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;">📊 导出</button>
|
||
<button onclick="refreshAlerts()" style="background: rgba(0,228,255,0.2); border: 1px solid #00e4ff; color: #00e4ff; padding: 5px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;">🔄 刷新</button>
|
||
</div>
|
||
</div>
|
||
<div style="height: calc(100% - 100px); overflow-y: auto; border: 1px solid rgba(0,228,255,0.2); border-radius: 6px;">
|
||
<table style="width: 100%; border-collapse: collapse; color: #fff; font-size: 13px;">
|
||
<thead style="position: sticky; top: 0; z-index: 10;">
|
||
<tr style="background: rgba(0,228,255,0.3); border-bottom: 2px solid #00e4ff;">
|
||
<th style="padding: 12px 8px; text-align: left; font-weight: bold; border-right: 1px solid rgba(0,228,255,0.2);">时间</th>
|
||
<th style="padding: 12px 8px; text-align: left; font-weight: bold; border-right: 1px solid rgba(0,228,255,0.2);">类型</th>
|
||
<th style="padding: 12px 8px; text-align: left; font-weight: bold; border-right: 1px solid rgba(0,228,255,0.2);">级别</th>
|
||
<th style="padding: 12px 8px; text-align: left; font-weight: bold; border-right: 1px solid rgba(0,228,255,0.2);">状态</th>
|
||
<th style="padding: 12px 8px; text-align: left; font-weight: bold; border-right: 1px solid rgba(0,228,255,0.2);">用户</th>
|
||
<th style="padding: 12px 8px; text-align: left; font-weight: bold; border-right: 1px solid rgba(0,228,255,0.2);">部门</th>
|
||
<th style="padding: 12px 8px; text-align: left; font-weight: bold; border-right: 1px solid rgba(0,228,255,0.2);">设备</th>
|
||
<th style="padding: 12px 8px; text-align: left; font-weight: bold;">描述</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${alerts.map((alert, index) => `
|
||
<tr style="border-bottom: 1px solid rgba(255,255,255,0.1); transition: all 0.2s ease; ${alert.severity_level === 'critical' ? 'background: rgba(255,68,68,0.15);' : index % 2 === 0 ? 'background: rgba(0,21,41,0.3);' : 'background: rgba(0,21,41,0.1);'}"
|
||
onmouseover="this.style.background='rgba(0,228,255,0.1)'"
|
||
onmouseout="this.style.background='${alert.severity_level === 'critical' ? 'rgba(255,68,68,0.15)' : index % 2 === 0 ? 'rgba(0,21,41,0.3)' : 'rgba(0,21,41,0.1)'}'">
|
||
<td style="padding: 10px 8px; font-size: 11px; border-right: 1px solid rgba(255,255,255,0.05);">
|
||
<div style="color: #7ecfff;">${alert.alert_timestamp.split(' ')[0]}</div>
|
||
<div style="color: #fff; font-size: 10px;">${alert.alert_timestamp.split(' ')[1]}</div>
|
||
</td>
|
||
<td style="padding: 10px 8px; border-right: 1px solid rgba(255,255,255,0.05);">
|
||
<span style="background: ${getAlertTypeColor(alert.alert_type)}; padding: 3px 8px; border-radius: 12px; font-size: 10px; font-weight: bold; color: #000;">
|
||
${translateAlertType(alert.alert_type)}
|
||
</span>
|
||
</td>
|
||
<td style="padding: 10px 8px; border-right: 1px solid rgba(255,255,255,0.05);">
|
||
<span style="background: ${alert.severity_level === 'critical' ? '#ff4444' : '#ffbb00'}; padding: 3px 8px; border-radius: 12px; font-size: 10px; font-weight: bold; color: #000;">
|
||
${alert.severity_level === 'critical' ? '🔴 严重' : '🟡 中等'}
|
||
</span>
|
||
</td>
|
||
<td style="padding: 10px 8px; border-right: 1px solid rgba(255,255,255,0.05);">
|
||
<span style="background: ${alert.alert_status === 'pending' ? '#ff4444' : '#00e4ff'}; padding: 3px 8px; border-radius: 12px; font-size: 10px; font-weight: bold; color: ${alert.alert_status === 'pending' ? '#fff' : '#000'};">
|
||
${alert.alert_status === 'pending' ? '⏳ 待处理' : '✅ 已处理'}
|
||
</span>
|
||
</td>
|
||
<td style="padding: 10px 8px; font-size: 12px; border-right: 1px solid rgba(255,255,255,0.05);">
|
||
<div style="color: #00e4ff; font-weight: bold;">${alert.user_name}</div>
|
||
<div style="color: #7ecfff; font-size: 10px;">ID: ${alert.user_id}</div>
|
||
</td>
|
||
<td style="padding: 10px 8px; font-size: 12px; border-right: 1px solid rgba(255,255,255,0.05);">
|
||
<div style="color: #fff;">${alert.dept_name}</div>
|
||
<div style="color: #7ecfff; font-size: 10px;">部门ID: ${alert.dept_id}</div>
|
||
</td>
|
||
<td style="padding: 10px 8px; font-size: 11px; border-right: 1px solid rgba(255,255,255,0.05);">
|
||
<div style="color: #ffbb00; font-family: monospace;">${alert.device_sn}</div>
|
||
${alert.health_id ? `<div style="color: #7ecfff; font-size: 10px;">健康ID: ${alert.health_id}</div>` : ''}
|
||
</td>
|
||
<td style="padding: 10px 8px; font-size: 11px; max-width: 200px; word-wrap: break-word; line-height: 1.4;">
|
||
<div style="color: #fff;">${alert.alert_desc || '无详细描述'}</div>
|
||
${alert.alert_status === 'pending' ? `
|
||
<button onclick="handleAlert('${alert.alert_id}')"
|
||
style="margin-top: 5px; background: #ff4444; color: #fff; border: none; padding: 3px 8px; border-radius: 4px; cursor: pointer; font-size: 10px; transition: all 0.2s ease;"
|
||
onmouseover="this.style.background='#ff6666'"
|
||
onmouseout="this.style.background='#ff4444'">
|
||
🚨 立即处理
|
||
</button>
|
||
` : ''}
|
||
</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
${alerts.length === 0 ? `
|
||
<div style="text-align: center; padding: 40px; color: #7ecfff;">
|
||
<div style="font-size: 48px; margin-bottom: 10px;">📭</div>
|
||
<div style="font-size: 16px;">暂无告警记录</div>
|
||
<div style="font-size: 12px; margin-top: 5px; color: rgba(255,255,255,0.5);">系统运行正常</div>
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
document.body.appendChild(modal);
|
||
|
||
// 关闭事件
|
||
modal.querySelector('.modal-close').onclick = () => modal.remove();
|
||
modal.onclick = (e) => {
|
||
if (e.target === modal) modal.remove();
|
||
};
|
||
|
||
// 添加键盘事件
|
||
document.addEventListener('keydown', function escHandler(e) {
|
||
if (e.key === 'Escape') {
|
||
modal.remove();
|
||
document.removeEventListener('keydown', escHandler);
|
||
}
|
||
});
|
||
}
|
||
|
||
// 导出告警数据
|
||
// exportAlerts() 和 refreshAlerts() 函数已移至 alert-panel.js 模块 (Phase 8)
|
||
|
||
// 修改设备管理图表初始化函数
|
||
function initDeviceChart(data) {
|
||
const statsContainer = document.getElementById('statsChart');
|
||
if (!statsContainer) return;
|
||
|
||
// 检查是否已存在图表实例,如果存在则复用
|
||
let statsChart;
|
||
if (globalCharts && globalCharts.stats) {
|
||
statsChart = globalCharts.stats;
|
||
} else {
|
||
if (globalCharts) {
|
||
globalCharts.stats = statsChart;
|
||
}
|
||
}
|
||
|
||
const deviceInfo = data.device_info || {};
|
||
const totalDevices = deviceInfo.totalDevices || 0;
|
||
document.getElementById('totalWatchDevices').textContent = totalDevices;
|
||
|
||
// 计算各项统计数据
|
||
const activeDevices = deviceInfo.deviceStatusCount?.ACTIVE || 0;
|
||
const chargingDevices = deviceInfo.deviceChargingCount?.CHARGING || 0;
|
||
const faultDevices = deviceInfo.deviceStatusCount?.FAULT || 0;
|
||
const offlineDevices = deviceInfo.deviceStatusCount?.INACTIVE || 0;
|
||
|
||
// 计算在线率
|
||
const onlineRate = totalDevices > 0 ? ((activeDevices / totalDevices) * 100).toFixed(1) : 0;
|
||
|
||
// 只在首次创建时添加点击事件监听器
|
||
if (!statsContainer.hasClickListener) {
|
||
statsContainer.onclick = () => {
|
||
const legendData = {
|
||
'部门分布': deviceInfo.departmentDeviceCount || {},
|
||
'充电状态': deviceInfo.deviceChargingCount || {},
|
||
'设备状态': deviceInfo.deviceStatusCount || {},
|
||
'系统版本': deviceInfo.deviceSystemVersionCount || {},
|
||
'佩戴状态': deviceInfo.deviceWearableCount || {}
|
||
};
|
||
showFullLegend(legendData);
|
||
};
|
||
statsContainer.hasClickListener = true;
|
||
}
|
||
|
||
// 重新构建面板内容,删除中间卡片,只保留顶部总览和底部图表
|
||
if (!statsContainer.querySelector('.device-overview-header')) {
|
||
statsContainer.innerHTML = `
|
||
<div style="position: relative; height: 100%; padding: 6px;">
|
||
<!-- 顶部数据总览条 - 更紧凑 -->
|
||
<div class="device-overview-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; padding: 6px 12px; background: rgba(0,228,255,0.1); border-radius: 6px; border-left: 3px solid #00e4ff;">
|
||
<div style="display: flex; align-items: center; gap: 15px;">
|
||
<div class="overview-item">
|
||
<span style="color: #00e4ff; font-size: 20px; font-weight: bold;">${totalDevices}</span>
|
||
<span style="color: #fff; font-size: 12px; margin-left: 4px;">设备总数</span>
|
||
</div>
|
||
<div class="overview-item">
|
||
<span style="color: #52c41a; font-size: 18px; font-weight: bold;">${activeDevices}</span>
|
||
<span style="color: #fff; font-size: 12px; margin-left: 4px;">在线设备</span>
|
||
</div>
|
||
<div class="overview-item">
|
||
<span style="color: #faad14; font-size: 18px; font-weight: bold;">${onlineRate}%</span>
|
||
<span style="color: #fff; font-size: 12px; margin-left: 4px;">在线率</span>
|
||
</div>
|
||
</div>
|
||
<div style="color: #7ecfff; font-size: 11px; cursor: pointer;">详情 →</div>
|
||
</div>
|
||
|
||
<!-- 底部图表区域 - 更紧凑 -->
|
||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 6px; height: calc(100% - 45px);">
|
||
<div id="deviceDeptChart" style="background: rgba(0,21,41,0.4); border: 1px solid rgba(0,228,255,0.3); border-radius: 6px; position: relative;">
|
||
<div style="position: absolute; top: 6px; left: 10px; color: #00e4ff; font-size: 12px; font-weight: bold; z-index: 10;">部门设备分布</div>
|
||
</div>
|
||
<div id="deviceStatusChart" style="background: rgba(0,21,41,0.4); border: 1px solid rgba(0,228,255,0.3); border-radius: 6px; position: relative;">
|
||
<div style="position: absolute; top: 6px; left: 10px; color: #00e4ff; font-size: 12px; font-weight: bold; z-index: 10;">设备状态统计</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// 延时初始化图表
|
||
setTimeout(() => {
|
||
// 部门设备分布饼图(左侧大图表)
|
||
const deptChart = echarts.init(document.getElementById('deviceDeptChart'));
|
||
const deptData = Object.entries(deviceInfo.departmentDeviceCount || {}).map(([name, count]) => ({
|
||
name: name.length > 8 ? name.substring(0, 8) + '...' : name,
|
||
value: count,
|
||
itemStyle: { color: `hsl(${Math.random() * 360}, 60%, 50%)` }
|
||
}));
|
||
|
||
deptChart.setOption({
|
||
tooltip: {
|
||
trigger: 'item',
|
||
backgroundColor: 'rgba(0,21,41,0.95)',
|
||
borderColor: '#00e4ff',
|
||
textStyle: { color: '#fff', fontSize: 11 },
|
||
formatter: '{b}: {c}台 ({d}%)'
|
||
},
|
||
legend: {
|
||
orient: 'vertical',
|
||
left: 'left',
|
||
top: 'center',
|
||
textStyle: { color: '#fff', fontSize: 9 },
|
||
itemWidth: 10,
|
||
itemHeight: 6,
|
||
itemGap: 4
|
||
},
|
||
series: [{
|
||
type: 'pie',
|
||
radius: ['25%', '65%'],
|
||
center: ['70%', '50%'],
|
||
data: deptData.length > 0 ? deptData : [{ name: '暂无数据', value: 1, itemStyle: { color: '#666' } }],
|
||
label: { show: false },
|
||
labelLine: { show: false },
|
||
emphasis: {
|
||
itemStyle: {
|
||
shadowBlur: 10,
|
||
shadowOffsetX: 0,
|
||
shadowColor: 'rgba(0, 228, 255, 0.6)'
|
||
}
|
||
}
|
||
}]
|
||
});
|
||
|
||
// 设备状态统计柱状图(右侧大图表)
|
||
const statusChart = echarts.init(document.getElementById('deviceStatusChart'));
|
||
const statusCategories = ['在线', '离线', '充电', '故障', '正常'];
|
||
const statusValues = [
|
||
activeDevices,
|
||
offlineDevices,
|
||
chargingDevices,
|
||
faultDevices,
|
||
totalDevices - faultDevices - offlineDevices
|
||
];
|
||
const statusColors = ['#52c41a', '#f5222d', '#1890ff', '#fa8c16', '#00e4ff'];
|
||
|
||
statusChart.setOption({
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
backgroundColor: 'rgba(0,21,41,0.95)',
|
||
borderColor: '#00e4ff',
|
||
textStyle: { color: '#fff', fontSize: 11 },
|
||
formatter: function(params) {
|
||
return params[0].name + ': ' + params[0].value + '台';
|
||
}
|
||
},
|
||
grid: { top: 25, left: 30, right: 15, bottom: 25 },
|
||
xAxis: {
|
||
type: 'category',
|
||
data: statusCategories,
|
||
axisLabel: { color: '#7ecfff', fontSize: 9, 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)' } },
|
||
axisLine: { show: false }
|
||
},
|
||
series: [{
|
||
type: 'bar',
|
||
data: statusValues.map((value, index) => ({
|
||
value: value,
|
||
itemStyle: {
|
||
color: statusColors[index],
|
||
borderRadius: [3, 3, 0, 0]
|
||
}
|
||
})),
|
||
barWidth: '45%',
|
||
label: {
|
||
show: true,
|
||
position: 'top',
|
||
color: '#fff',
|
||
fontSize: 10,
|
||
formatter: '{c}'
|
||
}
|
||
}]
|
||
});
|
||
|
||
if (globalCharts) {
|
||
globalCharts.stats = { deptChart, statusChart };
|
||
}
|
||
}, 200);
|
||
|
||
return statsChart;
|
||
}
|
||
|
||
// 修改趋势分析图表,增加预测数据
|
||
function updateTrendChart(data) {
|
||
const trendContainer = document.getElementById('trendChart');
|
||
if (!trendContainer) return;
|
||
|
||
const trendChart = echarts.init(trendContainer);
|
||
|
||
const trendOption = {
|
||
title: {
|
||
text: '健康指标趋势及预测',
|
||
textStyle: {
|
||
color: '#fff',
|
||
fontSize: 16
|
||
}
|
||
},
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
backgroundColor: 'rgba(0,21,41,0.9)',
|
||
borderColor: 'rgba(0,228,255,0.2)',
|
||
textStyle: { color: '#fff' }
|
||
},
|
||
legend: {
|
||
data: ['心率', '血压', '压力指数', '距离', '卡路里', '步数', '预测值'],
|
||
textStyle: { color: '#fff' },
|
||
top: 30
|
||
},
|
||
grid: {
|
||
left: '3%',
|
||
right: '4%',
|
||
bottom: '3%',
|
||
containLabel: true
|
||
},
|
||
xAxis: {
|
||
type: 'category',
|
||
boundaryGap: false,
|
||
data: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00', '预测'],
|
||
axisLabel: { color: '#fff' },
|
||
axisLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } }
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
axisLabel: { color: '#fff' },
|
||
splitLine: {
|
||
lineStyle: {
|
||
color: 'rgba(255,255,255,0.1)',
|
||
type: 'dashed'
|
||
}
|
||
}
|
||
},
|
||
series: [
|
||
{
|
||
name: '心率',
|
||
type: 'line',
|
||
smooth: true,
|
||
data: [75, 72, 78, 85, 80, 75, 77],
|
||
itemStyle: { color: '#ff4444' }
|
||
},
|
||
{
|
||
name: '血压',
|
||
type: 'line',
|
||
smooth: true,
|
||
data: [120, 118, 122, 125, 121, 119, 120],
|
||
itemStyle: { color: '#00e4ff' }
|
||
},
|
||
{
|
||
name: '压力指数',
|
||
type: 'line',
|
||
smooth: true,
|
||
data: [45, 48, 52, 55, 50, 47, 49],
|
||
itemStyle: { color: '#ffbb00' }
|
||
},
|
||
{
|
||
name: '距离',
|
||
type: 'line',
|
||
smooth: true,
|
||
data: [2.1, 2.3, 2.8, 3.2, 3.5, 3.8, 4.0],
|
||
itemStyle: { color: '#00ff9d' }
|
||
},
|
||
{
|
||
name: '卡路里',
|
||
type: 'line',
|
||
smooth: true,
|
||
data: [150, 180, 220, 280, 320, 350, 380],
|
||
itemStyle: { color: '#ff7777' }
|
||
},
|
||
{
|
||
name: '步数',
|
||
type: 'line',
|
||
smooth: true,
|
||
data: [2000, 2500, 3000, 3800, 4200, 4500, 4800],
|
||
itemStyle: { color: '#7777ff' }
|
||
}
|
||
]
|
||
};
|
||
|
||
// 为每个系列添加预测区域
|
||
trendOption.series.forEach(series => {
|
||
const lastIndex = series.data.length - 1;
|
||
series.markArea = {
|
||
itemStyle: {
|
||
color: 'rgba(0, 228, 255, 0.1)'
|
||
},
|
||
data: [[{
|
||
xAxis: trendOption.xAxis.data[lastIndex - 1]
|
||
}, {
|
||
xAxis: trendOption.xAxis.data[lastIndex]
|
||
}]]
|
||
};
|
||
});
|
||
|
||
trendChart.setOption(trendOption);
|
||
return trendChart;
|
||
}
|
||
|
||
// 人员管理面板交互函数
|
||
function showPersonnelDetails() {
|
||
showCustomAlert('人员详情功能:显示完整的人员管理统计信息');
|
||
}
|
||
|
||
function filterByDepartment() {
|
||
showCustomAlert('部门筛选功能:按部门查看人员分布详情');
|
||
}
|
||
|
||
function filterByOnlineStatus() {
|
||
showCustomAlert('在线状态筛选功能:显示在线/离线人员列表');
|
||
}
|
||
|
||
function filterByDeviceStatus() {
|
||
showCustomAlert('设备状态筛选功能:显示已绑定/未绑定设备人员');
|
||
}
|
||
|
||
// filterByAlertStatus() 函数已移至 alert-panel.js 模块 (Phase 8)
|
||
|
||
// 显示部门详情
|
||
function showDepartmentDetails(deptName, userCount) {
|
||
const modal = document.createElement('div');
|
||
modal.className = 'modal-container';
|
||
modal.innerHTML = `
|
||
<div class="modal-content" style="width: 60%; height: 70%;">
|
||
<button class="modal-close">✕</button>
|
||
<h3 style="color: #00e4ff; margin-bottom: 20px; text-align: center; font-size: 18px;">📊 ${deptName} 部门详情</h3>
|
||
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; padding: 20px;">
|
||
<div style="background: rgba(0,228,255,0.1); padding: 20px; border-radius: 8px; text-align: center;">
|
||
<div style="color: #00e4ff; font-size: 24px; font-weight: bold;">${userCount}</div>
|
||
<div style="color: #fff; margin-top: 5px;">总人数</div>
|
||
</div>
|
||
<div style="background: rgba(0,255,157,0.1); padding: 20px; border-radius: 8px; text-align: center;">
|
||
<div style="color: #00ff9d; font-size: 24px; font-weight: bold;">${Math.floor(userCount * 0.8)}</div>
|
||
<div style="color: #fff; margin-top: 5px;">在线人数</div>
|
||
</div>
|
||
<div style="background: rgba(255,187,0,0.1); padding: 20px; border-radius: 8px; text-align: center;">
|
||
<div style="color: #ffbb00; font-size: 24px; font-weight: bold;">${Math.floor(userCount * 0.9)}</div>
|
||
<div style="color: #fff; margin-top: 5px;">设备绑定</div>
|
||
</div>
|
||
</div>
|
||
<div style="text-align: center; margin-top: 20px; color: #7ecfff;">
|
||
点击可查看该部门的详细人员列表和设备状态
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
document.body.appendChild(modal);
|
||
|
||
// 关闭事件
|
||
modal.querySelector('.modal-close').onclick = () => modal.remove();
|
||
modal.onclick = (e) => {
|
||
if (e.target === modal) modal.remove();
|
||
};
|
||
}
|
||
|
||
// 显示状态详情
|
||
function showStatusDetails(statusName, statusValue) {
|
||
const statusMap = {
|
||
'在线': '当前在线的用户列表',
|
||
'离线': '当前离线的用户列表',
|
||
'绑定': '已绑定设备的用户',
|
||
'未绑定': '未绑定设备的用户',
|
||
'告警': '当前有告警的用户',
|
||
'正常': '状态正常的用户'
|
||
};
|
||
|
||
const modal = document.createElement('div');
|
||
modal.className = 'modal-container';
|
||
modal.innerHTML = `
|
||
<div class="modal-content" style="width: 70%; height: 80%;">
|
||
<button class="modal-close">✕</button>
|
||
<h3 style="color: #00e4ff; margin-bottom: 20px; text-align: center; font-size: 18px;">📋 ${statusName}用户详情</h3>
|
||
<div style="background: rgba(0,228,255,0.1); padding: 15px; border-radius: 8px; margin-bottom: 20px; text-align: center;">
|
||
<div style="color: #00e4ff; font-size: 28px; font-weight: bold;">${statusValue}</div>
|
||
<div style="color: #fff; margin-top: 5px;">${statusMap[statusName] || '用户统计'}</div>
|
||
</div>
|
||
<div style="color: #7ecfff; text-align: center; padding: 40px;">
|
||
<div style="font-size: 48px; margin-bottom: 15px;">👥</div>
|
||
<div style="font-size: 16px;">详细用户列表功能开发中...</div>
|
||
<div style="font-size: 12px; margin-top: 10px; color: rgba(255,255,255,0.5);">将显示具体的用户信息、设备状态和健康数据</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
document.body.appendChild(modal);
|
||
|
||
// 关闭事件
|
||
modal.querySelector('.modal-close').onclick = () => modal.remove();
|
||
modal.onclick = (e) => {
|
||
if (e.target === modal) modal.remove();
|
||
};
|
||
}
|
||
|
||
</script>
|
||
|
||
|
||
|
||
</script>
|
||
<script src="{{ url_for('static', filename='libs/jquery.min.js') }}"></script>
|
||
<script>
|
||
$(document).ready(function() {
|
||
// 获取部门数据
|
||
$.ajax({
|
||
url: `/get_departments?orgId={{ customerId }}&customerId={{ 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);
|
||
|
||
// 如果选择了特定用户,地图跳转到该用户位置
|
||
if (currentUser && lastTotalInfo) {
|
||
jumpToUserLocation(currentUser, lastTotalInfo);
|
||
}
|
||
});
|
||
|
||
|
||
|
||
// 更新部门选择框
|
||
function updateDepartmentSelect(departments) {
|
||
const deptSelect = document.getElementById('deptSelect');
|
||
if (!deptSelect) return;
|
||
|
||
// 清空现有选项
|
||
deptSelect.innerHTML = '<option value="">选择部门</option>';
|
||
|
||
// 递归添加部门选项并收集所有用户数据
|
||
const allDeptUsers = [];
|
||
async function addDepartmentOptions(departments, level = 0) {
|
||
for (const dept of departments) {
|
||
const option = document.createElement('option');
|
||
option.value = dept.id;
|
||
const indent = ' '.repeat(level);
|
||
option.textContent = indent + dept.name;
|
||
deptSelect.appendChild(option);
|
||
|
||
// 获取该部门的用户数据并添加到全局数组
|
||
try {
|
||
const response = await api.users.list(dept.id);
|
||
const users = await response.json();
|
||
if (users) {
|
||
users.forEach(user => {
|
||
allDeptUsers.push({
|
||
...user,
|
||
dept_name: dept.name,
|
||
dept_id: dept.id,
|
||
user_id: user.id
|
||
});
|
||
});
|
||
}
|
||
} catch (e) {
|
||
console.error('获取部门用户失败:', e);
|
||
}
|
||
|
||
if (dept.children && dept.children.length > 0) {
|
||
await addDepartmentOptions(dept.children, level + 1);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 异步处理并设置用户数据
|
||
addDepartmentOptions(departments).then(() => {
|
||
setUsersData(allDeptUsers);
|
||
});
|
||
|
||
deptSelect.style.fontFamily = 'monospace';
|
||
}
|
||
|
||
// 更新用户选择框
|
||
async function updateUsers(deptId) {
|
||
if (deptId) {
|
||
// 如果选择了部门,过滤该部门的用户
|
||
filteredUsers = allUsers.filter(u => u.dept_id == deptId);
|
||
} else {
|
||
// 如果没有选择部门,显示所有用户
|
||
filteredUsers = allUsers;
|
||
}
|
||
updateUserSelect();
|
||
// 清空搜索框
|
||
const searchInput = document.getElementById('searchInput');
|
||
if (searchInput) searchInput.value = '';
|
||
}
|
||
|
||
// 添加样式
|
||
const style = document.createElement('style');
|
||
style.textContent = `
|
||
.filter-select {
|
||
font-family: monospace;
|
||
white-space: pre;
|
||
width: 100%;
|
||
padding: 8px;
|
||
margin-bottom: 10px;
|
||
background: rgba(0, 0, 0, 0.3);
|
||
color: #fff;
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.filter-select option {
|
||
font-family: monospace;
|
||
white-space: pre;
|
||
background: #1a1a1a;
|
||
color: #fff;
|
||
}
|
||
|
||
|
||
`;
|
||
document.head.appendChild(style);
|
||
});
|
||
</script>
|
||
|
||
<div id="infoPanel" class="info-panel" style="display: none;">
|
||
<div class="info-panel-header">
|
||
<div class="info-panel-title">
|
||
<i class="fas fa-info-circle"></i>
|
||
<span>实时信息</span>
|
||
</div>
|
||
<button class="info-panel-close" onclick="toggleInfoPanel()">×</button>
|
||
</div>
|
||
<div class="info-panel-content" id="infoPanelContent">
|
||
<!-- 内容将通过JavaScript动态填充 -->
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// 全局变量
|
||
let infoPanelVisible = false;
|
||
let lastTotalInfo = null;
|
||
|
||
// 切换信息面板显示状态
|
||
function toggleInfoPanel() {
|
||
const panel = document.getElementById('infoPanel');
|
||
infoPanelVisible = !infoPanelVisible;
|
||
panel.style.display = infoPanelVisible ? 'block' : 'none';
|
||
}
|
||
|
||
// 添加键盘快捷键切换信息面板
|
||
// keydown 事件监听器已移至 app-initializer.js 模块 (Phase 7)
|
||
// Ctrl+I 快捷键由 AppInitializer.bindGlobalEvents() 处理
|
||
|
||
function openInfoPanelWithAlertId(alertId) {
|
||
infoPanelVisible = true;
|
||
document.getElementById('infoPanel').style.display = 'block';
|
||
updateInfoPanel(lastTotalInfo, alertId);
|
||
}
|
||
|
||
// 面板显示状态管理,防止重复面板
|
||
let panelDisplaying = false;
|
||
|
||
function showCustomMapInfo(f){
|
||
// 检查是否已存在地图信息面板,避免重复显示
|
||
const existingMapInfo = document.querySelector('.custom-map-info');
|
||
if(existingMapInfo) {
|
||
console.log('地图信息面板已存在,跳过重复调用');
|
||
return;
|
||
}
|
||
|
||
panelDisplaying = true; // 标记面板正在显示
|
||
|
||
// 移除可能存在的健康详情面板
|
||
const existingHealthModal = document.querySelector('.health-modal-overlay');
|
||
if(existingHealthModal) {
|
||
existingHealthModal.remove(); // 移除健康详情面板
|
||
}
|
||
|
||
const d=f.properties,toStr=x=>x===undefined||x===null?'':String(x);
|
||
const get=(...k)=>k.map(x=>d[x]).find(x=>x!==undefined&&x!==null&&x!=='')||'-';
|
||
// 判断是否为告警点:有alert_id/alertId且有alert_type/alertType,且type不是health
|
||
const isAlert=!!(get('alert_id','alertId')&&get('alert_type','alertType')&&d.type!=='health');
|
||
console.log('showCustomMapInfo.d',d);
|
||
console.log('showCustomMapInfo.isAlert',isAlert);
|
||
const level=get('severity_level','severityLevel');
|
||
const levelColor=level==='critical'?'#ff4d4f':level==='high'?'#ffbb00':'#ffe066';
|
||
const avatarUrl=d.avatar||'/static/images/avatar-tech.svg';
|
||
const div=document.createElement('div');
|
||
div.className='custom-map-info';
|
||
div.style.cssText='position:absolute;z-index:9999;min-width:360px;max-width:420px;background:rgba(10,24,48,0.98);border:1.5px solid #00e4ff;border-radius:16px;box-shadow:0 0 24px #00e4ff44;padding:22px 28px 18px 28px;color:#fff;top:120px;left:50%;transform:translateX(-50%);font-size:15px;font-family:Roboto,Arial,sans-serif;backdrop-filter:blur(6px);';
|
||
// 获取位置信息
|
||
const longitude = get('longitude');
|
||
const latitude = get('latitude');
|
||
if(longitude && latitude){
|
||
reverseGeocode(longitude, latitude)
|
||
.then(address => {
|
||
const locationInfo = document.getElementById('locationInfo');
|
||
if(locationInfo){
|
||
locationInfo.textContent = address || '未知位置';
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('获取位置信息失败:', error);
|
||
const locationInfo = document.getElementById('locationInfo');
|
||
if(locationInfo){
|
||
locationInfo.textContent = '获取位置信息失败';
|
||
}
|
||
});
|
||
}
|
||
if(isAlert){
|
||
|
||
// 告警点内容
|
||
// 告警点内容 - 专业科技风格
|
||
div.innerHTML=`
|
||
<div style="
|
||
background: linear-gradient(135deg, rgba(15,25,45,0.95) 0%, rgba(25,35,65,0.98) 50%, rgba(15,25,45,0.95) 100%);
|
||
border-radius: 20px;
|
||
border: 2px solid rgba(255,68,68,0.5);
|
||
box-shadow: 0 20px 60px rgba(255,68,68,0.3), 0 0 30px rgba(255,68,68,0.2), inset 0 1px 0 rgba(255,255,255,0.1);
|
||
padding: 24px;
|
||
color: #fff;
|
||
position: relative;
|
||
overflow: hidden;
|
||
animation: alertPulse 2s infinite, slideIn 0.5s ease-out;
|
||
min-width: 380px;
|
||
">
|
||
|
||
<!-- 背景动态效果 -->
|
||
<div style="
|
||
position: absolute; top: 0; left: -100%; width: 100%; height: 100%;
|
||
background: linear-gradient(90deg, transparent, rgba(255,68,68,0.1), transparent);
|
||
animation: scanLine 3s infinite linear;
|
||
"></div>
|
||
|
||
<!-- 告警级别指示器 -->
|
||
<div style="
|
||
position: absolute; top: -1px; left: -1px; right: -1px; height: 4px;
|
||
background: linear-gradient(90deg, #ff4444, #ff6b6b, #ff4444);
|
||
border-radius: 20px 20px 0 0;
|
||
animation: levelGlow 1.5s infinite alternate;
|
||
"></div>
|
||
|
||
<!-- 头部信息 -->
|
||
<div style="display:flex;align-items:center;gap:20px;margin-bottom:20px;position:relative;z-index:2;">
|
||
<div style="position:relative;">
|
||
<img src="${avatarUrl}" style="
|
||
width:64px;height:64px;border-radius:50%;
|
||
border:3px solid #ff4444;
|
||
box-shadow:0 0 20px rgba(255,68,68,0.6), inset 0 0 10px rgba(255,68,68,0.2);
|
||
object-fit:cover;background:#001529;
|
||
animation: avatarGlow 2s infinite alternate;
|
||
">
|
||
<!-- 告警状态指示器 -->
|
||
<div style="
|
||
position:absolute;top:-2px;right:-2px;
|
||
width:20px;height:20px;border-radius:50%;
|
||
background: radial-gradient(circle, #ff4444 30%, transparent 70%);
|
||
border:2px solid #fff;
|
||
animation: alertBlink 1s infinite;
|
||
"></div>
|
||
</div>
|
||
<div style="flex:1;">
|
||
<div style="
|
||
font-size:20px;font-weight:700;letter-spacing:1.2px;
|
||
color:#fff;text-shadow:0 0 10px rgba(255,255,255,0.5);
|
||
margin-bottom:4px;
|
||
">${get('dept_name','deptName')}</div>
|
||
<div style="
|
||
font-size:18px;color:#00e4ff;font-weight:600;
|
||
text-shadow:0 0 8px rgba(0,228,255,0.6);
|
||
">${get('user_name','userName')}</div>
|
||
</div>
|
||
<!-- 告警图标 -->
|
||
<div style="
|
||
width:48px;height:48px;border-radius:12px;
|
||
background:linear-gradient(135deg, rgba(255,68,68,0.3) 0%, rgba(220,38,38,0.5) 100%);
|
||
display:flex;align-items:center;justify-content:center;
|
||
font-size:24px;
|
||
box-shadow:0 4px 15px rgba(255,68,68,0.4);
|
||
animation: iconPulse 1.5s infinite ease-in-out;
|
||
">⚠️</div>
|
||
</div>
|
||
|
||
<!-- 告警详情卡片组 -->
|
||
<div style="
|
||
display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));
|
||
gap:16px;margin-bottom:20px;
|
||
">
|
||
<!-- 告警类别卡片 -->
|
||
<div style="
|
||
background:rgba(0,21,41,0.6);border-radius:12px;padding:16px;
|
||
border:1px solid rgba(255,68,68,0.3);
|
||
transition:all 0.3s ease;
|
||
" onmouseover="this.style.transform='translateY(-4px)';this.style.boxShadow='0 8px 25px rgba(255,68,68,0.4)';"
|
||
onmouseout="this.style.transform='translateY(0)';this.style.boxShadow='none';">
|
||
<div style="color:#7ecfff;font-size:12px;margin-bottom:8px;text-transform:uppercase;letter-spacing:1px;">告警类别</div>
|
||
<div style="
|
||
color:${getAlertTypeColor(get('alert_type','alertType','-'))};
|
||
font-weight:700;font-size:16px;
|
||
text-shadow:0 0 8px currentColor;
|
||
">${translateAlertType(get('alert_type','alertType','-'))}</div>
|
||
</div>
|
||
|
||
<!-- 告警级别卡片 -->
|
||
<div style="
|
||
background:rgba(0,21,41,0.6);border-radius:12px;padding:16px;
|
||
border:1px solid rgba(255,187,0,0.3);
|
||
transition:all 0.3s ease;
|
||
" onmouseover="this.style.transform='translateY(-4px)';this.style.boxShadow='0 8px 25px rgba(255,187,0,0.4)';"
|
||
onmouseout="this.style.transform='translateY(0)';this.style.boxShadow='none';">
|
||
<div style="color:#7ecfff;font-size:12px;margin-bottom:8px;text-transform:uppercase;letter-spacing:1px;">告警级别</div>
|
||
<div style="
|
||
color:${getAlertSeverityColor(level||'-')};
|
||
font-weight:700;font-size:16px;
|
||
text-shadow:0 0 8px currentColor;
|
||
">${translateAlertSeverity(level||'-')}</div>
|
||
</div>
|
||
|
||
<!-- 告警状态卡片 -->
|
||
<div style="
|
||
background:rgba(0,21,41,0.6);border-radius:12px;padding:16px;
|
||
border:1px solid rgba(0,228,255,0.3);
|
||
transition:all 0.3s ease;
|
||
" onmouseover="this.style.transform='translateY(-4px)';this.style.boxShadow='0 8px 25px rgba(0,228,255,0.4)';"
|
||
onmouseout="this.style.transform='translateY(0)';this.style.boxShadow='none';">
|
||
<div style="color:#7ecfff;font-size:12px;margin-bottom:8px;text-transform:uppercase;letter-spacing:1px;">处理状态</div>
|
||
<div style="
|
||
color:${getAlertStatusColor(get('alert_status','status','-'))};
|
||
font-weight:700;font-size:16px;
|
||
text-shadow:0 0 8px currentColor;
|
||
">${translateAlertStatus(get('alert_status','status','-'))}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 健康信息链接 -->
|
||
<div style="
|
||
background:linear-gradient(135deg, rgba(0,228,255,0.1) 0%, rgba(0,180,255,0.2) 100%);
|
||
border-radius:12px;padding:16px;margin-bottom:16px;
|
||
border:1px solid rgba(0,228,255,0.3);
|
||
position:relative;overflow:hidden;
|
||
">
|
||
<div style="color:#7ecfff;font-size:12px;margin-bottom:8px;text-transform:uppercase;letter-spacing:1px;">健康数据详情</div>
|
||
<a href="javascript:void(0)" onclick="showHealthProfile('${get('health_id','healthId')}')" style="
|
||
color:#00e4ff;text-decoration:none;font-family:monospace;font-size:16px;font-weight:600;
|
||
display:inline-flex;align-items:center;gap:8px;
|
||
padding:8px 16px;border-radius:8px;
|
||
background:rgba(0,228,255,0.1);border:1px solid rgba(0,228,255,0.3);
|
||
transition:all 0.3s ease;
|
||
text-shadow:0 0 10px rgba(0,228,255,0.5);
|
||
" onmouseover="this.style.background='rgba(0,228,255,0.2)';this.style.transform='scale(1.05)';"
|
||
onmouseout="this.style.background='rgba(0,228,255,0.1)';this.style.transform='scale(1)';">
|
||
<span>📊</span>${get('health_id','healthId')}
|
||
</a>
|
||
</div>
|
||
|
||
<!-- 位置和时间信息 -->
|
||
<div style="
|
||
display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:20px;
|
||
">
|
||
<div style="
|
||
background:rgba(0,21,41,0.4);border-radius:10px;padding:12px;
|
||
border:1px solid rgba(0,228,255,0.2);
|
||
">
|
||
<div style="color:#7ecfff;font-size:11px;margin-bottom:6px;text-transform:uppercase;">位置信息</div>
|
||
<div style="color:#fff;font-size:13px;" id="locationInfo">🌍 正在获取...</div>
|
||
</div>
|
||
<div style="
|
||
background:rgba(0,21,41,0.4);border-radius:10px;padding:12px;
|
||
border:1px solid rgba(0,228,255,0.2);
|
||
">
|
||
<div style="color:#7ecfff;font-size:11px;margin-bottom:6px;text-transform:uppercase;">告警时间</div>
|
||
<div style="color:#fff;font-size:13px;">⏰ ${get('alert_timestamp','timestamp','-')}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 操作按钮区 -->
|
||
<div style="display:flex;gap:16px;align-items:center;position:relative;z-index:2;">
|
||
<button onclick="handleAlert('${get('alert_id','alertId')}')" style="
|
||
padding:12px 24px;
|
||
background:linear-gradient(135deg, ${levelColor} 0%, ${levelColor}dd 100%);
|
||
color:#001529;border:none;border-radius:10px;cursor:pointer;
|
||
font-weight:700;font-size:14px;
|
||
box-shadow:0 4px 15px ${levelColor}66, inset 0 1px 0 rgba(255,255,255,0.2);
|
||
transition:all 0.3s ease;
|
||
text-transform:uppercase;letter-spacing:1px;
|
||
" onmouseover="this.style.transform='translateY(-2px) scale(1.05)';this.style.boxShadow='0 8px 25px ${levelColor}88';"
|
||
onmouseout="this.style.transform='translateY(0) scale(1)';this.style.boxShadow='0 4px 15px ${levelColor}66';">
|
||
🚀 一键处理
|
||
</button>
|
||
<div style="flex:1;"></div>
|
||
<button onclick="removeCustomMapInfo()" style="
|
||
width:44px;height:44px;border-radius:50%;
|
||
background:rgba(0,228,255,0.1);border:2px solid rgba(0,228,255,0.3);
|
||
color:#00e4ff;cursor:pointer;display:flex;align-items:center;justify-content:center;
|
||
font-size:20px;font-weight:700;
|
||
transition:all 0.3s ease;
|
||
backdrop-filter:blur(10px);
|
||
" onmouseover="this.style.background='rgba(0,228,255,0.2)';this.style.transform='scale(1.1) rotate(90deg)';this.style.boxShadow='0 0 20px rgba(0,228,255,0.6)';"
|
||
onmouseout="this.style.background='rgba(0,228,255,0.1)';this.style.transform='scale(1) rotate(0deg)';this.style.boxShadow='none';">
|
||
✕
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
document.body.appendChild(div);
|
||
|
||
|
||
} else {
|
||
// 健康点内容
|
||
// 健康点内容 - 科技健康风格
|
||
// 健康点内容 - 科技健康风格
|
||
div.innerHTML=`
|
||
<div style="
|
||
background: linear-gradient(135deg, rgba(10,24,48,0.95) 0%, rgba(15,35,65,0.98) 50%, rgba(10,24,48,0.95) 100%);
|
||
border-radius: 24px;
|
||
border: 2px solid rgba(0,228,255,0.4);
|
||
box-shadow: 0 25px 80px rgba(0,228,255,0.3), 0 0 40px rgba(0,228,255,0.2), inset 0 1px 0 rgba(255,255,255,0.1);
|
||
padding: 28px;
|
||
color: #fff;
|
||
position: relative;
|
||
overflow: hidden;
|
||
animation: healthGlow 3s infinite ease-in-out, slideIn 0.6s ease-out;
|
||
min-width: 420px;
|
||
">
|
||
|
||
<!-- 背景科技纹理 -->
|
||
<div style="
|
||
position: absolute; top: 0; left: 0; right: 0; bottom: 0;
|
||
background: radial-gradient(circle at 20% 20%, rgba(0,228,255,0.1) 0%, transparent 50%),
|
||
radial-gradient(circle at 80% 80%, rgba(0,180,255,0.1) 0%, transparent 50%);
|
||
animation: bgShift 6s infinite ease-in-out;
|
||
"></div>
|
||
|
||
<!-- 健康状态顶部指示条 -->
|
||
<div style="
|
||
position: absolute; top: -1px; left: -1px; right: -1px; height: 4px;
|
||
background: linear-gradient(90deg, #00ff88, #00e4ff, #00ff88);
|
||
border-radius: 24px 24px 0 0;
|
||
animation: healthPulse 2s infinite alternate;
|
||
"></div>
|
||
|
||
<!-- 头部用户信息 -->
|
||
<div style="display:flex;align-items:center;gap:20px;margin-bottom:24px;position:relative;z-index:2;">
|
||
<div style="position:relative;">
|
||
<img src="${avatarUrl}" style="
|
||
width:72px;height:72px;border-radius:50%;
|
||
border:3px solid #00e4ff;
|
||
box-shadow:0 0 25px rgba(0,228,255,0.6), inset 0 0 15px rgba(0,228,255,0.2);
|
||
object-fit:cover;background:#001529;
|
||
animation: healthAvatarGlow 3s infinite ease-in-out;
|
||
">
|
||
<!-- 健康状态指示器 -->
|
||
<div style="
|
||
position:absolute;bottom:-2px;right:-2px;
|
||
width:24px;height:24px;border-radius:50%;
|
||
background: radial-gradient(circle, #00ff88 40%, transparent 70%);
|
||
border:3px solid #fff;
|
||
animation: healthBeat 1.5s infinite ease-in-out;
|
||
">💚</div>
|
||
</div>
|
||
<div style="flex:1;">
|
||
<div style="
|
||
font-size:22px;font-weight:700;letter-spacing:1.2px;
|
||
color:#fff;text-shadow:0 0 15px rgba(255,255,255,0.5);
|
||
margin-bottom:6px;
|
||
">${get('dept_name','deptName')}</div>
|
||
<div style="
|
||
font-size:18px;color:#00e4ff;font-weight:600;
|
||
text-shadow:0 0 12px rgba(0,228,255,0.6);
|
||
">${get('user_name','userName')}</div>
|
||
</div>
|
||
<!-- 健康图标 -->
|
||
|
||
<!-- 个人大屏按钮 -->
|
||
<div>
|
||
<button onclick="window.open('personal_cool?deviceSn=CRFTQ23409001890', '_blank')" style="
|
||
padding:12px 20px;
|
||
background:linear-gradient(135deg, rgba(0,228,255,0.3) 0%, rgba(0,180,255,0.5) 100%);
|
||
border:2px solid rgba(0,228,255,0.4);
|
||
border-radius:12px;
|
||
color:#00e4ff;
|
||
font-size:14px;
|
||
font-weight:700;
|
||
cursor:pointer;
|
||
display:flex;
|
||
align-items:center;
|
||
gap:8px;
|
||
transition:all 0.3s ease;
|
||
text-shadow:0 0 8px rgba(0,228,255,0.5);
|
||
box-shadow:0 4px 15px rgba(0,228,255,0.2);
|
||
backdrop-filter:blur(10px);
|
||
" onmouseover="this.style.background='linear-gradient(135deg, rgba(0,228,255,0.5) 0%, rgba(0,180,255,0.7) 100%)';this.style.transform='translateY(-3px) scale(1.05)';this.style.boxShadow='0 8px 25px rgba(0,228,255,0.4)';"
|
||
onmouseout="this.style.background='linear-gradient(135deg, rgba(0,228,255,0.3) 0%, rgba(0,180,255,0.5) 100%)';this.style.transform='translateY(0) scale(1)';this.style.boxShadow='0 4px 15px rgba(0,228,255,0.2)';">
|
||
<span>📊</span>
|
||
<span>个人大屏</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 健康指标网格 -->
|
||
<div style="
|
||
display:grid;grid-template-columns:repeat(2,1fr);
|
||
gap:16px;margin-bottom:20px;
|
||
">
|
||
<!-- 心率血压卡片 -->
|
||
<div style="
|
||
background:rgba(0,21,41,0.6);border-radius:16px;padding:18px;
|
||
border:1px solid rgba(255,107,107,0.3);
|
||
transition:all 0.4s ease;position:relative;overflow:hidden;
|
||
" onmouseover="this.style.transform='translateY(-6px)';this.style.boxShadow='0 12px 35px rgba(255,107,107,0.4)';"
|
||
onmouseout="this.style.transform='translateY(0)';this.style.boxShadow='none';">
|
||
<div style="
|
||
display:flex;align-items:center;gap:12px;margin-bottom:12px;
|
||
">
|
||
<div style="
|
||
width:40px;height:40px;border-radius:10px;
|
||
background:linear-gradient(135deg, rgba(255,107,107,0.3) 0%, rgba(238,90,36,0.5) 100%);
|
||
display:flex;align-items:center;justify-content:center;font-size:20px;
|
||
">❤️</div>
|
||
<div>
|
||
<div style="color:#7ecfff;font-size:11px;text-transform:uppercase;letter-spacing:1px;">生命体征</div>
|
||
<div style="color:#fff;font-size:14px;font-weight:600;">心率 & 血压</div>
|
||
</div>
|
||
</div>
|
||
<div style="
|
||
display:flex;justify-content:space-between;align-items:center;
|
||
">
|
||
<div>
|
||
<div style="color:#ff6b6b;font-size:18px;font-weight:700;">${get('heartRate','heart_rate')} <span style="font-size:12px;color:#888;">bpm</span></div>
|
||
<div style="color:#7ecfff;font-size:14px;">${get('pressureHigh','pressure_high')}/${get('pressureLow','pressure_low')} mmHg</div>
|
||
</div>
|
||
<!-- 心率可视化 -->
|
||
<div style="
|
||
width:50px;height:30px;
|
||
background:linear-gradient(90deg, transparent, rgba(255,107,107,0.3), transparent);
|
||
border-radius:4px;position:relative;overflow:hidden;
|
||
">
|
||
<div style="
|
||
width:4px;height:100%;background:#ff6b6b;
|
||
animation: heartbeatLine 1.5s infinite ease-in-out;
|
||
box-shadow:0 0 10px #ff6b6b;
|
||
"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 血氧体温卡片 -->
|
||
<div style="
|
||
background:rgba(0,21,41,0.6);border-radius:16px;padding:18px;
|
||
border:1px solid rgba(0,255,136,0.3);
|
||
transition:all 0.4s ease;position:relative;overflow:hidden;
|
||
" onmouseover="this.style.transform='translateY(-6px)';this.style.boxShadow='0 12px 35px rgba(0,255,136,0.4)';"
|
||
onmouseout="this.style.transform='translateY(0)';this.style.boxShadow='none';">
|
||
<div style="
|
||
display:flex;align-items:center;gap:12px;margin-bottom:12px;
|
||
">
|
||
<div style="
|
||
width:40px;height:40px;border-radius:10px;
|
||
background:linear-gradient(135deg, rgba(0,255,136,0.3) 0%, rgba(0,204,102,0.5) 100%);
|
||
display:flex;align-items:center;justify-content:center;font-size:20px;
|
||
">🫁</div>
|
||
<div>
|
||
<div style="color:#7ecfff;font-size:11px;text-transform:uppercase;letter-spacing:1px;">呼吸体温</div>
|
||
<div style="color:#fff;font-size:14px;font-weight:600;">血氧 & 体温</div>
|
||
</div>
|
||
</div>
|
||
<div style="
|
||
display:flex;justify-content:space-between;align-items:center;
|
||
">
|
||
<div>
|
||
<div style="color:#00ff88;font-size:18px;font-weight:700;">${get('bloodOxygen','blood_oxygen')} <span style="font-size:12px;color:#888;">%</span></div>
|
||
<div style="color:#7ecfff;font-size:14px;">${get('temperature','temp')} ℃</div>
|
||
</div>
|
||
<!-- 血氧环形进度 -->
|
||
<div style="position:relative;width:40px;height:40px;">
|
||
<svg width="40" height="40" style="transform:rotate(-90deg);">
|
||
<circle cx="20" cy="20" r="16" stroke="rgba(0,255,136,0.2)" stroke-width="3" fill="none"/>
|
||
<circle cx="20" cy="20" r="16" stroke="#00ff88" stroke-width="3" fill="none"
|
||
stroke-dasharray="100.48" stroke-dashoffset="${100.48 * (1 - (get('bloodOxygen','blood_oxygen')||95)/100)}"
|
||
style="transition:stroke-dashoffset 1s ease;"/>
|
||
</svg>
|
||
<div style="
|
||
position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
|
||
font-size:10px;font-weight:700;color:#00ff88;
|
||
">${get('bloodOxygen','blood_oxygen')||'-'}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 运动数据横条 -->
|
||
<div style="
|
||
background:linear-gradient(135deg, rgba(138,43,226,0.2) 0%, rgba(75,0,130,0.3) 100%);
|
||
border-radius:16px;padding:20px;margin-bottom:20px;
|
||
border:1px solid rgba(138,43,226,0.4);
|
||
position:relative;overflow:hidden;
|
||
">
|
||
<div style="
|
||
display:flex;align-items:center;gap:16px;margin-bottom:16px;
|
||
">
|
||
<div style="
|
||
width:48px;height:48px;border-radius:12px;
|
||
background:linear-gradient(135deg, rgba(138,43,226,0.4) 0%, rgba(75,0,130,0.6) 100%);
|
||
display:flex;align-items:center;justify-content:center;font-size:24px;
|
||
">🏃</div>
|
||
<div>
|
||
<div style="color:#9d4edd;font-size:18px;font-weight:700;">运动数据统计</div>
|
||
<div style="color:#7ecfff;font-size:12px;">Activity & Fitness Metrics</div>
|
||
</div>
|
||
</div>
|
||
<div style="
|
||
display:grid;grid-template-columns:repeat(4,1fr);gap:16px;
|
||
">
|
||
<div style="text-align:center;padding:12px;border-radius:10px;background:rgba(0,0,0,0.3);">
|
||
<div style="color:#9d4edd;font-size:20px;font-weight:700;">${get('step','steps')||'-'}</div>
|
||
<div style="color:#7ecfff;font-size:10px;text-transform:uppercase;">步数</div>
|
||
</div>
|
||
<div style="text-align:center;padding:12px;border-radius:10px;background:rgba(0,0,0,0.3);">
|
||
<div style="color:#9d4edd;font-size:20px;font-weight:700;">${get('distance','distance')||'-'}</div>
|
||
<div style="color:#7ecfff;font-size:10px;text-transform:uppercase;">距离(米)</div>
|
||
</div>
|
||
<div style="text-align:center;padding:12px;border-radius:10px;background:rgba(0,0,0,0.3);">
|
||
<div style="color:#9d4edd;font-size:20px;font-weight:700;">${get('calorie','calories')||'-'}</div>
|
||
<div style="color:#7ecfff;font-size:10px;text-transform:uppercase;">卡路里</div>
|
||
</div>
|
||
<div style="text-align:center;padding:12px;border-radius:10px;background:rgba(0,0,0,0.3);">
|
||
<div style="color:#9d4edd;font-size:20px;font-weight:700;">${get('stress','pressure')||'-'}</div>
|
||
<div style="color:#7ecfff;font-size:10px;text-transform:uppercase;">压力值</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 位置和时间信息 -->
|
||
<div style="
|
||
display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:20px;
|
||
">
|
||
<div style="
|
||
background:rgba(0,21,41,0.5);border-radius:12px;padding:16px;
|
||
border:1px solid rgba(0,228,255,0.3);
|
||
">
|
||
<div style="color:#7ecfff;font-size:12px;margin-bottom:8px;text-transform:uppercase;letter-spacing:1px;">位置信息</div>
|
||
<div style="color:#fff;font-size:14px;display:flex;align-items:center;gap:8px;" id="locationInfo">
|
||
<span>🌍</span><span>正在获取位置...</span>
|
||
</div>
|
||
</div>
|
||
<div style="
|
||
background:rgba(0,21,41,0.5);border-radius:12px;padding:16px;
|
||
border:1px solid rgba(0,228,255,0.3);
|
||
">
|
||
<div style="color:#7ecfff;font-size:12px;margin-bottom:8px;text-transform:uppercase;letter-spacing:1px;">采集时间</div>
|
||
<div style="color:#fff;font-size:14px;display:flex;align-items:center;gap:8px;">
|
||
<span>⏰</span><span>${get('timestamp')}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 关闭按钮 -->
|
||
<div style="display:flex;justify-content:flex-end;position:relative;z-index:2;">
|
||
<button onclick="removeCustomMapInfo()" style="
|
||
width:48px;height:48px;border-radius:50%;
|
||
background:rgba(0,228,255,0.1);border:2px solid rgba(0,228,255,0.3);
|
||
color:#00e4ff;cursor:pointer;display:flex;align-items:center;justify-content:center;
|
||
font-size:22px;font-weight:700;
|
||
transition:all 0.3s ease;
|
||
backdrop-filter:blur(15px);
|
||
" onmouseover="this.style.background='rgba(0,228,255,0.2)';this.style.transform='scale(1.15) rotate(90deg)';this.style.boxShadow='0 0 25px rgba(0,228,255,0.7)';"
|
||
onmouseout="this.style.background='rgba(0,228,255,0.1)';this.style.transform='scale(1) rotate(0deg)';this.style.boxShadow='none';">
|
||
✕
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
document.body.appendChild(div);
|
||
|
||
// 延迟重置防抖标志,给面板渲染留出时间
|
||
setTimeout(() => {
|
||
panelDisplaying = false;
|
||
console.log('地图面板渲染完成,重置显示状态');
|
||
}, 300);
|
||
|
||
// 获取位置信息
|
||
const longitude = get('longitude');
|
||
const latitude = get('latitude');
|
||
if(longitude && latitude){
|
||
reverseGeocode(longitude, latitude)
|
||
.then(address => {
|
||
const locationInfo = document.getElementById('locationInfo');
|
||
if(locationInfo){
|
||
locationInfo.textContent = address || '未知位置';
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('获取位置信息失败:', error);
|
||
const locationInfo = document.getElementById('locationInfo');
|
||
if(locationInfo){
|
||
locationInfo.textContent = '获取位置信息失败';
|
||
}
|
||
});
|
||
}
|
||
}
|
||
}
|
||
function showHealthProfile(healthId){
|
||
// 检查是否已存在健康详情面板,避免重复显示
|
||
const existingHealthModal = document.querySelector('.health-modal-overlay');
|
||
if(existingHealthModal) {
|
||
console.log('健康详情面板已存在,跳过重复调用');
|
||
return;
|
||
}
|
||
|
||
panelDisplaying = true; // 标记面板正在显示
|
||
|
||
apiCall(`/fetchHealthDataById?id=${healthId}`).then(r=>r.json()).then(j=>{
|
||
if(!j.success||!j.data)return showCustomAlert('无健康数据');
|
||
const d=j.data,g=(...k)=>k.map(x=>d[x]).find(x=>x!==undefined&&x!==null&&x!=='')||'-';
|
||
let sleepStr=g('sleepData','scientificSleepData'),sleep='-';
|
||
try{
|
||
if(typeof sleepStr==='string'&&sleepStr.startsWith('{')){
|
||
const o=JSON.parse(sleepStr),arr=o.data||[];
|
||
if(arr.length){
|
||
const s=arr[0],fmt=m=>m?`${Math.floor(m/60)}h${m%60}m`:'-';
|
||
sleep=`总:${fmt(s.total)} 深:${fmt(s.deep)} 浅:${fmt(s.light)} 醒:${fmt(s.awake)}`;
|
||
}
|
||
}
|
||
}catch(e){sleep='-';}
|
||
|
||
// 健康数据评分计算
|
||
const heartRate = parseInt(g('heartRate','heart_rate')) || 0;
|
||
const bloodOxygen = parseInt(g('bloodOxygen','blood_oxygen')) || 0;
|
||
const temperature = parseFloat(g('temperature','temp')) || 0;
|
||
const steps = parseInt(g('step','steps')) || 0;
|
||
|
||
const getHealthScore = () => {
|
||
let score = 0;
|
||
if(heartRate >= 60 && heartRate <= 100) score += 25;
|
||
if(bloodOxygen >= 95) score += 25;
|
||
if(temperature >= 36.1 && temperature <= 37.2) score += 25;
|
||
if(steps >= 8000) score += 25;
|
||
return Math.min(score, 100);
|
||
};
|
||
|
||
const healthScore = getHealthScore();
|
||
const getScoreColor = (score) => {
|
||
if(score >= 80) return '#00ff88';
|
||
if(score >= 60) return '#ffbb00';
|
||
return '#ff4444';
|
||
};
|
||
|
||
const html=`
|
||
<div class="health-profile-header">
|
||
<div class="health-icon">
|
||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none">
|
||
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" fill="url(#heartGradient)"/>
|
||
<defs>
|
||
<linearGradient id="heartGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||
<stop offset="0%" style="stop-color:#ff6b6b;stop-opacity:1" />
|
||
<stop offset="100%" style="stop-color:#ee5a24;stop-opacity:1" />
|
||
</linearGradient>
|
||
</defs>
|
||
</svg>
|
||
</div>
|
||
<div class="health-title">
|
||
<div class="title-main">智能健康监测</div>
|
||
<div class="title-sub">Health Profile Analysis</div>
|
||
</div>
|
||
<div class="health-score-circle">
|
||
<svg width="60" height="60" class="score-ring">
|
||
<circle cx="30" cy="30" r="25" stroke="rgba(0,228,255,0.2)" stroke-width="3" fill="none"/>
|
||
<circle cx="30" cy="30" r="25" stroke="${getScoreColor(healthScore)}" stroke-width="3" fill="none"
|
||
stroke-dasharray="${2 * Math.PI * 25}" stroke-dashoffset="${2 * Math.PI * 25 * (1 - healthScore/100)}"
|
||
transform="rotate(-90 30 30)" class="score-progress"/>
|
||
</svg>
|
||
<div class="score-text">${healthScore}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="health-metrics-grid">
|
||
<div class="metric-card vital">
|
||
<div class="metric-icon">
|
||
<svg width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||
<path stroke="currentColor" stroke-width="2" d="M12 8V4m0 4V2m0 6v4m0 0v6m0-6h8m-8 0H4"/>
|
||
</svg>
|
||
</div>
|
||
<div class="metric-info">
|
||
<div class="metric-label">心率监测</div>
|
||
<div class="metric-value ${heartRate>=60&&heartRate<=100?'normal':'abnormal'}">${g('heartRate','heart_rate')||'-'} <span class="unit">bpm</span></div>
|
||
<div class="metric-bar">
|
||
<div class="bar-fill heart-rate" style="width:${Math.min((heartRate/120)*100,100)}%"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="metric-card pressure">
|
||
<div class="metric-icon">
|
||
<svg width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
|
||
<path stroke="currentColor" stroke-width="2" d="M12 8v8m-4-4h8"/>
|
||
</svg>
|
||
</div>
|
||
<div class="metric-info">
|
||
<div class="metric-label">血压监测</div>
|
||
<div class="metric-value">${(g('pressureHigh','pressure_high')||'-')+'/'+(g('pressureLow','pressure_low')||'-')} <span class="unit">mmHg</span></div>
|
||
<div class="metric-status ${parseInt(g('pressureHigh'))<140&&parseInt(g('pressureLow'))<90?'normal':'warning'}">
|
||
${parseInt(g('pressureHigh'))<140&&parseInt(g('pressureLow'))<90?'正常范围':'需要关注'}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="metric-card oxygen">
|
||
<div class="metric-icon">
|
||
<svg width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||
<path stroke="currentColor" stroke-width="2" d="M12 2L2 12l10 10 10-10L12 2z"/>
|
||
<path stroke="currentColor" stroke-width="2" d="M12 8v8"/>
|
||
</svg>
|
||
</div>
|
||
<div class="metric-info">
|
||
<div class="metric-label">血氧饱和度</div>
|
||
<div class="metric-value ${bloodOxygen>=95?'normal':'abnormal'}">${g('bloodOxygen','blood_oxygen')||'-'} <span class="unit">%</span></div>
|
||
<div class="metric-bar">
|
||
<div class="bar-fill oxygen" style="width:${bloodOxygen}%"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="metric-card temperature">
|
||
<div class="metric-icon">
|
||
<svg width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||
<path stroke="currentColor" stroke-width="2" d="M14 3v7a3 3 0 1 1-6 0V3a3 3 0 1 1 6 0z"/>
|
||
<path stroke="currentColor" stroke-width="2" d="M11 14h2"/>
|
||
</svg>
|
||
</div>
|
||
<div class="metric-info">
|
||
<div class="metric-label">体温监测</div>
|
||
<div class="metric-value ${temperature>=36.1&&temperature<=37.2?'normal':'abnormal'}">${g('temperature','temp')||'-'} <span class="unit">℃</span></div>
|
||
<div class="temperature-indicator ${temperature>=36.1&&temperature<=37.2?'normal':'abnormal'}">
|
||
${temperature>=36.1&&temperature<=37.2?'体温正常':'异常体温'}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="metric-card activity">
|
||
<div class="metric-icon">
|
||
<svg width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||
<path stroke="currentColor" stroke-width="2" d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.29 1.51 4.04 3 5.5"/>
|
||
<path stroke="currentColor" stroke-width="2" d="M12 5L8 21h8l-4-16z"/>
|
||
</svg>
|
||
</div>
|
||
<div class="metric-info">
|
||
<div class="metric-label">运动数据</div>
|
||
<div class="activity-stats">
|
||
<div class="activity-item">
|
||
<span class="activity-num">${g('step','steps')||'-'}</span>
|
||
<span class="activity-label">步数</span>
|
||
</div>
|
||
<div class="activity-item">
|
||
<span class="activity-num">${g('distance','distance')||'-'}</span>
|
||
<span class="activity-label">米</span>
|
||
</div>
|
||
<div class="activity-item">
|
||
<span class="activity-num">${g('calorie','calories')||'-'}</span>
|
||
<span class="activity-label">卡路里</span>
|
||
</div>
|
||
</div>
|
||
<div class="progress-ring-container">
|
||
<div class="progress-ring">
|
||
<div class="ring-progress" style="--progress:${Math.min((steps/10000)*100,100)}%"></div>
|
||
<div class="ring-text">${Math.min(Math.round((steps/10000)*100),100)}%</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="metric-card stress">
|
||
<div class="metric-icon">
|
||
<svg width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||
<path stroke="currentColor" stroke-width="2" d="M12 2v20M2 12h20"/>
|
||
<circle cx="12" cy="12" r="8" stroke="currentColor" stroke-width="2"/>
|
||
</svg>
|
||
</div>
|
||
<div class="metric-info">
|
||
<div class="metric-label">压力指数</div>
|
||
<div class="metric-value">${g('stress','pressure')||'-'} <span class="unit">分</span></div>
|
||
<div class="stress-level ${parseInt(g('stress','pressure'))<=50?'low':parseInt(g('stress','pressure'))<=70?'medium':'high'}">
|
||
${parseInt(g('stress','pressure'))<=50?'压力较低':parseInt(g('stress','pressure'))<=70?'压力适中':'压力偏高'}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sleep-analysis">
|
||
<div class="sleep-header">
|
||
<div class="sleep-icon">
|
||
<svg width="20" height="20" fill="none" viewBox="0 0 24 24">
|
||
<path stroke="currentColor" stroke-width="2" d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
||
</svg>
|
||
</div>
|
||
<div class="sleep-title">睡眠分析</div>
|
||
</div>
|
||
<div class="sleep-content">${sleep}</div>
|
||
</div>
|
||
|
||
<div class="timestamp-info">
|
||
<div class="time-icon">⏰</div>
|
||
<div class="time-text">数据采集时间:${g('timestamp')||'-'}</div>
|
||
</div>
|
||
`;
|
||
|
||
const m=document.createElement('div');
|
||
m.className = 'health-modal-overlay';
|
||
m.innerHTML=`
|
||
<div class="health-modal-content">
|
||
<div class="modal-close" onclick="closeHealthModal(event)">
|
||
<svg width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||
<path stroke="currentColor" stroke-width="2" d="M18 6L6 18M6 6l12 12"/>
|
||
</svg>
|
||
</div>
|
||
${html}
|
||
</div>
|
||
`;
|
||
|
||
// 添加CSS样式
|
||
if(!document.querySelector('#health-profile-styles')){
|
||
const style = document.createElement('style');
|
||
style.id = 'health-profile-styles';
|
||
style.textContent = `
|
||
.health-modal-overlay {
|
||
position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
|
||
background: radial-gradient(ellipse at center, rgba(0,21,41,0.95) 0%, rgba(0,10,25,0.98) 100%);
|
||
z-index: 10000; display: flex; align-items: center; justify-content: center;
|
||
backdrop-filter: blur(8px); animation: fadeIn 0.3s ease-out;
|
||
}
|
||
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
||
|
||
.health-modal-content {
|
||
background: linear-gradient(135deg, rgba(10,24,48,0.95) 0%, rgba(15,35,65,0.98) 100%);
|
||
border-radius: 20px; box-shadow: 0 20px 60px rgba(0,228,255,0.3), inset 0 1px 0 rgba(255,255,255,0.1);
|
||
padding: 32px; min-width: 480px; max-width: 600px; color: #fff; position: relative;
|
||
border: 1px solid rgba(0,228,255,0.2); animation: slideUp 0.4s ease-out;
|
||
max-height: 90vh; overflow-y: auto;
|
||
}
|
||
@keyframes slideUp { from { transform: translateY(30px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
|
||
|
||
.modal-close {
|
||
position: absolute; right: 16px; top: 16px; cursor: pointer;
|
||
width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center;
|
||
background: rgba(0,228,255,0.1); border: 1px solid rgba(0,228,255,0.3);
|
||
color: #00e4ff; transition: all 0.3s ease;
|
||
}
|
||
.modal-close:hover { background: rgba(0,228,255,0.2); transform: scale(1.1); }
|
||
|
||
.health-profile-header {
|
||
display: flex; align-items: center; gap: 20px; margin-bottom: 32px;
|
||
padding: 20px; background: linear-gradient(90deg, rgba(0,228,255,0.05) 0%, rgba(0,180,255,0.1) 100%);
|
||
border-radius: 16px; border: 1px solid rgba(0,228,255,0.1);
|
||
}
|
||
|
||
.health-icon {
|
||
width: 64px; height: 64px; display: flex; align-items: center; justify-content: center;
|
||
background: radial-gradient(circle, rgba(255,107,107,0.2) 0%, rgba(238,90,36,0.3) 100%);
|
||
border-radius: 50%; animation: pulse 2s infinite;
|
||
}
|
||
@keyframes pulse { 0%, 100% { box-shadow: 0 0 0 0 rgba(255,107,107,0.7); } 50% { box-shadow: 0 0 0 20px rgba(255,107,107,0); } }
|
||
|
||
.health-title { flex: 1; }
|
||
.title-main { font-size: 24px; font-weight: 700; color: #00e4ff; margin-bottom: 4px; letter-spacing: 1px; }
|
||
.title-sub { font-size: 12px; color: #7ecfff; opacity: 0.8; text-transform: uppercase; letter-spacing: 2px; }
|
||
|
||
.health-score-circle {
|
||
position: relative; width: 60px; height: 60px;
|
||
}
|
||
.score-ring { width: 60px; height: 60px; }
|
||
.score-progress { transition: stroke-dashoffset 1s ease-out; }
|
||
.score-text {
|
||
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
||
font-size: 18px; font-weight: 700; color: #00ff88;
|
||
}
|
||
|
||
.health-metrics-grid {
|
||
display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||
gap: 20px; margin-bottom: 24px;
|
||
}
|
||
|
||
.metric-card {
|
||
background: linear-gradient(135deg, rgba(0,21,41,0.6) 0%, rgba(0,35,70,0.8) 100%);
|
||
border-radius: 16px; padding: 20px; border: 1px solid rgba(0,228,255,0.1);
|
||
transition: all 0.3s ease; position: relative; overflow: hidden;
|
||
}
|
||
.metric-card::before {
|
||
content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%;
|
||
background: linear-gradient(90deg, transparent, rgba(0,228,255,0.1), transparent);
|
||
transition: left 0.6s ease;
|
||
}
|
||
.metric-card:hover::before { left: 100%; }
|
||
.metric-card:hover { transform: translateY(-4px); box-shadow: 0 8px 32px rgba(0,228,255,0.2); }
|
||
|
||
.metric-card { display: flex; gap: 16px; align-items: flex-start; }
|
||
.metric-icon {
|
||
width: 48px; height: 48px; border-radius: 12px; display: flex; align-items: center; justify-content: center;
|
||
background: linear-gradient(135deg, rgba(0,228,255,0.2) 0%, rgba(0,180,255,0.3) 100%);
|
||
color: #00e4ff; flex-shrink: 0;
|
||
}
|
||
.metric-info { flex: 1; }
|
||
.metric-label { font-size: 12px; color: #7ecfff; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 1px; }
|
||
.metric-value { font-size: 20px; font-weight: 700; margin-bottom: 8px; }
|
||
.metric-value.normal { color: #00ff88; }
|
||
.metric-value.abnormal { color: #ff4444; }
|
||
.unit { font-size: 14px; color: #888; font-weight: 400; }
|
||
|
||
.metric-bar {
|
||
height: 4px; background: rgba(126,207,255,0.2); border-radius: 2px; overflow: hidden; margin-top: 8px;
|
||
}
|
||
.bar-fill {
|
||
height: 100%; border-radius: 2px; transition: width 1s ease-out;
|
||
}
|
||
.bar-fill.heart-rate { background: linear-gradient(90deg, #ff6b6b, #ee5a24); }
|
||
.bar-fill.oxygen { background: linear-gradient(90deg, #00ff88, #00cc66); }
|
||
|
||
.metric-status, .temperature-indicator, .stress-level {
|
||
font-size: 11px; padding: 4px 8px; border-radius: 12px; display: inline-block; margin-top: 4px;
|
||
}
|
||
.metric-status.normal, .temperature-indicator.normal, .stress-level.low {
|
||
background: rgba(0,255,136,0.2); color: #00ff88;
|
||
}
|
||
.metric-status.warning, .stress-level.medium {
|
||
background: rgba(255,187,0,0.2); color: #ffbb00;
|
||
}
|
||
.temperature-indicator.abnormal, .stress-level.high {
|
||
background: rgba(255,68,68,0.2); color: #ff4444;
|
||
}
|
||
|
||
.activity-stats {
|
||
display: flex; gap: 16px; margin-bottom: 12px;
|
||
}
|
||
.activity-item {
|
||
text-align: center;
|
||
}
|
||
.activity-num {
|
||
display: block; font-size: 16px; font-weight: 700; color: #00e4ff;
|
||
}
|
||
.activity-label {
|
||
font-size: 10px; color: #7ecfff; text-transform: uppercase;
|
||
}
|
||
|
||
.progress-ring-container {
|
||
display: flex; justify-content: center; margin-top: 12px;
|
||
}
|
||
.progress-ring {
|
||
position: relative; width: 40px; height: 40px;
|
||
}
|
||
.ring-progress {
|
||
width: 40px; height: 40px; border-radius: 50%;
|
||
background: conic-gradient(#00ff88 calc(var(--progress) * 1%), rgba(126,207,255,0.2) 0%);
|
||
display: flex; align-items: center; justify-content: center;
|
||
}
|
||
.ring-text {
|
||
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
||
font-size: 10px; font-weight: 700; color: #00ff88;
|
||
}
|
||
|
||
.sleep-analysis {
|
||
background: linear-gradient(135deg, rgba(0,21,41,0.6) 0%, rgba(15,25,45,0.8) 100%);
|
||
border-radius: 16px; padding: 20px; margin-bottom: 20px;
|
||
border: 1px solid rgba(0,228,255,0.1);
|
||
}
|
||
.sleep-header {
|
||
display: flex; align-items: center; gap: 12px; margin-bottom: 12px;
|
||
}
|
||
.sleep-icon {
|
||
width: 32px; height: 32px; border-radius: 8px; display: flex; align-items: center; justify-content: center;
|
||
background: linear-gradient(135deg, rgba(138,43,226,0.2) 0%, rgba(75,0,130,0.3) 100%);
|
||
color: #9d4edd;
|
||
}
|
||
.sleep-title {
|
||
font-size: 16px; font-weight: 600; color: #00e4ff;
|
||
}
|
||
.sleep-content {
|
||
font-size: 14px; color: #fff; font-family: 'SF Mono', 'Monaco', 'Inconsolata', monospace;
|
||
background: rgba(0,0,0,0.3); padding: 12px; border-radius: 8px;
|
||
}
|
||
|
||
.timestamp-info {
|
||
display: flex; align-items: center; gap: 12px; padding: 16px;
|
||
background: rgba(0,228,255,0.05); border-radius: 12px;
|
||
border: 1px solid rgba(0,228,255,0.1);
|
||
}
|
||
.time-icon { font-size: 16px; }
|
||
.time-text { font-size: 13px; color: #7ecfff; }
|
||
`;
|
||
document.head.appendChild(style);
|
||
}
|
||
|
||
document.body.appendChild(m);
|
||
|
||
// 添加事件处理,防止点击弹出框内容区域时事件冒泡
|
||
const healthContent = m.querySelector('.health-modal-content');
|
||
if(healthContent){
|
||
healthContent.addEventListener('click', function(e) {
|
||
e.stopPropagation();
|
||
});
|
||
}
|
||
|
||
// 点击遮罩层关闭健康详情面板,返回告警面板
|
||
m.addEventListener('click', function(e) {
|
||
if(e.target === m){
|
||
e.stopPropagation();
|
||
e.preventDefault();
|
||
closeHealthModal();
|
||
}
|
||
});
|
||
|
||
// 延迟重置防抖标志,给健康面板渲染留出时间
|
||
setTimeout(() => {
|
||
panelDisplaying = false;
|
||
console.log('健康面板渲染完成,重置显示状态');
|
||
}, 300);
|
||
}).catch(()=>{
|
||
showCustomAlert('获取健康数据失败');
|
||
// 获取数据失败时也要重置状态
|
||
panelDisplaying = false;
|
||
});
|
||
}
|
||
|
||
function removeCustomMapInfo(){
|
||
// 移除地图信息面板
|
||
const mapInfo=document.querySelector('.custom-map-info');
|
||
if(mapInfo)mapInfo.remove();
|
||
|
||
// 移除健康详情面板
|
||
const healthModal=document.querySelector('.health-modal-overlay');
|
||
if(healthModal)healthModal.remove();
|
||
|
||
// 重置面板显示状态,允许新的面板显示
|
||
panelDisplaying = false;
|
||
}
|
||
|
||
function closeHealthModal(event){
|
||
// 阻止事件冒泡,防止触发地图点击事件
|
||
if(event && event.stopPropagation){
|
||
event.stopPropagation();
|
||
event.preventDefault();
|
||
}
|
||
|
||
// 只移除健康详情面板,不影响告警弹出框
|
||
const healthModal=document.querySelector('.health-modal-overlay');
|
||
if(healthModal)healthModal.remove();
|
||
|
||
// 重置面板显示状态,允许新的面板显示
|
||
panelDisplaying = false;
|
||
}
|
||
|
||
function filterData(data){
|
||
//console.log('filterData.data',currentDept,currentUser);
|
||
|
||
//console.log('filterData.data.health_count || 0',data.health_count || 0);
|
||
const toStr=x=>x===undefined||x===null?'':String(x);
|
||
const dept=toStr(currentDept),user=toStr(currentUser);
|
||
const alerts=(data.alert_info?.alerts||[]).filter(a=>
|
||
(!dept||[a.dept_id,a.deptId].some(v=>toStr(v)===dept))&&
|
||
(!user||[a.user_id,a.userId].some(v=>toStr(v)===user))&&
|
||
(['pending','1'].includes(toStr(a.alert_status||a.status)))
|
||
);
|
||
const healths=(data.health_data?.healthData||[]).filter(h=>
|
||
(!dept||[h.dept_id,h.deptId].some(v=>toStr(v)===dept))&&
|
||
(!user||[h.user_id,h.userId].some(v=>toStr(v)===user))
|
||
);
|
||
//console.log('filterData.alerts',alerts);
|
||
//console.log('filterData.healths',healths);
|
||
return {alerts,healths};
|
||
}
|
||
window.updateMapData = async function(data) {
|
||
console.log('updateMapData.data', data);
|
||
// 增强地图初始化检查
|
||
if (!data) {
|
||
console.warn('❌ 数据为空,无法更新地图');
|
||
return;
|
||
}
|
||
|
||
// 检查地图实例是否存在,如果不存在则重新初始化
|
||
if (!map || !loca) {
|
||
console.warn('🗺️ 地图未初始化,正在重新初始化...');
|
||
try {
|
||
// 重新初始化地图
|
||
// 从URL获取userId参数,如果没有则不传递
|
||
const userId = urlParams.get('userId') || null;
|
||
initializeMap(GLOBAL_CUSTOMER_ID, userId);
|
||
|
||
// 等待地图初始化完成再更新数据
|
||
setTimeout(() => {
|
||
window.updateMapData(data);
|
||
}, 1000);
|
||
return;
|
||
} catch (error) {
|
||
console.error('❌ 地图重新初始化失败:', error);
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 检查地图图层是否存在
|
||
if (!breathRed || !breathYellow || !breathGreen || !breathOrange) {
|
||
console.warn('🔧 地图图层未完全初始化,等待图层创建...');
|
||
setTimeout(() => {
|
||
window.updateMapData(data);
|
||
}, 500);
|
||
return;
|
||
}
|
||
|
||
const {alerts, healths} = filterData(data);
|
||
|
||
function isValidCoordinate(lng, lat) {
|
||
try {
|
||
const longitude = parseFloat(lng);
|
||
const latitude = parseFloat(lat);
|
||
return !isNaN(longitude) && !isNaN(latitude) &&
|
||
isFinite(longitude) && isFinite(latitude) &&
|
||
longitude >= -180 && longitude <= 180 &&
|
||
latitude >= -90 && latitude <= 90 &&
|
||
longitude !== 0 && latitude !== 0;
|
||
} catch (e) {
|
||
console.warn('坐标验证错误:', e);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
const validHealths = healths.filter(h => {
|
||
const isValid = isValidCoordinate(h.longitude, h.latitude);
|
||
if (!isValid) {
|
||
console.log('无效健康点坐标被过滤:', h.userName, h.longitude, h.latitude);
|
||
}
|
||
return isValid;
|
||
});
|
||
|
||
// 获取健康评分 - 以人为本,使用customerId查询
|
||
let healthScores = {};
|
||
|
||
// 使用customerId获取所有用户的健康评分
|
||
if (GLOBAL_CUSTOMER_ID) {
|
||
try {
|
||
const scoreResponse = await apiCall(`/api/v2/health/scores/comprehensive?customerId=${GLOBAL_CUSTOMER_ID}&includeDeviceBreakdown=true`);
|
||
|
||
if (scoreResponse.ok) {
|
||
const scoreData = await scoreResponse.json();
|
||
if (scoreData.success) {
|
||
healthScores = scoreData.data;
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.warn('获取健康评分失败,使用默认评分:', error);
|
||
}
|
||
}
|
||
|
||
const healthFeaturesWithScore = validHealths.map(h => {
|
||
// 使用预计算的健康评分,如果没有则使用默认值
|
||
const scoreData = healthScores[h.deviceSn];
|
||
const healthScore = scoreData ? scoreData.health_score : 75; // 默认评分75
|
||
const healthColor = scoreData ? scoreData.color : '#faad14'; // 默认黄色
|
||
|
||
return {
|
||
type: 'Feature',
|
||
geometry: {type: 'Point', coordinates: [parseFloat(h.longitude), parseFloat(h.latitude)]},
|
||
properties: {
|
||
...h,
|
||
dept_name: h.deptName || '未知部门',
|
||
user_name: h.userName || '未知员工',
|
||
label: `${h.deptName || '未知部门'}-${h.userName || '未知员工'}`,
|
||
type: 'health',
|
||
health_score: healthScore,
|
||
health_color: healthColor,
|
||
score_source: scoreData ? 'backend' : 'default'
|
||
}
|
||
};
|
||
});
|
||
|
||
// 根据健康评分分组健康点
|
||
const healthyPoints = healthFeaturesWithScore.filter(f => f.properties.health_score >= 80); // 绿色 - 健康
|
||
const attentionPoints = healthFeaturesWithScore.filter(f => f.properties.health_score >= 60 && f.properties.health_score < 80); // 黄色 - 注意
|
||
const mildAbnormalPoints = healthFeaturesWithScore.filter(f => f.properties.health_score >= 40 && f.properties.health_score < 60); // 橙色 - 轻微异常
|
||
const severeAbnormalPoints = healthFeaturesWithScore.filter(f => f.properties.health_score < 40); // 红色 - 严重异常
|
||
|
||
console.log(`健康点颜色分布: 绿色${healthyPoints.length}个, 黄色${attentionPoints.length}个, 橙色${mildAbnormalPoints.length}个, 红色${severeAbnormalPoints.length}个`);
|
||
|
||
const validAlerts = alerts.filter(a => isValidCoordinate(a.longitude, a.latitude));
|
||
|
||
console.log('updateMapData.validAlerts',alerts);
|
||
|
||
const alertFeatures = validAlerts.map(a => ({
|
||
type: 'Feature',
|
||
geometry: {type: 'Point', coordinates: [parseFloat(a.longitude), parseFloat(a.latitude)]},
|
||
properties: {
|
||
...a,
|
||
label: `⚠️${a.dept_name || '未知部门'}-${a.user_name || '未知员工'}`,
|
||
type: 'alert'
|
||
}
|
||
}));
|
||
|
||
const criticalAlerts = {type: 'FeatureCollection', features: alertFeatures.filter(f => f.properties.severity_level === 'critical')};
|
||
const highAlerts = {type: 'FeatureCollection', features: alertFeatures.filter(f => f.properties.severity_level === 'high' || f.properties.severity_level === 'medium')};
|
||
|
||
// 创建不同健康状态的 FeatureCollection
|
||
const healthyData = {type: 'FeatureCollection', features: healthyPoints}; // 绿色 - 健康状态良好
|
||
const attentionData = {type: 'FeatureCollection', features: attentionPoints}; // 黄色 - 健康状态需要注意
|
||
const mildAbnormalData = {type: 'FeatureCollection', features: mildAbnormalPoints}; // 橙色 - 轻微异常
|
||
const severeAbnormalData = {type: 'FeatureCollection', features: severeAbnormalPoints}; // 红色 - 严重异常
|
||
|
||
console.log(`处理数据: 总计${healthFeaturesWithScore.length}个健康点(绿色${healthyPoints.length}, 黄色${attentionPoints.length}, 橙色${mildAbnormalPoints.length}, 红色${severeAbnormalPoints.length}), ${alertFeatures.length}个告警点`);
|
||
|
||
try {
|
||
// 更新告警图层 - criticalAlerts 使用红色图层
|
||
if (breathRed && typeof breathRed.setSource === 'function') {
|
||
// 合并严重告警和严重异常健康点到红色图层
|
||
const redData = {
|
||
type: 'FeatureCollection',
|
||
features: [...criticalAlerts.features, ...severeAbnormalData.features]
|
||
};
|
||
breathRed.setSource(new Loca.GeoJSONSource({data: redData}));
|
||
}
|
||
|
||
// 更新黄色图层 - 高级告警和需要注意的健康点
|
||
if (breathYellow && typeof breathYellow.setSource === 'function') {
|
||
// 合并高级告警和需要注意的健康点到黄色图层
|
||
const yellowData = {
|
||
type: 'FeatureCollection',
|
||
features: [...highAlerts.features, ...attentionData.features]
|
||
};
|
||
breathYellow.setSource(new Loca.GeoJSONSource({data: yellowData}));
|
||
}
|
||
|
||
// 更新橙色图层 - 轻微异常健康点
|
||
if (breathOrange && typeof breathOrange.setSource === 'function') {
|
||
breathOrange.setSource(new Loca.GeoJSONSource({data: mildAbnormalData}));
|
||
}
|
||
|
||
// 更新绿色图层 - 健康状态良好的健康点
|
||
if (breathGreen && typeof breathGreen.setSource === 'function') {
|
||
breathGreen.setSource(new Loca.GeoJSONSource({data: healthyData}));
|
||
}
|
||
|
||
const allValidFeatures = [...alertFeatures, ...healthFeaturesWithScore];
|
||
if (allValidFeatures.length > 0) {
|
||
const firstFeature = allValidFeatures[0];
|
||
const [lng, lat] = firstFeature.geometry.coordinates;
|
||
if (map && typeof map.setCenter === 'function') {
|
||
map.setCenter([lng, lat]);
|
||
console.log(`地图中心设置为: [${lng}, ${lat}]`);
|
||
}
|
||
} else if (map && typeof map.setCenter === 'function') {
|
||
map.setCenter([114.01508952, 22.54036796]);
|
||
}
|
||
|
||
// 重新启动动画
|
||
if (loca && loca.animate && typeof loca.animate.start === 'function') {
|
||
loca.animate.start();
|
||
}
|
||
|
||
console.log('✅ 地图更新完成 - 健康状态颜色功能已启用');
|
||
} catch (error) {
|
||
console.error('❌ 地图更新失败:', error);
|
||
}
|
||
};
|
||
|
||
// 地图跳转到指定用户位置的函数
|
||
window.jumpToUserLocation = function(userId, data) {
|
||
console.log('jumpToUserLocation - userId:', userId, 'data:', data);
|
||
|
||
if (!userId || !data || !map) {
|
||
console.warn('❌ 跳转用户位置失败 - 缺少必要参数');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 从数据中找到指定用户的健康数据
|
||
const healths = data.health_data?.healthData || [];
|
||
const userHealth = healths.find(h =>
|
||
String(h.user_id || h.userId) === String(userId) ||
|
||
String(h.id) === String(userId)
|
||
);
|
||
|
||
console.log('找到用户健康数据:', userHealth);
|
||
|
||
if (userHealth && userHealth.longitude && userHealth.latitude) {
|
||
const lng = parseFloat(userHealth.longitude);
|
||
const lat = parseFloat(userHealth.latitude);
|
||
|
||
// 验证坐标有效性
|
||
if (!isNaN(lng) && !isNaN(lat) &&
|
||
lng >= -180 && lng <= 180 &&
|
||
lat >= -90 && lat <= 90 &&
|
||
lng !== 0 && lat !== 0) {
|
||
|
||
console.log(`🎯 地图跳转到用户位置: ${userHealth.userName || '未知用户'} (${lng}, ${lat})`);
|
||
|
||
// 地图飞行到用户位置,保持当前缩放级别不变
|
||
map.setCenter([lng, lat]);
|
||
// 不改变缩放级别,只改变中心点
|
||
|
||
// 可选:添加跳转动画效果
|
||
setTimeout(() => {
|
||
// 可以添加一些视觉提示,比如闪烁效果
|
||
console.log(`✅ 已跳转到 ${userHealth.userName || '未知用户'} 的位置`);
|
||
}, 500);
|
||
|
||
} else {
|
||
console.warn('❌ 用户坐标无效:', lng, lat);
|
||
}
|
||
} else {
|
||
console.warn('❌ 未找到用户位置数据:', userId);
|
||
|
||
// 如果没找到健康数据,尝试从告警数据中查找
|
||
const alerts = data.alert_info?.alerts || [];
|
||
const userAlert = alerts.find(a =>
|
||
String(a.user_id || a.userId) === String(userId) ||
|
||
String(a.id) === String(userId)
|
||
);
|
||
|
||
if (userAlert && userAlert.longitude && userAlert.latitude) {
|
||
const lng = parseFloat(userAlert.longitude);
|
||
const lat = parseFloat(userAlert.latitude);
|
||
|
||
if (!isNaN(lng) && !isNaN(lat) &&
|
||
lng >= -180 && lng <= 180 &&
|
||
lat >= -90 && lat <= 90 &&
|
||
lng !== 0 && lat !== 0) {
|
||
|
||
console.log(`🚨 从告警数据跳转到用户位置: ${userAlert.user_name || '未知用户'} (${lng}, ${lat})`);
|
||
map.setCenter([lng, lat]);
|
||
// 保持当前缩放级别不变
|
||
} else {
|
||
console.warn('❌ 告警数据中用户坐标也无效:', lng, lat);
|
||
}
|
||
} else {
|
||
console.warn('❌ 在健康数据和告警数据中都未找到用户位置');
|
||
}
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('❌ 跳转用户位置出错:', error);
|
||
}
|
||
};
|
||
|
||
window.updateMapData1 = function(data){
|
||
//console.log('updateMapData.data',data);
|
||
if(!data||!map||!loca)return;
|
||
const {alerts,healths}=filterData(data);
|
||
console.log('updateMapData1.alerts',alerts);
|
||
const f=[];
|
||
// 处理告警数据
|
||
alerts.forEach(a=>{
|
||
if((a.longitude||a.longitude===0)&&(a.latitude||a.latitude===0)){
|
||
f.push({
|
||
type:'Feature',
|
||
geometry:{type:'Point',coordinates:[+a.longitude,+a.latitude]},
|
||
properties:{
|
||
...a,
|
||
alert_id: a.alert_id,
|
||
alert_type: a.alert_type,
|
||
alert_status: a.alert_status,
|
||
severity_level: a.severity_level,
|
||
dept_name: a.dept_name,
|
||
user_name: a.user_name,
|
||
health_id: a.health_id,
|
||
device_sn: a.device_sn,
|
||
alert_timestamp: a.alert_timestamp,
|
||
type: 'alert'
|
||
}
|
||
});
|
||
}
|
||
});
|
||
// 处理健康数据
|
||
healths.forEach(h=>{
|
||
if((h.longitude||h.longitude===0)&&(h.latitude||h.latitude===0)){
|
||
f.push({
|
||
type:'Feature',
|
||
geometry:{type:'Point',coordinates:[+h.longitude,+h.latitude]},
|
||
properties:{
|
||
...h,
|
||
dept_name: h.deptName,
|
||
user_name: h.userName,
|
||
heart_rate: h.heartRate,
|
||
blood_oxygen: h.bloodOxygen,
|
||
temperature: h.temperature,
|
||
pressure_high: h.pressureHigh,
|
||
pressure_low: h.pressureLow,
|
||
step: h.step,
|
||
stress: h.stress,
|
||
device_sn: h.deviceSn,
|
||
timestamp: h.timestamp,
|
||
avatar: h.avatar,
|
||
sleepData: h.sleepData,
|
||
timestamp: h.timestamp,
|
||
type: 'health'
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
|
||
const geoJSON={type:'FeatureCollection',features:f};
|
||
const criticalAlerts={type:'FeatureCollection',features:geoJSON.features.filter(f=>f.properties.severity_level==='critical')};
|
||
const highAlerts={type:'FeatureCollection',features:geoJSON.features.filter(f=>f.properties.severity_level==='high'||f.properties.severity_level==='medium')};
|
||
const healthData={type:'FeatureCollection',features:geoJSON.features.filter(f=>f.properties.type==='health')};
|
||
|
||
breathRed.setSource(new Loca.GeoJSONSource({data:criticalAlerts}));
|
||
breathYellow.setSource(new Loca.GeoJSONSource({data:highAlerts}));
|
||
breathGreen.setSource(new Loca.GeoJSONSource({data:healthData}));
|
||
|
||
// 设置地图中心
|
||
let center=null;
|
||
const getCoord=f=>f&&f.geometry&&f.geometry.coordinates;
|
||
const findFirst=(arr,cond)=>{for(let i=0;i<arr.length;i++)if(cond(arr[i]))return arr[i];return null;};
|
||
const cAlert=findFirst(geoJSON.features,f=>f.properties.type==='alert'&&['critical','high','medium'].includes(f.properties.severity_level));
|
||
const cHealth=findFirst(geoJSON.features,f=>f.properties.type==='health');
|
||
if(cAlert)center=getCoord(cAlert);
|
||
else if(cHealth)center=getCoord(cHealth);
|
||
if(center&¢er.length>=2)map.setCenter([center[0],center[1]]);
|
||
else if(navigator.geolocation){
|
||
navigator.geolocation.getCurrentPosition(pos=>{
|
||
map.setCenter([pos.coords.longitude,pos.coords.latitude]);
|
||
});
|
||
}
|
||
}
|
||
|
||
// 健康数据统计计算函数
|
||
function calculateHealthStats(metrics) {
|
||
const stats = {
|
||
healthScore: 0,
|
||
normalCount: 0,
|
||
riskCount: 0,
|
||
avgHeartRate: 0,
|
||
avgBloodOxygen: 0,
|
||
avgTemperature: 0,
|
||
avgSteps: 0
|
||
};
|
||
|
||
if (!metrics || metrics.length === 0) return stats;
|
||
|
||
let totalScore = 0, validMetrics = 0;
|
||
|
||
metrics.forEach(metric => {
|
||
const values = metric.values.filter(v => v !== null && v !== undefined);
|
||
if (values.length === 0) return;
|
||
|
||
const avg = values.reduce((a, b) => a + b, 0) / values.length;
|
||
|
||
switch(metric.name) {
|
||
case '心率':
|
||
stats.avgHeartRate = Math.round(avg);
|
||
const heartScore = avg >= 60 && avg <= 100 ? 85 : (avg < 60 ? 70 : 60); // 正常范围评分
|
||
totalScore += heartScore;
|
||
if (heartScore >= 80) stats.normalCount++; else stats.riskCount++;
|
||
validMetrics++;
|
||
break;
|
||
case '血氧':
|
||
stats.avgBloodOxygen = Math.round(avg);
|
||
const oxygenScore = avg >= 95 ? 90 : (avg >= 90 ? 75 : 50);
|
||
totalScore += oxygenScore;
|
||
if (oxygenScore >= 80) stats.normalCount++; else stats.riskCount++;
|
||
validMetrics++;
|
||
break;
|
||
case '体温':
|
||
stats.avgTemperature = avg.toFixed(1);
|
||
const tempScore = avg >= 36.1 && avg <= 37.2 ? 88 : 65;
|
||
totalScore += tempScore;
|
||
if (tempScore >= 80) stats.normalCount++; else stats.riskCount++;
|
||
validMetrics++;
|
||
break;
|
||
case '步数':
|
||
stats.avgSteps = Math.round(avg);
|
||
const stepScore = avg >= 8000 ? 85 : (avg >= 5000 ? 70 : 55);
|
||
totalScore += stepScore;
|
||
if (stepScore >= 80) stats.normalCount++; else stats.riskCount++;
|
||
validMetrics++;
|
||
break;
|
||
}
|
||
});
|
||
|
||
stats.healthScore = validMetrics > 0 ? Math.round(totalScore / validMetrics) : 0;
|
||
return stats;
|
||
}
|
||
|
||
// 更新健康统计UI
|
||
function updateHealthStats(stats) {
|
||
try {
|
||
document.getElementById('healthScore').textContent = stats.healthScore;
|
||
document.getElementById('normalCount').textContent = stats.normalCount;
|
||
document.getElementById('riskCount').textContent = stats.riskCount;
|
||
document.getElementById('avgHeartRate').textContent = stats.avgHeartRate;
|
||
document.getElementById('avgBloodOxygen').textContent = stats.avgBloodOxygen + '%';
|
||
document.getElementById('avgTemperature').textContent = stats.avgTemperature + '°C';
|
||
document.getElementById('avgSteps').textContent = stats.avgSteps;
|
||
} catch (e) {
|
||
console.warn('更新健康统计UI失败:', e);
|
||
}
|
||
}
|
||
|
||
// 计算雷达图数据
|
||
function calculateRadarData(metrics) {
|
||
const indicators = [];
|
||
const values = [];
|
||
|
||
const metricMap = {
|
||
'心率': { max: 100, unit: 'bpm' },
|
||
'血氧': { max: 100, unit: '%' },
|
||
'体温': { max: 100, unit: '°C' },
|
||
'步数': { max: 100, unit: '步' },
|
||
'压力': { max: 100, unit: '级' },
|
||
'睡眠': { max: 100, unit: '小时' },
|
||
'卡路里': { max: 100, unit: 'kcal' },
|
||
'血压': { max: 100, unit: 'mmHg' }
|
||
};
|
||
|
||
metrics.forEach(metric => {
|
||
const config = metricMap[metric.name];
|
||
if (!config) return;
|
||
|
||
const validValues = metric.values.filter(v => v !== null && v !== undefined);
|
||
if (validValues.length === 0) return;
|
||
|
||
const avg = validValues.reduce((a, b) => a + b, 0) / validValues.length;
|
||
|
||
// 根据指标类型计算健康评分
|
||
let score = 0;
|
||
switch(metric.name) {
|
||
case '心率':
|
||
score = avg >= 60 && avg <= 100 ? 85 : (avg < 60 ? 70 : 60);
|
||
break;
|
||
case '血氧':
|
||
score = avg >= 95 ? 90 : (avg >= 90 ? 75 : 50);
|
||
break;
|
||
case '体温':
|
||
score = avg >= 36.1 && avg <= 37.2 ? 88 : 65;
|
||
break;
|
||
case '步数':
|
||
score = avg >= 8000 ? 85 : (avg >= 5000 ? 70 : 55);
|
||
break;
|
||
case '压力':
|
||
score = avg <= 3 ? 85 : (avg <= 5 ? 70 : 50);
|
||
break;
|
||
case '睡眠':
|
||
score = avg >= 7 ? 85 : (avg >= 6 ? 70 : 55);
|
||
break;
|
||
default:
|
||
score = Math.min(90, Math.max(50, 100 - (avg * 0.5))); // 默认计算
|
||
}
|
||
|
||
indicators.push({
|
||
name: metric.name,
|
||
max: 100,
|
||
min: 0
|
||
});
|
||
|
||
values.push(Math.round(score));
|
||
});
|
||
|
||
return { indicators, values };
|
||
}
|
||
|
||
|
||
function optimizeMapDisplay() {
|
||
// 根据缩放级别动态调整显示策略
|
||
map.on('zoomend', () => {
|
||
const zoom = map.getZoom();
|
||
|
||
if (zoom < 12) {
|
||
// 低缩放级别:隐藏文本,显示聚合点
|
||
textLayer.hide();
|
||
alertTextLayer.hide();
|
||
enableClustering();
|
||
} else if (zoom < 15) {
|
||
// 中等缩放级别:显示部分文本
|
||
textLayer.show();
|
||
alertTextLayer.show();
|
||
disableClustering();
|
||
setTextFilter(0.3); // 只显示30%的文本避免重叠
|
||
} else {
|
||
// 高缩放级别:显示所有标识
|
||
textLayer.show();
|
||
alertTextLayer.show();
|
||
disableClustering();
|
||
setTextFilter(1); // 显示所有文本
|
||
}
|
||
});
|
||
}
|
||
|
||
// 聚合显示实现
|
||
function enableClustering() {
|
||
// 使用Loca的聚合功能
|
||
const clusterLayer = new Loca.PointLayer({
|
||
loca,
|
||
zIndex: 115
|
||
});
|
||
|
||
clusterLayer.setSource(geo);
|
||
clusterLayer.setStyle({
|
||
unit: 'meter',
|
||
size: [20, 20],
|
||
color: (feature) => {
|
||
const count = feature.properties.count || 1;
|
||
return count > 10 ? '#ff4d4f' : count > 5 ? '#faad14' : '#52c41a';
|
||
},
|
||
text: {
|
||
field: 'count',
|
||
font: 'bold 12px Arial',
|
||
fillColor: '#fff'
|
||
}
|
||
});
|
||
|
||
loca.add(clusterLayer);
|
||
}
|
||
|
||
// 文本过滤显示
|
||
function setTextFilter(ratio) {
|
||
textLayer.setStyle({
|
||
filter: (feature, index) => {
|
||
return Math.random() < ratio; // 随机显示部分文本
|
||
}
|
||
});
|
||
}
|
||
|
||
// 交互功能函数
|
||
function showHealthDetails() {
|
||
const data = window.healthAnalysisData;
|
||
if (!data) {
|
||
alert('健康详情功能开发中...\n\n当前显示的是7天健康数据汇总');
|
||
return;
|
||
}
|
||
|
||
const { health_summary, risk_alerts } = data;
|
||
let message = `健康数据详情报告\n\n`;
|
||
message += `📊 综合评分: ${health_summary.overall_score}分\n`;
|
||
message += `✅ 正常指标: ${health_summary.normal_indicators}项\n`;
|
||
message += `⚠️ 风险指标: ${health_summary.risk_indicators}项\n`;
|
||
message += `👥 活跃用户: ${health_summary.active_users}/${health_summary.total_users}人\n\n`;
|
||
|
||
if (risk_alerts && risk_alerts.length > 0) {
|
||
message += `🚨 风险预警:\n`;
|
||
risk_alerts.slice(0, 3).forEach(alert => {
|
||
message += `• ${alert.message}\n`;
|
||
});
|
||
} else {
|
||
message += `✨ 暂无风险预警,整体健康状况良好`;
|
||
}
|
||
|
||
alert(message);
|
||
}
|
||
|
||
function filterByHeartRate() {
|
||
const data = window.healthAnalysisData;
|
||
if (!data) {
|
||
alert('心率筛选功能:显示心率异常的时间段和用户');
|
||
return;
|
||
}
|
||
|
||
const heartRateMetric = data.metrics.find(m => m.name === '心率');
|
||
if (heartRateMetric) {
|
||
const abnormalDays = heartRateMetric.daily_stats ?
|
||
heartRateMetric.daily_stats.filter(d => d.status === 'risk').length : 0;
|
||
alert(`心率分析结果\n\n平均心率: ${heartRateMetric.avg_value}bpm\n异常天数: ${abnormalDays}天\n正常范围: ${heartRateMetric.normal_range[0]}-${heartRateMetric.normal_range[1]}bpm`);
|
||
} else {
|
||
alert('暂无心率数据');
|
||
}
|
||
}
|
||
|
||
function filterByBloodOxygen() {
|
||
const data = window.healthAnalysisData;
|
||
if (!data) {
|
||
alert('血氧筛选功能:显示血氧偏低的时间段和用户');
|
||
return;
|
||
}
|
||
|
||
const oxygenMetric = data.metrics.find(m => m.name === '血氧');
|
||
if (oxygenMetric) {
|
||
const abnormalDays = oxygenMetric.daily_stats ?
|
||
oxygenMetric.daily_stats.filter(d => d.status === 'risk').length : 0;
|
||
alert(`血氧分析结果\n\n平均血氧: ${oxygenMetric.avg_value}%\n异常天数: ${abnormalDays}天\n正常范围: ${oxygenMetric.normal_range[0]}-${oxygenMetric.normal_range[1]}%`);
|
||
} else {
|
||
alert('暂无血氧数据');
|
||
}
|
||
}
|
||
|
||
function filterByTemperature() {
|
||
const data = window.healthAnalysisData;
|
||
if (!data) {
|
||
alert('体温筛选功能:显示体温异常的时间段和用户');
|
||
return;
|
||
}
|
||
|
||
const tempMetric = data.metrics.find(m => m.name === '体温');
|
||
if (tempMetric) {
|
||
const abnormalDays = tempMetric.daily_stats ?
|
||
tempMetric.daily_stats.filter(d => d.status === 'risk').length : 0;
|
||
alert(`体温分析结果\n\n平均体温: ${tempMetric.avg_value}°C\n异常天数: ${abnormalDays}天\n正常范围: ${tempMetric.normal_range[0]}-${tempMetric.normal_range[1]}°C`);
|
||
} else {
|
||
alert('暂无体温数据');
|
||
}
|
||
}
|
||
|
||
function filterBySteps() {
|
||
const data = window.healthAnalysisData;
|
||
if (!data) {
|
||
alert('步数筛选功能:显示运动量不足的用户');
|
||
return;
|
||
}
|
||
|
||
const stepsMetric = data.metrics.find(m => m.name === '步数');
|
||
if (stepsMetric) {
|
||
const lowActivityDays = stepsMetric.daily_stats ?
|
||
stepsMetric.daily_stats.filter(d => d.value && d.value < 5000).length : 0;
|
||
alert(`步数分析结果\n\n平均步数: ${stepsMetric.avg_value}步\n运动不足天数: ${lowActivityDays}天\n建议目标: ${stepsMetric.normal_range[0]}步以上`);
|
||
} else {
|
||
alert('暂无步数数据');
|
||
}
|
||
}
|
||
|
||
function showMetricDetails(metricName, value, date) {
|
||
const data = window.healthAnalysisData;
|
||
if (!data) {
|
||
alert(`指标详情\n\n指标: ${metricName}\n数值: ${value}\n日期: ${date}\n\n点击可查看该指标的详细分析和建议`);
|
||
return;
|
||
}
|
||
|
||
const metric = data.metrics.find(m => m.name === metricName);
|
||
if (metric) {
|
||
const dayData = metric.daily_stats ? metric.daily_stats.find(d => d.date === date) : null;
|
||
let message = `${metricName}详细信息\n\n`;
|
||
message += `📅 日期: ${date}\n`;
|
||
message += `📊 数值: ${value}${metric.unit}\n`;
|
||
message += `📈 7天平均: ${metric.avg_value}${metric.unit}\n`;
|
||
message += `📏 正常范围: ${metric.normal_range[0]}-${metric.normal_range[1]}${metric.unit}\n`;
|
||
|
||
if (dayData) {
|
||
message += `⭐ 健康评分: ${dayData.score}分\n`;
|
||
message += `🔍 状态: ${dayData.status === 'normal' ? '正常' : '需关注'}\n`;
|
||
}
|
||
|
||
message += `\n💡 建议: 保持规律监测,如有异常请及时就医`;
|
||
alert(message);
|
||
}
|
||
}
|
||
|
||
|
||
// 获取指标平均值的辅助函数
|
||
function getMetricAvg(metrics, metricName) {
|
||
const metric = metrics.find(m => m.name === metricName);
|
||
if (!metric || !metric.values) return 0;
|
||
|
||
const validValues = metric.values.filter(v => v !== null && v !== undefined);
|
||
return validValues.length > 0 ? Math.round(validValues.reduce((a,b) => a+b, 0) / validValues.length) : 0;
|
||
}
|
||
|
||
// 从后端metrics数据计算雷达图数据
|
||
function calculateRadarDataFromMetrics(metrics) {
|
||
const indicators = [];
|
||
const values = [];
|
||
|
||
// 选择主要健康指标
|
||
const mainMetrics = ['心率', '血氧', '体温', '步数', '压力', '收缩压', '睡眠'];
|
||
|
||
metrics.forEach(metric => {
|
||
if (mainMetrics.includes(metric.name) && metric.avg_value > 0) {
|
||
// 根据指标类型计算健康评分
|
||
let score = 0;
|
||
const avg = metric.avg_value;
|
||
|
||
// 睡眠数据特殊处理
|
||
if (metric.name === '睡眠') {
|
||
score = avg >= 7 ? 85 : (avg >= 6 ? 70 : 55);
|
||
} else {
|
||
const [min, max] = metric.normal_range;
|
||
|
||
if (avg >= min && avg <= max) {
|
||
score = 85 + (15 * (1 - Math.abs(avg - (min + max)/2) / ((max - min)/2)));
|
||
} else {
|
||
if (avg < min) {
|
||
score = Math.max(50, 85 - (min - avg) / min * 35);
|
||
} else {
|
||
score = Math.max(50, 85 - (avg - max) / max * 35);
|
||
}
|
||
}
|
||
}
|
||
|
||
indicators.push({
|
||
name: metric.name,
|
||
max: 100,
|
||
min: 0
|
||
});
|
||
|
||
values.push(Math.round(Math.min(100, Math.max(0, score))));
|
||
}
|
||
});
|
||
|
||
// 如果指标不足,添加默认指标
|
||
if (indicators.length < 4) {
|
||
const defaultIndicators = [
|
||
{ name: '心率', max: 100, min: 0 },
|
||
{ name: '血氧', max: 100, min: 0 },
|
||
{ name: '体温', max: 100, min: 0 },
|
||
{ name: '步数', max: 100, min: 0 }
|
||
];
|
||
const defaultValues = [75, 85, 80, 65];
|
||
|
||
for (let i = indicators.length; i < 4; i++) {
|
||
indicators.push(defaultIndicators[i]);
|
||
values.push(defaultValues[i]);
|
||
}
|
||
}
|
||
|
||
return { indicators, values };
|
||
}
|
||
|
||
function showHealthRadarDetails(radarData) {
|
||
const data = window.healthAnalysisData;
|
||
const avgScore = radarData.values.reduce((a,b)=>a+b,0) / radarData.values.length;
|
||
|
||
let message = `健康雷达详情\n\n`;
|
||
message += `🎯 综合评分: ${avgScore.toFixed(1)}分\n\n`;
|
||
message += `📋 各项指标评分:\n`;
|
||
radarData.indicators.forEach((ind,i) => {
|
||
const score = radarData.values[i];
|
||
const status = score >= 80 ? '✅' : score >= 60 ? '⚠️' : '❌';
|
||
message += `${status} ${ind.name}: ${score}分\n`;
|
||
});
|
||
|
||
if (data && data.risk_alerts && data.risk_alerts.length > 0) {
|
||
message += `\n🚨 需要关注:\n`;
|
||
data.risk_alerts.slice(0, 2).forEach(alert => {
|
||
message += `• ${alert.metric}: ${alert.current_value}\n`;
|
||
});
|
||
}
|
||
|
||
alert(message);
|
||
}
|
||
|
||
|
||
|
||
|
||
// 获取统计数据
|
||
function loadStatisticsData() {
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const customerId = urlParams.get('customerId') || '1';
|
||
//const today = new Date().toISOString().split('T')[0];
|
||
// 获取北京时间日期(UTC+8) - 修复时区问题
|
||
|
||
|
||
|
||
|
||
const today = new Date().toLocaleDateString('zh-CN', {
|
||
timeZone: 'Asia/Shanghai',
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit'
|
||
}).replace(/\//g, '-');
|
||
console.log("today", today);
|
||
|
||
// 设置当前日期(如果元素存在)
|
||
const statsDateElement = document.getElementById('statsDate');
|
||
if (statsDateElement) {
|
||
statsDateElement.textContent = today;
|
||
}
|
||
|
||
// 获取统计概览数据
|
||
apiCall(`/api/v2/statistics/overview?customerId=${GLOBAL_CUSTOMER_ID}&date=${today}`)
|
||
.then(response => response.json())
|
||
.then(result => {
|
||
if (result.success) {
|
||
const data = result.data;
|
||
console.log("statistics", data);
|
||
|
||
// 修复字段映射 - 使用API实际返回的字段名并添加过滤逻辑
|
||
document.getElementById('healthDataCount').textContent = formatNumber(data.health_count || 0);
|
||
|
||
// 过滤并统计真实数据
|
||
let pendingAlerts = data.alert_count || 0;
|
||
let activeDevices = data.active_devices || 0;
|
||
let unreadMessages = data.message_count || 0;
|
||
|
||
// 如果有lastTotalInfo缓存数据,使用过滤逻辑进行验证
|
||
if (typeof lastTotalInfo !== 'undefined' && lastTotalInfo) {
|
||
console.log('🔍 使用lastTotalInfo过滤验证:', lastTotalInfo);
|
||
|
||
// 过滤pending状态的告警
|
||
if (lastTotalInfo.alert_info && lastTotalInfo.alert_info.alerts) {
|
||
const filteredPendingAlerts = lastTotalInfo.alert_info.alerts.filter(
|
||
alert => alert.alert_status === 'pending' || alert.status === 'pending'
|
||
);
|
||
console.log(`📊 过滤pending告警: ${filteredPendingAlerts.length}/${lastTotalInfo.alert_info.alerts.length}`);
|
||
}
|
||
|
||
// 过滤message_status=1的消息
|
||
if (lastTotalInfo.message_info && lastTotalInfo.message_info.messages) {
|
||
const filteredUnreadMessages = lastTotalInfo.message_info.messages.filter(
|
||
msg => msg.message_status === 1 || msg.message_status === '1'
|
||
);
|
||
console.log(`📊 过滤未读消息: ${filteredUnreadMessages.length}/${lastTotalInfo.message_info.messages.length}`);
|
||
}
|
||
|
||
// 过滤status=ACTIVE的设备
|
||
if (lastTotalInfo.device_info && lastTotalInfo.device_info.devices) {
|
||
const filteredActiveDevices = lastTotalInfo.device_info.devices.filter(
|
||
device => device.status === 'ACTIVE'
|
||
);
|
||
console.log(`📊 过滤活跃设备: ${filteredActiveDevices.length}/${lastTotalInfo.device_info.devices.length}`);
|
||
}
|
||
}
|
||
|
||
// 更新前端显示
|
||
document.getElementById('pendingAlerts').textContent = formatNumber(pendingAlerts);
|
||
document.getElementById('activeDevices').textContent = activeDevices;
|
||
document.getElementById('unreadMessages').textContent = formatNumber(unreadMessages);
|
||
|
||
// 模拟系统状态数据(因为API未返回此数据)
|
||
const mockSummary = {
|
||
systemStatus: data.alert_count > 20 ? 'warning' : (data.alert_count > 10 ? 'normal' : 'normal'),
|
||
healthScore: Math.max(100 - Math.floor(data.alert_count * 2), 60) // 基于告警数量计算健康分数
|
||
};
|
||
updateSystemStatus(mockSummary);
|
||
|
||
// 使用实时统计API获取真实的增长率数据
|
||
apiCall(`/api/v2/statistics/realtime?date=${today}`)
|
||
.then(response => response.json())
|
||
.then(statsResult => {
|
||
if (statsResult.success) {
|
||
const statsData = statsResult.data;
|
||
const realTrends = {
|
||
changes: {
|
||
healthDataChange: statsData.health_data.growth,
|
||
alertsChange: statsData.pending_alerts.growth,
|
||
activeDevicesChange: statsData.active_devices.growth,
|
||
messagesChange: statsData.unread_messages.growth
|
||
}
|
||
};
|
||
updateTrends(realTrends);
|
||
console.log('✅ 使用真实增长率数据更新趋势');
|
||
} else {
|
||
// 降级到基于当前数据的计算趋势
|
||
const mockTrends = {
|
||
changes: {
|
||
healthDataChange: '+0%', // 无数据时为0%
|
||
alertsChange: data.alert_count > 15 ? '+12%' : '+3%', // 基于告警数量估算
|
||
activeDevicesChange: '+0%', // 无设备数据时为0%
|
||
messagesChange: data.message_count > 5 ? '+8%' : '+2%' // 基于消息数量估算
|
||
}
|
||
};
|
||
updateTrends(mockTrends);
|
||
console.log('⚠️ 降级使用估算增长率');
|
||
}
|
||
})
|
||
.catch(() => {
|
||
// API失败时的默认趋势
|
||
const defaultTrends = {
|
||
changes: {
|
||
healthDataChange: '+0%',
|
||
alertsChange: '+0%',
|
||
activeDevicesChange: '+0%',
|
||
messagesChange: '+0%'
|
||
}
|
||
};
|
||
updateTrends(defaultTrends);
|
||
console.log('❌ API失败,使用默认增长率');
|
||
});
|
||
|
||
// 添加数据更新动画
|
||
animateStatCards();
|
||
|
||
console.log('✅ 实时统计数据已更新:', {
|
||
health_count: data.health_count,
|
||
alert_count: data.alert_count,
|
||
active_devices: data.active_devices,
|
||
message_count: data.message_count
|
||
});
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('获取统计数据失败:', error);
|
||
// 显示错误状态
|
||
showErrorState();
|
||
});
|
||
|
||
// 使用新的实时统计API更新数据
|
||
updateRealtimeStats(customerId);
|
||
}
|
||
|
||
// formatNumber() 函数已移至 utils.js 模块
|
||
|
||
// 更新统计日期
|
||
function updateStatsDate() {
|
||
const now = new Date();
|
||
const dateStr = now.toLocaleDateString('zh-CN');
|
||
const statsDateElement = document.getElementById('statsDate');
|
||
if (statsDateElement) {
|
||
statsDateElement.textContent = dateStr;
|
||
}
|
||
}
|
||
|
||
// 更新实时统计数据的新函数 - 使用统一的统计概览API
|
||
function updateRealtimeStats(customerId) {
|
||
console.log('🔄 正在更新实时统计数据...');
|
||
|
||
// 获取选择的日期,如果没有则默认使用今天
|
||
const datePickerElement = document.getElementById('statsDatePicker');
|
||
let selectedDate = datePickerElement ? datePickerElement.value : '';
|
||
|
||
// 如果没有选择日期,默认使用今天
|
||
if (!selectedDate) {
|
||
const today = new Date();
|
||
selectedDate = today.toISOString().split('T')[0];
|
||
if (datePickerElement) {
|
||
datePickerElement.value = selectedDate;
|
||
}
|
||
}
|
||
|
||
// 构建统一的统计概览API URL
|
||
let apiUrl = `/api/v2/statistics/overview?customer_id=${customerId}&date=${selectedDate}`;
|
||
|
||
console.log('📡 调用统计概览API:', apiUrl);
|
||
|
||
fetch(apiUrl)
|
||
.then(response => response.json())
|
||
.then(result => {
|
||
if (result.success) {
|
||
const data = result.data;
|
||
console.log('📊 统计概览API返回数据:', data);
|
||
|
||
// 从API返回的实际数据结构中获取数据
|
||
const currentData = data.current || {};
|
||
const changesData = data.changes || {};
|
||
|
||
// 更新健康数据
|
||
document.getElementById('healthDataCount').textContent = currentData.health_count || 0;
|
||
document.getElementById('healthTrend').textContent = changesData.health_count || '0%';
|
||
updateTrendClass('healthTrend', changesData.health_count || '0%');
|
||
|
||
// 更新告警数据
|
||
document.getElementById('pendingAlerts').textContent = currentData.pending_alerts || currentData.alert_count || 0;
|
||
document.getElementById('alertTrend').textContent = changesData.alert_count || '0%';
|
||
updateTrendClass('alertTrend', changesData.alert_count || '0%');
|
||
|
||
// 更新设备数据
|
||
document.getElementById('activeDevices').textContent = currentData.active_devices || currentData.device_count || 0;
|
||
document.getElementById('deviceTrend').textContent = changesData.device_count || '0%';
|
||
updateTrendClass('deviceTrend', changesData.device_count || '0%');
|
||
|
||
// 更新消息数据
|
||
document.getElementById('unreadMessages').textContent = currentData.unread_messages || currentData.message_count || 0;
|
||
document.getElementById('messageTrend').textContent = changesData.message_count || '0%';
|
||
updateTrendClass('messageTrend', changesData.message_count || '0%');
|
||
|
||
// 更新对比信息
|
||
const compareDate = data.compare ? data.compare.date : selectedDate;
|
||
document.getElementById('compareInfo').textContent = `与前日对比 (${compareDate})`;
|
||
|
||
// 更新系统状态
|
||
const tenantInfo = data.tenant_info || {};
|
||
const systemStatus = tenantInfo.system_status || 'normal';
|
||
const healthScore = Math.max(85 - (currentData.alert_count || 0) * 2, 60); // 根据告警数计算健康分数
|
||
|
||
console.log(`✅ 统计数据更新: 健康${currentData.health_count || 0}, 告警${currentData.alert_count || 0}, 设备${currentData.device_count || 0}, 消息${currentData.message_count || 0}`);
|
||
console.log(`📅 对比信息: 与前日对比 (${compareDate}), 目标日期: ${selectedDate}`);
|
||
|
||
updateSystemStatus({
|
||
systemStatus: systemStatus,
|
||
healthScore: healthScore
|
||
});
|
||
|
||
// 更新租户信息头部数据
|
||
updateTenantInfoFromStats(data);
|
||
|
||
console.log('✅ 实时统计数据更新完成');
|
||
|
||
} else {
|
||
console.error('❌ 统计概览数据获取失败:', result.error);
|
||
console.log('🚫 统计概览API失败,显示错误状态');
|
||
showStatsErrorState();
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('❌ 统计概览API请求失败:', error);
|
||
console.log('🚫 统计概览API请求失败,显示错误状态');
|
||
showStatsErrorState();
|
||
});
|
||
}
|
||
|
||
// 从统计数据更新租户信息头部
|
||
function updateTenantInfoFromStats(statsData) {
|
||
try {
|
||
console.log('🏢 更新租户信息头部数据');
|
||
|
||
// 从API实际返回的数据结构中获取数据
|
||
const currentData = statsData.current || {};
|
||
const tenantInfo = statsData.tenant_info || {};
|
||
|
||
// 更新租户信息数据
|
||
if (tenantInfo) {
|
||
// 更新租户名称
|
||
const tenantNameElement = document.getElementById('tenantName');
|
||
if (tenantNameElement && tenantInfo.tenant_name) {
|
||
tenantNameElement.textContent = tenantInfo.tenant_name;
|
||
}
|
||
|
||
// 更新租户ID
|
||
const tenantIdElement = document.getElementById('tenantId');
|
||
if (tenantIdElement && tenantInfo.customer_id) {
|
||
tenantIdElement.textContent = tenantInfo.customer_id;
|
||
}
|
||
}
|
||
|
||
// 更新用户统计
|
||
const tenantUserCountElement = document.getElementById('tenantUserCount');
|
||
if (tenantUserCountElement) {
|
||
tenantUserCountElement.textContent = currentData.user_count || 0;
|
||
}
|
||
|
||
// 更新设备统计
|
||
const tenantDeviceCountElement = document.getElementById('tenantDeviceCount');
|
||
if (tenantDeviceCountElement) {
|
||
tenantDeviceCountElement.textContent = currentData.device_count || 0;
|
||
}
|
||
|
||
// 更新告警统计
|
||
const tenantAlertCountElement = document.getElementById('tenantAlertCount');
|
||
if (tenantAlertCountElement) {
|
||
tenantAlertCountElement.textContent = currentData.alert_count || 0;
|
||
}
|
||
|
||
// 更新消息统计
|
||
const tenantMessageCountElement = document.getElementById('tenantMessageCount');
|
||
if (tenantMessageCountElement) {
|
||
tenantMessageCountElement.textContent = currentData.message_count || 0;
|
||
}
|
||
|
||
// 更新健康数据统计
|
||
const tenantHealthDataCountElement = document.getElementById('tenantHealthDataCount');
|
||
if (tenantHealthDataCountElement) {
|
||
tenantHealthDataCountElement.textContent = currentData.health_count || 0;
|
||
}
|
||
|
||
// 更新组织统计(如果有的话)
|
||
const tenantOrgCountElement = document.getElementById('tenantOrgCount');
|
||
if (tenantOrgCountElement) {
|
||
tenantOrgCountElement.textContent = currentData.org_count || 0;
|
||
}
|
||
|
||
console.log('✅ 租户信息头部数据更新完成');
|
||
console.log(`📊 租户统计: 用户${currentData.user_count || 0}, 设备${currentData.device_count || 0}, 告警${currentData.alert_count || 0}, 消息${currentData.message_count || 0}, 健康数据${currentData.health_count || 0}`);
|
||
|
||
} catch (error) {
|
||
console.error('❌ 租户信息头部数据更新失败:', error);
|
||
}
|
||
}
|
||
|
||
// 显示实时统计错误状态
|
||
function showStatsErrorState() {
|
||
console.log('📊 显示统计数据错误状态');
|
||
|
||
// 显示错误状态,保持当前数据不变,或显示 '--'
|
||
document.getElementById('healthDataCount').textContent = '--';
|
||
document.getElementById('healthTrend').textContent = '--';
|
||
|
||
document.getElementById('pendingAlerts').textContent = '--';
|
||
document.getElementById('alertTrend').textContent = '--';
|
||
|
||
document.getElementById('activeDevices').textContent = '--';
|
||
document.getElementById('deviceTrend').textContent = '--';
|
||
|
||
document.getElementById('unreadMessages').textContent = '--';
|
||
document.getElementById('messageTrend').textContent = '--';
|
||
|
||
// 更新趋势样式为中性
|
||
['healthTrend', 'alertTrend', 'deviceTrend', 'messageTrend'].forEach(id => {
|
||
const element = document.getElementById(id);
|
||
if (element) {
|
||
element.className = ''; // 清除所有趋势样式类
|
||
}
|
||
});
|
||
}
|
||
|
||
|
||
// 更新趋势样式类
|
||
function updateTrendClass(elementId, trendValue) {
|
||
const element = document.getElementById(elementId);
|
||
if (!element) return;
|
||
|
||
element.className = 'stat-trend';
|
||
if (trendValue.startsWith('+') || (!trendValue.startsWith('-') && parseFloat(trendValue) > 0)) {
|
||
element.classList.add('positive');
|
||
} else if (trendValue.startsWith('-') || parseFloat(trendValue) < 0) {
|
||
element.classList.add('negative');
|
||
}
|
||
}
|
||
|
||
// 初始化实时统计日期选择器
|
||
function initStatsDatePicker() {
|
||
const datePickerElement = document.getElementById('statsDatePicker');
|
||
const compareInfoElement = document.getElementById('compareInfo');
|
||
|
||
if (!datePickerElement) {
|
||
console.warn('日期选择器元素未找到');
|
||
return;
|
||
}
|
||
|
||
// 设置默认日期为今天
|
||
const today = new Date();
|
||
const todayString = today.toISOString().split('T')[0];
|
||
datePickerElement.value = todayString;
|
||
|
||
console.log('📅 初始化日期选择器,默认日期:', todayString);
|
||
|
||
// 添加日期变化监听器
|
||
datePickerElement.addEventListener('change', function() {
|
||
const selectedDate = this.value;
|
||
console.log('📅 日期选择器变化:', selectedDate);
|
||
|
||
// 更新对比信息显示
|
||
if (selectedDate) {
|
||
const selected = new Date(selectedDate);
|
||
const todayDate = new Date();
|
||
|
||
if (selected.toDateString() === todayDate.toDateString()) {
|
||
compareInfoElement.textContent = '与昨日对比';
|
||
} else {
|
||
const yesterday = new Date(selected);
|
||
yesterday.setDate(yesterday.getDate() - 1);
|
||
const yesterdayString = yesterday.toLocaleDateString('zh-CN', {
|
||
month: '2-digit',
|
||
day: '2-digit'
|
||
});
|
||
compareInfoElement.textContent = `与${yesterdayString}对比`;
|
||
}
|
||
}
|
||
|
||
// 获取当前customerId并刷新统计数据
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const customerId = urlParams.get('customerId') || '1';
|
||
|
||
console.log('🔄 日期变化,刷新实时统计数据...');
|
||
updateRealtimeStats(customerId);
|
||
});
|
||
|
||
console.log('✅ 日期选择器初始化完成');
|
||
}
|
||
|
||
// 更新系统状态
|
||
function updateSystemStatus(summary) {
|
||
if (!summary) return;
|
||
|
||
const indicator = document.getElementById('statusIndicator');
|
||
const statusText = document.getElementById('statusText');
|
||
const healthScore = document.getElementById('systemHealthScore');
|
||
|
||
// 移除所有状态类
|
||
indicator.className = 'status-indicator';
|
||
|
||
// 根据系统状态设置样式和文本
|
||
switch (summary.systemStatus) {
|
||
case 'normal':
|
||
indicator.classList.add('normal');
|
||
statusText.textContent = '系统正常';
|
||
break;
|
||
case 'warning':
|
||
indicator.classList.add('warning');
|
||
statusText.textContent = '系统警告';
|
||
break;
|
||
case 'critical':
|
||
indicator.classList.add('critical');
|
||
statusText.textContent = '系统异常';
|
||
break;
|
||
}
|
||
|
||
// 更新健康评分
|
||
healthScore.textContent = summary.healthScore;
|
||
|
||
// 根据评分设置颜色
|
||
if (summary.healthScore >= 80) {
|
||
healthScore.style.color = '#00ff9d';
|
||
} else if (summary.healthScore >= 60) {
|
||
healthScore.style.color = '#ffbb00';
|
||
} else {
|
||
healthScore.style.color = '#ff6b6b';
|
||
}
|
||
}
|
||
|
||
// 更新趋势显示
|
||
// 更新趋势显示
|
||
function updateTrends(data) {
|
||
// 使用接口返回的真实变化数据
|
||
if (data.changes) {
|
||
const trends = {
|
||
health: data.changes.healthDataChange || '0%',
|
||
alert: data.changes.alertsChange || '0%',
|
||
device: data.changes.activeDevicesChange || '0%',
|
||
message: data.changes.messagesChange || '0%'
|
||
};
|
||
|
||
// 更新趋势显示
|
||
updateTrendElement('healthTrend', trends.health);
|
||
updateTrendElement('alertTrend', trends.alert);
|
||
updateTrendElement('deviceTrend', trends.device);
|
||
updateTrendElement('messageTrend', trends.message);
|
||
|
||
console.log('趋势数据已更新:', trends);
|
||
console.log('昨天对比数据:', data.yesterday);
|
||
} else {
|
||
// 兜底:如果没有changes数据,显示无数据状态
|
||
updateTrendElement('healthTrend', '0%');
|
||
updateTrendElement('alertTrend', '0%');
|
||
updateTrendElement('deviceTrend', '0%');
|
||
updateTrendElement('messageTrend', '0%');
|
||
|
||
console.warn('接口未返回changes数据,使用默认值');
|
||
}
|
||
}
|
||
|
||
// 更新单个趋势元素
|
||
function updateTrendElement(elementId, trend) {
|
||
const element = document.getElementById(elementId);
|
||
element.textContent = trend;
|
||
element.className = 'stat-trend';
|
||
|
||
if (trend.startsWith('-')) {
|
||
element.classList.add('negative');
|
||
}
|
||
}
|
||
|
||
// 统计卡片动画
|
||
function animateStatCards() {
|
||
const cards = document.querySelectorAll('.stat-card');
|
||
cards.forEach((card, index) => {
|
||
setTimeout(() => {
|
||
card.style.transform = 'scale(1.05)';
|
||
setTimeout(() => {
|
||
card.style.transform = 'scale(1)';
|
||
}, 200);
|
||
}, index * 100);
|
||
});
|
||
}
|
||
|
||
// 显示错误状态
|
||
function showErrorState() {
|
||
const statusText = document.getElementById('statusText');
|
||
const indicator = document.getElementById('statusIndicator');
|
||
|
||
statusText.textContent = '数据获取失败';
|
||
indicator.className = 'status-indicator critical';
|
||
|
||
// 显示默认值
|
||
document.getElementById('healthDataCount').textContent = '--';
|
||
document.getElementById('pendingAlerts').textContent = '--';
|
||
document.getElementById('activeDevices').textContent = '--';
|
||
document.getElementById('unreadMessages').textContent = '--';
|
||
}
|
||
|
||
|
||
</script>
|
||
|
||
<script>
|
||
|
||
|
||
function initializeTenantInfo() {
|
||
console.log('🏢 初始化租户信息显示');
|
||
|
||
// 从URL获取customerId
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const customerId = urlParams.get('customerId') || '1';
|
||
|
||
// 更新租户ID显示
|
||
const tenantIdElement = document.getElementById('tenantId');
|
||
if (tenantIdElement) {
|
||
tenantIdElement.textContent = customerId;
|
||
}
|
||
}
|
||
|
||
// DOMContentLoaded 事件监听器已移至 app-initializer.js 模块 (Phase 7)
|
||
// 租户信息和健康卡片初始化已由 AppInitializer.initTenantInfo() 和 initHealthCards() 处理
|
||
|
||
// ============= 健康卡片管理函数 =============
|
||
function initializeHealthCards() {
|
||
console.log('🏆 初始化健康功能卡片');
|
||
|
||
// 加载健康预测
|
||
loadHealthPredictions();
|
||
|
||
// 加载健康评分
|
||
loadCompactHealthScore();
|
||
|
||
// 加载健康建议
|
||
loadHealthRecommendations();
|
||
|
||
// 加载消息卡片
|
||
loadMessageCard();
|
||
}
|
||
|
||
// 健康预测加载函数
|
||
function loadHealthPredictions() {
|
||
console.log('🔮 加载健康预测...');
|
||
|
||
const loadingEl = document.getElementById('predictionLoading');
|
||
const itemsEl = document.getElementById('predictionItems');
|
||
|
||
if (loadingEl) loadingEl.style.display = 'flex';
|
||
if (itemsEl) itemsEl.style.display = 'none';
|
||
|
||
apiCall(`/api/v2/health/scores/comprehensive?customerId=${GLOBAL_CUSTOMER_ID}&days=7`)
|
||
.then(response => response.json())
|
||
.then(result => {
|
||
if (result.success && result.data && result.data.predictions) {
|
||
console.log('✅ 健康预测加载成功');
|
||
updateHealthPredictions(result.data.predictions);
|
||
} else {
|
||
console.warn('⚠️ 使用默认预测数据');
|
||
updateHealthPredictionsFallback();
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('❌ 健康预测加载失败:', error);
|
||
updateHealthPredictionsFallback();
|
||
});
|
||
}
|
||
|
||
function updateHealthPredictions(predictions) {
|
||
const loadingEl = document.getElementById('predictionLoading');
|
||
const itemsEl = document.getElementById('predictionItems');
|
||
|
||
if (loadingEl) loadingEl.style.display = 'none';
|
||
if (itemsEl) {
|
||
itemsEl.style.display = 'block';
|
||
itemsEl.innerHTML = predictions.slice(0, 2).map(pred => `
|
||
<div class="prediction-item-small">
|
||
<div class="prediction-label-small">${pred.type || '健康预测'}</div>
|
||
<div class="prediction-content-small">
|
||
${pred.description || pred.text || '正在分析您的健康数据...'}
|
||
<span class="prediction-risk-small">${pred.level === 'high' ? '⚠️' : pred.level === 'medium' ? '🟡' : '✅'} ${pred.risk || pred.level || '正常'}</span>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
}
|
||
|
||
function updateHealthPredictionsFallback() {
|
||
const predictions = [
|
||
{ type: '环境适应性', description: '当前状态适合继续轻度作业', level: 'low', risk: '适宜' },
|
||
{ type: '作业适宜性', description: '建议每小时休息5-10分钟', level: 'medium', risk: '注意' }
|
||
];
|
||
updateHealthPredictions(predictions);
|
||
}
|
||
|
||
// 紧凑版健康评分加载函数
|
||
function loadCompactHealthScore() {
|
||
console.log('🏆 加载紧凑版健康评分...');
|
||
|
||
const scoreEl = document.getElementById('compactHealthScore');
|
||
const statusEl = document.getElementById('compactHealthStatus');
|
||
|
||
apiCall(`/api/v2/health/scores/comprehensive?customerId=${GLOBAL_CUSTOMER_ID}&startDate=${formatDate(new Date(Date.now() - 24 * 60 * 60 * 1000))}&endDate=${formatDate(new Date())}`)
|
||
.then(response => response.json())
|
||
.then(result => {
|
||
if (result.success && result.data && result.data.summary) {
|
||
console.log('✅ 紧凑版健康评分加载成功');
|
||
const score = Math.round(result.data.summary.overallScore || 0);
|
||
if (scoreEl) scoreEl.textContent = score;
|
||
if (statusEl) {
|
||
statusEl.textContent = getHealthStatus(score);
|
||
}
|
||
// 更新色彩
|
||
updateScoreCircleColor(score);
|
||
} else {
|
||
console.warn('⚠️ 使用默认评分');
|
||
if (scoreEl) scoreEl.textContent = '0';
|
||
if (statusEl) statusEl.textContent = '正在计算...';
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('❌ 紧凑版健康评分加载失败:', error);
|
||
if (scoreEl) scoreEl.textContent = '0';
|
||
if (statusEl) statusEl.textContent = '数据加载失败';
|
||
});
|
||
}
|
||
|
||
function getHealthStatus(score) {
|
||
if (score >= 90) return '优秀状态';
|
||
if (score >= 80) return '良好状态';
|
||
if (score >= 70) return '一般状态';
|
||
if (score >= 60) return '需要关注';
|
||
return '需要改善';
|
||
}
|
||
|
||
function updateScoreCircleColor(score) {
|
||
const circleEl = document.querySelector('.score-circle-compact');
|
||
if (circleEl) {
|
||
let gradient = 'conic-gradient(from 0deg, #ff4757 0%, #ff6b6b 25%, #ffa726 50%, #4ecdc4 75%, #00e4ff 100%)';
|
||
if (score >= 90) {
|
||
gradient = 'conic-gradient(from 0deg, #00e4ff 0%, #4ecdc4 100%)';
|
||
} else if (score >= 80) {
|
||
gradient = 'conic-gradient(from 0deg, #4ecdc4 0%, #ffa726 100%)';
|
||
} else if (score < 60) {
|
||
gradient = 'conic-gradient(from 0deg, #ff4757 0%, #ff6b6b 100%)';
|
||
}
|
||
circleEl.style.background = gradient;
|
||
}
|
||
}
|
||
|
||
// 健康建议加载函数
|
||
function loadHealthRecommendations() {
|
||
console.log('💡 加载健康建议...');
|
||
|
||
const loadingEl = document.getElementById('recommendationLoading');
|
||
const itemsEl = document.getElementById('recommendationItems');
|
||
|
||
if (loadingEl) loadingEl.style.display = 'flex';
|
||
if (itemsEl) itemsEl.style.display = 'none';
|
||
|
||
apiCall(`/api/health/recommendations?analysisType=comprehensive`)
|
||
.then(response => response.json())
|
||
.then(result => {
|
||
if (result.success && result.data && result.data.recommendations) {
|
||
console.log('✅ 健康建议加载成功');
|
||
updateHealthRecommendations(result.data.recommendations);
|
||
} else {
|
||
console.warn('⚠️ 使用默认建议数据');
|
||
updateHealthRecommendationsFallback();
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('❌ 健康建议加载失败:', error);
|
||
updateHealthRecommendationsFallback();
|
||
});
|
||
}
|
||
|
||
function updateHealthRecommendations(recommendations) {
|
||
const loadingEl = document.getElementById('recommendationLoading');
|
||
const itemsEl = document.getElementById('recommendationItems');
|
||
|
||
if (loadingEl) loadingEl.style.display = 'none';
|
||
if (itemsEl) {
|
||
itemsEl.style.display = 'block';
|
||
itemsEl.innerHTML = recommendations.slice(0, 2).map(rec => `
|
||
<div class="recommendation-item-small">
|
||
<div class="recommendation-icon-small">${rec.icon || '💡'}</div>
|
||
<div class="recommendation-text-small">
|
||
${rec.text || rec.content || rec.description || '健康建议加载中...'}
|
||
<div class="recommendation-priority-small">${rec.priority || rec.level || '建议'}</div>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
}
|
||
|
||
function updateHealthRecommendationsFallback() {
|
||
const recommendations = [
|
||
{ icon: '💧', text: '适当补充水分,建议每小时饮水150-200ml', priority: '高优先级' },
|
||
{ icon: '😴', text: '保证7-8小时优质睡眠时间', priority: '日常' }
|
||
];
|
||
updateHealthRecommendations(recommendations);
|
||
}
|
||
|
||
// 消息卡片加载函数
|
||
function loadMessageCard() {
|
||
console.log('📬 加载消息卡片...');
|
||
|
||
const loadingEl = document.getElementById('messageLoading');
|
||
const itemsEl = document.getElementById('messageItems');
|
||
const countEl = document.getElementById('messageCount');
|
||
|
||
if (loadingEl) loadingEl.style.display = 'flex';
|
||
if (itemsEl) itemsEl.style.display = 'none';
|
||
|
||
// 使用已加载的消息数据或发起API调用
|
||
api.data.getMessages(GLOBAL_CUSTOMER_ID, null, null, GLOBAL_CUSTOMER_ID)
|
||
.then(response => response.json())
|
||
.then(result => {
|
||
if (result.success && result.data && result.data.messages) {
|
||
console.log('✅ 消息加载成功:', result.data.messages.length, '条消息');
|
||
updateMessageCard(result.data.messages);
|
||
} else {
|
||
console.warn('⚠️ 使用默认消息数据');
|
||
updateMessageCardFallback();
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('❌ 消息加载失败:', error);
|
||
updateMessageCardFallback();
|
||
});
|
||
}
|
||
|
||
function updateMessageCard(messages) {
|
||
const loadingEl = document.getElementById('messageLoading');
|
||
const itemsEl = document.getElementById('messageItems');
|
||
const countEl = document.getElementById('messageCount');
|
||
|
||
if (loadingEl) loadingEl.style.display = 'none';
|
||
if (countEl) countEl.textContent = messages.length;
|
||
|
||
if (itemsEl) {
|
||
itemsEl.style.display = 'block';
|
||
if (messages.length > 0) {
|
||
itemsEl.innerHTML = messages.slice(0, 3).map(msg => `
|
||
<div class="message-item-compact">
|
||
<div class="message-type-icon ${getMessageTypeClass(msg.message_type)}">${getMessageIcon(msg.message_type)}</div>
|
||
<div class="message-item-text">${msg.message || '消息内容'}</div>
|
||
<div class="message-item-time">${formatMessageTime(msg.send_time)}</div>
|
||
</div>
|
||
`).join('');
|
||
} else {
|
||
itemsEl.innerHTML = '<div class="message-item-compact"><div class="message-type-icon">📭</div><div class="message-item-text">暂无消息</div><div class="message-item-time">--</div></div>';
|
||
}
|
||
}
|
||
}
|
||
|
||
function updateMessageCardFallback() {
|
||
const loadingEl = document.getElementById('messageLoading');
|
||
const itemsEl = document.getElementById('messageItems');
|
||
const countEl = document.getElementById('messageCount');
|
||
|
||
if (loadingEl) loadingEl.style.display = 'none';
|
||
if (countEl) countEl.textContent = '0';
|
||
if (itemsEl) {
|
||
itemsEl.style.display = 'block';
|
||
itemsEl.innerHTML = '<div class="message-item-compact"><div class="message-type-icon">📭</div><div class="message-item-text">暂无新消息</div><div class="message-item-time">--</div></div>';
|
||
}
|
||
}
|
||
|
||
function getMessageTypeClass(messageType) {
|
||
const typeMap = {
|
||
'warning': 'warning',
|
||
'announcement': 'announcement',
|
||
'task': 'task',
|
||
'notification': 'notification'
|
||
};
|
||
return typeMap[messageType] || 'notification';
|
||
}
|
||
|
||
function getMessageIcon(messageType) {
|
||
const iconMap = {
|
||
'warning': '⚠️',
|
||
'announcement': '📢',
|
||
'task': '📋',
|
||
'notification': '📨'
|
||
};
|
||
return iconMap[messageType] || '📨';
|
||
}
|
||
|
||
// formatMessageTime() 函数已移至 utils.js 模块
|
||
|
||
// 根据实时数据更新健康卡片
|
||
function updateHealthCardsData(data, customerId) {
|
||
try {
|
||
console.log('🔄 更新健康卡片数据', customerId);
|
||
|
||
// 如果数据中包含健康评分信息,优先使用实时数据
|
||
if (data.health_score) {
|
||
const scoreEl = document.getElementById('compactHealthScore');
|
||
const statusEl = document.getElementById('compactHealthStatus');
|
||
|
||
if (scoreEl) {
|
||
const score = Math.round(data.health_score || 0);
|
||
scoreEl.textContent = score;
|
||
|
||
if (statusEl) {
|
||
statusEl.textContent = getHealthStatus(score);
|
||
}
|
||
|
||
// 更新色彩
|
||
updateScoreCircleColor(score);
|
||
}
|
||
}
|
||
|
||
// 如果有健康预测数据
|
||
if (data.health_predictions && data.health_predictions.length > 0) {
|
||
updateHealthPredictions(data.health_predictions);
|
||
}
|
||
|
||
// 如果有健康建议数据
|
||
if (data.health_recommendations && data.health_recommendations.length > 0) {
|
||
updateHealthRecommendations(data.health_recommendations);
|
||
}
|
||
|
||
console.log('✅ 健康卡片数据更新完成');
|
||
|
||
} catch (error) {
|
||
console.error('❌ 健康卡片数据更新失败:', error);
|
||
}
|
||
}
|
||
|
||
// 🗺️ 地图初始化调用
|
||
// DOMContentLoaded 事件监听器已移至 app-initializer.js 模块 (Phase 7)
|
||
// 地图初始化已由 AppInitializer.initMap() 处理
|
||
|
||
// ============================================
|
||
// V2.0 架构初始化 (Phase 3)
|
||
// ============================================
|
||
|
||
/**
|
||
* V2.0 架构初始化函数
|
||
* 使用新的 DataManager, EventBus, ComponentManager
|
||
*/
|
||
async function initV2Architecture() {
|
||
console.log('🚀 开始 V2.0 架构初始化...');
|
||
|
||
try {
|
||
// 1. 初始化事件总线
|
||
window.eventBus = new EventBus();
|
||
console.log('✅ EventBus 初始化完成');
|
||
|
||
// 2. 初始化 API 适配器
|
||
window.apiAdapter = new ApiAdapter({
|
||
baseURL: '/api',
|
||
timeout: 10000,
|
||
enableCache: true,
|
||
cacheTTL: 60000
|
||
});
|
||
console.log('✅ ApiAdapter 初始化完成');
|
||
|
||
// 3. 初始化数据管理器
|
||
window.dataManager = new DataManager(window.GLOBAL_CUSTOMER_ID, {
|
||
apiAdapter: window.apiAdapter,
|
||
eventBus: window.eventBus,
|
||
debug: true
|
||
});
|
||
console.log('✅ DataManager 初始化完成');
|
||
|
||
// 4. 初始化组件管理器
|
||
window.componentManager = new ComponentManager({
|
||
eventBus: window.eventBus,
|
||
dataManager: window.dataManager
|
||
});
|
||
console.log('✅ ComponentManager 初始化完成');
|
||
|
||
// 5. 注册业务组件
|
||
await registerComponents();
|
||
|
||
// 6. 启动自动刷新
|
||
window.dataManager.startAutoRefresh();
|
||
console.log('✅ 自动刷新已启动');
|
||
|
||
console.log('✅ V2.0 架构初始化完成');
|
||
} catch (error) {
|
||
console.error('❌ V2.0 架构初始化失败:', error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 注册所有业务组件
|
||
*/
|
||
async function registerComponents() {
|
||
console.log('📦 开始注册组件...');
|
||
|
||
// 注册地图组件
|
||
const mapLayer = new MapLayer('map-container', {
|
||
center: [116.397428, 39.90923],
|
||
zoom: 12
|
||
});
|
||
window.componentManager.register('map', mapLayer);
|
||
|
||
// ========== V3 Panels (基于 BasePanel 重构) ==========
|
||
// 使用 V3 版本替代 V2,提供统一生命周期管理和自动刷新
|
||
|
||
// 注册 Alert Panel V3
|
||
const alertPanelV3 = new AlertPanelV3({
|
||
containerId: 'alertPanel',
|
||
chartContainer: 'alertChart',
|
||
listContainer: 'alertList',
|
||
maxListItems: 10
|
||
});
|
||
await alertPanelV3.init();
|
||
window.componentManager.register('alertPanelV3', alertPanelV3);
|
||
|
||
// 注册 Device Panel V3
|
||
const devicePanelV3 = new DevicePanelV3({
|
||
containerId: 'devicePanel',
|
||
chartContainer: 'deviceChart',
|
||
listContainer: 'deviceList',
|
||
pageSize: 20
|
||
});
|
||
await devicePanelV3.init();
|
||
window.componentManager.register('devicePanelV3', devicePanelV3);
|
||
|
||
// 注册 Health Cards V3
|
||
const healthCardsV3 = new HealthCardsV3({
|
||
containerId: 'healthCardsContainer'
|
||
});
|
||
await healthCardsV3.init();
|
||
window.componentManager.register('healthCardsV3', healthCardsV3);
|
||
|
||
// 注册 Stats Panel V3
|
||
const statsPanelV3 = new StatsPanelV3({
|
||
containerId: 'statsContainer'
|
||
});
|
||
await statsPanelV3.init();
|
||
window.componentManager.register('statsPanelV3', statsPanelV3);
|
||
|
||
// 注册 Message Panel V2 (暂未重构)
|
||
const messagePanel = new MessagePanelV2({
|
||
listContainer: 'messageList',
|
||
panelContainer: 'messagePanel'
|
||
});
|
||
window.componentManager.register('messagePanel', messagePanel);
|
||
|
||
// ========== Legacy V2 Panels (已废弃,保留以备回退) ==========
|
||
// 以下 V2 panels 已被 V3 版本替代,如需回退可取消注释
|
||
/*
|
||
// 注册 Alert Panel V2
|
||
const alertPanel = new AlertPanelV2({
|
||
chartContainer: 'alertChart',
|
||
listContainer: 'alertList',
|
||
panelContainer: 'alertPanel'
|
||
});
|
||
window.componentManager.register('alertPanel', alertPanel);
|
||
|
||
// 注册 Device Panel V2
|
||
const devicePanel = new DevicePanelV2({
|
||
chartContainer: 'deviceChart',
|
||
listContainer: 'deviceList',
|
||
panelContainer: 'devicePanel'
|
||
});
|
||
window.componentManager.register('devicePanel', devicePanel);
|
||
*/
|
||
|
||
console.log('✅ 组件注册完成 (V3 Panels + Message Panel V2)');
|
||
}
|
||
|
||
// 在 DOMContentLoaded 时启动 V2.0 架构
|
||
// ⚠️ DISABLED: 旧的初始化代码已被 V4.0 Panel 替代
|
||
// 如需恢复旧版本,取消下面的注释
|
||
/*
|
||
document.addEventListener('DOMContentLoaded', async () => {
|
||
console.log('📄 DOM 加载完成');
|
||
|
||
// 先执行旧的初始化逻辑(兼容性)
|
||
if (typeof AppInitializer !== 'undefined' && AppInitializer.init) {
|
||
AppInitializer.init();
|
||
}
|
||
|
||
// 延迟启动 V2.0 架构(等待地图库加载)
|
||
setTimeout(async () => {
|
||
await initV2Architecture();
|
||
}, 1000);
|
||
});
|
||
*/
|
||
|
||
</script>
|
||
|
||
<!-- ========== V4.0 Panel 初始化脚本 ========== -->
|
||
<script>
|
||
// V4.0 Panel 初始化(使用 Flask 中间层 API)
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
// 获取配置参数
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const customerId = '{{ customerId }}';
|
||
const orgId = urlParams.get('orgId') || customerId; // Fallback to customerId
|
||
|
||
// 初始化 BigScreen 应用
|
||
window.bigScreenApp = initBigScreen({
|
||
orgId: orgId,
|
||
customerId: customerId,
|
||
date: new Date().toISOString().split('T')[0]
|
||
});
|
||
|
||
console.log('✅ BigScreen V4.0 initialized with Flask API', { orgId, customerId });
|
||
});
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|
||
|
||
|
||
|