Files
health-wellness-device-2/data/index.html

699 lines
23 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
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-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>健康监测系统 · 设备配网</title>
<style>
:root {
--bg: #f0f4ff;
--card: #ffffff;
--text: #1e293b;
--muted: #94a3b8;
--border: #e2e8f0;
--accent: #6366f1;
--accent-hover: #4f46e5;
--accent-light: #eef2ff;
--green: #16a34a;
--red: #dc2626;
--amber: #f59e0b;
--radius: 16px;
--radius-sm: 12px;
--transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
min-height: 100dvh;
display: flex;
flex-direction: column;
align-items: center;
padding: 24px 16px 40px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* ---- Decorative blobs ---- */
body::before,
body::after {
content: '';
position: fixed;
border-radius: 50%;
pointer-events: none;
z-index: 0;
}
body::before {
top: -180px; right: -180px;
width: 380px; height: 380px;
background: radial-gradient(circle, rgba(99,102,241,0.07) 0%, transparent 70%);
}
body::after {
bottom: -120px; left: -120px;
width: 300px; height: 300px;
background: radial-gradient(circle, rgba(34,197,94,0.05) 0%, transparent 70%);
}
.container {
width: 100%;
max-width: 440px;
position: relative;
z-index: 1;
}
/* ---- Header ---- */
.header {
text-align: center;
padding: 12px 0 20px;
}
.logo {
width: 60px;
height: 60px;
margin: 0 auto 14px;
background: linear-gradient(135deg, #6366f1, #8b5cf6);
border-radius: 18px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 10px 28px rgba(99, 102, 241, 0.28);
}
.logo svg {
width: 30px;
height: 30px;
}
.header h1 {
font-size: 22px;
font-weight: 700;
letter-spacing: -0.3px;
color: var(--text);
margin-bottom: 4px;
}
.header .sub {
font-size: 13px;
color: var(--muted);
line-height: 1.5;
}
/* ---- Card ---- */
.card {
background: var(--card);
border-radius: var(--radius);
padding: 20px 22px;
margin-bottom: 14px;
box-shadow:
0 1px 3px rgba(0,0,0,0.03),
0 6px 18px rgba(0,0,0,0.05);
transition: box-shadow var(--transition);
}
.card-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
font-size: 15px;
font-weight: 700;
color: var(--text);
}
.card-header .icon-circle {
width: 32px; height: 32px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
flex-shrink: 0;
}
.icon-wifi { background: #eef2ff; color: #6366f1; }
.icon-mqtt { background: #ecfdf5; color: #16a34a; }
.icon-room { background: #fffbeb; color: #f59e0b; }
.icon-device{ background: #fdf2f8; color: #db2777; }
/* ---- Form ---- */
.form-row {
margin-bottom: 12px;
}
.form-row:last-child {
margin-bottom: 0;
}
.form-row label {
display: block;
font-size: 11.5px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.7px;
color: var(--accent);
margin-bottom: 5px;
}
.form-row input {
width: 100%;
height: 44px;
padding: 0 14px;
font-size: 15px;
color: var(--text);
background: #f8fafc;
border: 2px solid var(--border);
border-radius: var(--radius-sm);
outline: none;
-webkit-appearance: none;
transition: all var(--transition);
}
.form-row input:focus {
border-color: var(--accent);
background: #fff;
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.08);
}
.form-row input::placeholder {
color: #cbd5e1;
}
/* inline row */
.form-inline {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
/* ---- Highlight card (环境备注) ---- */
.card-highlight {
border: 2px solid #fcd34d;
background: linear-gradient(135deg, #fffdf5, #fffbea);
}
.card-highlight .card-header {
color: #92400e;
}
.card-highlight label {
color: #b45309;
}
.card-highlight input {
background: #fff;
border-color: #fcd34d;
font-weight: 500;
}
.card-highlight input:focus {
border-color: var(--amber);
box-shadow: 0 0 0 4px rgba(245, 158, 11, 0.1);
}
.card-highlight .hint {
margin-top: 6px;
font-size: 12px;
color: #92400e;
opacity: 0.8;
}
/* ---- Buttons ---- */
.btn {
width: 100%;
height: 50px;
border: none;
border-radius: 14px;
font-size: 16px;
font-weight: 600;
letter-spacing: 0.5px;
cursor: pointer;
transition: all var(--transition);
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn:active {
transform: scale(0.97);
}
.btn-primary {
background: linear-gradient(135deg, #6366f1, #8b5cf6);
color: #fff;
box-shadow: 0 6px 20px rgba(99, 102, 241, 0.28);
margin-top: 4px;
}
.btn-primary:active {
box-shadow: 0 3px 10px rgba(99, 102, 241, 0.25);
}
.btn-danger {
background: transparent;
color: var(--red);
border: 2px solid #fecaca;
margin-top: 10px;
height: 44px;
font-size: 14px;
font-weight: 500;
}
.btn-danger:active {
background: #fef2f2;
}
/* ---- Status bar ---- */
.status-bar {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 18px 0;
font-size: 12.5px;
color: var(--muted);
}
.status-dot {
width: 8px; height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.status-dot.on { background: var(--green); animation: dotPulse 2s infinite; }
.status-dot.off { background: var(--red); animation: dotPulse 2s infinite; }
.status-dot.wait{ background: var(--amber); animation: dotPulse 1s infinite; }
@keyframes dotPulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.35; transform: scale(0.85); }
}
/* ---- Toast ---- */
.toast-stack {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 9999;
display: flex;
flex-direction: column;
gap: 8px;
pointer-events: none;
}
.toast {
pointer-events: auto;
padding: 12px 20px;
border-radius: 24px;
font-size: 14px;
font-weight: 500;
color: #fff;
white-space: nowrap;
text-align: center;
box-shadow: 0 6px 20px rgba(0,0,0,0.15);
animation: toastIn 0.35s ease forwards;
transition: opacity 0.3s, transform 0.3s;
}
.toast.out {
opacity: 0;
transform: translateY(-10px);
}
.toast-ok { background: #16a34a; }
.toast-warn { background: #f59e0b; color: #1e293b; }
.toast-error { background: #dc2626; }
@keyframes toastIn {
from { opacity: 0; transform: translateY(-12px) scale(0.95); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
/* ---- WiFi scan list ---- */
.scan-btn {
font-size: 12px; font-weight: 600;
color: var(--accent); background: var(--accent-light);
border: none; padding: 6px 14px; border-radius: 20px;
cursor: pointer; transition: all var(--transition);
margin-top: 4px; margin-bottom: 8px;
}
.scan-btn:active { background: #dde2ff; }
.ssid-list {
max-height: 180px; overflow-y: auto;
border: 2px solid var(--border); border-radius: var(--radius-sm);
margin-bottom: 10px; display: none;
}
.ssid-list.open { display: block; }
.ssid-item {
padding: 11px 14px; font-size: 14px; border-bottom: 1px solid #f1f5f9;
cursor: pointer; transition: background 0.15s;
display: flex; justify-content: space-between; align-items: center;
}
.ssid-item:last-child { border-bottom: none; }
.ssid-item:hover, .ssid-item:active { background: #f8fafc; }
.ssid-item .rssi { font-size: 12px; color: var(--muted); }
/* ---- Responsive ---- */
@media (max-width: 380px) {
body { padding: 12px 12px 32px; }
.card { padding: 16px 16px; }
.header h1 { font-size: 20px; }
.logo { width: 48px; height: 48px; border-radius: 14px; }
.logo svg { width: 24px; height: 24px; }
}
</style>
</head>
<body>
<!-- Toast stack -->
<div class="toast-stack" id="toast-stack"></div>
<div class="container">
<!-- ====== Header ====== -->
<div class="header">
<div class="logo">
<!-- Medical cross / heart icon -->
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 5.5C12 3.02 14.02 1 16.5 1S21 3.02 21 5.5c0 4.5-6 10.5-9 13.5-3-3-9-9-9-13.5C3 3.02 5.02 1 7.5 1S12 3.02 12 5.5z"
fill="white" stroke="none"/>
<circle cx="17" cy="6" r="1.8" fill="#6366f1" opacity="0.5"/>
</svg>
</div>
<h1>健康监测系统</h1>
<p class="sub">
ESP32-S3 · 60G 毫米波雷达<br>五劳无感康养监测
</p>
</div>
<!-- ====== WiFi 设置 ====== -->
<div class="card">
<div class="card-header">
<div class="icon-circle icon-wifi">📶</div>
无线网络
</div>
<div class="form-row">
<label>WiFi 名称 (SSID)</label>
<input type="text" id="wifi-ssid" placeholder="选择或输入 WiFi 名称" autocomplete="off" autocorrect="off">
</div>
<button class="scan-btn" id="scan-btn" type="button">
🔍 扫描附近 WiFi
</button>
<div class="ssid-list" id="ssid-list"></div>
<div class="form-row">
<label>WiFi 密码</label>
<input type="password" id="wifi-pass" placeholder="请输入 WiFi 密码">
</div>
</div>
<!-- ====== MQTT 设置 ====== -->
<div class="card">
<div class="card-header">
<div class="icon-circle icon-mqtt">☁️</div>
云平台连接
</div>
<div class="form-inline">
<div class="form-row">
<label>服务器地址</label>
<input type="text" id="mqtt-server" placeholder="IP 或域名">
</div>
<div class="form-row">
<label>端口</label>
<input type="number" id="mqtt-port" placeholder="1883">
</div>
</div>
<div class="form-inline">
<div class="form-row">
<label>用户名</label>
<input type="text" id="mqtt-user" placeholder="选填" autocomplete="off">
</div>
<div class="form-row">
<label>密码</label>
<input type="password" id="mqtt-pass" placeholder="选填">
</div>
</div>
</div>
<!-- ====== 环境备注 ====== -->
<div class="card card-highlight">
<div class="card-header">
<div class="icon-circle icon-room">🏠</div>
安装位置
</div>
<div class="form-row">
<label>房间名称 / 环境备注</label>
<input type="text" id="env-note" placeholder="如:主卧 / 老人房 / 客厅" autocomplete="off">
</div>
<p class="hint">
💡 请备注设备安装的房间,便于在云平台区分不同区域的数据
</p>
</div>
<!-- ====== 设备 ID ====== -->
<div class="card">
<div class="card-header">
<div class="icon-circle icon-device">📱</div>
设备标识
</div>
<div class="form-row">
<label>设备编号</label>
<input type="text" id="device-id" placeholder="如health_001" autocomplete="off">
</div>
</div>
<!-- ====== 按钮组 ====== -->
<button class="btn btn-primary" id="btn-submit" type="button">
<span>💾</span> 保存并连接
</button>
<button class="btn btn-danger" id="btn-reset" type="button">
<span>🔄</span> 恢复出厂设置
</button>
<!-- ====== 状态 ====== -->
<div class="status-bar" id="status-bar">
<span class="status-dot off"></span>
<span id="status-text">等待配置…</span>
</div>
</div><!-- /.container -->
<script>
// ── 全局缓冲 ──────────────────────────────────
const SELF = (() => {
try {
return JSON.parse(localStorage.getItem('health_config') || '{}');
} catch (_) { return {}; }
})();
const E = id => document.getElementById(id);
// ── Toast ─────────────────────────────────────
function toast(msg, type = 'ok', duration = 2800) {
const stack = E('toast-stack');
const el = document.createElement('div');
el.className = `toast toast-${type}`;
el.textContent = msg;
stack.appendChild(el);
setTimeout(() => {
el.classList.add('out');
setTimeout(() => el.remove(), 300);
}, duration);
}
// ── 状态栏 ────────────────────────────────────
function setStatus(state) {
const dot = E('status-bar').querySelector('.status-dot');
const txt = E('status-text');
dot.className = `status-dot ${state}`;
const map = {
on: '✅ 已连接',
off: '⏳ 等待配置…',
wait: '⏳ 连接中…',
ok: '📋 配置已就绪',
};
txt.textContent = map[state] || state;
}
// ── 加载已保存配置 ────────────────────────────
function loadConfig() {
const f = (id, val) => { if (val) E(id).value = val; };
f('wifi-ssid', SELF.wifi_ssid);
f('mqtt-server', SELF.mqtt_server || '202.110.234.102');
f('mqtt-port', SELF.mqtt_port || '8000');
f('mqtt-user', SELF.mqtt_user || 'test');
f('mqtt-pass', SELF.mqtt_pass || '');
f('device-id', SELF.device_id || '10100001001');
f('env-note', SELF.env_note || '主卧');
}
// ── 持久化到 localStorage ────────────────────
function saveLocal() {
const cfg = {
wifi_ssid: E('wifi-ssid').value,
wifi_pass: E('wifi-pass').value,
mqtt_server: E('mqtt-server').value,
mqtt_port: E('mqtt-port').value,
mqtt_user: E('mqtt-user').value,
mqtt_pass: E('mqtt-pass').value,
device_id: E('device-id').value,
env_note: E('env-note').value,
};
localStorage.setItem('health_config', JSON.stringify(cfg));
}
// ── 提交 ──────────────────────────────────────
function submitConfig() {
const env = E('env-note').value.trim();
if (!env) {
toast('⚠️ 请先填写安装位置(房间名称)', 'warn');
E('env-note').focus();
return;
}
const ssid = E('wifi-ssid').value.trim();
if (!ssid) {
toast('⚠️ 请填写 WiFi 名称', 'warn');
E('wifi-ssid').focus();
return;
}
saveLocal();
setStatus('wait');
toast('📡 正在连接…', 'ok', 2000);
const params = new URLSearchParams();
params.append('s', ssid);
params.append('p', E('wifi-pass').value);
params.append('ms', E('mqtt-server').value);
params.append('mp', E('mqtt-port').value);
params.append('mu', E('mqtt-user').value);
params.append('mw', E('mqtt-pass').value);
params.append('id', E('device-id').value);
params.append('env',env);
fetch('/save', { method: 'POST', body: params })
.then(() => {
setStatus('on');
toast('🎉 设备已上线WiFi 连接成功');
})
.catch(() => {
setStatus('ok');
toast('📋 配置已保存(设备端已接收)');
});
}
// ── 重置 ──────────────────────────────────────
function resetConfig() {
if (!confirm('⚠️ 确定恢复出厂设置吗?\n\n所有 WiFi / MQTT / 设备配置将被清除,设备将重启。')) return;
localStorage.removeItem('health_config');
['wifi-ssid','wifi-pass','mqtt-server','mqtt-port','mqtt-user','mqtt-pass','device-id','env-note'].forEach(id => {
E(id).value = '';
});
E('mqtt-server').value = '202.110.234.102';
E('mqtt-port').value = '8000';
E('mqtt-user').value = 'test';
E('device-id').value = '10100001001';
E('env-note').value = '主卧';
setStatus('off');
toast('🔄 已恢复出厂设置');
}
// ── WiFi 扫描 (模拟 - 实际由 ESP32 提供) ────
function scanWiFi() {
const btn = E('scan-btn');
const list = E('ssid-list');
btn.textContent = '⏳ 扫描中…';
btn.disabled = true;
fetch('/scan')
.then(r => r.json())
.then(nets => {
list.innerHTML = '';
nets.forEach(n => {
const div = document.createElement('div');
div.className = 'ssid-item';
const bars = n.rssi > -50 ? '▂▄▆█' : n.rssi > -70 ? '▂▄▆' : '▂▄';
div.innerHTML = `<span>${escHtml(n.ssid)}</span><span class="rssi">${bars} ${n.rssi}dBm</span>`;
div.addEventListener('click', () => {
E('wifi-ssid').value = n.ssid;
list.classList.remove('open');
});
list.appendChild(div);
});
list.classList.add('open');
btn.textContent = '🔍 扫描附近 WiFi';
btn.disabled = false;
})
.catch(() => {
// 离线/ESP32 未连接时用 demo 数据
const demo = [
{ ssid: 'TP-LINK_Health', rssi: -42 },
{ ssid: 'ChinaNet-5G', rssi: -58 },
{ ssid: 'Xiaomi_Home', rssi: -63 },
{ ssid: 'ASUS_Medical_Ward', rssi: -71 },
{ ssid: 'HUAWEI-WiFi6', rssi: -77 },
];
list.innerHTML = '';
demo.forEach(n => {
const div = document.createElement('div');
div.className = 'ssid-item';
const bars = n.rssi > -50 ? '▂▄▆█' : n.rssi > -70 ? '▂▄▆' : '▂▄';
div.innerHTML = `<span>${escHtml(n.ssid)}</span><span class="rssi">${bars} ${n.rssi}dBm</span>`;
div.addEventListener('click', () => {
E('wifi-ssid').value = n.ssid;
list.classList.remove('open');
});
list.appendChild(div);
});
list.classList.add('open');
btn.textContent = '🔍 扫描附近 WiFi';
btn.disabled = false;
toast('⚠️ 离线模式,显示模拟列表', 'warn');
});
}
function escHtml(s) {
const m = { '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;' };
return String(s).replace(/[&<>"']/g, c => m[c]);
}
// ── Init ──────────────────────────────────────
loadConfig();
E('btn-submit').addEventListener('click', submitConfig);
E('btn-reset').addEventListener('click', resetConfig);
E('scan-btn').addEventListener('click', scanWiFi);
// 点击输入框时关闭 SSID 列表
document.addEventListener('click', e => {
if (!e.target.closest('#ssid-list') && !e.target.closest('#scan-btn')) {
E('ssid-list').classList.remove('open');
}
});
// ── 空闲动画 ──────────────────────────────────
let idleTick = 0;
setInterval(() => {
const txt = E('status-text');
if (txt.textContent.includes('等待配置')) {
const frames = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
idleTick = (idleTick + 1) % frames.length;
// 只在未连接时显示动画
}
}, 300);
</script>
</body>
</html>