Files
ljwx-health/ljwx-bigscreen/app/templates/main.html
T
brunoandClaude Sonnet 4.5 6fcf4b4d83
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
docs: 深度分析 API 架构并准备激进式重构
## 主要变更

### 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>
2026-01-24 23:27:02 +08:00

7181 lines
303 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>智能健康数据分析平台</title>
<script src="{{ 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&&center.length>=2)map.setCenter([center[0],center[1]]);
else if(navigator.geolocation){
navigator.geolocation.getCurrentPosition(pos=>{
map.setCenter([pos.coords.longitude,pos.coords.latitude]);
});
}
}
// 健康数据统计计算函数
function calculateHealthStats(metrics) {
const stats = {
healthScore: 0,
normalCount: 0,
riskCount: 0,
avgHeartRate: 0,
avgBloodOxygen: 0,
avgTemperature: 0,
avgSteps: 0
};
if (!metrics || metrics.length === 0) return stats;
let totalScore = 0, validMetrics = 0;
metrics.forEach(metric => {
const values = metric.values.filter(v => v !== null && v !== undefined);
if (values.length === 0) return;
const avg = values.reduce((a, b) => a + b, 0) / values.length;
switch(metric.name) {
case '心率':
stats.avgHeartRate = Math.round(avg);
const heartScore = avg >= 60 && avg <= 100 ? 85 : (avg < 60 ? 70 : 60); // 正常范围评分
totalScore += heartScore;
if (heartScore >= 80) stats.normalCount++; else stats.riskCount++;
validMetrics++;
break;
case '血氧':
stats.avgBloodOxygen = Math.round(avg);
const oxygenScore = avg >= 95 ? 90 : (avg >= 90 ? 75 : 50);
totalScore += oxygenScore;
if (oxygenScore >= 80) stats.normalCount++; else stats.riskCount++;
validMetrics++;
break;
case '体温':
stats.avgTemperature = avg.toFixed(1);
const tempScore = avg >= 36.1 && avg <= 37.2 ? 88 : 65;
totalScore += tempScore;
if (tempScore >= 80) stats.normalCount++; else stats.riskCount++;
validMetrics++;
break;
case '步数':
stats.avgSteps = Math.round(avg);
const stepScore = avg >= 8000 ? 85 : (avg >= 5000 ? 70 : 55);
totalScore += stepScore;
if (stepScore >= 80) stats.normalCount++; else stats.riskCount++;
validMetrics++;
break;
}
});
stats.healthScore = validMetrics > 0 ? Math.round(totalScore / validMetrics) : 0;
return stats;
}
// 更新健康统计UI
function updateHealthStats(stats) {
try {
document.getElementById('healthScore').textContent = stats.healthScore;
document.getElementById('normalCount').textContent = stats.normalCount;
document.getElementById('riskCount').textContent = stats.riskCount;
document.getElementById('avgHeartRate').textContent = stats.avgHeartRate;
document.getElementById('avgBloodOxygen').textContent = stats.avgBloodOxygen + '%';
document.getElementById('avgTemperature').textContent = stats.avgTemperature + '°C';
document.getElementById('avgSteps').textContent = stats.avgSteps;
} catch (e) {
console.warn('更新健康统计UI失败:', e);
}
}
// 计算雷达图数据
function calculateRadarData(metrics) {
const indicators = [];
const values = [];
const metricMap = {
'心率': { max: 100, unit: 'bpm' },
'血氧': { max: 100, unit: '%' },
'体温': { max: 100, unit: '°C' },
'步数': { max: 100, unit: '步' },
'压力': { max: 100, unit: '级' },
'睡眠': { max: 100, unit: '小时' },
'卡路里': { max: 100, unit: 'kcal' },
'血压': { max: 100, unit: 'mmHg' }
};
metrics.forEach(metric => {
const config = metricMap[metric.name];
if (!config) return;
const validValues = metric.values.filter(v => v !== null && v !== undefined);
if (validValues.length === 0) return;
const avg = validValues.reduce((a, b) => a + b, 0) / validValues.length;
// 根据指标类型计算健康评分
let score = 0;
switch(metric.name) {
case '心率':
score = avg >= 60 && avg <= 100 ? 85 : (avg < 60 ? 70 : 60);
break;
case '血氧':
score = avg >= 95 ? 90 : (avg >= 90 ? 75 : 50);
break;
case '体温':
score = avg >= 36.1 && avg <= 37.2 ? 88 : 65;
break;
case '步数':
score = avg >= 8000 ? 85 : (avg >= 5000 ? 70 : 55);
break;
case '压力':
score = avg <= 3 ? 85 : (avg <= 5 ? 70 : 50);
break;
case '睡眠':
score = avg >= 7 ? 85 : (avg >= 6 ? 70 : 55);
break;
default:
score = Math.min(90, Math.max(50, 100 - (avg * 0.5))); // 默认计算
}
indicators.push({
name: metric.name,
max: 100,
min: 0
});
values.push(Math.round(score));
});
return { indicators, values };
}
function optimizeMapDisplay() {
// 根据缩放级别动态调整显示策略
map.on('zoomend', () => {
const zoom = map.getZoom();
if (zoom < 12) {
// 低缩放级别:隐藏文本,显示聚合点
textLayer.hide();
alertTextLayer.hide();
enableClustering();
} else if (zoom < 15) {
// 中等缩放级别:显示部分文本
textLayer.show();
alertTextLayer.show();
disableClustering();
setTextFilter(0.3); // 只显示30%的文本避免重叠
} else {
// 高缩放级别:显示所有标识
textLayer.show();
alertTextLayer.show();
disableClustering();
setTextFilter(1); // 显示所有文本
}
});
}
// 聚合显示实现
function enableClustering() {
// 使用Loca的聚合功能
const clusterLayer = new Loca.PointLayer({
loca,
zIndex: 115
});
clusterLayer.setSource(geo);
clusterLayer.setStyle({
unit: 'meter',
size: [20, 20],
color: (feature) => {
const count = feature.properties.count || 1;
return count > 10 ? '#ff4d4f' : count > 5 ? '#faad14' : '#52c41a';
},
text: {
field: 'count',
font: 'bold 12px Arial',
fillColor: '#fff'
}
});
loca.add(clusterLayer);
}
// 文本过滤显示
function setTextFilter(ratio) {
textLayer.setStyle({
filter: (feature, index) => {
return Math.random() < ratio; // 随机显示部分文本
}
});
}
// 交互功能函数
function showHealthDetails() {
const data = window.healthAnalysisData;
if (!data) {
alert('健康详情功能开发中...\n\n当前显示的是7天健康数据汇总');
return;
}
const { health_summary, risk_alerts } = data;
let message = `健康数据详情报告\n\n`;
message += `📊 综合评分: ${health_summary.overall_score}分\n`;
message += `✅ 正常指标: ${health_summary.normal_indicators}项\n`;
message += `⚠️ 风险指标: ${health_summary.risk_indicators}项\n`;
message += `👥 活跃用户: ${health_summary.active_users}/${health_summary.total_users}人\n\n`;
if (risk_alerts && risk_alerts.length > 0) {
message += `🚨 风险预警:\n`;
risk_alerts.slice(0, 3).forEach(alert => {
message += `• ${alert.message}\n`;
});
} else {
message += `✨ 暂无风险预警,整体健康状况良好`;
}
alert(message);
}
function filterByHeartRate() {
const data = window.healthAnalysisData;
if (!data) {
alert('心率筛选功能:显示心率异常的时间段和用户');
return;
}
const heartRateMetric = data.metrics.find(m => m.name === '心率');
if (heartRateMetric) {
const abnormalDays = heartRateMetric.daily_stats ?
heartRateMetric.daily_stats.filter(d => d.status === 'risk').length : 0;
alert(`心率分析结果\n\n平均心率: ${heartRateMetric.avg_value}bpm\n异常天数: ${abnormalDays}天\n正常范围: ${heartRateMetric.normal_range[0]}-${heartRateMetric.normal_range[1]}bpm`);
} else {
alert('暂无心率数据');
}
}
function filterByBloodOxygen() {
const data = window.healthAnalysisData;
if (!data) {
alert('血氧筛选功能:显示血氧偏低的时间段和用户');
return;
}
const oxygenMetric = data.metrics.find(m => m.name === '血氧');
if (oxygenMetric) {
const abnormalDays = oxygenMetric.daily_stats ?
oxygenMetric.daily_stats.filter(d => d.status === 'risk').length : 0;
alert(`血氧分析结果\n\n平均血氧: ${oxygenMetric.avg_value}%\n异常天数: ${abnormalDays}天\n正常范围: ${oxygenMetric.normal_range[0]}-${oxygenMetric.normal_range[1]}%`);
} else {
alert('暂无血氧数据');
}
}
function filterByTemperature() {
const data = window.healthAnalysisData;
if (!data) {
alert('体温筛选功能:显示体温异常的时间段和用户');
return;
}
const tempMetric = data.metrics.find(m => m.name === '体温');
if (tempMetric) {
const abnormalDays = tempMetric.daily_stats ?
tempMetric.daily_stats.filter(d => d.status === 'risk').length : 0;
alert(`体温分析结果\n\n平均体温: ${tempMetric.avg_value}°C\n异常天数: ${abnormalDays}天\n正常范围: ${tempMetric.normal_range[0]}-${tempMetric.normal_range[1]}°C`);
} else {
alert('暂无体温数据');
}
}
function filterBySteps() {
const data = window.healthAnalysisData;
if (!data) {
alert('步数筛选功能:显示运动量不足的用户');
return;
}
const stepsMetric = data.metrics.find(m => m.name === '步数');
if (stepsMetric) {
const lowActivityDays = stepsMetric.daily_stats ?
stepsMetric.daily_stats.filter(d => d.value && d.value < 5000).length : 0;
alert(`步数分析结果\n\n平均步数: ${stepsMetric.avg_value}步\n运动不足天数: ${lowActivityDays}天\n建议目标: ${stepsMetric.normal_range[0]}步以上`);
} else {
alert('暂无步数数据');
}
}
function showMetricDetails(metricName, value, date) {
const data = window.healthAnalysisData;
if (!data) {
alert(`指标详情\n\n指标: ${metricName}\n数值: ${value}\n日期: ${date}\n\n点击可查看该指标的详细分析和建议`);
return;
}
const metric = data.metrics.find(m => m.name === metricName);
if (metric) {
const dayData = metric.daily_stats ? metric.daily_stats.find(d => d.date === date) : null;
let message = `${metricName}详细信息\n\n`;
message += `📅 日期: ${date}\n`;
message += `📊 数值: ${value}${metric.unit}\n`;
message += `📈 7天平均: ${metric.avg_value}${metric.unit}\n`;
message += `📏 正常范围: ${metric.normal_range[0]}-${metric.normal_range[1]}${metric.unit}\n`;
if (dayData) {
message += `⭐ 健康评分: ${dayData.score}分\n`;
message += `🔍 状态: ${dayData.status === 'normal' ? '正常' : '需关注'}\n`;
}
message += `\n💡 建议: 保持规律监测,如有异常请及时就医`;
alert(message);
}
}
// 获取指标平均值的辅助函数
function getMetricAvg(metrics, metricName) {
const metric = metrics.find(m => m.name === metricName);
if (!metric || !metric.values) return 0;
const validValues = metric.values.filter(v => v !== null && v !== undefined);
return validValues.length > 0 ? Math.round(validValues.reduce((a,b) => a+b, 0) / validValues.length) : 0;
}
// 从后端metrics数据计算雷达图数据
function calculateRadarDataFromMetrics(metrics) {
const indicators = [];
const values = [];
// 选择主要健康指标
const mainMetrics = ['心率', '血氧', '体温', '步数', '压力', '收缩压', '睡眠'];
metrics.forEach(metric => {
if (mainMetrics.includes(metric.name) && metric.avg_value > 0) {
// 根据指标类型计算健康评分
let score = 0;
const avg = metric.avg_value;
// 睡眠数据特殊处理
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>