699 lines
23 KiB
HTML
699 lines
23 KiB
HTML
<!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 = { '&':'&','<':'<','>':'>','"':'"',"'":''' };
|
||
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>
|