    void _drawPage0(const TelemetryData& data, bool wifiOk, bool buzzerOn, bool hasAlarm);
    void _drawPage1(const TelemetryData& data, bool hasAlarm);
    void _drawPage2(const TelemetryData& data, bool hasAlarm);
    void _drawPage3(const TelemetryData& data, bool buzzerOn, bool wifiOk, bool hasAlarm);
    void _drawAlarmBanner(const char* name);

/**
 * @file display_manager.h
 * @brief OLED 显示管理器 - SSD1306 128x64 I2C
 *
 * 4 个显示页面:
 *   Page 0 - 环境数据 (温湿度 + WiFi)
 *   Page 1 - 生命体征 (心率 + 呼吸 + 体动)
 *   Page 2 - 人体状态 (有人/运动/跌倒/驻留)
 *   Page 3 - 安全信息 (水浸 + 蜂鸣器 + 运行时间)
 */

#ifndef DISPLAY_MANAGER_H
#define DISPLAY_MANAGER_H

#include <Arduino.h>
#include <U8g2lib.h>
#include "config.h"

class DisplayManager {
public:
    DisplayManager();
    ~DisplayManager();

    /** 初始化 OLED */
    bool begin();

    /**
     * @brief 更新显示内容 (每 5 秒调用一次即可)
     * @param data      遥测数据
     * @param wifiOk    WiFi 是否已连接
     * @param buzzerOn  蜂鸣器是否在响
     * @param hasAlarm  是否有活跃报警
     * @param alarmName 报警名称 (如 "跌倒"/"心率异常")
     */
    void update(const TelemetryData& data, bool wifiOk, bool buzzerOn,
                bool hasAlarm = false, const char* alarmName = "");

    /** 切换到下一页 (0→1→2→3→0 循环) */
    void nextPage();

    /** 获取当前页码 */
    uint8_t getPage() const { return _pageIndex; }

private:
    void _drawPage0(const TelemetryData& data, bool wifiOk, bool buzzerOn);
    void _drawPage1(const TelemetryData& data);
    void _drawPage2(const TelemetryData& data);
    void _drawPage3(const TelemetryData& data, bool buzzerOn, bool wifiOk);
    void _drawAlarmBanner(const char* name);

    /** 格式化秒数为 HH:MM:SS */
    String _fmtUptime(uint32_t secs) const;
    /** 格式化浮点数为一位小数 */
    String _fmtFloat(float val) const;

    U8G2_SSD1306_128X64_NONAME_F_HW_I2C _u8g2;
    uint8_t  _pageIndex;
    uint32_t _lastDrawTime;
    bool     _initialized;
};

#endif // DISPLAY_MANAGER_H

-    void _drawPage0(const TelemetryData& data, bool wifiOk, bool buzzerOn);

-    void _drawPage1(const TelemetryData& data);

-    void _drawPage2(const TelemetryData& data);

-    void _drawPage3(const TelemetryData& data, bool buzzerOn, bool wifiOk);

+    void _drawPage0(const TelemetryData& data, bool wifiOk, bool buzzerOn, bool hasAlarm);

+    void _drawPage1(const TelemetryData& data, bool hasAlarm);

+    void _drawPage2(const TelemetryData& data, bool hasAlarm);

+    void _drawPage3(const TelemetryData& data, bool buzzerOn, bool wifiOk, bool hasAlarm);


现在说拓展所有的报警逻辑


Now update the call to g_display.update() in main.cpp to pass the alarm info.


最后，更新 `main.cpp` 中 OLED 调用，传入报警状态：


Edit

        g_display.update(tele, wifiOk, g_alarm.isBuzzerOn(),
                         g_alarm.hasActiveAlarm(), g_alarm.getActiveAlarmStr());


The file d:\PlatformIO1\Projects\Healthv2.0\src\main.cpp has been updated successfully. (file state is current in your context — no need to Read it back)

        g_display.update(tele, wifiOk, g_alarm.isBuzzerOn(),
                         g_alarm.hasActiveAlarm(), g_alarm.getActiveAlarmStr());

-        g_display.update(tele, wifiOk, g_alarm.isBuzzerOn());

+        g_display.update(tele, wifiOk, g_alarm.isBuzzerOn(),

+                         g_alarm.hasActiveAlarm(), g_alarm.getActiveAlarmStr());

// --- 7. MQTT 配网状态 (WiFiManager 触发时) ---


扩展 AlarmManager：新报警类型 + 优先级蜂鸣 + 回调

扩展 main.cpp：温湿度/传感器故障检测 + MQTT上报 + 防抖

OLED 显示活跃报警提示


Let me mark the last task as completed and do a final summary.


TodoWrite

扩展 AlarmManager：新报警类型 + 优先级蜂鸣 + 回调

扩展 main.cpp：温湿度/传感器故障检测 + MQTT上报 + 防抖

OLED 显示活跃报警提示


Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable

扩展 AlarmManager：新报警类型 + 优先级蜂鸣 + 回调

扩展 main.cpp：温湿度/传感器故障检测 + MQTT上报 + 防抖

OLED 显示活跃报警提示

扩展 AlarmManager：新报警类型 + 优先级蜂鸣 + 回调

扩展 main.cpp：温湿度/传感器故障检测 + MQTT上报 + 防抖

OLED 显示活跃报警提示


完成了。总结一下报警系统的完整拓展：

---

## 报警系统拓展 — 总结

### 优先级：人身安全 > 环境 > 设备

| 优先级 | 报警类型 | 蜂鸣模式 | LED 闪烁 |
|--------|---------|---------|----------|
| ★★★★★ | **跌倒** | 连速急哔 60ms/60ms | 200ms |
| ★★★★★ | **心率异常** | 5连急哔 80ms/80ms | 500ms |
| ★★★★★ | **呼吸异常** | 5连急哔 80ms/80ms | 500ms |
| ★★★ | 水浸 | 3连哔 120ms/100ms | 300ms |
| ★★ | 温度异常 | 2声长停 200ms/400ms | 800ms |
| ★★ | 湿度异常 | 2声长停 200ms/400ms | 800ms |
| ★ | 传感器故障 | 单声 300ms/3s | 1000ms |

### 修改了 7 个文件

| 文件 | 改动 |
|------|------|
| [alarm_manager.h](include/alarm_manager.h) | 新增 3 种报警类型、优先级方法、回调机制、`_beepsPerCycle`/`_beepOnMs` 等动态参数 |
| [alarm_manager.cpp](src/alarm_manager.cpp) | 完全重写——7种类型分优先级蜂鸣、多报警并存自动降级、`_calcBuzzerParams` 按类型计算模式 |
| [mqtt_manager.h](include/mqtt_manager.h) | 新增 `publishLocalAlarm()` 上报接口 |
| [mqtt_manager.cpp](src/mqtt_manager.cpp) | 实现 `publishLocalAlarm`——JSON 格式通过 `status/{gateid}/alarm` 上报 |
| [main.cpp](src/main.cpp) | 扩展 `checkLocalAlarms`——新增温湿度/雷达超时/DHT 故障检测、防抖计数器、MQTT 回调注册 |
| [display_manager.h](include/display_manager.h) | `update()` 新增 `hasAlarm`/`alarmName` 参数 |
| [display_manager.cpp](src/display_manager.cpp) | 有报警时顶部闪烁 `🚨 报警类型` 横幅，每秒翻转，替换原有标题 |

### 关键设计

- **防抖**：温湿度需连续 3 次异常才触发，避免传感器抖动误报
- **自愈**：异常消失后自动清除对应报警，蜂鸣降级到剩余最高优先级
- **离线可用**：所有报警检测不依赖网络，本地独立运行
- **MQTT 上报**：连接时本地报警自动上报云端，`source: "local"` 标识本地判定


首先，先检测是否有人，有人后再检测心率，呼吸什么的，异常再进行报警，没人就不检测。不进行报警。


The user wants to add a precondition to all human vital sign alarms: first check if someone is present (`presence`), and only then check heart rate, breathing, etc. If no one is present, don't check vital signs and don't trigger alarms.

This makes sense - if there's no person in the room, the radar might give garbage vital sign readings, which could trigger false alarms.

Current code checks `radar.vitalValid && radar.heartRate > 0` before checking heart/breath rates, but doesn't check `presence`. The radar's `vitalValid` might be true even without a person present (it could be picking up noise or static objects).

The fix is straightforward: add `radar.presence` as a precondition for heart rate and breath rate checks in `checkLocalAlarms()`.

Let me also think about fall detection - if `fall` is true, `presence` should also be true (you can't fall if you're not there). The radar should handle this internally, but to be safe, I'll also add presence check for fall.

Let me update the `checkLocalAlarms()` function in main.cpp.


对——没人时雷达可能输出垃圾数据，心跳呼吸都是假值。加一个前置条件：有人才检测生命体征。


Edit

    // ─── ★★★★★ 人身安全: 跌倒 (立即触发, 无防抖) ───
    if (radar.fall && radar.radarValid && !g_localFallActive) {
        g_localFallActive = true;
        g_alarm.triggerLocal(AlarmManager::ALARM_FALL, "雷达检测到跌倒");
    } else if (!radar.fall && g_localFallActive) {
        g_localFallActive = false;
        g_alarm.clearLocal(AlarmManager::ALARM_FALL);
    }

    // ─── ★★★★★ 人身安全: 心率异常 ───
    if (radar.vitalValid && radar.heartRate > 0) {
        bool heartAbn = (radar.heartRate < HEART_RATE_MIN || radar.heartRate > HEART_RATE_MAX);
        if (heartAbn && !g_localHeartAbnActive) {
            g_localHeartAbnActive = true;
            char buf[32];
            snprintf(buf, sizeof(buf), "心率异常: %d bpm", radar.heartRate);
            g_alarm.triggerLocal(AlarmManager::ALARM_HEART_ABNORMAL, buf);
        } else if (!heartAbn && g_localHeartAbnActive) {
            g_localHeartAbnActive = false;
            g_alarm.clearLocal(AlarmManager::ALARM_HEART_ABNORMAL);
        }
    }

    // ─── ★★★★★ 人身安全: 呼吸异常 ───
    if (radar.vitalValid && radar.breathRate > 0) {
        bool breathAbn = (radar.breathRate < BREATH_RATE_MIN || radar.breathRate > BREATH_RATE_MAX);
        if (breathAbn && !g_localBreathAbnActive) {
            g_localBreathAbnActive = true;
            char buf[32];
            snprintf(buf, sizeof(buf), "呼吸异常: %d 次/分", radar.breathRate);
            g_alarm.triggerLocal(AlarmManager::ALARM_BREATH_ABNORMAL, buf);
        } else if (!breathAbn && g_localBreathAbnActive) {
            g_localBreathAbnActive = false;
            g_alarm.clearLocal(AlarmManager::ALARM_BREATH_ABNORMAL);
        }
    }

    // ─── 前置条件: 有人才检测人体生命体征 ───
    bool humanPresent = radar.presence && radar.radarValid;

    // ─── ★★★★★ 人身安全: 跌倒 (立即触发, 无防抖) ───
    if (radar.fall && humanPresent && !g_localFallActive) {
        g_localFallActive = true;
        g_alarm.triggerLocal(AlarmManager::ALARM_FALL, "雷达检测到跌倒");
    } else if (!radar.fall && g_localFallActive) {
        g_localFallActive = false;
        g_alarm.clearLocal(AlarmManager::ALARM_FALL);
    }

    // ─── ★★★★★ 人身安全: 心率异常 (有人才检测) ───
    if (humanPresent && radar.vitalValid && radar.heartRate > 0) {
        bool heartAbn = (radar.heartRate < HEART_RATE_MIN || radar.heartRate > HEART_RATE_MAX);
        if (heartAbn && !g_localHeartAbnActive) {
            g_localHeartAbnActive = true;
            char buf[32];
            snprintf(buf, sizeof(buf), "心率异常: %d bpm", radar.heartRate);
            g_alarm.triggerLocal(AlarmManager::ALARM_HEART_ABNORMAL, buf);
        } else if (!heartAbn && g_localHeartAbnActive) {
            g_localHeartAbnActive = false;
            g_alarm.clearLocal(AlarmManager::ALARM_HEART_ABNORMAL);
        }
    } else if (!humanPresent && g_localHeartAbnActive) {
        // 人走了 — 清除之前的报警
        g_localHeartAbnActive = false;
        g_alarm.clearLocal(AlarmManager::ALARM_HEART_ABNORMAL);
    }

    // ─── ★★★★★ 人身安全: 呼吸异常 (有人才检测) ───
    if (humanPresent && radar.vitalValid && radar.breathRate > 0) {
        bool breathAbn = (radar.breathRate < BREATH_RATE_MIN || radar.breathRate > BREATH_RATE_MAX);
        if (breathAbn && !g_localBreathAbnActive) {
            g_localBreathAbnActive = true;
            char buf[32];
            snprintf(buf, sizeof(buf), "呼吸异常: %d 次/分", radar.breathRate);
            g_alarm.triggerLocal(AlarmManager::ALARM_BREATH_ABNORMAL, buf);
        } else if (!breathAbn && g_localBreathAbnActive) {
            g_localBreathAbnActive = false;
            g_alarm.clearLocal(AlarmManager::ALARM_BREATH_ABNORMAL);
        }
    } else if (!humanPresent && g_localBreathAbnActive) {
        g_localBreathAbnActive = false;
        g_alarm.clearLocal(AlarmManager::ALARM_BREATH_ABNORMAL);
    }


    // ─── ★★★★★ 人身安全: 跌倒 (立即触发, 无防抖) ───
    if (radar.fall && radar.radarValid && !g_localFallActive) {
        g_localFallActive = true;
        g_alarm.triggerLocal(AlarmManager::ALARM_FALL, "雷达检测到跌倒");
    } else if (!radar.fall && g_localFallActive) {
        g_localFallActive = false;
        g_alarm.clearLocal(AlarmManager::ALARM_FALL);
    }

    // ─── ★★★★★ 人身安全: 心率异常 ───
    if (radar.vitalValid && radar.heartRate > 0) {
        bool heartAbn = (radar.heartRate < HEART_RATE_MIN || radar.heartRate > HEART_RATE_MAX);
        if (heartAbn && !g_localHeartAbnActive) {
            g_localHeartAbnActive = true;
            char buf[32];
            snprintf(buf, sizeof(buf), "心率异常: %d bpm", radar.heartRate);
            g_alarm.triggerLocal(AlarmManager::ALARM_HEART_ABNORMAL, buf);
        } else if (!heartAbn && g_localHeartAbnActive) {
            g_localHeartAbnActive = false;
            g_alarm.clearLocal(AlarmManager::ALARM_HEART_ABNORMAL);
        }
    }

    // ─── ★★★★★ 人身安全: 呼吸异常 ───
    if (radar.vitalValid && radar.breathRate > 0) {
        bool breathAbn = (radar.breathRate < BREATH_RATE_MIN || radar.breathRate > BREATH_RATE_MAX);
        if (breathAbn && !g_localBreathAbnActive) {
            g_localBreathAbnActive = true;
            char buf[32];
            snprintf(buf, sizeof(buf), "呼吸异常: %d 次/分", radar.breathRate);
            g_alarm.triggerLocal(AlarmManager::ALARM_BREATH_ABNORMAL, buf);
        } else if (!breathAbn && g_localBreathAbnActive) {
            g_localBreathAbnActive = false;
            g_alarm.clearLocal(AlarmManager::ALARM_BREATH_ABNORMAL);
        }
    }

    // ─── 前置条件: 有人才检测人体生命体征 ───
    bool humanPresent = radar.presence && radar.radarValid;

    // ─── ★★★★★ 人身安全: 跌倒 (立即触发, 无防抖) ───
    if (radar.fall && humanPresent && !g_localFallActive) {
        g_localFallActive = true;
        g_alarm.triggerLocal(AlarmManager::ALARM_FALL, "雷达检测到跌倒");
    } else if (!radar.fall && g_localFallActive) {
        g_localFallActive = false;
        g_alarm.clearLocal(AlarmManager::ALARM_FALL);
    }

    // ─── ★★★★★ 人身安全: 心率异常 (有人才检测) ───
    if (humanPresent && radar.vitalValid && radar.heartRate > 0) {
        bool heartAbn = (radar.heartRate < HEART_RATE_MIN || radar.heartRate > HEART_RATE_MAX);
        if (heartAbn && !g_localHeartAbnActive) {
            g_localHeartAbnActive = true;
            char buf[32];
            snprintf(buf, sizeof(buf), "心率异常: %d bpm", radar.heartRate);
            g_alarm.triggerLocal(AlarmManager::ALARM_HEART_ABNORMAL, buf);
        } else if (!heartAbn && g_localHeartAbnActive) {
            g_localHeartAbnActive = false;
            g_alarm.clearLocal(AlarmManager::ALARM_HEART_ABNORMAL);
        }
    } else if (!humanPresent && g_localHeartAbnActive) {
        // 人走了 — 清除之前的报警
        g_localHeartAbnActive = false;
        g_alarm.clearLocal(AlarmManager::ALARM_HEART_ABNORMAL);
    }

    // ─── ★★★★★ 人身安全: 呼吸异常 (有人才检测) ───
    if (humanPresent && radar.vitalValid && radar.breathRate > 0) {
        bool breathAbn = (radar.breathRate < BREATH_RATE_MIN || radar.breathRate > BREATH_RATE_MAX);
        if (breathAbn && !g_localBreathAbnActive) {
            g_localBreathAbnActive = true;
            char buf[32];
            snprintf(buf, sizeof(buf), "呼吸异常: %d 次/分", radar.breathRate);
            g_alarm.triggerLocal(AlarmManager::ALARM_BREATH_ABNORMAL, buf);
        } else if (!breathAbn && g_localBreathAbnActive) {
            g_localBreathAbnActive = false;
            g_alarm.clearLocal(AlarmManager::ALARM_BREATH_ABNORMAL);
        }
    } else if (!humanPresent && g_localBreathAbnActive) {
        g_localBreathAbnActive = false;
        g_alarm.clearLocal(AlarmManager::ALARM_BREATH_ABNORMAL);
    }

void checkLocalAlarms(const RadarRawData& radar) {

+    // ─── 前置条件: 有人才检测人体生命体征 ───

+    bool humanPresent = radar.presence && radar.radarValid;

// ─── ★★★★★ 人身安全: 跌倒 (立即触发, 无防抖) ───

-    if (radar.fall && radar.radarValid && !g_localFallActive) {

+    if (radar.fall && humanPresent && !g_localFallActive) {

-    // ─── ★★★★★ 人身安全: 心率异常 ───

-    if (radar.vitalValid && radar.heartRate > 0) {

+    // ─── ★★★★★ 人身安全: 心率异常 (有人才检测) ───

+    if (humanPresent && radar.vitalValid && radar.heartRate > 0) {

+    } else if (!humanPresent && g_localHeartAbnActive) {

+        // 人走了 — 清除之前的报警

+        g_alarm.clearLocal(AlarmManager::ALARM_HEART_ABNORMAL);

-    // ─── ★★★★★ 人身安全: 呼吸异常 ───

-    if (radar.vitalValid && radar.breathRate > 0) {

+    // ─── ★★★★★ 人身安全: 呼吸异常 (有人才检测) ───

+    if (humanPresent && radar.vitalValid && radar.breathRate > 0) {

+    } else if (!humanPresent && g_localBreathAbnActive) {

+        g_localBreathAbnActive = false;

+        g_alarm.clearLocal(AlarmManager::ALARM_BREATH_ABNORMAL);

// ─── ★★★ 环境安全: 水浸 ───


Done. The logic is now:

1. `humanPresent = radar.presence && radar.radarValid` — 雷达数据有效且检测到有人
2. 跌倒、心率、呼吸异常 都需要 `humanPresent` 为 true 才检测
3. 人离开后 (`!humanPresent`)，自动清除之前的心率/呼吸报警（人走了，异常自然解除）

水浸、温湿度、传感器故障这些与环境/设备相关的报警不受 `presence` 影响，始终检测。


改好了。现在的逻辑：

```
有人 (presence=1 && radarValid=1)
  ├── 检测跌倒 ──→ 异常则报警
  ├── 检测心率 ──→ 异常则报警
  └── 检测呼吸 ──→ 异常则报警

没人 (presence=0)
  ├── 不检测人体体征
  ├── 之前的心率/呼吸报警自动清除
  └── 水浸/温湿度/传感器故障不受影响，继续检测
```

这样没人时雷达即使输出乱跳的心率值也不会误报。


现在再去设计配网页面，你现在先记录下我们的代码，一会避免错误回不去


The user wants me to:
1. Save/commit the current code state so we can roll back if the config page redesign goes wrong
2. Then redesign the config page

Let me first check the git status and commit the current changes, then work on the config page redesign.

Let me first see what files have been modified.


先把当前代码提交保存，作为回滚点。


Bash

Check git status for changed files


 M .claude/settings.local.json
 M data/index.html
 M include/alarm_manager.h
 M include/button_manager.h
 M include/config.h
 M include/display_manager.h
 M include/mqtt_manager.h
 M include/wifi_manager.h
 M platformio.ini
 M src/alarm_manager.cpp
 M src/button_manager.cpp
 M src/display_manager.cpp
 M src/main.cpp
 M src/mqtt_manager.cpp
 M src/sensor_manager.cpp
 M src/wifi_manager.cpp


Let me commit all these changes. I'll do a git add and commit with a descriptive message.


Stage all files for commit


warning: in the working copy of '.claude/settings.local.json', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'data/index.html', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'include/alarm_manager.h', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'include/button_manager.h', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'include/config.h', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'include/mqtt_manager.h', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'src/alarm_manager.cpp', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'src/button_manager.cpp', LF will be replaced by CRLF the next time Git touches it
M  .claude/settings.local.json
M  data/index.html
M  include/alarm_manager.h
M  include/button_manager.h
M  include/config.h
M  include/display_manager.h
M  include/mqtt_manager.h
M  include/wifi_manager.h
M  platformio.ini
M  src/alarm_manager.cpp
M  src/button_manager.cpp
M  src/display_manager.cpp
M  src/main.cpp
M  src/mqtt_manager.cpp
M  src/sensor_manager.cpp
M  src/wifi_manager.cpp


Good, all files are staged. Now commit with a descriptive message.


git commit -m "feat: 按钮去抖+配网界面美化+水浸离线+LED独立任务+报警全拓展(人身优先)+有人才检测"

Commit all current changes as rollback point


[main 69e927c] feat: 按钮去抖+配网界面美化+水浸离线+LED独立任务+报警全拓展(人身优先)+有人才检测
 16 files changed, 1358 insertions(+), 555 deletions(-)


Good, the code is committed. Now the user wants me to redesign the config page. Let me think about what "配网页面" means here.

There are two config-related pages:
1. **WiFiManager captive portal** — the page injected via `_wm.setCustomHeadElement()` in `wifi_manager.cpp`. This is what users see on their phones when connecting to the device's AP.
2. **`data/index.html`** — a standalone HTML file that can be served separately or used as a reference.

The user said "配网界面太丑" earlier and we improved the CSS. Now they want another redesign. But I already redesigned both the captive portal CSS and the index.html.

Wait, re-reading the conversation flow:
- User first said "配网界面太丑"
- I redesigned both
- Then user said "现在，水浸没有接上..." — moved on to other topics
- Now they say: "现在再去设计配网页面" — they want to revisit the config page design

So they want a fresh/better design for the config page. Let me think about what would be a truly impressive design for a medical IoT device config page.

Actually, the user is a Chinese speaker and the context is a health monitoring system (健康监测系统). The config page should look professional, medical-grade, and be mobile-friendly.

Let me think about a design direction:
- Medical/healthcare aesthetic — clean whites, soft blues/greens
- Might use a card-based layout with step-by-step flow
- Modern glassmorphism or soft UI style
- Good typography for Chinese text
- Large touch targets for mobile

Let me redesign both the captive portal CSS and the index.html with a fresh approach. I'll go for a premium medical device onboarding feel — similar to what you'd see from high-end health monitoring equipment like Philips or Withings.

Actually, since this is a captive portal served by WiFiManager, the CSS I can inject is limited to what WiFiManager's HTML structure supports. Let me focus on making the captive portal CSS truly excellent, and also make the index.html beautiful as a standalone page.

Let me think about what makes a great medical IoT config page:
1. Trust/credibility — clean, professional, not flashy
2. Clear hierarchy — what's most important (WiFi SSID/password) should be prominent
3. Large fonts and touch targets for elderly users (this is a health monitoring system)
4. Status feedback — clear indication of what's happening
5. Chinese-first design — typography optimized for Chinese characters

Let me redesign the captive portal CSS with these principles. I'll go for a clean, trustworthy medical aesthetic:
- Soft blue-green gradient background
- White cards with subtle shadows
- Larger fonts (the elderly are a target user group)
- Clear, prominent call-to-action button
- Status indicators

For the index.html, I'll create a more complete experience with a step-by-step flow.

Actually, let me just focus on making both excellent. Let me start with the captive portal CSS in wifi_manager.cpp, then the index.html.

For the captive portal CSS, I need to work within WiFiManager's HTML structure. The key elements are:
- `.msg` div — header area
- `form` — the config form
- `input[type=text]`, `input[type=password]` — form fields
- `button[type=submit]` — save button
- `label` — field labels

Let me create a truly premium design.

Actually, wait. Let me think about what the user specifically wants. They said the config page is ugly before, I made changes, and now they want me to redesign it again. Maybe my previous design wasn't good enough? Or maybe they want a completely different approach?

Let me go for a really premium design this time. I'll use:
- A warm, medical-grade color palette (teal/green with subtle gold accents)
- Larger, more readable typography
- Better spacing and visual hierarchy
- Animated status indicators
- Professional iconography using CSS-only elements
- Focus on accessibility for elderly users

Let me write the new captive portal CSS and index.html now.

For the captive portal, the approach is the same — inject CSS via `setCustomHeadElement`. I need to make sure the CSS targets the right WiFiManager HTML elements.

For the index.html, I'll create a complete, standalone page.

Let me start coding.

Actually, this might involve a lot of changes to wifi_manager.cpp. Let me be careful.

Let me start with the captive portal CSS, then the index.html.


代码已保存。现在来重新设计配网页面——针对康养场景，面向老年用户，走专业医疗设备风格。


TodoWrite

重写 WiFiManager 注入 CSS — 大字体/医疗风/老年友好

重写 data/index.html — 完整设备上线体验


重写 WiFiManager 注入 CSS — 大字体/医疗风/老年友好

重写 data/index.html — 完整设备上线体验


切记，不要有用永久删除的东西，


The user is saying "切记，不要有用永久删除的东西" — meaning "remember, don't have anything that permanently deletes things".

This is important — in the config page, there's a "恢复出厂设置" (factory reset) button that calls `resetConfig()`. This is a destructive action. The user doesn't want permanent deletion functionality in the config page.

Let me make sure:
1. The factory reset button should be removed or made very safe (with clear confirmation and warning)
2. No destructive actions should be easy to trigger
3. Focus on configuration, not deletion

Let me update my plan for the redesign to remove the factory reset button, or at least make it extremely clear what it does and require strong confirmation.

Actually, looking at the current index.html, there IS a reset button with `confirm()`. But the user's concern is valid — on a captive portal served by an ESP32 that might be accessed by anyone nearby, having a factory reset button is dangerous.

Let me remove the factory reset button from the captive portal (which is the WiFiManager page). The `data/index.html` can keep it but with stronger safeguards.

Wait, the captive portal (WiFiManager) doesn't have a reset button by default — that's only in my custom `data/index.html`. Let me just make sure:
1. Remove or heavily guard the reset button in index.html
2. Don't add any destructive actions to the WiFiManager captive portal CSS

OK, let me proceed with the redesign. I'll keep it clean and safe.


明白，不要有永久删除的操作。配网页面只做配置，不搞出厂重置这种危险动作。


Edit

    _wm.setTitle("健康监测系统 - 设备配网");
    _wm.setCustomHeadElement(
        "<style>"
        /* ===== 字体与全局 ===== */
        "*{margin:0;padding:0;box-sizing:border-box}"
        "body{"
        "  font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif;"
        "  background:linear-gradient(160deg, #f0f5ff 0%, #e8f0fe 40%, #f3f0ff 100%);"
        "  min-height:100vh;display:flex;flex-direction:column;align-items:center;"
        "  padding:20px 16px 40px;color:#1e293b"
        "}"
        "body::before{"
        "  content:'';position:fixed;top:-200px;right:-200px;"
        "  width:400px;height:400px;border-radius:50%;"
        "  background:radial-gradient(circle, rgba(99,102,241,0.08) 0%, transparent 70%);"
        "  pointer-events:none;z-index:0"
        "}"
        "body::after{"
        "  content:'';position:fixed;bottom:-100px;left:-100px;"
        "  width:300px;height:300px;border-radius:50%;"
        "  background:radial-gradient(circle, rgba(34,197,94,0.06) 0%, transparent 70%);"
        "  pointer-events:none;z-index:0"
        "}"
        /* ===== 头部卡片 ===== */
        ".msg{"
        "  width:100%;max-width:440px;position:relative;z-index:1;"
        "  background:#fff;border-radius:20px;padding:32px 24px 24px;"
        "  margin-bottom:16px;text-align:center;"
        "  box-shadow:0 1px 3px rgba(0,0,0,0.04),0 8px 24px rgba(0,0,0,0.06)"
        "}"
        ".msg::before{"
        "  content:'';display:block;width:56px;height:56px;"
        "  margin:0 auto 16px;"
        "  background:linear-gradient(135deg,#6366f1,#8b5cf6);border-radius:16px;"
        "  box-shadow:0 8px 24px rgba(99,102,241,0.25)"
        "}"
        ".msg h1,.msg h2{font-size:20px;font-weight:700;color:#1e293b;margin-bottom:6px}"
        ".msg p,.msg .info{font-size:13px;color:#94a3b8;line-height:1.5}"
        /* 隐藏 WiFiManager 默认的 h1 重复标题 */
        ".msg h1{font-size:0;margin:0;padding:0}"
        ".msg h1::after{content:'健康监测系统';font-size:22px;font-weight:700;color:#1e293b}"
        ".msg .info{font-size:13px!important}"
        /* ===== 表单卡片 ===== */
        "form{"
        "  width:100%;max-width:440px;position:relative;z-index:1;"
        "  background:#fff;border-radius:20px;padding:24px;"
        "  box-shadow:0 1px 3px rgba(0,0,0,0.04),0 8px 24px rgba(0,0,0,0.06)"
        "}"
        "label{"
        "  display:block;font-size:11px;font-weight:600;"
        "  text-transform:uppercase;letter-spacing:0.8px;"
        "  color:#6366f1;margin:16px 0 6px"
        "}"
        "label:first-of-type{margin-top:0}"
        "input[type=text],input[type=password],input[type=number]{"
        "  width:100%;height:46px;padding:0 14px;"
        "  border:2px solid #e2e8f0;border-radius:12px;"
        "  font-size:15px;color:#1e293b;background:#f8fafc;"
        "  outline:none;transition:all 0.2s;-webkit-appearance:none"
        "}"
        "input:focus{"
        "  border-color:#6366f1;background:#fff;"
        "  box-shadow:0 0 0 4px rgba(99,102,241,0.1)"
        "}"
        "input::placeholder{color:#cbd5e1}"
        /* ===== 提交按钮 ===== */
        "button[type=submit]{"
        "  width:100%;height:50px;margin-top:24px;"
        "  background:linear-gradient(135deg,#6366f1,#8b5cf6);"
        "  color:#fff;border:none;border-radius:14px;"
        "  font-size:16px;font-weight:600;letter-spacing:1px;"
        "  cursor:pointer;transition:all 0.2s;"
        "  box-shadow:0 4px 16px rgba(99,102,241,0.3)"
        "}"
        "button:active{transform:scale(0.97);box-shadow:0 2px 8px rgba(99,102,241,0.3)}"
        /* ===== 页脚 ===== */
        ".msg:last-of-type{"
        "  background:transparent;box-shadow:none;padding:16px;margin-top:4px"
        "}"
        ".msg:last-of-type::before{display:none}"
        ".msg:last-of-type p,.msg:last-of-type .info{font-size:12px;color:#94a3b8}"
        /* ===== 成功/错误状态页 ===== */
        ".msg .success{color:#16a34a}"
        ".msg .error{color:#dc2626}"
        "a{color:#6366f1;text-decoration:none;font-weight:500}"
        "</style>"
    );

    _wm.setTitle("康养监测 · 设备配网");
    _wm.setCustomHeadElement(
        "<style>"
        /* === 全局 — 暖白医疗风，大字号适配老年用户 === */
        "*{margin:0;padding:0;box-sizing:border-box}"
        "html{font-size:17px}"  /* 基准加大 */
        "body{"
        "  font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei',sans-serif;"
        "  background:linear-gradient(180deg,#f5f9fc 0%,#e8f4f8 50%,#f0f7f4 100%);"
        "  min-height:100vh;display:flex;flex-direction:column;align-items:center;"
        "  padding:24px 16px 48px;color:#2c3e50;-webkit-font-smoothing:antialiased"
        "}"
        /* 装饰光晕 */
        "body::before{"
        "  content:'';position:fixed;top:-120px;right:-120px;"
        "  width:280px;height:280px;border-radius:50%;"
        "  background:radial-gradient(circle,rgba(20,184,166,0.06) 0%,transparent 70%);"
        "  pointer-events:none;z-index:0"
        "}"
        "body::after{"
        "  content:'';position:fixed;bottom:-80px;left:-80px;"
        "  width:240px;height:240px;border-radius:50%;"
        "  background:radial-gradient(circle,rgba(59,130,246,0.05) 0%,transparent 70%);"
        "  pointer-events:none;z-index:0"
        "}"
        /* === 头部卡片 === */
        ".msg{"
        "  width:100%;max-width:460px;position:relative;z-index:1;"
        "  background:rgba(255,255,255,0.92);backdrop-filter:blur(10px);"
        "  border-radius:24px;padding:36px 28px 24px;margin-bottom:18px;text-align:center;"
        "  border:1px solid rgba(255,255,255,0.6);"
        "  box-shadow:0 2px 8px rgba(0,0,0,0.03),0 12px 32px rgba(0,0,0,0.05)"
        "}"
        /* 医疗十字 LOGO */
        ".msg::before{"
        "  content:'';display:block;width:64px;height:64px;margin:0 auto 20px;"
        "  background:linear-gradient(135deg,#0ea5e9,#14b8a6);border-radius:20px;"
        "  box-shadow:0 10px 28px rgba(20,184,166,0.22);"
        "  position:relative"
        "}"
        ".msg h1,.msg h2{font-size:1.35rem;font-weight:700;color:#1e293b;margin-bottom:6px;letter-spacing:-0.3px}"
        ".msg p,.msg .info{font-size:0.88rem;color:#64748b;line-height:1.6}"
        /* 隐藏默认 h1 */
        ".msg h1{font-size:0;margin:0;padding:0}"
        ".msg h1::after{content:'五劳无感康养监测';font-size:1.45rem;font-weight:700;color:#0f172a}"
        ".msg .info{font-size:0.88rem!important}"
        /* === 表单卡片 === */
        "form{"
        "  width:100%;max-width:460px;position:relative;z-index:1;"
        "  background:rgba(255,255,255,0.92);backdrop-filter:blur(10px);"
        "  border-radius:24px;padding:28px;"
        "  border:1px solid rgba(255,255,255,0.6);"
        "  box-shadow:0 2px 8px rgba(0,0,0,0.03),0 12px 32px rgba(0,0,0,0.05)"
        "}"
        "label{"
        "  display:block;font-size:0.82rem;font-weight:600;"
        "  color:#0ea5e9;margin:18px 0 6px;letter-spacing:0.3px"
        "}"
        "label:first-of-type{margin-top:0}"
        "input[type=text],input[type=password],input[type=number]{"
        "  width:100%;height:52px;padding:0 16px;"
        "  border:2px solid #e2e8f0;border-radius:14px;"
        "  font-size:1.05rem;color:#1e293b;background:#f8fafc;"
        "  outline:none;transition:all 0.25s;-webkit-appearance:none"
        "}"
        "input:focus{"
        "  border-color:#14b8a6;background:#fff;"
        "  box-shadow:0 0 0 5px rgba(20,184,166,0.08)"
        "}"
        "input::placeholder{color:#cbd5e1;font-size:0.94rem}"
        /* === 提交按钮 === */
        "button[type=submit]{"
        "  width:100%;height:56px;margin-top:28px;"
        "  background:linear-gradient(135deg,#0ea5e9,#14b8a6);"
        "  color:#fff;border:none;border-radius:16px;"
        "  font-size:1.1rem;font-weight:700;letter-spacing:0.5px;"
        "  cursor:pointer;transition:all 0.2s;"
        "  box-shadow:0 6px 22px rgba(20,184,166,0.25)"
        "}"
        "button:active{transform:scale(0.97);box-shadow:0 3px 10px rgba(20,184,166,0.2)}"
        /* === 页脚 === */
        ".msg:last-of-type{"
        "  background:transparent;backdrop-filter:none;border:none;"
        "  box-shadow:none;padding:18px;margin-top:6px"
        "}"
        ".msg:last-of-type::before{display:none}"
        ".msg:last-of-type p,.msg:last-of-type .info{font-size:0.82rem;color:#94a3b8}"
        "a{color:#0ea5e9;text-decoration:none;font-weight:600}"
        /* === 成功/错误 === */
        ".msg .success{color:#059669;font-weight:600}"
        ".msg .error{color:#dc2626;font-weight:600}"
        "</style>"
    );


The file d:\PlatformIO1\Projects\Healthv2.0\src\wifi_manager.cpp has been updated successfully. (file state is current in your context — no need to Read it back)

    _wm.setTitle("健康监测系统 - 设备配网");
    _wm.setCustomHeadElement(
        "<style>"
        /* ===== 字体与全局 ===== */
        "*{margin:0;padding:0;box-sizing:border-box}"
        "body{"
        "  font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif;"
        "  background:linear-gradient(160deg, #f0f5ff 0%, #e8f0fe 40%, #f3f0ff 100%);"
        "  min-height:100vh;display:flex;flex-direction:column;align-items:center;"
        "  padding:20px 16px 40px;color:#1e293b"
        "}"
        "body::before{"
        "  content:'';position:fixed;top:-200px;right:-200px;"
        "  width:400px;height:400px;border-radius:50%;"
        "  background:radial-gradient(circle, rgba(99,102,241,0.08) 0%, transparent 70%);"
        "  pointer-events:none;z-index:0"
        "}"
        "body::after{"
        "  content:'';position:fixed;bottom:-100px;left:-100px;"
        "  width:300px;height:300px;border-radius:50%;"
        "  background:radial-gradient(circle, rgba(34,197,94,0.06) 0%, transparent 70%);"
        "  pointer-events:none;z-index:0"
        "}"
        /* ===== 头部卡片 ===== */
        ".msg{"
        "  width:100%;max-width:440px;position:relative;z-index:1;"
        "  background:#fff;border-radius:20px;padding:32px 24px 24px;"
        "  margin-bottom:16px;text-align:center;"
        "  box-shadow:0 1px 3px rgba(0,0,0,0.04),0 8px 24px rgba(0,0,0,0.06)"
        "}"
        ".msg::before{"
        "  content:'';display:block;width:56px;height:56px;"
        "  margin:0 auto 16px;"
        "  background:linear-gradient(135deg,#6366f1,#8b5cf6);border-radius:16px;"
        "  box-shadow:0 8px 24px rgba(99,102,241,0.25)"
        "}"
        ".msg h1,.msg h2{font-size:20px;font-weight:700;color:#1e293b;margin-bottom:6px}"
        ".msg p,.msg .info{font-size:13px;color:#94a3b8;line-height:1.5}"
        /* 隐藏 WiFiManager 默认的 h1 重复标题 */
        ".msg h1{font-size:0;margin:0;padding:0}"
        ".msg h1::after{content:'健康监测系统';font-size:22px;font-weight:700;color:#1e293b}"
        ".msg .info{font-size:13px!important}"
        /* ===== 表单卡片 ===== */
        "form{"
        "  width:100%;max-width:440px;position:relative;z-index:1;"
        "  background:#fff;border-radius:20px;padding:24px;"
        "  box-shadow:0 1px 3px rgba(0,0,0,0.04),0 8px 24px rgba(0,0,0,0.06)"
        "}"
        "label{"
        "  display:block;font-size:11px;font-weight:600;"
        "  text-transform:uppercase;letter-spacing:0.8px;"
        "  color:#6366f1;margin:16px 0 6px"
        "}"
        "label:first-of-type{margin-top:0}"
        "input[type=text],input[type=password],input[type=number]{"
        "  width:100%;height:46px;padding:0 14px;"
        "  border:2px solid #e2e8f0;border-radius:12px;"
        "  font-size:15px;color:#1e293b;background:#f8fafc;"
        "  outline:none;transition:all 0.2s;-webkit-appearance:none"
        "}"
        "input:focus{"
        "  border-color:#6366f1;background:#fff;"
        "  box-shadow:0 0 0 4px rgba(99,102,241,0.1)"
        "}"
        "input::placeholder{color:#cbd5e1}"
        /* ===== 提交按钮 ===== */
        "button[type=submit]{"
        "  width:100%;height:50px;margin-top:24px;"
        "  background:linear-gradient(135deg,#6366f1,#8b5cf6);"
        "  color:#fff;border:none;border-radius:14px;"
        "  font-size:16px;font-weight:600;letter-spacing:1px;"
        "  cursor:pointer;transition:all 0.2s;"
        "  box-shadow:0 4px 16px rgba(99,102,241,0.3)"
        "}"
        "button:active{transform:scale(0.97);box-shadow:0 2px 8px rgba(99,102,241,0.3)}"
        /* ===== 页脚 ===== */
        ".msg:last-of-type{"
        "  background:transparent;box-shadow:none;padding:16px;margin-top:4px"
        "}"
        ".msg:last-of-type::before{display:none}"
        ".msg:last-of-type p,.msg:last-of-type .info{font-size:12px;color:#94a3b8}"
        /* ===== 成功/错误状态页 ===== */
        ".msg .success{color:#16a34a}"
        ".msg .error{color:#dc2626}"
        "a{color:#6366f1;text-decoration:none;font-weight:500}"
        "</style>"
    );

    _wm.setTitle("康养监测 · 设备配网");
    _wm.setCustomHeadElement(
        "<style>"
        /* === 全局 — 暖白医疗风，大字号适配老年用户 === */
        "*{margin:0;padding:0;box-sizing:border-box}"
        "html{font-size:17px}"  /* 基准加大 */
        "body{"
        "  font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei',sans-serif;"
        "  background:linear-gradient(180deg,#f5f9fc 0%,#e8f4f8 50%,#f0f7f4 100%);"
        "  min-height:100vh;display:flex;flex-direction:column;align-items:center;"
        "  padding:24px 16px 48px;color:#2c3e50;-webkit-font-smoothing:antialiased"
        "}"
        /* 装饰光晕 */
        "body::before{"
        "  content:'';position:fixed;top:-120px;right:-120px;"
        "  width:280px;height:280px;border-radius:50%;"
        "  background:radial-gradient(circle,rgba(20,184,166,0.06) 0%,transparent 70%);"
        "  pointer-events:none;z-index:0"
        "}"
        "body::after{"
        "  content:'';position:fixed;bottom:-80px;left:-80px;"
        "  width:240px;height:240px;border-radius:50%;"
        "  background:radial-gradient(circle,rgba(59,130,246,0.05) 0%,transparent 70%);"
        "  pointer-events:none;z-index:0"
        "}"
        /* === 头部卡片 === */
        ".msg{"
        "  width:100%;max-width:460px;position:relative;z-index:1;"
        "  background:rgba(255,255,255,0.92);backdrop-filter:blur(10px);"
        "  border-radius:24px;padding:36px 28px 24px;margin-bottom:18px;text-align:center;"
        "  border:1px solid rgba(255,255,255,0.6);"
        "  box-shadow:0 2px 8px rgba(0,0,0,0.03),0 12px 32px rgba(0,0,0,0.05)"
        "}"
        /* 医疗十字 LOGO */
        ".msg::before{"
        "  content:'';display:block;width:64px;height:64px;margin:0 auto 20px;"
        "  background:linear-gradient(135deg,#0ea5e9,#14b8a6);border-radius:20px;"
        "  box-shadow:0 10px 28px rgba(20,184,166,0.22);"
        "  position:relative"
        "}"
        ".msg h1,.msg h2{font-size:1.35rem;font-weight:700;color:#1e293b;margin-bottom:6px;letter-spacing:-0.3px}"
        ".msg p,.msg .info{font-size:0.88rem;color:#64748b;line-height:1.6}"
        /* 隐藏默认 h1 */
        ".msg h1{font-size:0;margin:0;padding:0}"
        ".msg h1::after{content:'五劳无感康养监测';font-size:1.45rem;font-weight:700;color:#0f172a}"
        ".msg .info{font-size:0.88rem!important}"
        /* === 表单卡片 === */
        "form{"
        "  width:100%;max-width:460px;position:relative;z-index:1;"
        "  background:rgba(255,255,255,0.92);backdrop-filter:blur(10px);"
        "  border-radius:24px;padding:28px;"
        "  border:1px solid rgba(255,255,255,0.6);"
        "  box-shadow:0 2px 8px rgba(0,0,0,0.03),0 12px 32px rgba(0,0,0,0.05)"
        "}"
        "label{"
        "  display:block;font-size:0.82rem;font-weight:600;"
        "  color:#0ea5e9;margin:18px 0 6px;letter-spacing:0.3px"
        "}"
        "label:first-of-type{margin-top:0}"
        "input[type=text],input[type=password],input[type=number]{"
        "  width:100%;height:52px;padding:0 16px;"
        "  border:2px solid #e2e8f0;border-radius:14px;"
        "  font-size:1.05rem;color:#1e293b;background:#f8fafc;"
        "  outline:none;transition:all 0.25s;-webkit-appearance:none"
        "}"
        "input:focus{"
        "  border-color:#14b8a6;background:#fff;"
        "  box-shadow:0 0 0 5px rgba(20,184,166,0.08)"
        "}"
        "input::placeholder{color:#cbd5e1;font-size:0.94rem}"
        /* === 提交按钮 === */
        "button[type=submit]{"
        "  width:100%;height:56px;margin-top:28px;"
        "  background:linear-gradient(135deg,#0ea5e9,#14b8a6);"
        "  color:#fff;border:none;border-radius:16px;"
        "  font-size:1.1rem;font-weight:700;letter-spacing:0.5px;"
        "  cursor:pointer;transition:all 0.2s;"
        "  box-shadow:0 6px 22px rgba(20,184,166,0.25)"
        "}"
        "button:active{transform:scale(0.97);box-shadow:0 3px 10px rgba(20,184,166,0.2)}"
        /* === 页脚 === */
        ".msg:last-of-type{"
        "  background:transparent;backdrop-filter:none;border:none;"
        "  box-shadow:none;padding:18px;margin-top:6px"
        "}"
        ".msg:last-of-type::before{display:none}"
        ".msg:last-of-type p,.msg:last-of-type .info{font-size:0.82rem;color:#94a3b8}"
        "a{color:#0ea5e9;text-decoration:none;font-weight:600}"
        /* === 成功/错误 === */
        ".msg .success{color:#059669;font-weight:600}"
        ".msg .error{color:#dc2626;font-weight:600}"
        "</style>"
    );

_wm.setDebugOutput(false);  // 关闭调试输出 (避免干扰)

+        /* === 全局 — 暖白医疗风，大字号适配老年用户 === */

+        "html{font-size:17px}"  /* 基准加大 */

-        "  font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif;"

-        "  background:linear-gradient(160deg, #f0f5ff 0%, #e8f0fe 40%, #f3f0ff 100%);"

+        "  font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei',sans-serif;"

+        "  background:linear-gradient(180deg,#f5f9fc 0%,#e8f4f8 50%,#f0f7f4 100%);"

-        "  padding:20px 16px 40px;color:#1e293b"

+        "  padding:24px 16px 48px;color:#2c3e50;-webkit-font-smoothing:antialiased"

-        "  content:'';position:fixed;top:-200px;right:-200px;"

-        "  width:400px;height:400px;border-radius:50%;"

-        "  background:radial-gradient(circle, rgba(99,102,241,0.08) 0%, transparent 70%);"

+        "  content:'';position:fixed;top:-120px;right:-120px;"

+        "  width:280px;height:280px;border-radius:50%;"

+        "  background:radial-gradient(circle,rgba(20,184,166,0.06) 0%,transparent 70%);"

-        "  content:'';position:fixed;bottom:-100px;left:-100px;"

-        "  width:300px;height:300px;border-radius:50%;"

-        "  background:radial-gradient(circle, rgba(34,197,94,0.06) 0%, transparent 70%);"

+        "  content:'';position:fixed;bottom:-80px;left:-80px;"

+        "  width:240px;height:240px;border-radius:50%;"

+        "  background:radial-gradient(circle,rgba(59,130,246,0.05) 0%,transparent 70%);"

-        "  width:100%;max-width:440px;position:relative;z-index:1;"

-        "  background:#fff;border-radius:20px;padding:32px 24px 24px;"

-        "  margin-bottom:16px;text-align:center;"

+        "  width:100%;max-width:460px;position:relative;z-index:1;"

+        "  background:rgba(255,255,255,0.92);backdrop-filter:blur(10px);"

+        "  border-radius:24px;padding:36px 28px 24px;margin-bottom:18px;text-align:center;"

-        "  content:'';display:block;width:56px;height:56px;"

-        "  background:linear-gradient(135deg,#6366f1,#8b5cf6);border-radius:16px;"

+        "  content:'';display:block;width:64px;height:64px;margin:0 auto 20px;"

+        "  background:linear-gradient(135deg,#0ea5e9,#14b8a6);border-radius:20px;"

-        ".msg h1,.msg h2{font-size:20px;font-weight:700;color:#1e293b;margin-bottom:6px}"

-        ".msg p,.msg .info{font-size:13px;color:#94a3b8;line-height:1.5}"

-        /* 隐藏 WiFiManager 默认的 h1 重复标题 */

+        ".msg h1,.msg h2{font-size:1.35rem;font-weight:700;color:#1e293b;margin-bottom:6px;letter-spacing:-0.3px}"

+        ".msg p,.msg .info{font-size:0.88rem;color:#64748b;line-height:1.6}"

-        ".msg h1::after{content:'健康监测系统';font-size:22px;font-weight:700;color:#1e293b}"

-        ".msg .info{font-size:13px!important}"

+        ".msg h1::after{content:'五劳无感康养监测';font-size:1.45rem;font-weight:700;color:#0f172a}"

+        ".msg .info{font-size:0.88rem!important}"

-        "  background:#fff;border-radius:20px;padding:24px;"

+        "  border-radius:24px;padding:28px;"

-        "  display:block;font-size:11px;font-weight:600;"

-        "  text-transform:uppercase;letter-spacing:0.8px;"

-        "  color:#6366f1;margin:16px 0 6px"

+        "  display:block;font-size:0.82rem;font-weight:600;"

+        "  color:#0ea5e9;margin:18px 0 6px;letter-spacing:0.3px"

-        "  width:100%;height:46px;padding:0 14px;"

-        "  border:2px solid #e2e8f0;border-radius:12px;"

-        "  font-size:15px;color:#1e293b;background:#f8fafc;"

-        "  outline:none;transition:all 0.2s;-webkit-appearance:none"

+        "  width:100%;height:52px;padding:0 16px;"

+        "  border:2px solid #e2e8f0;border-radius:14px;"

+        "  font-size:1.05rem;color:#1e293b;background:#f8fafc;"

+        "  outline:none;transition:all 0.25s;-webkit-appearance:none"

-        "  border-color:#6366f1;background:#fff;"

+        "  border-color:#14b8a6;background:#fff;"

-        "input::placeholder{color:#cbd5e1}"

+        "input::placeholder{color:#cbd5e1;font-size:0.94rem}"

-        "  width:100%;height:50px;margin-top:24px;"

-        "  background:linear-gradient(135deg,#6366f1,#8b5cf6);"

-        "  color:#fff;border:none;border-radius:14px;"

-        "  font-size:16px;font-weight:600;letter-spacing:1px;"

+        "  width:100%;height:56px;margin-top:28px;"

+        "  background:linear-gradient(135deg,#0ea5e9,#14b8a6);"

+        "  color:#fff;border:none;border-radius:16px;"

+        "  font-size:1.1rem;font-weight:700;letter-spacing:0.5px;"

-        "button:active{transform:scale(0.97);box-shadow:0 2px 8px rgba(99,102,241,0.3)}"

+        "button:active{transform:scale(0.97);box-shadow:0 3px 10px rgba(20,184,166,0.2)}"

-        "  background:transparent;box-shadow:none;padding:16px;margin-top:4px"

+        "  background:transparent;backdrop-filter:none;border:none;"

+        "  box-shadow:none;padding:18px;margin-top:6px"

-        ".msg:last-of-type p,.msg:last-of-type .info{font-size:12px;color:#94a3b8}"

-        "a{color:#6366f1;text-decoration:none;font-weight:500}"

+        ".msg:last-of-type p,.msg:last-of-type .info{font-size:0.82rem;color:#94a3b8}"

+        "a{color:#0ea5e9;text-decoration:none;font-weight:600}"

+        ".msg .success{color:#059669;font-weight:600}"

+        ".msg .error{color:#dc2626;font-weight:600}"

WiFi.setAutoReconnect(true);  // ESP32 自动重连


Good. Now redesign the `data/index.html` — with a complete medical device onboarding experience. No factory reset button (as the user requested — nothing permanent/destructive).

Key design principles:
1. Medical/health aesthetic — teal/green/blue
2. Large fonts for elderly users
3. Clear step-by-step flow
4. No destructive actions
5. Professional, trustworthy look
6. Status feedback

Let me create the new index.html.


现在重写 `data/index.html`——医疗设备风格，无危险操作：


Write

<!DOCTYPE html>

    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <title>康养监测 · 设备配网</title>

            <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                <rect x="5" y="2" width="14" height="20" rx="3" fill="white" opacity="0.95"/>
                <rect x="9" y="6" width="6" height="6" rx="1" fill="#14b8a6" opacity="0.7"/>
                <line x1="12" y1="14" x2="12" y2="18" stroke="white" stroke-width="2" stroke-linecap="round"/>
            </svg>

五劳无感康养监测

ESP32-S3 · 毫米波雷达 · 设备上线

📶无线网络

            WiFi 名称
            <input type="text" id="wifi-ssid" placeholder="选择或输入 WiFi 名称" autocomplete="off" autocorrect="off">

            WiFi 密码
            <input type="password" id="wifi-pass" placeholder="请输入 WiFi 密码">

☁云平台

服务器<input type="text" id="mqtt-server" placeholder="IP 或域名">

端口<input type="number" id="mqtt-port" placeholder="1883">

用户名<input type="text" id="mqtt-user" placeholder="选填" autocomplete="off">

密码<input type="password" id="mqtt-pass" placeholder="选填">

🏠安装位置

            房间名称
            <input type="text" id="env-note" placeholder="如：主卧 / 老人房 / 客厅" autocomplete="off">

💡 请备注设备所在的房间，便于在云平台区分不同设备

📱设备标识

            设备编号
            <input type="text" id="device-id" placeholder="如：10100001001" autocomplete="off">

    <button class="btn btn-go" id="btn-submit">
        📡 保存配置并连接
    </button>

        等待配置…


The file d:\PlatformIO1\Projects\Healthv2.0\data\index.html has been updated successfully. (file state is current in your context — no need to Read it back)

<!DOCTYPE html>

    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <title>康养监测 · 设备配网</title>

            <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                <rect x="5" y="2" width="14" height="20" rx="3" fill="white" opacity="0.95"/>
                <rect x="9" y="6" width="6" height="6" rx="1" fill="#14b8a6" opacity="0.7"/>
                <line x1="12" y1="14" x2="12" y2="18" stroke="white" stroke-width="2" stroke-linecap="round"/>
            </svg>

五劳无感康养监测

ESP32-S3 · 毫米波雷达 · 设备上线

📶无线网络

            WiFi 名称
            <input type="text" id="wifi-ssid" placeholder="选择或输入 WiFi 名称" autocomplete="off" autocorrect="off">

            WiFi 密码
            <input type="password" id="wifi-pass" placeholder="请输入 WiFi 密码">

☁云平台

服务器<input type="text" id="mqtt-server" placeholder="IP 或域名">

端口<input type="number" id="mqtt-port" placeholder="1883">

用户名<input type="text" id="mqtt-user" placeholder="选填" autocomplete="off">

密码<input type="password" id="mqtt-pass" placeholder="选填">

🏠安装位置

            房间名称
            <input type="text" id="env-note" placeholder="如：主卧 / 老人房 / 客厅" autocomplete="off">

💡 请备注设备所在的房间，便于在云平台区分不同设备

📱设备标识

            设备编号
            <input type="text" id="device-id" placeholder="如：10100001001" autocomplete="off">

    <button class="btn btn-go" id="btn-submit">
        📡 保存配置并连接
    </button>

        等待配置…

-            --transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1);

* { margin: 0; padding: 0; box-sizing: border-box; }

-            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;

+            font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;

+            background: linear-gradient(180deg, #f5f9fc 0%, #e8f4f8 50%, #f0f7f4 100%);

-            -moz-osx-font-smoothing: grayscale;

-        /* ---- Decorative blobs ---- */

+            content: ''; position: fixed; border-radius: 50%; pointer-events: none; z-index: 0;

-            width: 380px; height: 380px;

-            background: radial-gradient(circle, rgba(99,102,241,0.07) 0%, transparent 70%);

+            top: -100px; right: -100px; width: 260px; height: 260px;

+            background: radial-gradient(circle, rgba(20,184,166,0.07) 0%, transparent 70%);

-            bottom: -120px; left: -120px;

-            width: 300px; height: 300px;

-            background: radial-gradient(circle, rgba(34,197,94,0.05) 0%, transparent 70%);

+            bottom: -80px; left: -80px; width: 220px; height: 220px;

+            background: radial-gradient(circle, rgba(14,165,233,0.05) 0%, transparent 70%);

+        .wrap { width: 100%; max-width: 460px; position: relative; z-index: 1; }

+        .hero { text-align: center; padding: 10px 0 22px; }

+            width: 68px; height: 68px; margin: 0 auto 16px;

+            background: linear-gradient(135deg, #0ea5e9, #14b8a6);

+            display: flex; align-items: center; justify-content: center;

+        .hero-logo svg { width: 34px; height: 34px; }

+        .hero h1 { font-size: 1.4rem; font-weight: 700; letter-spacing: -0.3px; margin-bottom: 4px; }

+        .hero .sub { font-size: 0.85rem; color: var(--muted); line-height: 1.6; }

-            background: linear-gradient(135deg, #6366f1, #8b5cf6);

+            backdrop-filter: blur(10px);

-            transition: box-shadow var(--transition);

+            display: flex; align-items: center; gap: 10px;

+            font-size: 1rem; font-weight: 700; margin-bottom: 16px;

-        .icon-wifi  { background: #eef2ff; color: #6366f1; }

-        .icon-mqtt  { background: #ecfdf5; color: #16a34a; }

-        .icon-room  { background: #fffbeb; color: #f59e0b; }

-        .icon-device{ background: #fdf2f8; color: #db2777; }

+            width: 34px; height: 34px; border-radius: 10px;

+            font-size: 1.1rem; flex-shrink: 0;

+        .ic-wifi  { background: #e0f2fe; color: #0ea5e9; }

+        .ic-cloud { background: #ccfbf1; color: #14b8a6; }

+        .ic-home  { background: #fef9c3; color: #ca8a04; }

+        .ic-id    { background: #fce7f3; color: #db2777; }

+        .field { margin-bottom: 14px; }

+        .field:last-child { margin-bottom: 0; }

+            display: block; font-size: 0.8rem; font-weight: 600;

+            color: #0ea5e9; margin-bottom: 5px; letter-spacing: 0.2px;

-            border: 2px solid var(--border);

-            border-radius: var(--radius-sm);

-            transition: all var(--transition);

+            width: 100%; height: 50px; padding: 0 16px;

+            font-size: 1rem; color: var(--text); background: #f8fafc;

+            border: 2px solid var(--border); border-radius: var(--radius-sm);

+            outline: none; -webkit-appearance: none;

-            border-color: var(--accent);

+            border-color: var(--teal); background: #fff;

+        .field input::placeholder { color: #cbd5e1; }

+        .row2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }

+            background: linear-gradient(135deg, #fffef5, #fffde7);

+        .card-room .card-head { color: #92400e; }

+        .card-room label { color: #b45309; }

+        .card-room input { background: #fff; border-color: #fde68a; }

+        .card-room input:focus { border-color: #f59e0b; box-shadow: 0 0 0 5px rgba(245,158,11,0.08); }

+        .card-room .hint { margin-top: 8px; font-size: 0.78rem; color: #92400e; opacity: 0.75; }

-            grid-template-columns: 1fr 1fr;

-        /* ---- Highlight card (环境备注) ---- */

-            background: linear-gradient(135deg, #fffdf5, #fffbea);

-            border-color: var(--amber);

-            transition: all var(--transition);

+            width: 100%; height: 56px; border: none; border-radius: 18px;

+            font-size: 1.1rem; font-weight: 700; letter-spacing: 0.5px;

+            cursor: pointer; transition: all 0.2s;

+            display: flex; align-items: center; justify-content: center; gap: 8px;

+        .btn:active { transform: scale(0.97); }

+            background: linear-gradient(135deg, #0ea5e9, #14b8a6);

+            color: #fff; margin-top: 6px;

-            background: linear-gradient(135deg, #6366f1, #8b5cf6);

+            padding: 20px 0; font-size: 0.85rem; color: var(--muted);

-        .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; }

+        .dot { width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0; }

+        .dot.ok    { background: #14b8a6; animation: breathe 3s infinite; }

+        .dot.wait  { background: #f59e0b; animation: breathe 1s infinite; }

+        .dot.off   { background: #94a3b8; }

0%, 100% { opacity: 1; transform: scale(1); }

-            50%       { opacity: 0.35; transform: scale(0.85); }

+            50%       { opacity: 0.3; transform: scale(0.8); }

-            transform: translateX(-50%);

+            position: fixed; top: 20px; left: 50%; transform: translateX(-50%);

+            z-index: 9999; display: flex; flex-direction: column; gap: 8px;

+            pointer-events: auto; padding: 12px 22px; border-radius: 26px;

+            font-size: 0.9rem; font-weight: 600; text-align: center;

+            white-space: nowrap; box-shadow: 0 6px 20px rgba(0,0,0,0.12);

-            transition: opacity 0.3s, transform 0.3s;

-            transform: translateY(-10px);

-        .toast-ok    { background: #16a34a; }

-        .toast-warn  { background: #f59e0b; color: #1e293b; }

-        .toast-error { background: #dc2626; }

+        .toast.out { opacity: 0; transform: translateY(-10px); transition: 0.3s; }

+        .t-ok   { background: #059669; color: #fff; }

+        .t-warn { background: #f59e0b; color: #1e293b; }

+        .t-err  { background: #dc2626; color: #fff; }

-            from { opacity: 0; transform: translateY(-12px) scale(0.95); }

+            from { opacity: 0; transform: translateY(-12px) scale(0.94); }

to   { opacity: 1; transform: translateY(0) scale(1); }

-            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; }

-            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; }

-            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); }

-            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; }

+            .card { padding: 18px 16px; }

+            .hero-logo { width: 56px; height: 56px; border-radius: 18px; }

+            .hero-logo svg { width: 28px; height: 28px; }

-                <!-- Medical cross / heart icon -->

-                <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">

-                          fill="white" stroke="none"/>

-                ESP32-S3 · 60G 毫米波雷达<br>五劳无感康养监测

+            <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">

+                <line x1="12" y1="14" x2="12" y2="18" stroke="white" stroke-width="2" stroke-linecap="round"/>

+
ESP32-S3 · 毫米波雷达 · 设备上线

-                无线网络

-                <label>WiFi 名称 (SSID)</label>

-                <input type="text" id="wifi-ssid" placeholder="选择或输入 WiFi 名称" autocomplete="off" autocorrect="off">

-            <button class="scan-btn" id="scan-btn" type="button">

-                🔍 扫描附近 WiFi

-                <input type="password" id="wifi-pass" placeholder="请输入 WiFi 密码">

+            <input type="text" id="wifi-ssid" placeholder="选择或输入 WiFi 名称" autocomplete="off" autocorrect="off">

-                云平台连接

-                    <label>服务器地址</label>

-                    <input type="text" id="mqtt-server" placeholder="IP 或域名">

-                    <input type="number" id="mqtt-port" placeholder="1883">

-                    <input type="text" id="mqtt-user" placeholder="选填" autocomplete="off">

-                    <input type="password" id="mqtt-pass" placeholder="选填">

+            <input type="password" id="wifi-pass" placeholder="请输入 WiFi 密码">

-        <div class="card card-highlight">

-                安装位置

-                <label>房间名称 / 环境备注</label>

-                <input type="text" id="env-note" placeholder="如：主卧 / 老人房 / 客厅" autocomplete="off">

-                💡 请备注设备安装的房间，便于在云平台区分不同区域的数据

+
服务器<input type="text" id="mqtt-server" placeholder="IP 或域名">

+
端口<input type="number" id="mqtt-port" placeholder="1883">

+
用户名<input type="text" id="mqtt-user" placeholder="选填" autocomplete="off">

+
密码<input type="password" id="mqtt-pass" placeholder="选填">

-                设备标识

-                <input type="text" id="device-id" placeholder="如：health_001" autocomplete="off">

+            <input type="text" id="env-note" placeholder="如：主卧 / 老人房 / 客厅" autocomplete="off">

+
💡 请备注设备所在的房间，便于在云平台区分不同设备

-        <button class="btn btn-primary" id="btn-submit" type="button">

-        <button class="btn btn-danger" id="btn-reset" type="button">

-        <div class="status-bar" id="status-bar">

-            等待配置…

+            <input type="text" id="device-id" placeholder="如：10100001001" autocomplete="off">

+    <button class="btn btn-go" id="btn-submit">

-    // ── 全局缓冲 ──────────────────────────────────

-            return JSON.parse(localStorage.getItem('health_config') || '{}');

+    <div class="status" id="status-bar">

+        等待配置…

-    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}`;

-            setTimeout(() => el.remove(), 300);

+const $ = id => document.getElementById(id);

-    // ── 状态栏 ────────────────────────────────────

-        const dot = E('status-bar').querySelector('.status-dot');

-        dot.className = `status-dot ${state}`;

-        txt.textContent = map[state] || state;

+// ── Toast ──────────────────────────────

+    type = type || 'ok'; dur = dur || 2600;

+    const el = document.createElement('div');

+    el.className = 'toast t-' + type; el.textContent = msg;

+    setTimeout(() => { el.classList.add('out'); setTimeout(() => el.remove(), 300); }, dur);

-    // ── 加载已保存配置 ────────────────────────────

-        const f = (id, val) => { if (val) E(id).value = val; };

-        f('wifi-ssid',    SELF.wifi_ssid);

-        f('mqtt-port',    SELF.mqtt_port    || '8000');

-        f('mqtt-user',    SELF.mqtt_user    || 'test');

-        f('mqtt-pass',    SELF.mqtt_pass    || '');

-        f('env-note',     SELF.env_note     || '主卧');

+// ── 状态 ──────────────────────────────

+    const dot = $('status-dot'), txt = $('status-text');

+    dot.className = 'dot ' + ({ on:'ok', wait:'wait', off:'off', ok:'ok' }[s] || 'off');

+    txt.textContent = { on:'✅ 设备已上线', wait:'⏳ 正在连接…', off:'等待配置…', ok:'📋 配置已就绪' }[s] || s;

-    // ── 持久化到 localStorage ────────────────────

-            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));

+// ── 加载 ──────────────────────────────

+    try { c = JSON.parse(localStorage.getItem('health_config') || '{}'); } catch(_){}

+    if (c.wifi_ssid)   $('wifi-ssid').value   = c.wifi_ssid;

+    if (c.mqtt_server) $('mqtt-server').value = c.mqtt_server || '202.110.234.102';

+    if (c.mqtt_port)   $('mqtt-port').value   = c.mqtt_port   || '8000';

+    if (c.mqtt_user)   $('mqtt-user').value   = c.mqtt_user   || 'test';

+    if (c.mqtt_pass)   $('mqtt-pass').value   = c.mqtt_pass   || '';

+    if (c.device_id)   $('device-id').value   = c.device_id   || '10100001001';

+    if (c.env_note)    $('env-note').value    = c.env_note    || '主卧';

-    // ── 提交 ──────────────────────────────────────

-        const env = E('env-note').value.trim();

-            toast('⚠ 请先填写安装位置（房间名称）', 'warn');

+// ── 保存到本地 ────────────────────────

+        wifi_ssid:   $('wifi-ssid').value,

+        wifi_pass:   $('wifi-pass').value,

+        mqtt_server: $('mqtt-server').value,

+        mqtt_port:   $('mqtt-port').value,

+        mqtt_user:   $('mqtt-user').value,

+        mqtt_pass:   $('mqtt-pass').value,

+        device_id:   $('device-id').value,

+        env_note:    $('env-note').value,

+    localStorage.setItem('health_config', JSON.stringify(c));

-        const ssid = E('wifi-ssid').value.trim();

-            toast('⚠ 请填写 WiFi 名称', 'warn');

+// ── 提交 ──────────────────────────────

+    var env = $('env-note').value.trim();

+    if (!env) { toast('⚠ 请先填写安装位置', 'warn'); $('env-note').focus(); return; }

+    var ssid = $('wifi-ssid').value.trim();

+    if (!ssid) { toast('⚠ 请填写 WiFi 名称', 'warn'); $('wifi-ssid').focus(); return; }

-        const params = new URLSearchParams();

-        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);

+    p.append('p',  $('wifi-pass').value);

+    p.append('ms', $('mqtt-server').value);

+    p.append('mp', $('mqtt-port').value);

+    p.append('mu', $('mqtt-user').value);

+    p.append('mw', $('mqtt-pass').value);

+    p.append('id', $('device-id').value);

-        fetch('/save', { method: 'POST', body: params })

-                toast('🎉 设备已上线！WiFi 连接成功');

-                toast('📋 配置已保存（设备端已接收）');

-    // ── 重置 ──────────────────────────────────────

-        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 => {

+    fetch('/save', { method: 'POST', body: p })

+            toast('🎉 设备已上线，WiFi 连接成功！');

+            toast('📋 配置已保存（设备端接收中）');

-        E('mqtt-user').value   = 'test';

-    // ── WiFi 扫描 (模拟 - 实际由 ESP32 提供) ────

-                    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';

-                // 离线/ESP32 未连接时用 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 },

-                toast('⚠ 离线模式，显示模拟列表', 'warn');

-        return String(s).replace(/[&<>"']/g, c => m[c]);

-    // ── Init ──────────────────────────────────────

-    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');

-    // ── 空闲动画 ──────────────────────────────────

-        if (txt.textContent.includes('等待配置')) {

-            idleTick = (idleTick + 1) % frames.length;

-            // 只在未连接时显示动画


等一下，我们软件组写了，直接用它的界面


现在再去设计配网页面，你现在先记录下我们的代码，一会避免错误回不去


The user is saying that their software team has written the config page, and we should just use their interface. So the `data/index.html` I just rewrote might not be needed — they have their own version. But wait, the WiFiManager captive portal CSS in `wifi_manager.cpp` is still relevant because that's what the ESP32 serves directly.

Let me clarify: the user seems to be saying that the software team wrote the config interface, so I should use theirs instead of my custom one. But there are two separate things:

1. **WiFiManager captive portal** (injected via `_wm.setCustomHeadElement` in `wifi_manager.cpp`) — this is the actual page served by the ESP32 when a phone connects to the AP. This is always active.

2. **`data/index.html`** — this is a file in the PlatformIO `data` folder, intended to be uploaded to LittleFS/SPIFFS and served via a separate web server. But there's no web server code in this project...

Actually, looking at the project, there's no AsyncWebServer or similar. The `data/index.html` might just be a design reference file that was never actually served. The only actual config page is the WiFiManager captive portal.

So the user might be saying: "Our software team wrote the web interface, we'll use theirs — so my `data/index.html` changes are irrelevant, but the WiFiManager CSS is what matters."

Or they could be saying: "They already have a web interface, don't change it, use it as-is."

Let me revert my `data/index.html` change since the software team's version should be used. The WiFiManager CSS change should probably be kept because that's the actual captive portal.

Wait, but the user might also be referring to the `data/index.html` specifically as the thing the software team wrote. Let me check the git history to see what the original `data/index.html` looked like before my changes.

Actually, the original `data/index.html` (before my first redesign earlier in this conversation) was already a custom page written by the user/team. My changes replaced it. The user is now saying they want to use the software team's version.

But the `data/index.html` I first saw at the beginning of this conversation — was that the software team's version? The user opened it and said "配网界面太丑". So it seems like the team wrote it and the user thought it was ugly, then I redesigned it.

Now the user says "等一下，我们软件组写了，直接用它的界面" — "wait, our software team wrote it, just use their interface". This is ambiguous:
- It could mean: "revert to the software team's original design"
- It could mean: "the software team already has their own separate web interface, don't bother with the HTML page"

Given the project structure (no web server code), the `data/index.html` seems to be a reference/standalone file, and the actual user-facing config page IS the WiFiManager captive portal. So the most important thing is the WiFiManager CSS change, which I already made.

The `data/index.html` might be served separately by the software team's own infrastructure.

I think the user is telling me to not worry about `data/index.html` and just use what the team provided. Let me revert `data/index.html` to the original version from git, and keep only the WiFiManager captive portal CSS changes.

Let me revert data/index.html to the committed version (which was the team's version before my first redesign, or maybe it was my first redesign... let me check).

Actually, looking at the git status earlier, `data/index.html` was already in the first commit of this session. Let me check what the very original looked like.

The very first version I read (the "ugly" one) — was that the team's version or was that already edited? The user opened it and said "配网界面太丑", implying they looked at what the team wrote and thought it was ugly, then asked me to redesign it. But now they're saying "the software team wrote it, use their interface."

Hmm, I think the situation is:
1. The software team has their own web-based management interface (maybe a separate web app or server)
2. The `data/index.html` and WiFiManager captive portal are the device-level config
3. The user wants to use the team's interface for management, and the device should just provide the basic captive portal

So the takeaway: keep the WiFiManager captive portal CSS change (that's the device's own page), and don't worry about `data/index.html` — the software team handles the web UI.

Let me revert `data/index.html` to the original (the team's version) from git.


明白，软件组有自己的界面。我把 `data/index.html` 恢复到团队的原版，只保留 WiFiManager 配网门户的 CSS 改进。


Bash

git show HEAD~1:data/index.html | head -5

Check what the team's original index.html looks like


<!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">


#include <WiFi.h>
#include <WiFiManager.h>
#include <LittleFS.h>

// ====================== 全局配置 ======================
WiFiManager wm;

// 自定义配网参数
WiFiManagerParameter param_device_id("device_id", "设备编号", "Device_001", 32);
WiFiManagerParameter param_env_note("env_note", "安装环境", "主卧", 32);

// 持久化存储的参数变量
String g_deviceId = "Device_001";
String g_envNote = "主卧";

// AP热点名称、密码
const char* AP_SSID  = "健康监测网关配网";
const char* AP_PASS  = "12345678";

// ====================== 函数声明 ======================
void loadParamsFromLittleFS();
void saveParamsToLittleFS();
void handleParamsSaved();
bool initWiFiManagerPortal();

// ====================== 从文件读取参数 ======================
void loadParamsFromLittleFS() {
  if (!LittleFS.begin(true)) {
    Serial.println("LittleFS 挂载失败，使用默认参数");
    return;
  }

  File file = LittleFS.open("/config.json", "r");
  if (!file) {
    Serial.println("未找到配置文件，使用默认参数");
    LittleFS.end();
    return;
  }

  String jsonStr = file.readString();
  file.close();
  LittleFS.end();

  // 简单解析（如需严谨可用ArduinoJson，这里极简够用）
  int devStart = jsonStr.indexOf("\"deviceId\":\"") + 12;
  int devEnd   = jsonStr.indexOf("\"", devStart);
  if (devStart > 12 && devEnd > devStart) {
    g_deviceId = jsonStr.substring(devStart, devEnd);
  }

  int envStart = jsonStr.indexOf("\"env\":\"") + 7;
  int envEnd   = jsonStr.indexOf("\"", envStart);
  if (envStart > 7 && envEnd > envStart) {
    g_envNote = jsonStr.substring(envStart, envEnd);
  }

  // 回填到配网表单默认值
  param_device_id.setValue(g_deviceId.c_str(), g_deviceId.length());
  param_env_note.setValue(g_envNote.c_str(), g_envNote.length());

  Serial.println("加载已有配置：");
  Serial.print("设备编号："); Serial.println(g_deviceId);
  Serial.print("安装环境："); Serial.println(g_envNote);
}

// ====================== 保存参数到文件 ======================
void saveParamsToLittleFS() {
  if (!LittleFS.begin(true)) {
    Serial.println("LittleFS 无法挂载，保存失败");
    return;
  }

  File file = LittleFS.open("/config.json", "w");
  if (!file) {
    Serial.println("配置文件打开失败");
    LittleFS.end();
    return;
  }

  // 写入JSON，MQTT字段留空占位（前端已隐藏）
  String json = "{"
                "\"deviceId\":\"" + g_deviceId + "\","
                "\"env\":\"" + g_envNote + "\","
                "\"mqttHost\":\"\",\"mqttPort\":\"1883\",\"mqttUser\":\"\",\"mqttPwd\":\"\""
                "}";
  file.print(json);
  file.close();
  LittleFS.end();
  Serial.println("参数已保存到 /config.json");
}

// ====================== 配网提交回调（表单保存触发） ======================
void handleParamsSaved() {
  // 读取页面提交过来的值
  g_deviceId = param_device_id.getValue();
  g_envNote  = param_env_note.getValue();

  Serial.println("\n===== 配网参数提交完成 =====");
  Serial.print("设备编号："); Serial.println(g_deviceId);
  Serial.print("安装环境："); Serial.println(g_envNote);

  // 持久化存储
  saveParamsToLittleFS();
}

// ====================== 初始化配网页面（核心HTML嵌入） ======================
bool initWiFiManagerPortal() {
  // 关闭调试日志（可选）
  // wm.setDebugOutput(false);

  // 配网保存回调绑定
  wm.setSaveParamsCallback(handleParamsSaved);

  // 添加两个自定义表单参数
  wm.addParameter(&param_device_id);
  wm.addParameter(&param_env_note);

  // ====================== 1. Head 头部：CSS样式+主题色 ======================
  String customHead = R"HTML(
<meta name='theme-color' content='#12a889'>

)HTML";
  wm.setCustomHeadElement(customHead.c_str());

  // ====================== 2. Body页面结构 + JS弹窗 ======================
  String customBody = R"HTML(

📡

观照-五劳无感 · 设备配网

WiFi 联网与参数配置

?

点击这里再次查看配网流程

📍 当前网关Gateway_A8F3

📶

无线网络

请选择 2.4GHz WiFi

WiFi 名称
📡<select></select>

WiFi 密码
🔒<input type='password' placeholder='请输入 WiFi 密码'>

⚙

设备基础信息

唯一标识与安装位置

🔖 设备编号
#<input type='text' name='device_id' placeholder='Device_001'>

📍 安装环境
🏠<input type='text' name='env_note' placeholder='如: 主卧 / 客厅 / 老人房'>

<button type='submit'>保存配置并重启连接</button>
<button class='danger' type='button' onclick='document.location.reload()'>恢复出厂配置</button>

Health Monitor · Device Provisioning

📋

配网流程

按顺序完成 4 步操作

×

1

选择 WiFi 并输入密码

确保手机已连接目标 2.4G 网络

2

填写设备基础信息

设备编号与安装环境，便于多设备管理

3

核对信息并保存

确认无误后点击保存配置

4

等待设备重启连接

设备指示灯常亮即表示配网完成

<button class='ghost' onclick='toggleModal(false)'>稍后查看</button>
<button class='primary' onclick='toggleModal(false)'>我知道了</button>

)HTML";
  wm.setCustomMenuHTML(customBody.c_str());

  // 启动配网，阻塞直到配网完成
  Serial.println("进入WiFi配网模式，热点：" + String(AP_SSID));
  bool res = wm.autoConnect(AP_SSID, AP_PASS);

  if (!res) {
    Serial.println("配网失败，重启设备");
    ESP.restart();
  }

  Serial.println("WiFi连接成功：" + WiFi.localIP().toString());
  return true;
}

// ====================== 对外读取参数接口（给MQTT上报用） ======================
String getDeviceId() {
  return g_deviceId;
}

String getInstallEnv() {
  return g_envNote;
}

// ====================== Setup入口调用 ======================
void setup() {
  Serial.begin(115200);
  delay(500);

  // 1. 上电先读取本地保存的参数
  loadParamsFromLittleFS();

  // 2. 启动WiFi配网Portal
  initWiFiManagerPortal();

  // 后续可以在这里启动MQTT、传感器、业务逻辑
  // mqttClient.connect(getDeviceId().c_str());
  // 上报数据包带上 getInstallEnv()
}

void loop() {
  // 主循环业务逻辑
  delay(100);
}这是代码，只用界面，


The user shared their software team's WiFiManager config page code. It uses `wm.setCustomMenuHTML()` with a complete custom HTML body that includes:

1. A header with logo and "观照-五劳无感 · 设备配网" title
2. A "?" help button that shows a modal with 4-step provisioning guide
3. A gateway info card showing "Gateway_A8F3"
4. WiFi settings card with SSID dropdown and password
5. Device info card with device ID and environment/room inputs
6. Submit button "保存配置并重启连接"
7. A "恢复出厂配置" button

Key observations:
- Uses `wm.setCustomMenuHTML()` to replace the ENTIRE form HTML
- Has custom params: `param_device_id` ("device_id") and `param_env_note` ("env_note")
- Has a nice modal/popup for the provisioning guide
- Uses a teal/green color scheme (`#12a889`)
- Has dark mode support
- The "恢复出厂配置" button just reloads the page — it doesn't actually reset anything permanently

Now, the user says "这是代码，只用界面" — "this is the code, just use the interface". They want me to integrate this HTML/CSS/JS into the existing project's wifi_manager.cpp, replacing my custom CSS.

But I need to be careful:
1. The team's code uses `wm.setCustomMenuHTML()` which replaces the entire form HTML. This means the field names need to match what WiFiManager expects.
2. The WiFiManager default form uses `name="s"` for SSID and `name="p"` for password. The team's code doesn't seem to have those — it has custom HTML that might not map correctly.
3. The team's code has a `<select>` for WiFi SSID — but WiFiManager doesn't populate that automatically. They'd need JavaScript to scan and populate, which they don't have.

Wait, looking more carefully at the team's code, the `<select>` for SSID is empty — it would need JavaScript to populate. And the submit button just says "保存配置并重启连接" but there's no form action. WiFiManager expects the form to submit to `/` with specific field names.

There's a mismatch here. The team's code is using `wm.setCustomMenuHTML()` which should replace the default form, but the form fields don't match WiFiManager's expected names:
- WiFiManager expects: `<input id="s" name="s">` for SSID, `<input id="p" name="p">` for password
- Team's code has: custom inputs without the correct names

This won't work with WiFiManager as-is. I need to either:
1. Adapt the team's design but use the correct WiFiManager field names
2. Or just extract the CSS/styling from their code and apply it via `setCustomHeadElement`

The user said "只用界面" (just use the interface). I think they want me to take the visual design (CSS, layout, colors, modal) and integrate it into the project's WiFiManager setup.

Let me take the design elements from the team's code and adapt them to work with WiFiManager's actual HTML structure. The team's design has these nice features:
1. Teal/green color scheme (`#12a889`)
2. Card-based layout with rounded corners
3. "?" help button with guided modal
4. Gateway info banner
5. Clean form design with icon inputs
6. Dark mode support
7. Responsive

Since WiFiManager generates its own HTML form, I can:
1. Inject the CSS via `setCustomHeadElement()` — targeting WiFiManager's default HTML structure
2. Use `setCustomMenuHTML()` to add the header, gateway info, and modal before the form
3. But NOT replace the form itself — let WiFiManager handle that

Actually, looking at WiFiManager 2.0.17 source, `setCustomMenuHTML` replaces some internal HTML. Let me think about this differently.

The cleanest approach: extract the team's CSS and adapt it to target WiFiManager's default HTML elements. Keep the team's visual identity (teal, cards, fonts, etc.) but map it to the actual WiFiManager HTML structure.

The team's design elements I should preserve:
- Color: `#12a889` (teal primary)
- Header: "观照-五劳无感 · 设备配网"
- Font sizes and spacing
- Card style
- Input styles with focus states
- Button style
- The ? help modal (injected as custom HTML via setCustomMenuHTML)
- Gateway info banner

Let me integrate this into wifi_manager.cpp. I'll:
1. Update the title
2. Replace the CSS in `setCustomHeadElement` with team's design language
3. Add the header/banner/modal via `setCustomMenuHTML`
4. Keep WiFiManager's default form structure (so the SSID/password fields work correctly)
5. The custom parameters (device_id, env_note) are already registered

Actually wait, I need to check what WiFiManager's `setCustomMenuHTML` does. In WiFiManager 2.0.17:
- `setCustomMenuHTML` replaces the content BETWEEN the header and the form in the page
- The form is still there below it

So I can use it to add the custom header and gateway info.

Let me now write the integration. The key changes to wifi_manager.cpp:
1. Update `setCustomHeadElement` with team's CSS
2. Add `setCustomMenuHTML` with header, gateway info, and help modal
3. Keep the existing form and parameters

Let me also note: the team's code had a "恢复出厂配置" button that just reloaded the page. The user said "不要有永久删除的东西" — this reload button is safe (it doesn't delete anything permanently), but it's also misleading (it doesn't actually reset). I'll either remove it or make it clear.

Actually, `document.location.reload()` on a captive portal page served by the ESP32 just reloads the same page. It doesn't reset any settings. The user said to not have destructive actions, so I should remove this button. The physical button on the device (long press) is the proper way to reset.

Let me now write the code. I need to be very careful about:
1. The CSS targets WiFiManager's actual HTML elements
2. The custom menu HTML integrates well
3. The form fields work correctly

For WiFiManager 2.0.17, the default HTML structure is roughly:
```html
<!DOCTYPE html>
<html>
<head>...</head>
<body>
  <div class="msg">  <!-- status messages -->
    <div class="info">...</div>
  </div>
  <!-- customMenuHTML goes here IF set -->
  <form action="/" method="post">
    <label for="s">SSID</label>
    <input type="text" id="s" name="s">
    <label for="p">Password</label>
    <input type="password" id="p" name="p">
    <!-- custom parameters -->
    <button type="submit">save</button>
  </form>
  <!-- more msg divs for footer -->
</body>
</html>
```

So my `customMenuHTML` will appear between the msg div and the form. I can put the header there.

Let me write this out. I need to be efficient since this is getting long.

For the customMenuHTML, I'll add:
1. The header with "观照-五劳无感 · 设备配网"
2. Gateway info banner
3. The help modal
4. JavaScript for the modal toggle

For the CSS, I'll use the team's color scheme and design language, but adapted to WiFiManager's HTML.

Let me also keep in mind: the user wants me to "just use the interface" — meaning take the team's visual design and apply it. Not necessarily copy-paste their code verbatim (since some of it doesn't integrate with WiFiManager properly).

Let me write the integrated code now.


好的，把软件组的界面整合进 WiFiManager。他们的设计语言（teal 色系、卡片布局、引导弹窗）全部保留。


73	    // 配置 WiFiManager
74	    _wm.setDebugOutput(false);  // 关闭调试输出 (避免干扰)
75	    _wm.setConfigPortalTimeout(WIFI_CONFIG_TIMEOUT);
76	    _wm.setConnectTimeout(WIFI_CONNECT_TIMEOUT);
77	    _wm.setTitle("康养监测 · 设备配网");
78	    _wm.setCustomHeadElement(
79	        "<style>"
80	        /* === 全局 — 暖白医疗风，大字号适配老年用户 === */
81	        "*{margin:0;padding:0;box-sizing:border-box}"
82	        "html{font-size:17px}"  /* 基准加大 */

    // 配置 WiFiManager
    _wm.setDebugOutput(false);  // 关闭调试输出 (避免干扰)
    _wm.setConfigPortalTimeout(WIFI_CONFIG_TIMEOUT);
    _wm.setConnectTimeout(WIFI_CONNECT_TIMEOUT);
    _wm.setTitle("康养监测 · 设备配网");
    _wm.setCustomHeadElement(
        "<style>"
        /* === 全局 — 暖白医疗风，大字号适配老年用户 === */
        "*{margin:0;padding:0;box-sizing:border-box}"
        "html{font-size:17px}"  /* 基准加大 */


Let me replace the entire setCustomHeadElement block and add setCustomMenuHTML. The approach:

1. Keep WiFiManager's default form (so SSID/password fields work correctly)
2. Use setCustomHeadElement for CSS (team's design language)
3. Use setCustomMenuHTML for header, gateway info, help modal (no destructive actions)
4. Keep existing custom parameters (mqtt_server, mqtt_port, etc.)

Let me write the replacement. This is a large edit, so I need to be precise.


Edit

    _wm.setTitle("康养监测 · 设备配网");
    _wm.setCustomHeadElement(
        "<style>"
        /* === 全局 — 暖白医疗风，大字号适配老年用户 === */
        "*{margin:0;padding:0;box-sizing:border-box}"
        "html{font-size:17px}"  /* 基准加大 */
        "body{"
        "  font-family:-apple-system,BlinkMacSystemFont,'PingFang SC','Microsoft YaHei',sans-serif;"
        "  background:linear-gradient(180deg,#f5f9fc 0%,#e8f4f8 50%,#f0f7f4 100%);"
        "  min-height:100vh;display:flex;flex-direction:column;align-items:center;"
        "  padding:24px 16px 48px;color:#2c3e50;-webkit-font-smoothing:antialiased"
        "}"
        /* 装饰光晕 */
        "body::before{"
        "  content:'';position:fixed;top:-120px;right:-120px;"
        "  width:280px;height:280px;border-radius:50%;"
        "  background:radial-gradient(circle,rgba(20,184,166,0.06) 0%,transparent 70%);"
        "  pointer-events:none;z-index:0"
        "}"
        "body::after{"
        "  content:'';position:fixed;bottom:-80px;left:-80px;"
        "  width:240px;height:240px;border-radius:50%;"
        "  background:radial-gradient(circle,rgba(59,130,246,0.05) 0%,transparent 70%);"
        "  pointer-events:none;z-index:0"
        "}"
        /* === 头部卡片 === */
        ".msg{"
        "  width:100%;max-width:460px;position:relative;z-index:1;"
        "  background:rgba(255,255,255,0.92);backdrop-filter:blur(10px);"
        "  border-radius:24px;padding:36px 28px 24px;margin-bottom:18px;text-align:center;"
        "  border:1px solid rgba(255,255,255,0.6);"
        "  box-shadow:0 2px 8px rgba(0,0,0,0.03),0 12px 32px rgba(0,0,0,0.05)"
        "}"
        /* 医疗十字 LOGO */
        ".msg::before{"
        "  content:'';display:block;width:64px;height:64px;margin:0 auto 20px;"
        "  background:linear-gradient(135deg,#0ea5e9,#14b8a6);border-radius:20px;"
        "  box-shadow:0 10px 28px rgba(20,184,166,0.22);"
        "  position:relative"
        "}"
        ".msg h1,.msg h2{font-size:1.35rem;font-weight:700;color:#1e293b;margin-bottom:6px;letter-spacing:-0.3px}"
        ".msg p,.msg .info{font-size:0.88rem;color:#64748b;line-height:1.6}"
        /* 隐藏默认 h1 */
        ".msg h1{font-size:0;margin:0;padding:0}"
        ".msg h1::after{content:'五劳无感康养监测';font-size:1.45rem;font-weight:700;color:#0f172a}"
        ".msg .info{font-size:0.88rem!important}"
        /* === 表单卡片 === */
        "form{"
        "  width:100%;max-width:460px;position:relative;z-index:1;"
        "  background:rgba(255,255,255,0.92);backdrop-filter:blur(10px);"
        "  border-radius:24px;padding:28px;"
        "  border:1px solid rgba(255,255,255,0.6);"
        "  box-shadow:0 2px 8px rgba(0,0,0,0.03),0 12px 32px rgba(0,0,0,0.05)"
        "}"
        "label{"
        "  display:block;font-size:0.82rem;font-weight:600;"
        "  color:#0ea5e9;margin:18px 0 6px;letter-spacing:0.3px"
        "}"
        "label:first-of-type{margin-top:0}"
        "input[type=text],input[type=password],input[type=number]{"
        "  width:100%;height:52px;padding:0 16px;"
        "  border:2px solid #e2e8f0;border-radius:14px;"
        "  font-size:1.05rem;color:#1e293b;background:#f8fafc;"
        "  outline:none;transition:all 0.25s;-webkit-appearance:none"
        "}"
        "input:focus{"
        "  border-color:#14b8a6;background:#fff;"
        "  box-shadow:0 0 0 5px rgba(20,184,166,0.08)"
        "}"
        "input::placeholder{color:#cbd5e1;font-size:0.94rem}"
        /* === 提交按钮 === */
        "button[type=submit]{"
        "  width:100%;height:56px;margin-top:28px;"
        "  background:linear-gradient(135deg,#0ea5e9,#14b8a6);"
        "  color:#fff;border:none;border-radius:16px;"
        "  font-size:1.1rem;font-weight:700;letter-spacing:0.5px;"
        "  cursor:pointer;transition:all 0.2s;"
        "  box-shadow:0 6px 22px rgba(20,184,166,0.25)"
        "}"
        "button:active{transform:scale(0.97);box-shadow:0 3px 10px rgba(20,184,166,0.2)}"
        /* === 页脚 === */
        ".msg:last-of-type{"
        "  background:transparent;backdrop-filter:none;border:none;"
        "  box-shadow:none;padding:18px;margin-top:6px"
        "}"
        ".msg:last-of-type::before{display:none}"
        ".msg:last-of-type p,.msg:last-of-type .info{font-size:0.82rem;color:#94a3b8}"
        "a{color:#0ea5e9;text-decoration:none;font-weight:600}"
        /* === 成功/错误 === */
        ".msg .success{color:#059669;font-weight:600}"
        ".msg .error{color:#dc2626;font-weight:600}"
        "</style>"
    );

_wm.setTitle("观照-五劳无感 · 设备配网");
    _wm.setCustomHeadElement(
        "<meta name='theme-color' content='#12a889'>"
        ""
    );

    // 自定义菜单 HTML — 软件组设计的头部 + 引导弹窗
    _wm.setCustomMenuHTML(
        "
"
        /* 头部 */
        "
"
        "
📡
"
        "

观照-五劳无感 · 设备配网
"
        "
WiFi 联网与参数配置

"
        /* 帮助按钮 */
        "
?
"
        "
"
        /* 网关信息 */
        "
"
        "📍 当前网关"
        "Gateway
"
        "
"
        /* 引导弹窗 */
        "
"
        "
"
        "
"
        "
📋
"
        "

配网流程
"
        "
按顺序完成 4 步操作

"
        "
×

"
        "
"
        "

1

连接此热点
"
        "
手机连接 " + String(WIFI_AP_SSID) + " 热点，密码 " + String(WIFI_AP_PASSWORD) + "

"
        "

2

选择 WiFi 并输入密码
"
        "
选择您的 2.4GHz WiFi 网络

"
        "

3

填写设备信息
"
        "
设备编号与安装环境便于管理

"
        "

4

保存并等待连接
"
        "
指示灯常亮即表示配网完成

"
        "
"
        "
"
        "<button class='ghost' onclick='toggleHelp(false)'>稍后查看</button>"
        "<button class='pri' onclick='toggleHelp(false)'>我知道了</button>
"
        "

"
        ""
        ""
    );