版本1.2
This commit is contained in:
833
data/index.html
833
data/index.html
@@ -3,696 +3,459 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||||
<title>健康监测系统 · 设备配网</title>
|
<title>健康监测系统 - 配网设置</title>
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--bg: #f0f4ff;
|
--primary: #4A90D9;
|
||||||
--card: #ffffff;
|
--danger: #E74C3C;
|
||||||
--text: #1e293b;
|
--success: #27AE60;
|
||||||
--muted: #94a3b8;
|
--bg: #F5F7FA;
|
||||||
--border: #e2e8f0;
|
--card-bg: #FFFFFF;
|
||||||
--accent: #6366f1;
|
--text: #2C3E50;
|
||||||
--accent-hover: #4f46e5;
|
--text-light: #7F8C8D;
|
||||||
--accent-light: #eef2ff;
|
--border: #E1E8ED;
|
||||||
--green: #16a34a;
|
--radius: 12px;
|
||||||
--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; }
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
min-height: 100dvh;
|
padding: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
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 {
|
.container {
|
||||||
|
max-width: 420px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 440px;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Header ---- */
|
/* 头部 */
|
||||||
.header {
|
.header {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 12px 0 20px;
|
padding: 24px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.header .icon {
|
||||||
width: 60px;
|
font-size: 48px;
|
||||||
height: 60px;
|
margin-bottom: 8px;
|
||||||
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 {
|
.header h1 {
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: -0.3px;
|
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header .sub {
|
.header p {
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
color: var(--muted);
|
color: var(--text-light);
|
||||||
line-height: 1.5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Card ---- */
|
/* 卡片 */
|
||||||
.card {
|
.card {
|
||||||
background: var(--card);
|
background: var(--card-bg);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
padding: 20px 22px;
|
padding: 20px;
|
||||||
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;
|
margin-bottom: 12px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
|
||||||
}
|
}
|
||||||
.form-row:last-child {
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title .emoji {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表单 */
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-row label {
|
.form-group label {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 11.5px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
color: var(--text-light);
|
||||||
|
margin-bottom: 4px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.7px;
|
letter-spacing: 0.5px;
|
||||||
color: var(--accent);
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-row input {
|
.form-group input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 44px;
|
height: 44px;
|
||||||
padding: 0 14px;
|
padding: 0 14px;
|
||||||
|
border: 2px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
background: #f8fafc;
|
background: var(--bg);
|
||||||
border: 2px solid var(--border);
|
transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
outline: none;
|
outline: none;
|
||||||
-webkit-appearance: none;
|
|
||||||
transition: all var(--transition);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-row input:focus {
|
.form-group input:focus {
|
||||||
border-color: var(--accent);
|
border-color: var(--primary);
|
||||||
background: #fff;
|
box-shadow: 0 0 0 3px rgba(74, 144, 217, 0.15);
|
||||||
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.08);
|
background: var(--card-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-row input::placeholder {
|
.form-group input::placeholder {
|
||||||
color: #cbd5e1;
|
color: #BDC3C7;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* inline row */
|
/* 环境备注 - 高亮 */
|
||||||
.form-inline {
|
.env-note-card {
|
||||||
display: grid;
|
background: linear-gradient(135deg, #FFF9E6, #FFF3CD);
|
||||||
grid-template-columns: 1fr 1fr;
|
border: 2px solid #FFC107;
|
||||||
gap: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Highlight card (环境备注) ---- */
|
.env-note-card .card-title {
|
||||||
.card-highlight {
|
color: #856404;
|
||||||
border: 2px solid #fcd34d;
|
|
||||||
background: linear-gradient(135deg, #fffdf5, #fffbea);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-highlight .card-header {
|
.env-note-card input {
|
||||||
color: #92400e;
|
background: #FFFFFF;
|
||||||
}
|
border-color: #FFC107;
|
||||||
|
font-size: 16px;
|
||||||
.card-highlight label {
|
|
||||||
color: #b45309;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-highlight input {
|
|
||||||
background: #fff;
|
|
||||||
border-color: #fcd34d;
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-highlight input:focus {
|
.env-note-hint {
|
||||||
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;
|
font-size: 12px;
|
||||||
color: #92400e;
|
color: #856404;
|
||||||
opacity: 0.8;
|
margin-top: 6px;
|
||||||
|
padding-left: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Buttons ---- */
|
/* 提交按钮 */
|
||||||
.btn {
|
.btn-submit {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 14px;
|
border-radius: var(--radius);
|
||||||
font-size: 16px;
|
font-size: 17px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
letter-spacing: 0.5px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all var(--transition);
|
transition: background 0.2s, transform 0.1s;
|
||||||
display: flex;
|
margin-top: 8px;
|
||||||
align-items: center;
|
letter-spacing: 1px;
|
||||||
justify-content: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:active {
|
.btn-submit:active {
|
||||||
transform: scale(0.97);
|
transform: scale(0.98);
|
||||||
|
background: #357ABD;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-submit:hover {
|
||||||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
background: #3D7EC0;
|
||||||
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-reset {
|
||||||
}
|
width: 100%;
|
||||||
|
|
||||||
.btn-danger {
|
|
||||||
background: transparent;
|
|
||||||
color: var(--red);
|
|
||||||
border: 2px solid #fecaca;
|
|
||||||
margin-top: 10px;
|
|
||||||
height: 44px;
|
height: 44px;
|
||||||
font-size: 14px;
|
background: transparent;
|
||||||
|
color: var(--danger);
|
||||||
|
border: 2px solid var(--danger);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
font-size: 15px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: 8px;
|
||||||
|
transition: background 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-danger:active {
|
.btn-reset:active {
|
||||||
background: #fef2f2;
|
background: #FDEDEC;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Status bar ---- */
|
/* 状态栏 */
|
||||||
.status-bar {
|
.status-bar {
|
||||||
display: flex;
|
text-align: center;
|
||||||
align-items: center;
|
font-size: 12px;
|
||||||
justify-content: center;
|
color: var(--text-light);
|
||||||
gap: 8px;
|
padding: 16px 0;
|
||||||
padding: 18px 0;
|
|
||||||
font-size: 12.5px;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-dot {
|
.status-bar .dot {
|
||||||
width: 8px; height: 8px;
|
display: inline-block;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
flex-shrink: 0;
|
margin-right: 6px;
|
||||||
|
animation: pulse 2s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-dot.on { background: var(--green); animation: dotPulse 2s infinite; }
|
.dot.connected { background: var(--success); }
|
||||||
.status-dot.off { background: var(--red); animation: dotPulse 2s infinite; }
|
.dot.disconnected { background: var(--danger); }
|
||||||
.status-dot.wait{ background: var(--amber); animation: dotPulse 1s infinite; }
|
|
||||||
|
|
||||||
@keyframes dotPulse {
|
@keyframes pulse {
|
||||||
0%, 100% { opacity: 1; transform: scale(1); }
|
0%, 100% { opacity: 1; }
|
||||||
50% { opacity: 0.35; transform: scale(0.85); }
|
50% { opacity: 0.4; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Toast ---- */
|
/* Toast 提示 */
|
||||||
.toast-stack {
|
.toast {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 20px;
|
top: 20px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
z-index: 9999;
|
background: var(--success);
|
||||||
display: flex;
|
color: white;
|
||||||
flex-direction: column;
|
padding: 12px 24px;
|
||||||
gap: 8px;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast {
|
|
||||||
pointer-events: auto;
|
|
||||||
padding: 12px 20px;
|
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #fff;
|
z-index: 1000;
|
||||||
white-space: nowrap;
|
display: none;
|
||||||
text-align: center;
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||||
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 {
|
.toast.show {
|
||||||
opacity: 0;
|
display: block;
|
||||||
transform: translateY(-10px);
|
animation: slideDown 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast-ok { background: #16a34a; }
|
@keyframes slideDown {
|
||||||
.toast-warn { background: #f59e0b; color: #1e293b; }
|
from { opacity: 0; transform: translateX(-50%) translateY(-10px); }
|
||||||
.toast-error { background: #dc2626; }
|
to { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||||||
|
|
||||||
@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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<!-- Toast stack -->
|
|
||||||
<div class="toast-stack" id="toast-stack"></div>
|
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<!-- 头部 -->
|
||||||
<!-- ====== Header ====== -->
|
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="logo">
|
<div class="icon">🏥</div>
|
||||||
<!-- 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>
|
<h1>健康监测系统</h1>
|
||||||
<p class="sub">
|
<p>ESP32-S3 + 毫米波雷达 · 配网设置</p>
|
||||||
ESP32-S3 · 60G 毫米波雷达<br>五劳无感康养监测
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ====== WiFi 设置 ====== -->
|
<!-- WiFi 设置 -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-title">
|
||||||
<div class="icon-circle icon-wifi">📶</div>
|
<span class="emoji">📶</span> WiFi 设置
|
||||||
无线网络
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
<div class="form-row">
|
|
||||||
<label>WiFi 名称 (SSID)</label>
|
<label>WiFi 名称 (SSID)</label>
|
||||||
<input type="text" id="wifi-ssid" placeholder="选择或输入 WiFi 名称" autocomplete="off" autocorrect="off">
|
<input type="text" id="wifi-ssid" placeholder="请输入 WiFi 名称" autocomplete="off">
|
||||||
</div>
|
</div>
|
||||||
<button class="scan-btn" id="scan-btn" type="button">
|
<div class="form-group">
|
||||||
🔍 扫描附近 WiFi
|
|
||||||
</button>
|
|
||||||
<div class="ssid-list" id="ssid-list"></div>
|
|
||||||
|
|
||||||
<div class="form-row">
|
|
||||||
<label>WiFi 密码</label>
|
<label>WiFi 密码</label>
|
||||||
<input type="password" id="wifi-pass" placeholder="请输入 WiFi 密码">
|
<input type="password" id="wifi-pass" placeholder="请输入 WiFi 密码">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ====== MQTT 设置 ====== -->
|
<!-- MQTT 设置 -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-title">
|
||||||
<div class="icon-circle icon-mqtt">☁️</div>
|
<span class="emoji">☁️</span> MQTT 云平台设置
|
||||||
云平台连接
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
<div class="form-inline">
|
<label>MQTT 服务器地址</label>
|
||||||
<div class="form-row">
|
<input type="text" id="mqtt-server" placeholder="例如: 192.168.1.100" value="192.168.1.100">
|
||||||
<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>
|
||||||
|
<div class="form-group">
|
||||||
<div class="form-inline">
|
<label>MQTT 端口</label>
|
||||||
<div class="form-row">
|
<input type="number" id="mqtt-port" placeholder="1883" value="1883">
|
||||||
<label>用户名</label>
|
</div>
|
||||||
<input type="text" id="mqtt-user" placeholder="选填" autocomplete="off">
|
<div class="form-group">
|
||||||
</div>
|
<label>MQTT 用户名 (可选)</label>
|
||||||
<div class="form-row">
|
<input type="text" id="mqtt-user" placeholder="用户名">
|
||||||
<label>密码</label>
|
</div>
|
||||||
<input type="password" id="mqtt-pass" placeholder="选填">
|
<div class="form-group">
|
||||||
</div>
|
<label>MQTT 密码 (可选)</label>
|
||||||
|
<input type="password" id="mqtt-pass" placeholder="密码">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ====== 环境备注 ====== -->
|
<!-- 环境备注 - 重点突出 -->
|
||||||
<div class="card card-highlight">
|
<div class="card env-note-card">
|
||||||
<div class="card-header">
|
<div class="card-title">
|
||||||
<div class="icon-circle icon-room">🏠</div>
|
<span class="emoji">🏠</span> 环境备注
|
||||||
安装位置
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-group">
|
||||||
<label>房间名称 / 环境备注</label>
|
<label>设备位置 / 房间名称</label>
|
||||||
<input type="text" id="env-note" placeholder="如:主卧 / 老人房 / 客厅" autocomplete="off">
|
<input type="text" id="env-note" placeholder="例如: 主卧 / 客厅 / 老人房 / 婴儿房" value="主卧">
|
||||||
|
</div>
|
||||||
|
<div class="env-note-hint">
|
||||||
|
💡 请备注此设备安装的环境位置,方便在云平台上区分不同房间的数据
|
||||||
</div>
|
</div>
|
||||||
<p class="hint">
|
|
||||||
💡 请备注设备安装的房间,便于在云平台区分不同区域的数据
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ====== 设备 ID ====== -->
|
<!-- 设备设置 -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-title">
|
||||||
<div class="icon-circle icon-device">📱</div>
|
<span class="emoji">📱</span> 设备设置
|
||||||
设备标识
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-group">
|
||||||
<label>设备编号</label>
|
<label>设备名称 / ID</label>
|
||||||
<input type="text" id="device-id" placeholder="如:health_001" autocomplete="off">
|
<input type="text" id="device-id" placeholder="例如: health_001" value="health_001">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ====== 按钮组 ====== -->
|
<!-- 按钮 -->
|
||||||
<button class="btn btn-primary" id="btn-submit" type="button">
|
<button class="btn-submit" onclick="submitConfig()">
|
||||||
<span>💾</span> 保存并连接
|
💾 保存配置并连接
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger" id="btn-reset" type="button">
|
<button class="btn-reset" onclick="resetConfig()">
|
||||||
<span>🔄</span> 恢复出厂设置
|
🔄 恢复出厂设置
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- ====== 状态 ====== -->
|
<!-- 状态 -->
|
||||||
<div class="status-bar" id="status-bar">
|
<div class="status-bar" id="status-bar">
|
||||||
<span class="status-dot off"></span>
|
<span class="dot disconnected"></span> 等待配置...
|
||||||
<span id="status-text">等待配置…</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div><!-- /.container -->
|
<!-- Toast -->
|
||||||
|
<div class="toast" id="toast"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// ── 全局缓冲 ──────────────────────────────────
|
// 显示 Toast
|
||||||
const SELF = (() => {
|
function showToast(msg, duration = 3000) {
|
||||||
try {
|
const t = document.getElementById('toast');
|
||||||
return JSON.parse(localStorage.getItem('health_config') || '{}');
|
t.textContent = msg;
|
||||||
} catch (_) { return {}; }
|
t.classList.add('show');
|
||||||
})();
|
setTimeout(() => t.classList.remove('show'), duration);
|
||||||
|
|
||||||
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();
|
// 读取已保存的配置 (从 LocalStorage)
|
||||||
if (!ssid) {
|
function loadConfig() {
|
||||||
toast('⚠️ 请填写 WiFi 名称', 'warn');
|
try {
|
||||||
E('wifi-ssid').focus();
|
const saved = JSON.parse(localStorage.getItem('health_config') || '{}');
|
||||||
return;
|
if (saved.mqtt_server) document.getElementById('mqtt-server').value = saved.mqtt_server;
|
||||||
|
if (saved.mqtt_port) document.getElementById('mqtt-port').value = saved.mqtt_port;
|
||||||
|
if (saved.mqtt_user) document.getElementById('mqtt-user').value = saved.mqtt_user;
|
||||||
|
if (saved.mqtt_pass) document.getElementById('mqtt-pass').value = saved.mqtt_pass;
|
||||||
|
if (saved.device_id) document.getElementById('device-id').value = saved.device_id;
|
||||||
|
if (saved.env_note) document.getElementById('env-note').value = saved.env_note;
|
||||||
|
if (saved.wifi_ssid) document.getElementById('wifi-ssid').value = saved.wifi_ssid;
|
||||||
|
} catch(e) { /* ignore */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
saveLocal();
|
// 保存配置到 LocalStorage
|
||||||
setStatus('wait');
|
function saveConfigLocal() {
|
||||||
toast('📡 正在连接…', 'ok', 2000);
|
const config = {
|
||||||
|
wifi_ssid: document.getElementById('wifi-ssid').value,
|
||||||
|
wifi_pass: document.getElementById('wifi-pass').value,
|
||||||
|
mqtt_server: document.getElementById('mqtt-server').value,
|
||||||
|
mqtt_port: document.getElementById('mqtt-port').value,
|
||||||
|
mqtt_user: document.getElementById('mqtt-user').value,
|
||||||
|
mqtt_pass: document.getElementById('mqtt-pass').value,
|
||||||
|
device_id: document.getElementById('device-id').value,
|
||||||
|
env_note: document.getElementById('env-note').value,
|
||||||
|
};
|
||||||
|
localStorage.setItem('health_config', JSON.stringify(config));
|
||||||
|
}
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
// 提交配置
|
||||||
params.append('s', ssid);
|
function submitConfig() {
|
||||||
params.append('p', E('wifi-pass').value);
|
const envNote = document.getElementById('env-note').value.trim();
|
||||||
params.append('ms', E('mqtt-server').value);
|
if (!envNote) {
|
||||||
params.append('mp', E('mqtt-port').value);
|
showToast('⚠️ 请填写环境备注 (设备安装位置)');
|
||||||
params.append('mu', E('mqtt-user').value);
|
document.getElementById('env-note').focus();
|
||||||
params.append('mw', E('mqtt-pass').value);
|
return;
|
||||||
params.append('id', E('device-id').value);
|
}
|
||||||
params.append('env',env);
|
|
||||||
|
|
||||||
fetch('/save', { method: 'POST', body: params })
|
const ssid = document.getElementById('wifi-ssid').value.trim();
|
||||||
.then(() => {
|
if (!ssid) {
|
||||||
setStatus('on');
|
showToast('⚠️ 请填写 WiFi 名称');
|
||||||
toast('🎉 设备已上线!WiFi 连接成功');
|
document.getElementById('wifi-ssid').focus();
|
||||||
})
|
return;
|
||||||
.catch(() => {
|
}
|
||||||
setStatus('ok');
|
|
||||||
toast('📋 配置已保存(设备端已接收)');
|
// 保存到本地
|
||||||
|
saveConfigLocal();
|
||||||
|
|
||||||
|
// 构建提交参数
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append('s', ssid);
|
||||||
|
params.append('p', document.getElementById('wifi-pass').value);
|
||||||
|
params.append('ms', document.getElementById('mqtt-server').value);
|
||||||
|
params.append('mp', document.getElementById('mqtt-port').value);
|
||||||
|
params.append('mu', document.getElementById('mqtt-user').value);
|
||||||
|
params.append('mw', document.getElementById('mqtt-pass').value);
|
||||||
|
params.append('id', document.getElementById('device-id').value);
|
||||||
|
params.append('env', envNote);
|
||||||
|
|
||||||
|
// 更新状态
|
||||||
|
document.getElementById('status-bar').innerHTML =
|
||||||
|
'<span class="dot connected"></span> 正在保存配置...';
|
||||||
|
|
||||||
|
showToast('✅ 配置已保存! 设备正在连接...');
|
||||||
|
|
||||||
|
// 实际设备中,这里通过 WiFiManager API 提交
|
||||||
|
// 开发环境中模拟提交
|
||||||
|
fetch('/save', {
|
||||||
|
method: 'POST',
|
||||||
|
body: params
|
||||||
|
}).then(() => {
|
||||||
|
document.getElementById('status-bar').innerHTML =
|
||||||
|
'<span class="dot connected"></span> ✅ 配置成功! WiFi 已连接';
|
||||||
|
showToast('🎉 连接成功! 设备已上线');
|
||||||
|
}).catch(() => {
|
||||||
|
// 如果是离线 HTML (直接打开), 提示用户
|
||||||
|
document.getElementById('status-bar').innerHTML =
|
||||||
|
'<span class="dot connected"></span> 📋 配置已准备 (请通过 ESP32 配网页面提交)';
|
||||||
|
showToast('📋 配置参数已保存到本地浏览器');
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// ── 重置 ──────────────────────────────────────
|
|
||||||
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 = { '&':'&','<':'<','>':'>','"':'"',"'":''' };
|
|
||||||
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;
|
function resetConfig() {
|
||||||
setInterval(() => {
|
if (confirm('确定要恢复出厂设置吗? 所有配置将被清除!')) {
|
||||||
const txt = E('status-text');
|
localStorage.removeItem('health_config');
|
||||||
if (txt.textContent.includes('等待配置')) {
|
document.querySelectorAll('input').forEach(el => {
|
||||||
const frames = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
|
if (el.type !== 'button') {
|
||||||
idleTick = (idleTick + 1) % frames.length;
|
el.value = el.defaultValue || '';
|
||||||
// 只在未连接时显示动画
|
}
|
||||||
|
});
|
||||||
|
document.getElementById('env-note').value = '主卧';
|
||||||
|
document.getElementById('mqtt-server').value = '192.168.1.100';
|
||||||
|
document.getElementById('mqtt-port').value = '1883';
|
||||||
|
document.getElementById('device-id').value = 'health_001';
|
||||||
|
showToast('🔄 已恢复出厂设置');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, 300);
|
|
||||||
|
// 页面加载
|
||||||
|
loadConfig();
|
||||||
|
|
||||||
|
// 每 5 秒检查设备状态 (模拟)
|
||||||
|
setInterval(() => {
|
||||||
|
const bar = document.getElementById('status-bar');
|
||||||
|
if (!bar.textContent.includes('连接成功') && !bar.textContent.includes('配置已准备')) {
|
||||||
|
const dots = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
|
||||||
|
const d = dots[Math.floor(Date.now()/300) % dots.length];
|
||||||
|
bar.innerHTML = `<span class="dot disconnected"></span> ${d} 等待配置...`;
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user