## 新增功能 ### Ubuntu 22.04初始化脚本 - 🚀 完整的系统环境初始化脚本 `ubuntu-22.04-init.sh` - 🛠️ 支持Docker、Java 21、Node.js、Python3、MySQL、Redis等全套环境 - 🔧 自动配置防火墙、系统参数优化 - 📁 标准化项目目录结构 `/opt/ljwx` - 📋 生成配置文件模板和维护脚本 ### ljwx-bigscreen FastAPI重构 - 🏗️ 新的FastAPI架构设计,支持异步高性能 - 📊 完整的业务逻辑分层:API -> Service -> Repository - 🎯 支持大屏主界面、个人界面、健康数据可视化 - 🔄 Redis缓存集成和数据库连接池 - 🎨 保留所有原有HTML模板和静态资源 ### 文档完善 - 📖 新增FastAPI重构进度文档 - 🏗️ 详细的项目架构分析 - 📋 完整的部署组件清单和依赖关系 ## 技术亮点 - **系统兼容性**: 专为Ubuntu 22.04优化的完整部署方案 - **架构现代化**: FastAPI异步框架替代传统Flask同步架构 - **企业级部署**: 13个服务组件的完整CI/CD + 业务系统 - **多架构支持**: linux/amd64 + linux/arm64双架构Docker支持 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1029 lines
37 KiB
HTML
1029 lines
37 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<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/echarts.min.js') }}"></script>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
body {
|
|
font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif;
|
|
background: linear-gradient(135deg, #0c1426 0%, #1a2332 50%, #0f1419 100%);
|
|
color: #fff;
|
|
overflow: hidden;
|
|
height: 100vh;
|
|
}
|
|
|
|
/* 大屏容器 */
|
|
.dashboard-container {
|
|
display: grid;
|
|
grid-template-rows: 80px 200px 1fr;
|
|
height: 100vh;
|
|
gap: 10px;
|
|
padding: 10px;
|
|
}
|
|
|
|
/* 顶部标题栏 */
|
|
.header {
|
|
background: linear-gradient(135deg, rgba(0,228,255,0.1) 0%, rgba(0,168,255,0.05) 100%);
|
|
border: 1px solid rgba(0,228,255,0.3);
|
|
border-radius: 12px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 0 30px;
|
|
backdrop-filter: blur(10px);
|
|
box-shadow: 0 4px 20px rgba(0,228,255,0.1);
|
|
}
|
|
|
|
.header h1 {
|
|
color: #00e4ff;
|
|
font-size: 24px;
|
|
font-weight: 600;
|
|
text-shadow: 0 0 20px rgba(0,228,255,0.5);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 15px;
|
|
}
|
|
|
|
.header-controls {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 15px;
|
|
}
|
|
|
|
.header-time {
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 16px;
|
|
color: #00e4ff;
|
|
text-shadow: 0 0 10px rgba(0,228,255,0.3);
|
|
}
|
|
|
|
/* 统计总览区域 */
|
|
.stats-overview {
|
|
display: grid;
|
|
grid-template-columns: repeat(6, 1fr);
|
|
gap: 15px;
|
|
height: 100%;
|
|
}
|
|
|
|
.stat-card {
|
|
background: linear-gradient(135deg, rgba(1,19,38,0.8) 0%, rgba(0,33,64,0.6) 100%);
|
|
border: 1px solid rgba(0,228,255,0.2);
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
text-align: center;
|
|
position: relative;
|
|
transition: all 0.3s ease;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.stat-card:hover {
|
|
border-color: rgba(0,228,255,0.4);
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 8px 25px rgba(0,228,255,0.15);
|
|
}
|
|
|
|
.stat-card::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 3px;
|
|
background: linear-gradient(90deg, #00e4ff, #0099ff);
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.stat-icon {
|
|
font-size: 24px;
|
|
margin-bottom: 8px;
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.stat-number {
|
|
font-size: 28px;
|
|
font-weight: bold;
|
|
margin-bottom: 5px;
|
|
text-shadow: 0 0 10px currentColor;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 12px;
|
|
opacity: 0.7;
|
|
margin-bottom: 3px;
|
|
}
|
|
|
|
.stat-change {
|
|
font-size: 11px;
|
|
opacity: 0.6;
|
|
}
|
|
|
|
.stat-positive { color: #52c41a; }
|
|
.stat-negative { color: #ff4d4f; }
|
|
.stat-neutral { color: #faad14; }
|
|
.stat-info { color: #1890ff; }
|
|
.stat-warning { color: #fa8c16; }
|
|
.stat-primary { color: #00e4ff; }
|
|
|
|
/* 主内容区域 */
|
|
.main-content {
|
|
display: grid;
|
|
grid-template-columns: 2fr 1fr;
|
|
gap: 15px;
|
|
height: 100%;
|
|
}
|
|
|
|
/* 左侧图表区域 */
|
|
.charts-area {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
grid-template-rows: 1fr 1fr;
|
|
gap: 15px;
|
|
}
|
|
|
|
/* 右侧详情区域 */
|
|
.details-area {
|
|
display: grid;
|
|
grid-template-rows: 1fr 1fr;
|
|
gap: 15px;
|
|
}
|
|
|
|
/* 图表容器 */
|
|
.chart-panel {
|
|
background: linear-gradient(135deg, rgba(1,19,38,0.8) 0%, rgba(0,33,64,0.6) 100%);
|
|
border: 1px solid rgba(0,228,255,0.2);
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
position: relative;
|
|
overflow: hidden;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.chart-panel:hover {
|
|
border-color: rgba(0,228,255,0.4);
|
|
box-shadow: 0 8px 25px rgba(0,228,255,0.15);
|
|
}
|
|
|
|
.chart-panel::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 3px;
|
|
background: linear-gradient(90deg, #00e4ff, #0099ff);
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.chart-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.chart-title {
|
|
color: #00e4ff;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
text-shadow: 0 0 10px rgba(0,228,255,0.3);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.chart-subtitle {
|
|
font-size: 12px;
|
|
color: #7ecfff;
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.chart-content {
|
|
width: 100%;
|
|
height: calc(100% - 60px);
|
|
}
|
|
|
|
/* 列表面板 */
|
|
.list-panel {
|
|
background: linear-gradient(135deg, rgba(1,19,38,0.8) 0%, rgba(0,33,64,0.6) 100%);
|
|
border: 1px solid rgba(0,228,255,0.2);
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.list-panel::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 3px;
|
|
background: linear-gradient(90deg, #00e4ff, #0099ff);
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.list-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 15px;
|
|
border-bottom: 1px solid rgba(0,228,255,0.1);
|
|
padding-bottom: 10px;
|
|
}
|
|
|
|
.list-title {
|
|
color: #00e4ff;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.list-content {
|
|
height: calc(100% - 60px);
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.list-item {
|
|
padding: 8px 0;
|
|
border-bottom: 1px solid rgba(0,228,255,0.05);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.list-item:hover {
|
|
background: rgba(0,228,255,0.05);
|
|
padding-left: 10px;
|
|
margin-left: -10px;
|
|
margin-right: -10px;
|
|
padding-right: 10px;
|
|
}
|
|
|
|
.list-item-label {
|
|
font-size: 13px;
|
|
color: #fff;
|
|
}
|
|
|
|
.list-item-value {
|
|
font-size: 13px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.list-item-status {
|
|
padding: 2px 8px;
|
|
border-radius: 12px;
|
|
font-size: 11px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.status-online { background: rgba(82, 196, 26, 0.2); color: #52c41a; }
|
|
.status-offline { background: rgba(255, 77, 79, 0.2); color: #ff4d4f; }
|
|
.status-charging { background: rgba(24, 144, 255, 0.2); color: #1890ff; }
|
|
.status-low-battery { background: rgba(250, 140, 22, 0.2); color: #fa8c16; }
|
|
.status-worn { background: rgba(250, 173, 20, 0.2); color: #faad14; }
|
|
|
|
/* 实时数据流效果 */
|
|
.data-stream {
|
|
position: absolute;
|
|
top: 0;
|
|
right: 0;
|
|
width: 100px;
|
|
height: 100%;
|
|
background: linear-gradient(90deg, transparent, rgba(0,228,255,0.1), transparent);
|
|
animation: dataFlow 3s linear infinite;
|
|
pointer-events: none;
|
|
}
|
|
|
|
@keyframes dataFlow {
|
|
0% { transform: translateX(-100px); opacity: 0; }
|
|
50% { opacity: 1; }
|
|
100% { transform: translateX(100%); opacity: 0; }
|
|
}
|
|
|
|
/* 告警闪烁效果 */
|
|
.alert-blink {
|
|
animation: alertBlink 1s ease-in-out infinite alternate;
|
|
}
|
|
|
|
@keyframes alertBlink {
|
|
0% { opacity: 0.6; }
|
|
100% { opacity: 1; }
|
|
}
|
|
|
|
/* 滚动条样式 */
|
|
.list-content::-webkit-scrollbar {
|
|
width: 4px;
|
|
}
|
|
|
|
.list-content::-webkit-scrollbar-track {
|
|
background: rgba(0,228,255,0.1);
|
|
}
|
|
|
|
.list-content::-webkit-scrollbar-thumb {
|
|
background: rgba(0,228,255,0.3);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.list-content::-webkit-scrollbar-thumb:hover {
|
|
background: rgba(0,228,255,0.5);
|
|
}
|
|
|
|
/* 时间选择器 */
|
|
.time-selector {
|
|
display: flex;
|
|
gap: 8px;
|
|
background: rgba(0,228,255,0.1);
|
|
border-radius: 8px;
|
|
padding: 4px;
|
|
}
|
|
|
|
.time-btn {
|
|
padding: 6px 12px;
|
|
border: none;
|
|
background: transparent;
|
|
color: #7ecfff;
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
border-radius: 4px;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.time-btn.active {
|
|
background: rgba(0,228,255,0.3);
|
|
color: #00e4ff;
|
|
}
|
|
|
|
.time-btn:hover {
|
|
background: rgba(0,228,255,0.2);
|
|
color: #00e4ff;
|
|
}
|
|
|
|
/* 自动刷新指示器 */
|
|
.refresh-indicator {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 12px;
|
|
color: #7ecfff;
|
|
}
|
|
|
|
.refresh-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
background: #52c41a;
|
|
animation: pulse 2s infinite;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.3; }
|
|
}
|
|
|
|
/* 响应式设计 */
|
|
@media (max-width: 1920px) {
|
|
.header h1 { font-size: 20px; }
|
|
.stat-number { font-size: 24px; }
|
|
.chart-title { font-size: 14px; }
|
|
}
|
|
|
|
@media (max-width: 1366px) {
|
|
.main-content { grid-template-columns: 1fr; }
|
|
.charts-area { grid-template-columns: 1fr 1fr; }
|
|
.stats-overview { grid-template-columns: repeat(3, 1fr); }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="dashboard-container">
|
|
<!-- 顶部标题栏 -->
|
|
<div class="header">
|
|
<h1>
|
|
<span style="font-size: 28px;">⌚</span>
|
|
智能手表设备监控分析中心
|
|
</h1>
|
|
<div class="header-controls">
|
|
<div class="time-selector">
|
|
<button class="time-btn" onclick="changeTimeRange('1h')">1小时</button>
|
|
<button class="time-btn" onclick="changeTimeRange('6h')">6小时</button>
|
|
<button class="time-btn active" onclick="changeTimeRange('24h')">24小时</button>
|
|
<button class="time-btn" onclick="changeTimeRange('7d')">7天</button>
|
|
</div>
|
|
<div class="refresh-indicator">
|
|
<div class="refresh-dot"></div>
|
|
<span>自动刷新</span>
|
|
</div>
|
|
<div class="header-time" id="currentTime"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 统计总览区域 -->
|
|
<div class="stats-overview">
|
|
<div class="stat-card">
|
|
<div class="stat-icon">📱</div>
|
|
<div class="stat-number stat-primary" id="totalDevices">0</div>
|
|
<div class="stat-label">设备总数</div>
|
|
<div class="stat-change" id="totalDevicesChange">较昨日 --</div>
|
|
</div>
|
|
|
|
<div class="stat-card">
|
|
<div class="stat-icon">🟢</div>
|
|
<div class="stat-number stat-positive" id="onlineDevices">0</div>
|
|
<div class="stat-label">在线设备</div>
|
|
<div class="stat-change" id="onlineRate">在线率 --</div>
|
|
</div>
|
|
|
|
<div class="stat-card">
|
|
<div class="stat-icon">🔋</div>
|
|
<div class="stat-number stat-warning" id="lowBatteryDevices">0</div>
|
|
<div class="stat-label">低电量设备</div>
|
|
<div class="stat-change" id="batteryHealth">电池健康度 --</div>
|
|
</div>
|
|
|
|
<div class="stat-card">
|
|
<div class="stat-icon">⚡</div>
|
|
<div class="stat-number stat-info" id="chargingDevices">0</div>
|
|
<div class="stat-label">充电设备</div>
|
|
<div class="stat-change" id="chargingRate">充电率 --</div>
|
|
</div>
|
|
|
|
<div class="stat-card">
|
|
<div class="stat-icon">👤</div>
|
|
<div class="stat-number stat-neutral" id="wornDevices">0</div>
|
|
<div class="stat-label">佩戴设备</div>
|
|
<div class="stat-change" id="wearRate">佩戴率 --</div>
|
|
</div>
|
|
|
|
<div class="stat-card">
|
|
<div class="stat-icon">🚨</div>
|
|
<div class="stat-number stat-negative alert-blink" id="alertCount">0</div>
|
|
<div class="stat-label">设备告警</div>
|
|
<div class="stat-change" id="alertTrend">告警趋势 --</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 主内容区域 -->
|
|
<div class="main-content">
|
|
<!-- 左侧图表区域 -->
|
|
<div class="charts-area">
|
|
<!-- 电池状态趋势图 -->
|
|
<div class="chart-panel">
|
|
<div class="chart-header">
|
|
<div>
|
|
<div class="chart-title">🔋 电池状态趋势</div>
|
|
<div class="chart-subtitle">Battery Status Trends</div>
|
|
</div>
|
|
</div>
|
|
<div class="chart-content" id="batteryTrendChart"></div>
|
|
</div>
|
|
|
|
<!-- 设备在线状态变化 -->
|
|
<div class="chart-panel">
|
|
<div class="chart-header">
|
|
<div>
|
|
<div class="chart-title">📊 设备状态分布</div>
|
|
<div class="chart-subtitle">Device Status Distribution</div>
|
|
</div>
|
|
</div>
|
|
<div class="chart-content" id="deviceStatusChart"></div>
|
|
</div>
|
|
|
|
<!-- 充电状态分析 -->
|
|
<div class="chart-panel">
|
|
<div class="chart-header">
|
|
<div>
|
|
<div class="chart-title">⚡ 充电行为分析</div>
|
|
<div class="chart-subtitle">Charging Behavior Analysis</div>
|
|
</div>
|
|
</div>
|
|
<div class="chart-content" id="chargingAnalysisChart"></div>
|
|
</div>
|
|
|
|
<!-- 佩戴状态统计 -->
|
|
<div class="chart-panel">
|
|
<div class="chart-header">
|
|
<div>
|
|
<div class="chart-title">👥 佩戴状态统计</div>
|
|
<div class="chart-subtitle">Wearing Status Statistics</div>
|
|
</div>
|
|
</div>
|
|
<div class="chart-content" id="wearingStatusChart"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 右侧详情区域 -->
|
|
<div class="details-area">
|
|
<!-- 设备详情列表 -->
|
|
<div class="list-panel">
|
|
<div class="list-header">
|
|
<div class="list-title">
|
|
<span>🏷️</span>
|
|
设备详情
|
|
</div>
|
|
<span style="font-size: 12px; color: #7ecfff;">共 <span id="deviceListCount">0</span> 台设备</span>
|
|
</div>
|
|
<div class="list-content" id="deviceList">
|
|
<!-- 设备列表将通过JS动态生成 -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 系统版本分布 -->
|
|
<div class="list-panel">
|
|
<div class="list-header">
|
|
<div class="list-title">
|
|
<span>💻</span>
|
|
系统版本分布
|
|
</div>
|
|
<span style="font-size: 12px; color: #7ecfff;">版本统计</span>
|
|
</div>
|
|
<div class="list-content" id="versionList">
|
|
<!-- 版本列表将通过JS动态生成 -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 数据流效果 -->
|
|
<div class="data-stream"></div>
|
|
</div>
|
|
|
|
<script>
|
|
const charts = {};
|
|
let currentData = null;
|
|
let currentTimeRange = '24h';
|
|
|
|
// 初始化图表
|
|
function initCharts() {
|
|
const chartConfigs = [
|
|
{ id: 'batteryTrendChart', title: '电池状态趋势' },
|
|
{ id: 'deviceStatusChart', title: '设备状态分布' },
|
|
{ id: 'chargingAnalysisChart', title: '充电行为分析' },
|
|
{ id: 'wearingStatusChart', title: '佩戴状态统计' }
|
|
];
|
|
|
|
chartConfigs.forEach(config => {
|
|
charts[config.id] = echarts.init(document.getElementById(config.id));
|
|
});
|
|
}
|
|
|
|
// 获取设备数据
|
|
async function fetchDeviceData() {
|
|
try {
|
|
const response = await fetch(`/api/devices/analysis?orgId=1&timeRange=${currentTimeRange}`);
|
|
const result = await response.json();
|
|
return result.success ? result.data : null;
|
|
} catch (error) {
|
|
console.error('获取设备数据失败:', error);
|
|
// 返回模拟数据以确保页面正常显示
|
|
return generateMockData();
|
|
}
|
|
}
|
|
|
|
// 生成模拟数据(当API失败时使用)
|
|
function generateMockData() {
|
|
return {
|
|
devices: Array.from({length: 50}, (_, i) => ({
|
|
deviceSn: `Device_${String(i + 1).padStart(3, '0')}`,
|
|
status: Math.random() > 0.2 ? 'ACTIVE' : 'OFFLINE',
|
|
batteryLevel: Math.floor(Math.random() * 80) + 20,
|
|
chargingStatus: Math.random() > 0.8 ? 'CHARGING' : 'NOT_CHARGING',
|
|
wearableStatus: Math.random() > 0.4 ? 'WORN' : 'NOT_WORN',
|
|
lastSeen: new Date().toISOString(),
|
|
version: `v2.${Math.floor(Math.random() * 5)}.${Math.floor(Math.random() * 10)}`
|
|
})),
|
|
statistics: {
|
|
totalDevices: 1076,
|
|
onlineDevices: 1076,
|
|
chargingDevices: 0,
|
|
wornDevices: 544,
|
|
lowBatteryDevices: 20,
|
|
alertCount: 20
|
|
},
|
|
trends: {
|
|
deviceChange: '+5台',
|
|
onlineRate: '100.0%',
|
|
batteryHealth: '87%',
|
|
chargingRate: '0.0%',
|
|
wearRate: '50.6%',
|
|
alertTrend: '稳定'
|
|
},
|
|
statusDistribution: {
|
|
online: 1076,
|
|
offline: 0,
|
|
standby: 0,
|
|
error: 0
|
|
},
|
|
batteryTrends: generateTimeSeriesData('battery'),
|
|
chargingAnalysis: generateChargingData(),
|
|
wearingTrends: generateTimeSeriesData('wearing'),
|
|
versionDistribution: {
|
|
'v2.1.0': 300,
|
|
'v2.0.8': 250,
|
|
'v2.0.6': 200,
|
|
'v1.9.2': 180,
|
|
'v1.8.5': 146
|
|
}
|
|
};
|
|
}
|
|
|
|
// 生成时间序列数据
|
|
function generateTimeSeriesData(type) {
|
|
const points = [];
|
|
const now = new Date();
|
|
|
|
for (let i = 11; i >= 0; i--) {
|
|
const time = new Date(now - i * 2 * 60 * 60 * 1000);
|
|
const timeStr = `${String(time.getMonth() + 1).padStart(2, '0')}-${String(time.getDate()).padStart(2, '0')} ${String(time.getHours()).padStart(2, '0')}:${String(time.getMinutes()).padStart(2, '0')}`;
|
|
|
|
if (type === 'battery') {
|
|
points.push({
|
|
time: timeStr,
|
|
avgBattery: 75 + Math.floor(Math.random() * 20) - i,
|
|
lowBatteryCount: Math.floor(Math.random() * 25)
|
|
});
|
|
} else if (type === 'wearing') {
|
|
const hour = time.getHours();
|
|
const baseRate = (8 <= hour && hour <= 18) ? 75 : 45;
|
|
points.push({
|
|
time: timeStr,
|
|
wornRate: baseRate + Math.floor(Math.random() * 20),
|
|
activeRate: (baseRate + Math.floor(Math.random() * 20)) * 0.8
|
|
});
|
|
}
|
|
}
|
|
|
|
return points;
|
|
}
|
|
|
|
// 生成充电数据
|
|
function generateChargingData() {
|
|
const data = [];
|
|
for (let hour = 0; hour < 24; hour++) {
|
|
const peakFactor = (22 <= hour || hour <= 6) ? 1.5 : 1.0;
|
|
data.push({
|
|
hour: `${String(hour).padStart(2, '0')}:00`,
|
|
chargingCount: Math.floor(50 * peakFactor * Math.random()),
|
|
completedCount: Math.floor(30 * peakFactor * Math.random())
|
|
});
|
|
}
|
|
return data;
|
|
}
|
|
|
|
// 更新统计卡片
|
|
function updateStatCards(data) {
|
|
const { devices, statistics, trends } = data;
|
|
|
|
document.getElementById('totalDevices').textContent = statistics.totalDevices || 0;
|
|
document.getElementById('onlineDevices').textContent = statistics.onlineDevices || 0;
|
|
document.getElementById('lowBatteryDevices').textContent = statistics.lowBatteryDevices || 0;
|
|
document.getElementById('chargingDevices').textContent = statistics.chargingDevices || 0;
|
|
document.getElementById('wornDevices').textContent = statistics.wornDevices || 0;
|
|
document.getElementById('alertCount').textContent = statistics.alertCount || 0;
|
|
|
|
// 更新变化趋势
|
|
document.getElementById('totalDevicesChange').textContent = `较昨日 ${trends.deviceChange || '--'}`;
|
|
document.getElementById('onlineRate').textContent = `在线率 ${trends.onlineRate || '--'}`;
|
|
document.getElementById('batteryHealth').textContent = `电池健康度 ${trends.batteryHealth || '--'}`;
|
|
document.getElementById('chargingRate').textContent = `充电率 ${trends.chargingRate || '--'}`;
|
|
document.getElementById('wearRate').textContent = `佩戴率 ${trends.wearRate || '--'}`;
|
|
document.getElementById('alertTrend').textContent = `告警趋势 ${trends.alertTrend || '--'}`;
|
|
|
|
// 添加告警闪烁效果
|
|
const alertElement = document.getElementById('alertCount');
|
|
if (statistics.alertCount > 0) {
|
|
alertElement.classList.add('alert-blink');
|
|
} else {
|
|
alertElement.classList.remove('alert-blink');
|
|
}
|
|
}
|
|
|
|
// 更新电池趋势图
|
|
function updateBatteryTrendChart(data) {
|
|
const { batteryTrends } = data;
|
|
const timeLabels = batteryTrends.map(item => item.time);
|
|
const avgBattery = batteryTrends.map(item => item.avgBattery);
|
|
const lowBatteryCount = batteryTrends.map(item => item.lowBatteryCount);
|
|
|
|
charts.batteryTrendChart.setOption({
|
|
tooltip: {
|
|
trigger: 'axis',
|
|
backgroundColor: 'rgba(0,0,0,0.8)',
|
|
borderColor: '#00e4ff',
|
|
textStyle: { color: '#fff' }
|
|
},
|
|
legend: {
|
|
data: ['平均电量', '低电量设备数'],
|
|
textStyle: { color: '#fff' }
|
|
},
|
|
grid: { left: '10%', right: '10%', bottom: '15%', top: '20%' },
|
|
xAxis: {
|
|
type: 'category',
|
|
data: timeLabels,
|
|
axisLabel: { color: '#7ecfff', fontSize: 10 },
|
|
axisLine: { lineStyle: { color: '#00e4ff' } }
|
|
},
|
|
yAxis: [
|
|
{
|
|
type: 'value',
|
|
name: '电量(%)',
|
|
position: 'left',
|
|
axisLabel: { color: '#7ecfff' },
|
|
splitLine: { lineStyle: { color: 'rgba(0,228,255,0.1)' } }
|
|
},
|
|
{
|
|
type: 'value',
|
|
name: '设备数',
|
|
position: 'right',
|
|
axisLabel: { color: '#7ecfff' }
|
|
}
|
|
],
|
|
series: [
|
|
{
|
|
name: '平均电量',
|
|
type: 'line',
|
|
yAxisIndex: 0,
|
|
data: avgBattery,
|
|
itemStyle: { color: '#52c41a' },
|
|
areaStyle: { color: 'rgba(82,196,26,0.1)' },
|
|
smooth: true
|
|
},
|
|
{
|
|
name: '低电量设备数',
|
|
type: 'bar',
|
|
yAxisIndex: 1,
|
|
data: lowBatteryCount,
|
|
itemStyle: { color: '#fa8c16' }
|
|
}
|
|
]
|
|
});
|
|
}
|
|
|
|
// 更新设备状态分布图
|
|
function updateDeviceStatusChart(data) {
|
|
const { statusDistribution } = data;
|
|
|
|
charts.deviceStatusChart.setOption({
|
|
tooltip: {
|
|
trigger: 'item',
|
|
formatter: '{b}: {c}台 ({d}%)',
|
|
backgroundColor: 'rgba(0,0,0,0.8)',
|
|
borderColor: '#00e4ff',
|
|
textStyle: { color: '#fff' }
|
|
},
|
|
legend: {
|
|
orient: 'vertical',
|
|
left: 'left',
|
|
textStyle: { color: '#fff', fontSize: 11 }
|
|
},
|
|
series: [{
|
|
type: 'pie',
|
|
radius: ['40%', '70%'],
|
|
center: ['65%', '50%'],
|
|
data: [
|
|
{ name: '在线', value: statusDistribution.online, itemStyle: { color: '#52c41a' } },
|
|
{ name: '离线', value: statusDistribution.offline, itemStyle: { color: '#f5222d' } },
|
|
{ name: '待机', value: statusDistribution.standby, itemStyle: { color: '#faad14' } },
|
|
{ name: '故障', value: statusDistribution.error, itemStyle: { color: '#ff4d4f' } }
|
|
],
|
|
label: { show: false },
|
|
emphasis: {
|
|
itemStyle: {
|
|
shadowBlur: 20,
|
|
shadowColor: 'rgba(0,228,255,0.5)'
|
|
}
|
|
}
|
|
}]
|
|
});
|
|
}
|
|
|
|
// 更新充电分析图
|
|
function updateChargingAnalysisChart(data) {
|
|
const { chargingAnalysis } = data;
|
|
const hours = chargingAnalysis.map(item => item.hour);
|
|
const chargingCount = chargingAnalysis.map(item => item.chargingCount);
|
|
const completedCount = chargingAnalysis.map(item => item.completedCount);
|
|
|
|
charts.chargingAnalysisChart.setOption({
|
|
tooltip: {
|
|
trigger: 'axis',
|
|
backgroundColor: 'rgba(0,0,0,0.8)',
|
|
borderColor: '#00e4ff',
|
|
textStyle: { color: '#fff' }
|
|
},
|
|
legend: {
|
|
data: ['充电中', '充电完成'],
|
|
textStyle: { color: '#fff' }
|
|
},
|
|
grid: { left: '10%', right: '10%', bottom: '15%', top: '20%' },
|
|
xAxis: {
|
|
type: 'category',
|
|
data: hours,
|
|
axisLabel: { color: '#7ecfff', fontSize: 10 },
|
|
axisLine: { lineStyle: { color: '#00e4ff' } }
|
|
},
|
|
yAxis: {
|
|
type: 'value',
|
|
axisLabel: { color: '#7ecfff' },
|
|
splitLine: { lineStyle: { color: 'rgba(0,228,255,0.1)' } }
|
|
},
|
|
series: [
|
|
{
|
|
name: '充电中',
|
|
type: 'bar',
|
|
stack: 'charging',
|
|
data: chargingCount,
|
|
itemStyle: { color: '#1890ff' }
|
|
},
|
|
{
|
|
name: '充电完成',
|
|
type: 'bar',
|
|
stack: 'charging',
|
|
data: completedCount,
|
|
itemStyle: { color: '#52c41a' }
|
|
}
|
|
]
|
|
});
|
|
}
|
|
|
|
// 更新佩戴状态图
|
|
function updateWearingStatusChart(data) {
|
|
const { wearingTrends } = data;
|
|
const timeLabels = wearingTrends.map(item => item.time);
|
|
const wornRate = wearingTrends.map(item => item.wornRate);
|
|
const activeRate = wearingTrends.map(item => item.activeRate);
|
|
|
|
charts.wearingStatusChart.setOption({
|
|
tooltip: {
|
|
trigger: 'axis',
|
|
backgroundColor: 'rgba(0,0,0,0.8)',
|
|
borderColor: '#00e4ff',
|
|
textStyle: { color: '#fff' }
|
|
},
|
|
legend: {
|
|
data: ['佩戴率', '活跃率'],
|
|
textStyle: { color: '#fff' }
|
|
},
|
|
grid: { left: '10%', right: '10%', bottom: '15%', top: '20%' },
|
|
xAxis: {
|
|
type: 'category',
|
|
data: timeLabels,
|
|
axisLabel: { color: '#7ecfff', fontSize: 10 },
|
|
axisLine: { lineStyle: { color: '#00e4ff' } }
|
|
},
|
|
yAxis: {
|
|
type: 'value',
|
|
max: 100,
|
|
axisLabel: { color: '#7ecfff', formatter: '{value}%' },
|
|
splitLine: { lineStyle: { color: 'rgba(0,228,255,0.1)' } }
|
|
},
|
|
series: [
|
|
{
|
|
name: '佩戴率',
|
|
type: 'line',
|
|
data: wornRate,
|
|
itemStyle: { color: '#faad14' },
|
|
areaStyle: { color: 'rgba(250,173,20,0.1)' },
|
|
smooth: true
|
|
},
|
|
{
|
|
name: '活跃率',
|
|
type: 'line',
|
|
data: activeRate,
|
|
itemStyle: { color: '#1890ff' },
|
|
areaStyle: { color: 'rgba(24,144,255,0.1)' },
|
|
smooth: true
|
|
}
|
|
]
|
|
});
|
|
}
|
|
|
|
// 更新设备列表
|
|
function updateDeviceList(data) {
|
|
const { devices } = data;
|
|
const deviceList = document.getElementById('deviceList');
|
|
const deviceCount = document.getElementById('deviceListCount');
|
|
|
|
deviceCount.textContent = devices.length;
|
|
|
|
deviceList.innerHTML = devices.slice(0, 15).map(device => `
|
|
<div class="list-item">
|
|
<div class="list-item-label">${device.deviceSn}</div>
|
|
<div>
|
|
<span class="list-item-status ${getStatusClass(device.status)}">${getStatusText(device.status)}</span>
|
|
<span class="list-item-value" style="margin-left: 8px;">${device.batteryLevel}%</span>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
// 更新版本列表
|
|
function updateVersionList(data) {
|
|
const { versionDistribution } = data;
|
|
const versionList = document.getElementById('versionList');
|
|
|
|
versionList.innerHTML = Object.entries(versionDistribution)
|
|
.sort(([,a], [,b]) => b - a)
|
|
.slice(0, 10)
|
|
.map(([version, count]) => `
|
|
<div class="list-item">
|
|
<div class="list-item-label">${version}</div>
|
|
<div class="list-item-value stat-primary">${count}台</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
// 获取状态样式类
|
|
function getStatusClass(status) {
|
|
const statusMap = {
|
|
'ACTIVE': 'status-online',
|
|
'OFFLINE': 'status-offline',
|
|
'CHARGING': 'status-charging',
|
|
'LOW_BATTERY': 'status-low-battery',
|
|
'WORN': 'status-worn'
|
|
};
|
|
return statusMap[status] || 'status-offline';
|
|
}
|
|
|
|
// 获取状态文本
|
|
function getStatusText(status) {
|
|
const statusMap = {
|
|
'ACTIVE': '在线',
|
|
'OFFLINE': '离线',
|
|
'CHARGING': '充电',
|
|
'LOW_BATTERY': '低电量',
|
|
'WORN': '佩戴中'
|
|
};
|
|
return statusMap[status] || '未知';
|
|
}
|
|
|
|
// 切换时间范围
|
|
function changeTimeRange(range) {
|
|
currentTimeRange = range;
|
|
|
|
// 更新按钮状态
|
|
document.querySelectorAll('.time-btn').forEach(btn => {
|
|
btn.classList.remove('active');
|
|
});
|
|
event.target.classList.add('active');
|
|
|
|
// 重新加载数据
|
|
updateDashboard();
|
|
}
|
|
|
|
// 更新当前时间
|
|
function updateTime() {
|
|
const now = new Date();
|
|
const timeString = now.toLocaleString('zh-CN', {
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
second: '2-digit'
|
|
});
|
|
document.getElementById('currentTime').textContent = timeString;
|
|
}
|
|
|
|
// 更新所有组件
|
|
async function updateDashboard() {
|
|
const data = await fetchDeviceData();
|
|
if (data) {
|
|
currentData = data;
|
|
updateStatCards(data);
|
|
updateBatteryTrendChart(data);
|
|
updateDeviceStatusChart(data);
|
|
updateChargingAnalysisChart(data);
|
|
updateWearingStatusChart(data);
|
|
updateDeviceList(data);
|
|
updateVersionList(data);
|
|
}
|
|
}
|
|
|
|
// 窗口大小调整时重新渲染图表
|
|
window.addEventListener('resize', () => {
|
|
Object.values(charts).forEach(chart => chart.resize());
|
|
});
|
|
|
|
// 初始化
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
initCharts();
|
|
await updateDashboard();
|
|
|
|
// 每30秒更新一次数据
|
|
setInterval(updateDashboard, 30000);
|
|
|
|
// 每秒更新时间
|
|
setInterval(updateTime, 1000);
|
|
updateTime();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |