云计算百科
云计算领域专业知识百科平台

4G模块应用,内网穿透,前端网页的制作第六讲(更新前端的数据显示)

更新前端的数据显示(根据我们使用的温度,湿度,标志,心率),以及绘制图表

第四步关键词

我会在下位机通过温度:%d°C\\n,湿度:%d%%\\n,摔倒标志:%d\\n,心率异常标志:%d\\n,气体异常标志:%d\\n,心率值:%d BPM\\n的这个协议进行上报温度,湿度数据,摔倒标志,心率异常标志,气体异常标志,心率值请你更新前端,实时显示温度,湿度数据,摔倒标志,心率异常标志,气体异常标志,心率值上报数据,需要美观好看,并且可以实时绘制数据图表

frontend/index.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>iHelmet 监控中心</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link rel="stylesheet" href="style.css">
</head>
<body>

<div class="ambient-light"></div>

<div class="app-container">
<nav class="sidebar glass">
<div class="brand">
<div class="logo-icon"></div>
<span>iHelmet Pro</span>
</div>

<ul class="menu">
<li class="menu-item active" data-target="view-dashboard">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 12h-4l-3 9L9 3l-3 9H2"></path></svg>
生命体征
</li>
<li class="menu-item" data-target="view-debug">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect><line x1="8" y1="21" x2="16" y2="21"></line><line x1="12" y1="17" x2="12" y2="21"></line></svg>
数据终端
</li>
</ul>

<div class="connection-card">
<div class="status-indicator">
<div id="statusDot" class="dot offline"></div>
<span id="statusText">等待连接…</span>
</div>
<button id="btnGlobalConnect" class="btn-mini">重连系统</button>
</div>
</nav>

<main class="main-area">

<div id="view-dashboard" class="view-section active">
<header class="view-header">
<h1>实时监控中心</h1>
<span class="last-update" id="lastUpdateTime">最后更新: –:–:–</span>
</header>

<div class="sensor-grid">
<div class="sensor-card">
<div class="card-icon temp-color">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 14.76V3.5a2.5 2.5 0 0 0-5 0v11.26a4.5 4.5 0 1 0 5 0z"></path></svg>
</div>
<div class="card-data">
<span class="label">环境温度</span>
<div class="value-group">
<span class="value" id="val-temp">–</span>
<span class="unit">°C</span>
</div>
</div>
</div>

<div class="sensor-card">
<div class="card-icon humid-color">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2.69l5.74 5.88a6 6 0 0 1-8.49 8.49 6 6 0 0 1 0-8.49L12 2.69z"></path></svg>
</div>
<div class="card-data">
<span class="label">环境湿度</span>
<div class="value-group">
<span class="value" id="val-humid">–</span>
<span class="unit">%</span>
</div>
</div>
</div>

<div class="sensor-card">
<div class="card-icon heart-color">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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"></path></svg>
</div>
<div class="card-data">
<span class="label">实时心率</span>
<div class="value-group">
<span class="value" id="val-hr">–</span>
<span class="unit">BPM</span>
</div>
</div>
</div>
</div>

<div class="alert-grid">
<div class="alert-card safe" id="alert-fall">
<span class="icon">🏃</span>
<span class="text">体态正常</span>
</div>
<div class="alert-card safe" id="alert-gas">
<span class="icon">☁️</span>
<span class="text">空气正常</span>
</div>
<div class="alert-card safe" id="alert-hr-status">
<span class="icon">❤️</span>
<span class="text">心律正常</span>
</div>
</div>

<div class="charts-container">
<div class="chart-box glass-card">
<h3>环境趋势 (Temp & Humid)</h3>
<div class="chart-wrapper">
<canvas id="chartEnvironment"></canvas>
</div>
</div>
<div class="chart-box glass-card">
<h3>心率趋势 (Heart Rate)</h3>
<div class="chart-wrapper">
<canvas id="chartHeart"></canvas>
</div>
</div>
</div>
</div>

<div id="view-debug" class="view-section hidden">
<header class="view-header">
<h1>原始数据流</h1>
<div class="header-actions">
<button class="action-btn" onclick="clearLog()">清屏</button>
</div>
</header>
<div class="terminal-window glass-card">
<div id="logArea" class="chat-container"></div>
</div>
</div>

</main>
</div>

<script src="script.js"></script>
</body>
</html>

frontend/style.css

:root {
–bg-color: #f5f5f7;
–card-bg: rgba(255, 255, 255, 0.75);
–text-main: #1d1d1f;
–radius: 18px;
–red-alert: #ff3b30;
–green-safe: #34c759;
}

body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
background-color: var(–bg-color);
color: var(–text-main);
height: 100vh;
overflow: hidden;
}

.ambient-light {
position: absolute;
width: 100%; height: 100%;
background: radial-gradient(circle at 20% 20%, #e0f7fa 0%, transparent 50%),
radial-gradient(circle at 80% 80%, #ffe0e0 0%, transparent 50%);
z-index: -1;
}

.app-container {
display: flex; height: 100vh; padding: 16px; box-sizing: border-box; gap: 16px;
}

/* Sidebar & Generic (复用之前的 style, 这里省略重复部分,只列出核心改动) */
.sidebar { width: 220px; background: var(–card-bg); backdrop-filter: blur(20px); border-radius: var(–radius); padding: 24px 16px; display: flex; flex-direction: column; border: 1px solid rgba(255,255,255,0.5); }
.brand { font-size: 18px; font-weight: 700; margin-bottom: 40px; display: flex; align-items: center; gap: 8px; }
.menu { list-style: none; padding: 0; flex: 1; }
.menu-item { padding: 12px; margin-bottom: 6px; border-radius: 12px; cursor: pointer; display: flex; align-items: center; gap: 10px; color: #666; transition: 0.2s; }
.menu-item.active { background: white; color: #0071e3; box-shadow: 0 4px 12px rgba(0,0,0,0.05); }
.menu-item svg { width: 20px; height: 20px; }
.connection-card { background: white; padding: 12px; border-radius: 12px; text-align: center; }
.status-indicator { display: flex; align-items: center; justify-content: center; gap: 6px; margin-bottom: 8px; font-size: 12px; }
.dot { width: 8px; height: 8px; border-radius: 50%; }
.dot.online { background: var(–green-safe); box-shadow: 0 0 6px var(–green-safe); }
.dot.offline { background: #999; }
.btn-mini { width: 100%; padding: 6px; border: none; background: #f0f0f0; border-radius: 8px; cursor: pointer; }

/* — Dashboard Specific — */
.main-area { flex: 1; position: relative; overflow-y: auto; padding-right: 5px; } /* 允许主区域滚动 */
.view-section { display: none; flex-direction: column; gap: 20px; padding-bottom: 20px; }
.view-section.active { display: flex; animation: fadeIn 0.4s ease; }

@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }

.view-header { display: flex; justify-content: space-between; align-items: flex-end; margin-bottom: 10px; }
.view-header h1 { margin: 0; font-size: 26px; }
.last-update { font-size: 12px; color: #888; }

/* 1. Sensor Grid */
.sensor-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
.sensor-card {
background: var(–card-bg); backdrop-filter: blur(20px);
border-radius: var(–radius); padding: 20px;
display: flex; align-items: center; gap: 16px;
box-shadow: 0 4px 20px rgba(0,0,0,0.02);
border: 1px solid rgba(255,255,255,0.6);
}
.card-icon {
width: 48px; height: 48px; border-radius: 12px;
display: flex; align-items: center; justify-content: center;
color: white;
}
.temp-color { background: linear-gradient(135deg, #FF9A9E 0%, #FECFEF 100%); color: #d63384; }
.humid-color { background: linear-gradient(135deg, #a18cd1 0%, #fbc2eb 100%); color: #6f42c1; }
.heart-color { background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 99%, #fecfef 100%); color: #ff3b30; }

.card-data { display: flex; flex-direction: column; }
.card-data .label { font-size: 13px; color: #666; font-weight: 500; }
.value-group { display: flex; align-items: baseline; gap: 4px; }
.card-data .value { font-size: 28px; font-weight: 700; color: var(–text-main); }
.card-data .unit { font-size: 14px; color: #888; }

/* 2. Alert Grid */
.alert-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
.alert-card {
padding: 16px; border-radius: 14px;
display: flex; align-items: center; justify-content: center; gap: 10px;
font-weight: 600; font-size: 15px;
transition: all 0.3s;
}
.alert-card.safe { background: rgba(52, 199, 89, 0.15); color: #006400; border: 1px solid rgba(52, 199, 89, 0.2); }
.alert-card.danger {
background: var(–red-alert); color: white;
box-shadow: 0 4px 15px rgba(255, 59, 48, 0.4);
animation: pulse 1s infinite;
}

@keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.02); } 100% { transform: scale(1); } }

/* 3. Charts */
.charts-container { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; height: 300px; }
.chart-box {
background: var(–card-bg); backdrop-filter: blur(20px);
border-radius: var(–radius); padding: 16px;
border: 1px solid rgba(255,255,255,0.6);
display: flex; flex-direction: column;
}
.chart-box h3 { margin: 0 0 10px 0; font-size: 14px; color: #666; }
.chart-wrapper { flex: 1; position: relative; width: 100%; height: 100%; }

/* Debug View */
.terminal-window {
height: 100%; background: #1e1e1e; border-radius: 16px;
padding: 16px; color: #00ff00; font-family: monospace; display: flex; flex-direction: column;
}
.chat-container { flex: 1; overflow-y: auto; font-size: 12px; }
.hidden { display: none; }

frontend/script.js

let ws = null;
let isConnected = false;

// 图表实例
let chartEnv = null; // 温湿度图表
let chartHeart = null; // 心率图表
const MAX_DATA_POINTS = 20; // 图表只保留最近20个点,让它动起来

// DOM 元素
const els = {
temp: document.getElementById('val-temp'),
humid: document.getElementById('val-humid'),
hr: document.getElementById('val-hr'),
alertFall: document.getElementById('alert-fall'),
alertGas: document.getElementById('alert-gas'),
alertHr: document.getElementById('alert-hr-status'),
time: document.getElementById('lastUpdateTime'),
statusDot: document.getElementById('statusDot'),
statusText: document.getElementById('statusText'),
btnConnect: document.getElementById('btnGlobalConnect'),
logArea: document.getElementById('logArea')
};

// 初始化
window.onload = () => {
initCharts(); // 初始化空图表
connectServer();
setupMenu();
};

// — 1. WebSocket 连接与数据处理 —

function connectServer() {
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
const host = window.location.hostname;
const port = window.location.port ? `:${window.location.port}` : '';
const url = `${protocol}://${host}${port}`;

log(`正在连接: ${url}…`);

try {
ws = new WebSocket(url);

ws.onopen = () => {
isConnected = true;
updateStatusUI(true);
log('连接成功');
};

ws.onclose = () => {
isConnected = false;
updateStatusUI(false);
log('连接断开');
};

ws.onmessage = (event) => {
handleIncomingData(event.data);
};

} catch (e) {
log('连接 URL 错误');
}
}

document.getElementById('btnGlobalConnect').addEventListener('click', () => {
if(isConnected && ws) ws.close();
else connectServer();
});

// — 2. 核心:解析协议并更新 UI —

function handleIncomingData(rawString) {
// 原始调试日志
log(`[RX] ${rawString}`);

/**
* 协议格式:
* 温度:%d°C\\n,湿度:%d%%\\n,摔倒标志:%d\\n,心率异常标志:%d\\n,气体异常标志:%d\\n,心率值:%d BPM\\n
* * 使用正则表达式提取数字。
* \\d+ 匹配数字
* [\\s\\S]*? 匹配中间的换行符和逗号等杂乱字符
*/

const regex = /温度:(\\d+).*湿度:(\\d+).*摔倒标志:(\\d+).*心率异常标志:(\\d+).*气体异常标志:(\\d+).*心率值:(\\d+)/s;
const match = rawString.match(regex);

if (match) {
const data = {
temp: parseInt(match[1]),
humid: parseInt(match[2]),
fall: parseInt(match[3]),
hrFlag: parseInt(match[4]),
gasFlag: parseInt(match[5]),
hrValue: parseInt(match[6])
};

updateDashboard(data);
} else {
log("数据格式不匹配,忽略");
}
}

function updateDashboard(data) {
const now = new Date();
const timeLabel = now.toLocaleTimeString();
els.time.innerText = `最后更新: ${timeLabel}`;

// A. 更新数值
els.temp.innerText = data.temp;
els.humid.innerText = data.humid;
els.hr.innerText = data.hrValue;

// B. 更新警报状态 (0=正常, 1=异常)
updateAlertCard(els.alertFall, data.fall, "🏃 体态正常", "⚠️ 检测到摔倒!");
updateAlertCard(els.alertHr, data.hrFlag, "❤️ 心律正常", "⚠️ 心率异常!");
updateAlertCard(els.alertGas, data.gasFlag, "☁️ 空气正常", "☠️ 有害气体!");

// C. 更新图表
updateChartData(chartEnv, timeLabel, [data.temp, data.humid]);
updateChartData(chartHeart, timeLabel, [data.hrValue]);
}

function updateAlertCard(element, status, safeText, dangerText) {
const iconSpan = element.querySelector('.icon');
const textSpan = element.querySelector('.text');

if (status === 1) {
element.className = 'alert-card danger';
textSpan.innerText = dangerText;
} else {
element.className = 'alert-card safe';
textSpan.innerText = safeText;
}
}

// — 3. 图表逻辑 (Chart.js) —

function initCharts() {
// 1. 温湿度图表
const ctxEnv = document.getElementById('chartEnvironment').getContext('2d');
chartEnv = new Chart(ctxEnv, {
type: 'line',
data: {
labels: [],
datasets: [
{
label: '温度 (°C)',
data: [],
borderColor: '#ff6b6b',
backgroundColor: 'rgba(255, 107, 107, 0.1)',
borderWidth: 2,
tension: 0.4, // 平滑曲线
fill: true
},
{
label: '湿度 (%)',
data: [],
borderColor: '#4ecdc4',
backgroundColor: 'rgba(78, 205, 196, 0.1)',
borderWidth: 2,
tension: 0.4,
fill: true
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: false, // 实时数据建议关闭复杂动画
scales: {
x: { display: false }, // 隐藏X轴文字,显得干净
y: { beginAtZero: false }
},
plugins: { legend: { position: 'top' } }
}
});

// 2. 心率图表
const ctxHr = document.getElementById('chartHeart').getContext('2d');
chartHeart = new Chart(ctxHr, {
type: 'line',
data: {
labels: [],
datasets: [{
label: '心率 (BPM)',
data: [],
borderColor: '#ff3b30',
backgroundColor: 'rgba(255, 59, 48, 0.1)',
borderWidth: 2,
tension: 0.3,
fill: true,
pointRadius: 3
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: false,
scales: {
x: { display: false },
y: { min: 40, max: 180 } // 设置合理的Y轴范围
}
}
});
}

function updateChartData(chart, label, newValues) {
// 添加 X 轴标签
chart.data.labels.push(label);

// 添加 Y 轴数据 (对应 datasets 中的每一条线)
chart.data.datasets.forEach((dataset, i) => {
dataset.data.push(newValues[i]);
});

// 保持数据长度,移除最早的数据 (滑动窗口)
if (chart.data.labels.length > MAX_DATA_POINTS) {
chart.data.labels.shift();
chart.data.datasets.forEach((dataset) => {
dataset.data.shift();
});
}

chart.update();
}

// — 4. 辅助功能 —

function setupMenu() {
document.querySelectorAll('.menu-item').forEach(item => {
item.addEventListener('click', () => {
document.querySelectorAll('.menu-item').forEach(i => i.classList.remove('active'));
document.querySelectorAll('.view-section').forEach(v => v.classList.remove('active'));
item.classList.add('active');
const targetId = item.getAttribute('data-target');
document.getElementById(targetId).classList.add('active');
});
});
}

function updateStatusUI(online) {
if (online) {
els.statusDot.className = 'dot online';
els.statusText.innerText = '系统在线';
els.btnConnect.innerText = '断开';
els.btnConnect.style.background = '#ff3b30';
els.btnConnect.style.color = 'white';
} else {
els.statusDot.className = 'dot offline';
els.statusText.innerText = '离线';
els.btnConnect.innerText = '重连';
els.btnConnect.style.background = '#f0f0f0';
els.btnConnect.style.color = 'black';
}
}

function clearLog() {
els.logArea.innerHTML = '';
}

function log(text) {
const div = document.createElement('div');
div.innerText = `[${new Date().toLocaleTimeString()}] ${text}`;
div.style.borderBottom = "1px solid #333";
div.style.padding = "4px 0";
els.logArea.appendChild(div);
els.logArea.scrollTop = els.logArea.scrollHeight;
}

效果图如下

赞(0)
未经允许不得转载:网硕互联帮助中心 » 4G模块应用,内网穿透,前端网页的制作第六讲(更新前端的数据显示)
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!