/**
 * @file wifi_manager.cpp
 * @brief WiFi 配网管理器 — 原生 WebServer 实现
 */

#include "wifi_manager.h"

WiFiMgmt* WiFiMgmt::_instance = nullptr;

// ============================================================
// 构造 & 析构
// ============================================================

WiFiMgmt::WiFiMgmt()
    : _server(nullptr)
    , _dns(nullptr)
    , _connected(false)
    , _portalActive(false)
    , _portalStartTime(0)
    , _lastCheckTime(0)
    , _buttonPressTime(0)
    , _buttonPressed(false)
    , _ledTaskHandle(nullptr)
    , _ledMode(0)
{
    memset(&_config, 0, sizeof(_config));
    _instance = this;
}

WiFiMgmt::~WiFiMgmt() {
    _stopPortal();
    _instance = nullptr;
}

// ============================================================
// 初始化
// ============================================================

bool WiFiMgmt::begin() {
    Serial.println(F("\n========================================"));
    Serial.println(F("  WiFi 配网管理器 初始化 (原生模式)"));
    Serial.println(F("========================================"));

    pinMode(CONFIG_BUTTON_PIN, INPUT_PULLUP);
    pinMode(LED_WIFI_PIN, OUTPUT);
    digitalWrite(LED_WIFI_PIN, LOW);

    // LED 独立任务
    xTaskCreate(_ledTaskFunc, "WiFiLed", 1024, nullptr, 1, &_ledTaskHandle);

    // LittleFS
    if (!LittleFS.begin(true)) {
        Serial.println(F("[WiFi] ❌ LittleFS 挂载失败!"));
        return false;
    }
    Serial.println(F("[WiFi] ✅ LittleFS 已挂载"));

    // 加载配置
    if (!_loadConfig()) {
        Serial.println(F("[WiFi] 使用默认配置"));
        strncpy(_config.mqtt_server, MQTT_DEFAULT_SERVER,   sizeof(_config.mqtt_server) - 1);
        strncpy(_config.mqtt_port,   MQTT_DEFAULT_PORT_STR, sizeof(_config.mqtt_port) - 1);
        strncpy(_config.mqtt_user,   MQTT_DEFAULT_USER,     sizeof(_config.mqtt_user) - 1);
        strncpy(_config.mqtt_pass,   MQTT_DEFAULT_PASS,     sizeof(_config.mqtt_pass) - 1);
        strncpy(_config.device_id,   MQTT_DEFAULT_DEVICE,   sizeof(_config.device_id) - 1);
        strncpy(_config.env_note,    MQTT_DEFAULT_ENV,      sizeof(_config.env_note) - 1);
    }

    WiFi.setAutoReconnect(true);
    WiFi.mode(WIFI_STA);

    // 尝试连接已保存的 WiFi
    _connected = _connectWiFi();

    if (!_connected) {
        Serial.println(F("[WiFi] 自动连接失败, 启动配网模式 (60s 超时)..."));
        _setLEDMode(2);
        startConfigPortal();  // 60s 超时
        if (!_connected) {
            Serial.println(F("[WiFi] 配网超时, 尝试恢复已保存连接..."));
            _connected = _connectWiFi();
        }
    }

    if (_connected) {
        _setLEDMode(3);
    }

    _lastCheckTime = millis();
    return _connected;
}

// ============================================================
// 主循环
// ============================================================

bool WiFiMgmt::loop() {
    // 配网页面运行时
    if (_portalActive) {
        _dns->processNextRequest();
        _server->handleClient();

        // 60s 超时自动关闭
        if (millis() - _portalStartTime >= (WIFI_CONFIG_TIMEOUT * 1000UL)) {
            Serial.println(F("[WiFi] ⏰ 配网超时, 关闭页面"));
            _stopPortal();
            _setLEDMode(1);
        }
        return _connected;
    }

    // 长按按钮 → 强制配网
    if (_checkButtonLongPress()) {
        Serial.println(F("[WiFi] ⚠ 检测到长按, 清除配置并重启..."));
        resetSettings();
        return false;
    }

    // 定期检查 WiFi 状态
    if (millis() - _lastCheckTime > WIFI_RETRY_INTERVAL) {
        _lastCheckTime = millis();
        if (WiFi.status() != WL_CONNECTED) {
            _connected = false;
            _setLEDMode(1);
            Serial.println(F("[WiFi] ⚠ WiFi 断开, 尝试重连..."));
            _connectWiFi();
        } else {
            _connected = true;
            _setLEDMode(3);
        }
    }

    return _connected;
}

// ============================================================
// WiFi 连接 (仅已保存凭据)
// ============================================================

bool WiFiMgmt::_connectWiFi() {
    WiFi.mode(WIFI_STA);
    WiFi.begin();

    int retry = 0;
    while (WiFi.status() != WL_CONNECTED && retry < 40) {
        delay(500);
        Serial.print(".");
        retry++;
    }
    Serial.println();

    _connected = (WiFi.status() == WL_CONNECTED);

    if (_connected) {
        Serial.println(F("[WiFi] ✅ 已连接!"));
        Serial.printf("  SSID: %s\n", WiFi.SSID().c_str());
        Serial.printf("  IP:   %s\n", WiFi.localIP().toString().c_str());
        Serial.printf("  设备: %s  环境: %s\n", _config.device_id, _config.env_note);
    }

    return _connected;
}

bool WiFiMgmt::_tryConnect(const char* ssid, const char* pass) {
    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid, pass);

    int retry = 0;
    while (WiFi.status() != WL_CONNECTED && retry < 30) {
        delay(500);
        Serial.print(".");
        retry++;
    }
    Serial.println();

    return (WiFi.status() == WL_CONNECTED);
}

// ============================================================
// 配网页面 (原生 Captive Portal)
// ============================================================

void WiFiMgmt::startConfigPortal() {
    Serial.println(F("[WiFi] 🔧 启动配网页面..."));
    _setLEDMode(2);
    _startPortal();
}

void WiFiMgmt::_startPortal() {
    if (_portalActive) return;

    // 切换到 AP 模式
    WiFi.mode(WIFI_AP);
    WiFi.softAP(WIFI_AP_SSID, WIFI_AP_PASSWORD);

    Serial.printf("[WiFi] 📡 AP: %s  密码: %s\n", WIFI_AP_SSID, WIFI_AP_PASSWORD);
    Serial.printf("        IP: %s\n", WiFi.softAPIP().toString().c_str());

    // DNS 劫持 — 所有域名指向本机
    _dns = new DNSServer();
    _dns->start(53, "*", WiFi.softAPIP());

    // Web 服务器
    _server = new WebServer(80);
    _server->on("/",          HTTP_GET,  [this]() { _handleRoot(); });
    _server->on("/save",      HTTP_POST, [this]() { _handleSave(); });
    _server->on("/reset",     HTTP_GET,  [this]() { _handleReset(); });
    _server->onNotFound(      [this]() { _handleRoot(); });  // Captive portal
    _server->begin();

    _portalActive = true;
    _portalStartTime = millis();

    Serial.println(F("[WiFi] ✅ 配网页面已启动, 手机连接 AP 即可"));
}

void WiFiMgmt::_stopPortal() {
    if (!_portalActive) return;

    if (_server) { _server->stop(); delete _server; _server = nullptr; }
    if (_dns)    { _dns->stop();    delete _dns;    _dns = nullptr; }

    WiFi.softAPdisconnect(true);
    WiFi.mode(WIFI_STA);

    _portalActive = false;
    Serial.println(F("[WiFi] 配网页面已关闭"));
}

// ============================================================
// Web 页面处理器
// ============================================================

void WiFiMgmt::_handleRoot() {
    _server->send(200, "text/html; charset=utf-8", _htmlPage());
}

void WiFiMgmt::_handleSave() {
    // 读取表单参数
    if (_server->hasArg("s")) {
        String ssid = _server->arg("s");
        String pass = _server->arg("p");
        _server->send(200, "text/html; charset=utf-8",
            String("<!DOCTYPE html><meta charset='UTF-8'><meta name='viewport' content='width=device-width,initial-scale=1'><title>正在连接</title>")
            + "

"
            + "
正在连接 WiFi...

设备将自动重启配网页面
如连接成功请等待指示灯常亮

");

        delay(500);
        _stopPortal();

        // 保存参数
        if (_server->hasArg("device_id"))  strncpy(_config.device_id, _server->arg("device_id").c_str(), sizeof(_config.device_id)-1);
        if (_server->hasArg("env_note"))   strncpy(_config.env_note,  _server->arg("env_note").c_str(),  sizeof(_config.env_note)-1);
        if (_server->hasArg("mqtt_server")) strncpy(_config.mqtt_server, _server->arg("mqtt_server").c_str(), sizeof(_config.mqtt_server)-1);
        if (_server->hasArg("mqtt_port"))  strncpy(_config.mqtt_port,  _server->arg("mqtt_port").c_str(), sizeof(_config.mqtt_port)-1);
        if (_server->hasArg("mqtt_user"))  strncpy(_config.mqtt_user,  _server->arg("mqtt_user").c_str(), sizeof(_config.mqtt_user)-1);
        if (_server->hasArg("mqtt_pass"))  strncpy(_config.mqtt_pass,  _server->arg("mqtt_pass").c_str(), sizeof(_config.mqtt_pass)-1);

        _saveConfig();

        // 尝试连接
        Serial.printf("[WiFi] 尝试连接: %s\n", ssid.c_str());
        _connected = _tryConnect(ssid.c_str(), pass.c_str());

        if (_connected) {
            _setLEDMode(3);
            Serial.println(F("[WiFi] ✅ 配网成功!"));
        } else {
            _setLEDMode(1);
            Serial.println(F("[WiFi] ❌ 连接失败, 重新开启配网"));
            // 重新开启配网
            startConfigPortal();
        }
        return;
    }

    _handleRoot();  // 没有 WiFi 参数 → 返回表单
}

void WiFiMgmt::_handleReset() {
    _server->send(200, "text/html; charset=utf-8",
        String("<!DOCTYPE html><meta charset='UTF-8'><meta name='viewport' content='width=device-width,initial-scale=1'><title>已重置</title>")
        + "

🔄 配置已清除

设备正在重启...

");

    delay(500);
    _stopPortal();
    resetSettings();
}

// ============================================================
// 配网 HTML 页面 (完全匹配 data/index.html 设计)
// ============================================================

String WiFiMgmt::_htmlPage() {
    String html = String(R"HTML(<!DOCTYPE html>

<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width,initial-scale=1,user-scalable=no'>
<meta name='theme-color' content='#4A90D9'>
<title>健康监测系统 - 配网设置</title>

🏥

健康监测系统

ESP32-S3 + 毫米波雷达 · 配网设置

<form method='POST' action='/save'>

📶 WiFi 设置

WiFi 名称 (SSID)
<select id='s' name='s'></select>

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

☁ MQTT 云平台设置

MQTT 服务器地址
<input type='text' name='mqtt_server' value=')HTML");
    html += _config.mqtt_server;
    html += R"HTML('>

MQTT 端口
<input type='number' name='mqtt_port' value=')HTML";
    html += _config.mqtt_port;
    html += R"HTML('>

MQTT 用户名 (可选)
<input type='text' name='mqtt_user' value=')HTML";
    html += _config.mqtt_user;
    html += R"HTML('>

MQTT 密码 (可选)
<input type='password' name='mqtt_pass' value=')HTML";
    html += _config.mqtt_pass;
    html += R"HTML('>

🏠 环境备注

设备位置 / 房间名称
<input type='text' name='env_note' placeholder='例如: 主卧 / 客厅 / 老人房 / 婴儿房' value=')HTML");
    html += _config.env_note;
    html += R"HTML('>

💡 请备注此设备安装的环境位置，方便在云平台上区分不同房间的数据

📱 设备设置

设备名称 / ID
<input type='text' name='device_id' value=')HTML");
    html += _config.device_id;
    html += R"HTML('>

<button class='btn-submit' type='submit'>💾 保存配置并连接</button>
</form>
<button class='btn-reset' onclick="if(confirm('确定要恢复出厂设置吗?'))location.href='/reset'">🔄 恢复出厂设置</button>

 等待配置...

");
    return html;
}

// ============================================================
// 重置设置
// ============================================================

void WiFiMgmt::resetSettings() {
    Serial.println(F("[WiFi] 清除所有配置..."));
    if (LittleFS.exists(CONFIG_FILE_WIFI))   LittleFS.remove(CONFIG_FILE_WIFI);
    if (LittleFS.exists(CONFIG_FILE_MQTT))   LittleFS.remove(CONFIG_FILE_MQTT);
    if (LittleFS.exists(CONFIG_FILE_DEVICE)) LittleFS.remove(CONFIG_FILE_DEVICE);
    WiFi.disconnect(true, true);

    for (int i = 0; i < 10; i++) {
        digitalWrite(LED_WIFI_PIN, !digitalRead(LED_WIFI_PIN));
        delay(100);
    }
    Serial.println(F("[WiFi] 🔄 正在重启..."));
    delay(500);
    ESP.restart();
}

// ============================================================
// 按钮长按检测
// ============================================================

bool WiFiMgmt::_checkButtonLongPress() {
    bool pressed = (digitalRead(CONFIG_BUTTON_PIN) == LOW);
    if (pressed && !_buttonPressed) {
        _buttonPressed = true;
        _buttonPressTime = millis();
    } else if (!pressed && _buttonPressed) {
        _buttonPressed = false;
        if (millis() - _buttonPressTime >= BUTTON_LONG_PRESS_MS) return true;
    }
    return false;
}

// ============================================================
// LittleFS 配置持久化
// ============================================================

bool WiFiMgmt::_loadConfig() {
    if (!LittleFS.exists(CONFIG_FILE_DEVICE)) return false;
    File file = LittleFS.open(CONFIG_FILE_DEVICE, "r");
    if (!file) return false;
    JsonDocument doc;
    DeserializationError err = deserializeJson(doc, file);
    file.close();
    if (err) {
        Serial.printf("[WiFi] JSON 解析失败: %s\n", err.c_str());
        return false;
    }
    strncpy(_config.mqtt_server, doc["mqtt_server"] | MQTT_DEFAULT_SERVER, sizeof(_config.mqtt_server)-1);
    strncpy(_config.mqtt_port,   doc["mqtt_port"]   | MQTT_DEFAULT_PORT_STR, sizeof(_config.mqtt_port)-1);
    strncpy(_config.mqtt_user,   doc["mqtt_user"]   | MQTT_DEFAULT_USER,     sizeof(_config.mqtt_user)-1);
    strncpy(_config.mqtt_pass,   doc["mqtt_pass"]   | MQTT_DEFAULT_PASS,     sizeof(_config.mqtt_pass)-1);
    strncpy(_config.device_id,   doc["device_id"]   | MQTT_DEFAULT_DEVICE,   sizeof(_config.device_id)-1);
    strncpy(_config.env_note,    doc["env_note"]    | MQTT_DEFAULT_ENV,      sizeof(_config.env_note)-1);
    Serial.println(F("[WiFi] ✅ 设备配置已加载"));
    return true;
}

bool WiFiMgmt::_saveConfig() {
    JsonDocument doc;
    doc["mqtt_server"] = _config.mqtt_server;
    doc["mqtt_port"]   = _config.mqtt_port;
    doc["mqtt_user"]   = _config.mqtt_user;
    doc["mqtt_pass"]   = _config.mqtt_pass;
    doc["device_id"]   = _config.device_id;
    doc["env_note"]    = _config.env_note;
    File file = LittleFS.open(CONFIG_FILE_DEVICE, "w");
    if (!file) { Serial.println(F("[WiFi] ❌ 无法创建配置文件")); return false; }
    if (serializeJson(doc, file) == 0) { Serial.println(F("[WiFi] ❌ 配置写入失败")); file.close(); return false; }
    file.close();
    Serial.println(F("[WiFi] ✅ 设备配置已保存"));
    return true;
}

// ============================================================
// Getter
// ============================================================

bool WiFiMgmt::isConnected() const { return _connected && (WiFi.status() == WL_CONNECTED); }
DeviceConfig WiFiMgmt::getConfig() const { return _config; }
const char* WiFiMgmt::getDeviceId() const { return _config.device_id; }
const char* WiFiMgmt::getEnvNote() const { return _config.env_note; }

// ============================================================
// LED 模式控制
// ============================================================

void WiFiMgmt::_setLEDMode(uint8_t mode) { _ledMode = mode; }

void WiFiMgmt::_ledTaskFunc(void* param) {
    const TickType_t fastMs = pdMS_TO_TICKS(300);
    const TickType_t slowMs = pdMS_TO_TICKS(1000);
    while (true) {
        if (!_instance) { vTaskDelay(slowMs); continue; }
        uint8_t mode = _instance->_ledMode;
        switch (mode) {
        case 0: digitalWrite(LED_WIFI_PIN, LOW); vTaskDelay(slowMs); break;
        case 1: digitalWrite(LED_WIFI_PIN, HIGH); vTaskDelay(slowMs);
                digitalWrite(LED_WIFI_PIN, LOW);  vTaskDelay(slowMs); break;
        case 2: digitalWrite(LED_WIFI_PIN, HIGH); vTaskDelay(fastMs);
                digitalWrite(LED_WIFI_PIN, LOW);  vTaskDelay(fastMs); break;
        case 3: digitalWrite(LED_WIFI_PIN, HIGH); vTaskDelay(slowMs); break;
        }
    }
}


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)

/**
 * @file wifi_manager.cpp
 * @brief WiFi 配网管理器 — 原生 WebServer 实现
 */

#include "wifi_manager.h"

WiFiMgmt* WiFiMgmt::_instance = nullptr;

// ============================================================
// 构造 & 析构
// ============================================================

WiFiMgmt::WiFiMgmt()
    : _server(nullptr)
    , _dns(nullptr)
    , _connected(false)
    , _portalActive(false)
    , _portalStartTime(0)
    , _lastCheckTime(0)
    , _buttonPressTime(0)
    , _buttonPressed(false)
    , _ledTaskHandle(nullptr)
    , _ledMode(0)
{
    memset(&_config, 0, sizeof(_config));
    _instance = this;
}

WiFiMgmt::~WiFiMgmt() {
    _stopPortal();
    _instance = nullptr;
}

// ============================================================
// 初始化
// ============================================================

bool WiFiMgmt::begin() {
    Serial.println(F("\n========================================"));
    Serial.println(F("  WiFi 配网管理器 初始化 (原生模式)"));
    Serial.println(F("========================================"));

    pinMode(CONFIG_BUTTON_PIN, INPUT_PULLUP);
    pinMode(LED_WIFI_PIN, OUTPUT);
    digitalWrite(LED_WIFI_PIN, LOW);

    // LED 独立任务
    xTaskCreate(_ledTaskFunc, "WiFiLed", 1024, nullptr, 1, &_ledTaskHandle);

    // LittleFS
    if (!LittleFS.begin(true)) {
        Serial.println(F("[WiFi] ❌ LittleFS 挂载失败!"));
        return false;
    }
    Serial.println(F("[WiFi] ✅ LittleFS 已挂载"));

    // 加载配置
    if (!_loadConfig()) {
        Serial.println(F("[WiFi] 使用默认配置"));
        strncpy(_config.mqtt_server, MQTT_DEFAULT_SERVER,   sizeof(_config.mqtt_server) - 1);
        strncpy(_config.mqtt_port,   MQTT_DEFAULT_PORT_STR, sizeof(_config.mqtt_port) - 1);
        strncpy(_config.mqtt_user,   MQTT_DEFAULT_USER,     sizeof(_config.mqtt_user) - 1);
        strncpy(_config.mqtt_pass,   MQTT_DEFAULT_PASS,     sizeof(_config.mqtt_pass) - 1);
        strncpy(_config.device_id,   MQTT_DEFAULT_DEVICE,   sizeof(_config.device_id) - 1);
        strncpy(_config.env_note,    MQTT_DEFAULT_ENV,      sizeof(_config.env_note) - 1);
    }

    WiFi.setAutoReconnect(true);
    WiFi.mode(WIFI_STA);

    // 尝试连接已保存的 WiFi
    _connected = _connectWiFi();

    if (!_connected) {
        Serial.println(F("[WiFi] 自动连接失败, 启动配网模式 (60s 超时)..."));
        _setLEDMode(2);
        startConfigPortal();  // 60s 超时
        if (!_connected) {
            Serial.println(F("[WiFi] 配网超时, 尝试恢复已保存连接..."));
            _connected = _connectWiFi();
        }
    }

    if (_connected) {
        _setLEDMode(3);
    }

    _lastCheckTime = millis();
    return _connected;
}

// ============================================================
// 主循环
// ============================================================

bool WiFiMgmt::loop() {
    // 配网页面运行时
    if (_portalActive) {
        _dns->processNextRequest();
        _server->handleClient();

        // 60s 超时自动关闭
        if (millis() - _portalStartTime >= (WIFI_CONFIG_TIMEOUT * 1000UL)) {
            Serial.println(F("[WiFi] ⏰ 配网超时, 关闭页面"));
            _stopPortal();
            _setLEDMode(1);
        }
        return _connected;
    }

    // 长按按钮 → 强制配网
    if (_checkButtonLongPress()) {
        Serial.println(F("[WiFi] ⚠ 检测到长按, 清除配置并重启..."));
        resetSettings();
        return false;
    }

    // 定期检查 WiFi 状态
    if (millis() - _lastCheckTime > WIFI_RETRY_INTERVAL) {
        _lastCheckTime = millis();
        if (WiFi.status() != WL_CONNECTED) {
            _connected = false;
            _setLEDMode(1);
            Serial.println(F("[WiFi] ⚠ WiFi 断开, 尝试重连..."));
            _connectWiFi();
        } else {
            _connected = true;
            _setLEDMode(3);
        }
    }

    return _connected;
}

// ============================================================
// WiFi 连接 (仅已保存凭据)
// ============================================================

bool WiFiMgmt::_connectWiFi() {
    WiFi.mode(WIFI_STA);
    WiFi.begin();

    int retry = 0;
    while (WiFi.status() != WL_CONNECTED && retry < 40) {
        delay(500);
        Serial.print(".");
        retry++;
    }
    Serial.println();

    _connected = (WiFi.status() == WL_CONNECTED);

    if (_connected) {
        Serial.println(F("[WiFi] ✅ 已连接!"));
        Serial.printf("  SSID: %s\n", WiFi.SSID().c_str());
        Serial.printf("  IP:   %s\n", WiFi.localIP().toString().c_str());
        Serial.printf("  设备: %s  环境: %s\n", _config.device_id, _config.env_note);
    }

    return _connected;
}

bool WiFiMgmt::_tryConnect(const char* ssid, const char* pass) {
    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid, pass);

    int retry = 0;
    while (WiFi.status() != WL_CONNECTED && retry < 30) {
        delay(500);
        Serial.print(".");
        retry++;
    }
    Serial.println();

    return (WiFi.status() == WL_CONNECTED);
}

// ============================================================
// 配网页面 (原生 Captive Portal)
// ============================================================

void WiFiMgmt::startConfigPortal() {
    Serial.println(F("[WiFi] 🔧 启动配网页面..."));
    _setLEDMode(2);
    _startPortal();
}

void WiFiMgmt::_startPortal() {
    if (_portalActive) return;

    // 切换到 AP 模式
    WiFi.mode(WIFI_AP);
    WiFi.softAP(WIFI_AP_SSID, WIFI_AP_PASSWORD);

    Serial.printf("[WiFi] 📡 AP: %s  密码: %s\n", WIFI_AP_SSID, WIFI_AP_PASSWORD);
    Serial.printf("        IP: %s\n", WiFi.softAPIP().toString().c_str());

    // DNS 劫持 — 所有域名指向本机
    _dns = new DNSServer();
    _dns->start(53, "*", WiFi.softAPIP());

    // Web 服务器
    _server = new WebServer(80);
    _server->on("/",          HTTP_GET,  [this]() { _handleRoot(); });
    _server->on("/save",      HTTP_POST, [this]() { _handleSave(); });
    _server->on("/reset",     HTTP_GET,  [this]() { _handleReset(); });
    _server->onNotFound(      [this]() { _handleRoot(); });  // Captive portal
    _server->begin();

    _portalActive = true;
    _portalStartTime = millis();

    Serial.println(F("[WiFi] ✅ 配网页面已启动, 手机连接 AP 即可"));
}

void WiFiMgmt::_stopPortal() {
    if (!_portalActive) return;

    if (_server) { _server->stop(); delete _server; _server = nullptr; }
    if (_dns)    { _dns->stop();    delete _dns;    _dns = nullptr; }

    WiFi.softAPdisconnect(true);
    WiFi.mode(WIFI_STA);

    _portalActive = false;
    Serial.println(F("[WiFi] 配网页面已关闭"));
}

// ============================================================
// Web 页面处理器
// ============================================================

void WiFiMgmt::_handleRoot() {
    _server->send(200, "text/html; charset=utf-8", _htmlPage());
}

void WiFiMgmt::_handleSave() {
    // 读取表单参数
    if (_server->hasArg("s")) {
        String ssid = _server->arg("s");
        String pass = _server->arg("p");
        _server->send(200, "text/html; charset=utf-8",
            String("<!DOCTYPE html><meta charset='UTF-8'><meta name='viewport' content='width=device-width,initial-scale=1'><title>正在连接</title>")
            + "

"
            + "
正在连接 WiFi...

设备将自动重启配网页面
如连接成功请等待指示灯常亮

");

        delay(500);
        _stopPortal();

        // 保存参数
        if (_server->hasArg("device_id"))  strncpy(_config.device_id, _server->arg("device_id").c_str(), sizeof(_config.device_id)-1);
        if (_server->hasArg("env_note"))   strncpy(_config.env_note,  _server->arg("env_note").c_str(),  sizeof(_config.env_note)-1);
        if (_server->hasArg("mqtt_server")) strncpy(_config.mqtt_server, _server->arg("mqtt_server").c_str(), sizeof(_config.mqtt_server)-1);
        if (_server->hasArg("mqtt_port"))  strncpy(_config.mqtt_port,  _server->arg("mqtt_port").c_str(), sizeof(_config.mqtt_port)-1);
        if (_server->hasArg("mqtt_user"))  strncpy(_config.mqtt_user,  _server->arg("mqtt_user").c_str(), sizeof(_config.mqtt_user)-1);
        if (_server->hasArg("mqtt_pass"))  strncpy(_config.mqtt_pass,  _server->arg("mqtt_pass").c_str(), sizeof(_config.mqtt_pass)-1);

        _saveConfig();

        // 尝试连接
        Serial.printf("[WiFi] 尝试连接: %s\n", ssid.c_str());
        _connected = _tryConnect(ssid.c_str(), pass.c_str());

        if (_connected) {
            _setLEDMode(3);
            Serial.println(F("[WiFi] ✅ 配网成功!"));
        } else {
            _setLEDMode(1);
            Serial.println(F("[WiFi] ❌ 连接失败, 重新开启配网"));
            // 重新开启配网
            startConfigPortal();
        }
        return;
    }

    _handleRoot();  // 没有 WiFi 参数 → 返回表单
}

void WiFiMgmt::_handleReset() {
    _server->send(200, "text/html; charset=utf-8",
        String("<!DOCTYPE html><meta charset='UTF-8'><meta name='viewport' content='width=device-width,initial-scale=1'><title>已重置</title>")
        + "

🔄 配置已清除

设备正在重启...

");

    delay(500);
    _stopPortal();
    resetSettings();
}

// ============================================================
// 配网 HTML 页面 (完全匹配 data/index.html 设计)
// ============================================================

String WiFiMgmt::_htmlPage() {
    String html = String(R"HTML(<!DOCTYPE html>

<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width,initial-scale=1,user-scalable=no'>
<meta name='theme-color' content='#4A90D9'>
<title>健康监测系统 - 配网设置</title>

🏥

健康监测系统

ESP32-S3 + 毫米波雷达 · 配网设置

<form method='POST' action='/save'>

📶 WiFi 设置

WiFi 名称 (SSID)
<select id='s' name='s'></select>

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

☁ MQTT 云平台设置

MQTT 服务器地址
<input type='text' name='mqtt_server' value=')HTML");
    html += _config.mqtt_server;
    html += R"HTML('>

MQTT 端口
<input type='number' name='mqtt_port' value=')HTML";
    html += _config.mqtt_port;
    html += R"HTML('>

MQTT 用户名 (可选)
<input type='text' name='mqtt_user' value=')HTML";
    html += _config.mqtt_user;
    html += R"HTML('>

MQTT 密码 (可选)
<input type='password' name='mqtt_pass' value=')HTML";
    html += _config.mqtt_pass;
    html += R"HTML('>

🏠 环境备注

设备位置 / 房间名称
<input type='text' name='env_note' placeholder='例如: 主卧 / 客厅 / 老人房 / 婴儿房' value=')HTML");
    html += _config.env_note;
    html += R"HTML('>

💡 请备注此设备安装的环境位置，方便在云平台上区分不同房间的数据

📱 设备设置

设备名称 / ID
<input type='text' name='device_id' value=')HTML");
    html += _config.device_id;
    html += R"HTML('>

<button class='btn-submit' type='submit'>💾 保存配置并连接</button>
</form>
<button class='btn-reset' onclick="if(confirm('确定要恢复出厂设置吗?'))location.href='/reset'">🔄 恢复出厂设置</button>

 等待配置...

");
    return html;
}

// ============================================================
// 重置设置
// ============================================================

void WiFiMgmt::resetSettings() {
    Serial.println(F("[WiFi] 清除所有配置..."));
    if (LittleFS.exists(CONFIG_FILE_WIFI))   LittleFS.remove(CONFIG_FILE_WIFI);
    if (LittleFS.exists(CONFIG_FILE_MQTT))   LittleFS.remove(CONFIG_FILE_MQTT);
    if (LittleFS.exists(CONFIG_FILE_DEVICE)) LittleFS.remove(CONFIG_FILE_DEVICE);
    WiFi.disconnect(true, true);

    for (int i = 0; i < 10; i++) {
        digitalWrite(LED_WIFI_PIN, !digitalRead(LED_WIFI_PIN));
        delay(100);
    }
    Serial.println(F("[WiFi] 🔄 正在重启..."));
    delay(500);
    ESP.restart();
}

// ============================================================
// 按钮长按检测
// ============================================================

bool WiFiMgmt::_checkButtonLongPress() {
    bool pressed = (digitalRead(CONFIG_BUTTON_PIN) == LOW);
    if (pressed && !_buttonPressed) {
        _buttonPressed = true;
        _buttonPressTime = millis();
    } else if (!pressed && _buttonPressed) {
        _buttonPressed = false;
        if (millis() - _buttonPressTime >= BUTTON_LONG_PRESS_MS) return true;
    }
    return false;
}

// ============================================================
// LittleFS 配置持久化
// ============================================================

bool WiFiMgmt::_loadConfig() {
    if (!LittleFS.exists(CONFIG_FILE_DEVICE)) return false;
    File file = LittleFS.open(CONFIG_FILE_DEVICE, "r");
    if (!file) return false;
    JsonDocument doc;
    DeserializationError err = deserializeJson(doc, file);
    file.close();
    if (err) {
        Serial.printf("[WiFi] JSON 解析失败: %s\n", err.c_str());
        return false;
    }
    strncpy(_config.mqtt_server, doc["mqtt_server"] | MQTT_DEFAULT_SERVER, sizeof(_config.mqtt_server)-1);
    strncpy(_config.mqtt_port,   doc["mqtt_port"]   | MQTT_DEFAULT_PORT_STR, sizeof(_config.mqtt_port)-1);
    strncpy(_config.mqtt_user,   doc["mqtt_user"]   | MQTT_DEFAULT_USER,     sizeof(_config.mqtt_user)-1);
    strncpy(_config.mqtt_pass,   doc["mqtt_pass"]   | MQTT_DEFAULT_PASS,     sizeof(_config.mqtt_pass)-1);
    strncpy(_config.device_id,   doc["device_id"]   | MQTT_DEFAULT_DEVICE,   sizeof(_config.device_id)-1);
    strncpy(_config.env_note,    doc["env_note"]    | MQTT_DEFAULT_ENV,      sizeof(_config.env_note)-1);
    Serial.println(F("[WiFi] ✅ 设备配置已加载"));
    return true;
}

bool WiFiMgmt::_saveConfig() {
    JsonDocument doc;
    doc["mqtt_server"] = _config.mqtt_server;
    doc["mqtt_port"]   = _config.mqtt_port;
    doc["mqtt_user"]   = _config.mqtt_user;
    doc["mqtt_pass"]   = _config.mqtt_pass;
    doc["device_id"]   = _config.device_id;
    doc["env_note"]    = _config.env_note;
    File file = LittleFS.open(CONFIG_FILE_DEVICE, "w");
    if (!file) { Serial.println(F("[WiFi] ❌ 无法创建配置文件")); return false; }
    if (serializeJson(doc, file) == 0) { Serial.println(F("[WiFi] ❌ 配置写入失败")); file.close(); return false; }
    file.close();
    Serial.println(F("[WiFi] ✅ 设备配置已保存"));
    return true;
}

// ============================================================
// Getter
// ============================================================

bool WiFiMgmt::isConnected() const { return _connected && (WiFi.status() == WL_CONNECTED); }
DeviceConfig WiFiMgmt::getConfig() const { return _config; }
const char* WiFiMgmt::getDeviceId() const { return _config.device_id; }
const char* WiFiMgmt::getEnvNote() const { return _config.env_note; }

// ============================================================
// LED 模式控制
// ============================================================

void WiFiMgmt::_setLEDMode(uint8_t mode) { _ledMode = mode; }

void WiFiMgmt::_ledTaskFunc(void* param) {
    const TickType_t fastMs = pdMS_TO_TICKS(300);
    const TickType_t slowMs = pdMS_TO_TICKS(1000);
    while (true) {
        if (!_instance) { vTaskDelay(slowMs); continue; }
        uint8_t mode = _instance->_ledMode;
        switch (mode) {
        case 0: digitalWrite(LED_WIFI_PIN, LOW); vTaskDelay(slowMs); break;
        case 1: digitalWrite(LED_WIFI_PIN, HIGH); vTaskDelay(slowMs);
                digitalWrite(LED_WIFI_PIN, LOW);  vTaskDelay(slowMs); break;
        case 2: digitalWrite(LED_WIFI_PIN, HIGH); vTaskDelay(fastMs);
                digitalWrite(LED_WIFI_PIN, LOW);  vTaskDelay(fastMs); break;
        case 3: digitalWrite(LED_WIFI_PIN, HIGH); vTaskDelay(slowMs); break;
        }
    }
}

WiFiMgmt* WiFiMgmt::_instance = nullptr;

-// 构造函数 & 析构函数

-    Serial.println(F("  WiFi 配网管理器 初始化"));

+    Serial.println(F("  WiFi 配网管理器 初始化 (原生模式)"));

-    // 初始化引脚

-    // 启动 LED 独立任务 (避免 startConfigPortal 阻塞影响闪烁)

xTaskCreate(_ledTaskFunc, "WiFiLed", 1024, nullptr, 1, &_ledTaskHandle);

Serial.println(F("[WiFi] ❌ LittleFS 挂载失败!"));

-        // 设置默认值

strncpy(_config.env_note,    MQTT_DEFAULT_ENV,      sizeof(_config.env_note) - 1);

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

-    _wm.setConfigPortalTimeout(WIFI_CONFIG_TIMEOUT);

-    _wm.setConnectTimeout(WIFI_CONNECT_TIMEOUT);

-    _wm.setMinimumSignalQuality(1);   // 不过滤弱信号 WiFi (默认8%, 太低不显示)

-    _wm.setRemoveDuplicateAPs(false); // 显示所有同名 WiFi (不同信道/频段)

-    _wm.setCountry("CN");             // 中国区信道 (1-13, 默认只扫1-11)

-    _wm._preloadwifiscan = true;      // 提前扫描, 页面打开时列表已就绪

+    // 尝试连接已保存的 WiFi

+        Serial.println(F("[WiFi] 自动连接失败, 启动配网模式 (60s 超时)..."));

+        startConfigPortal();  // 60s 超时

+            Serial.println(F("[WiFi] 配网超时, 尝试恢复已保存连接..."));

+            _connected = _connectWiFi();

+    // 配网页面运行时

+        // 60s 超时自动关闭

+        if (millis() - _portalStartTime >= (WIFI_CONFIG_TIMEOUT * 1000UL)) {

+            Serial.println(F("[WiFi] ⏰ 配网超时, 关闭页面"));

+    // 长按按钮 → 强制配网

+        Serial.println(F("[WiFi] ⚠ 检测到长按, 清除配置并重启..."));

+    if (millis() - _lastCheckTime > WIFI_RETRY_INTERVAL) {

+        if (WiFi.status() != WL_CONNECTED) {

+            Serial.println(F("[WiFi] ⚠ WiFi 断开, 尝试重连..."));

+    while (WiFi.status() != WL_CONNECTED && retry < 40) {

+    _connected = (WiFi.status() == WL_CONNECTED);

+        Serial.println(F("[WiFi] ✅ 已连接!"));

+        Serial.printf("  SSID: %s\n", WiFi.SSID().c_str());

+        Serial.printf("  IP:   %s\n", WiFi.localIP().toString().c_str());

+        Serial.printf("  设备: %s  环境: %s\n", _config.device_id, _config.env_note);

+bool WiFiMgmt::_tryConnect(const char* ssid, const char* pass) {

+    while (WiFi.status() != WL_CONNECTED && retry < 30) {

+    return (WiFi.status() == WL_CONNECTED);

+    Serial.println(F("[WiFi] 🔧 启动配网页面..."));

+    // 切换到 AP 模式

+    WiFi.softAP(WIFI_AP_SSID, WIFI_AP_PASSWORD);

+    Serial.printf("[WiFi] 📡 AP: %s  密码: %s\n", WIFI_AP_SSID, WIFI_AP_PASSWORD);

+    Serial.printf("        IP: %s\n", WiFi.softAPIP().toString().c_str());

+    // DNS 劫持 — 所有域名指向本机

+    _dns->start(53, "*", WiFi.softAPIP());

+    _server->on("/",          HTTP_GET,  [this]() { _handleRoot(); });

+    _server->on("/save",      HTTP_POST, [this]() { _handleSave(); });

+    _server->on("/reset",     HTTP_GET,  [this]() { _handleReset(); });

+    _server->onNotFound(      [this]() { _handleRoot(); });  // Captive portal

+    Serial.println(F("[WiFi] ✅ 配网页面已启动, 手机连接 AP 即可"));

+    if (_server) { _server->stop(); delete _server; _server = nullptr; }

+    if (_dns)    { _dns->stop();    delete _dns;    _dns = nullptr; }

+    Serial.println(F("[WiFi] 配网页面已关闭"));

+    _server->send(200, "text/html; charset=utf-8", _htmlPage());

+    // 读取表单参数

+        String ssid = _server->arg("s");

+        String pass = _server->arg("p");

+        _server->send(200, "text/html; charset=utf-8",

+            String("<!DOCTYPE html><html lang='zh-CN'><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width,initial-scale=1'><title>正在连接</title>")

+            + "<style>body{font-family:'PingFang SC','Microsoft YaHei',sans-serif;background:#F5F7FA;display:flex;align-items:center;justify-content:center;height:100vh;margin:0}"

+            + ".box{text-align:center;background:#fff;padding:32px;border-radius:12px;box-shadow:0 2px 12px rgba(0,0,0,0.08);max-width:360px}"

+            + "h1{color:#4A90D9;font-size:20px}.spinner{display:inline-block;width:40px;height:40px;border:4px solid #eee;border-top-color:#4A90D9;border-radius:50%;animation:spin 0.8s linear infinite;margin:16px}"

+            + "@keyframes spin{to{transform:rotate(360deg)}}p{color:#7F8C8D;font-size:14px}</style>

"

+            + "
正在连接 WiFi...

设备将自动重启配网页面
如连接成功请等待指示灯常亮

");

+        // 保存参数

+        if (_server->hasArg("device_id"))  strncpy(_config.device_id, _server->arg("device_id").c_str(), sizeof(_config.device_id)-1);

+        if (_server->hasArg("env_note"))   strncpy(_config.env_note,  _server->arg("env_note").c_str(),  sizeof(_config.env_note)-1);

+        if (_server->hasArg("mqtt_server")) strncpy(_config.mqtt_server, _server->arg("mqtt_server").c_str(), sizeof(_config.mqtt_server)-1);

+        if (_server->hasArg("mqtt_port"))  strncpy(_config.mqtt_port,  _server->arg("mqtt_port").c_str(), sizeof(_config.mqtt_port)-1);

+        if (_server->hasArg("mqtt_user"))  strncpy(_config.mqtt_user,  _server->arg("mqtt_user").c_str(), sizeof(_config.mqtt_user)-1);

+        if (_server->hasArg("mqtt_pass"))  strncpy(_config.mqtt_pass,  _server->arg("mqtt_pass").c_str(), sizeof(_config.mqtt_pass)-1);

+        // 尝试连接

+        Serial.printf("[WiFi] 尝试连接: %s\n", ssid.c_str());

+        _connected = _tryConnect(ssid.c_str(), pass.c_str());

+            Serial.println(F("[WiFi] ✅ 配网成功!"));

+            Serial.println(F("[WiFi] ❌ 连接失败, 重新开启配网"));

+            // 重新开启配网

+    _handleRoot();  // 没有 WiFi 参数 → 返回表单

+    _server->send(200, "text/html; charset=utf-8",

+        String("<!DOCTYPE html><html lang='zh-CN'><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width,initial-scale=1'><title>已重置</title>")

+        + ".box{text-align:center;background:#fff;padding:32px;border-radius:12px;box-shadow:0 2px 12px rgba(0,0,0,0.08)}"

+        + "h1{color:#E74C3C;font-size:18px}p{color:#7F8C8D;font-size:14px}</style>

🔄 配置已清除

设备正在重启...

");

+// 配网 HTML 页面 (完全匹配 data/index.html 设计)

+    String html = String(R"HTML(<!DOCTYPE html>

.form-group input,.form-group select{width:100%;height:44px;padding:0 14px;border:2px solid var(--border);border-radius:8px;font-size:15px;color:var(--text);background:var(--bg);transition:border-color 0.2s,box-shadow 0.2s;outline:none}

.form-group input:focus,.form-group select:focus{border-color:var(--primary);box-shadow:0 0 0 3px rgba(74,144,217,0.15);background:var(--card-bg)}

-.form-group select{appearance:none;-webkit-appearance:none;background-image:url('data:image/svg+xml;charset=UTF-8,%3csvg xmlns=''http://www.w3.org/2000/svg'' width=''12'' height=''12'' viewBox=''0 0 12 12''%3e%3cpath fill=''%23BDC3C7'' d=''M6 9L1 3h10z''/%3e%3c/svg%3e');background-repeat:no-repeat;background-position:right 14px center;background-size:10px 10px;padding-right:30px}

+.form-group select{appearance:none;-webkit-appearance:none;background-image:url('data:image/svg+xml;charset=UTF-8,%3csvg xmlns=%27http://www.w3.org/2000/svg%27 width=%2712%27 height=%2712%27 viewBox=%270 0 12 12%27%3e%3cpath fill=%27%23BDC3C7%27 d=%27M6 9L1 3h10z%27/%3e%3c/svg%3e');background-repeat:no-repeat;background-position:right 14px center;background-size:10px 10px;padding-right:30px}

-    // 自定义菜单 HTML — 完全匹配 data/index.html 设计

ESP32-S3 + 毫米波雷达 · 配网设置

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

+<input type='password' name='p' placeholder='请输入 WiFi 密码'>

☁ MQTT 云平台设置

-<input type='text' name='mqtt_server' value=')HTML") + String(_config.mqtt_server) + String(R"HTML('>

+<input type='text' name='mqtt_server' value=')HTML");

-<input type='number' name='mqtt_port' value=')HTML") + String(_config.mqtt_port) + String(R"HTML('>

+<input type='number' name='mqtt_port' value=')HTML";

-<input type='text' name='mqtt_user' value=')HTML") + String(_config.mqtt_user) + String(R"HTML('>

+<input type='text' name='mqtt_user' value=')HTML";

-<input type='password' name='mqtt_pass' value=')HTML") + String(_config.mqtt_pass) + String(R"HTML('>

+<input type='password' name='mqtt_pass' value=')HTML";

-<input type='text' name='env_note' placeholder='例如: 主卧 / 客厅 / 老人房 / 婴儿房' value=')HTML") + String(_config.env_note) + String(R"HTML('>

+<input type='text' name='env_note' placeholder='例如: 主卧 / 客厅 / 老人房 / 婴儿房' value=')HTML");

💡 请备注此设备安装的环境位置，方便在云平台上区分不同房间的数据

-<input type='text' name='device_id' value=')HTML") + String(_config.device_id) + String(R"HTML('>

+<input type='text' name='device_id' value=')HTML");

<button class='btn-submit' type='submit'>💾 保存配置并连接</button>

-<button class='btn-reset' type='button' onclick='if(confirm("确定要恢复出厂设置吗?"))document.location="/reset"'>🔄 恢复出厂设置</button>

+<button class='btn-reset' onclick="if(confirm('确定要恢复出厂设置吗?'))location.href='/reset'">🔄 恢复出厂设置</button>

-        _wm.setCustomMenuHTML(html.c_str());

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

-    // 注册保存回调 (必须使用静态函数, 不能使用 lambda)

-    _wm.setSaveConfigCallback(_saveParamsCallback);

-    // 创建自定义参数

-    _param_mqtt_server = new WiFiManagerParameter(

-    _param_mqtt_port = new WiFiManagerParameter(

-    _param_mqtt_user = new WiFiManagerParameter(

-    _param_mqtt_pass = new WiFiManagerParameter(

-    _param_device_id = new WiFiManagerParameter(

-    _param_env_note = new WiFiManagerParameter(

-        "env_note", "🏠 环境备注 (如: 主卧/客厅/老人房)",

-    // 注册自定义参数

-    _wm.addParameter(_param_mqtt_server);

-    _wm.addParameter(_param_mqtt_port);

-    _wm.addParameter(_param_mqtt_user);

-    _wm.addParameter(_param_mqtt_pass);

-    _wm.addParameter(_param_device_id);

-    // 设置 AP 模式回调 (静态函数)

-    // 尝试连接 WiFi (先用已保存凭据)

-        // 自动连接失败, 启动配网页面 (60s 超时后自动退出)

-        Serial.println(F("[WiFi] 自动连接失败, 启动配网模式 (60s 超时)..."));

-        _connected = _autoConnect();  // 配网页面, 60s 超时自动退出

-            // 配网超时, 恢复尝试已保存连接

-            Serial.println(F("[WiFi] 配网超时, 尝试恢复已保存连接..."));

-            _connected = _connectWiFi();

-    // 检测按钮长按

-        Serial.println(F("[WiFi] ⚠  检测到长按, 清除配置并重启..."));

-    if (millis() - _lastCheckTime > WIFI_RETRY_INTERVAL) {

-        if (WiFi.status() != WL_CONNECTED) {

-            _setLEDMode(1);  // 慢闪 = 未连接

-            Serial.println(F("[WiFi] ⚠  WiFi 断开, 尝试重连..."));

-            _setLEDMode(3);  // 常亮 = 已连接

-    // LED 由独立 FreeRTOS 任务处理 (不受 startConfigPortal 阻塞影响)

-// WiFi 连接 (仅尝试已保存凭据, 不启动配网页面)

-    // 仅使用已保存的凭据尝试连接, 不启动配网页面

-    // WiFiManager 已在 Preferences 中保存了凭据

-    WiFi.begin();  // 使用 ESP32 内部保存的上一次 WiFi 凭据

-    while (WiFi.status() != WL_CONNECTED && retry < 40) {  // 20 秒超时

-    _connected = (WiFi.status() == WL_CONNECTED);

-        Serial.println(F("[WiFi] ✅ 已连接!"));

-        Serial.printf("  WiFi名称: %s\n", WiFi.SSID().c_str());

-        Serial.printf("  IP地址:   %s\n", WiFi.localIP().toString().c_str());

-        Serial.printf("  MAC地址:  %s\n", WiFi.macAddress().c_str());

-        // 重新加载配置 (确保获取最新值)

-        // 同步自定义参数

-        _param_mqtt_server->setValue(_config.mqtt_server, sizeof(_config.mqtt_server));

-        _param_mqtt_port->setValue(_config.mqtt_port, sizeof(_config.mqtt_port));

-        _param_mqtt_user->setValue(_config.mqtt_user, sizeof(_config.mqtt_user));

-        _param_mqtt_pass->setValue(_config.mqtt_pass, sizeof(_config.mqtt_pass));

-        _param_device_id->setValue(_config.device_id, sizeof(_config.device_id));

-        _param_env_note->setValue(_config.env_note, sizeof(_config.env_note));

-        Serial.printf("  设备名: %s\n", _config.device_id);

-        Serial.printf("  环境:   %s\n", _config.env_note);

-        Serial.printf("  MQTT服务器: %s:%s\n", _config.mqtt_server, _config.mqtt_port);

-// 自动配网 (尝试连接 → 失败则启动配网页面, 仅 begin() 使用)

-    // autoConnect 会先尝试已保存凭据, 失败后启动配网页面

-    _connected = _wm.autoConnect(WIFI_AP_SSID, WIFI_AP_PASSWORD);

-        Serial.println(F("[WiFi] ✅ 已连接 (配网完成)!"));

-        Serial.println(F("[WiFi] ⚠  配网超时 (60s), 将恢复尝试已保存连接..."));

-    Serial.println(F("[WiFi] 🔧 手动启动配网页面..."));

-    _wm.startConfigPortal(WIFI_AP_SSID, WIFI_AP_PASSWORD);

-    _connected = (WiFi.status() == WL_CONNECTED);

-    if (LittleFS.exists(CONFIG_FILE_WIFI)) LittleFS.remove(CONFIG_FILE_WIFI);

-    if (LittleFS.exists(CONFIG_FILE_MQTT)) LittleFS.remove(CONFIG_FILE_MQTT);

+    if (LittleFS.exists(CONFIG_FILE_WIFI))   LittleFS.remove(CONFIG_FILE_WIFI);

+    if (LittleFS.exists(CONFIG_FILE_MQTT))   LittleFS.remove(CONFIG_FILE_MQTT);

bool pressed = (digitalRead(CONFIG_BUTTON_PIN) == LOW);

} else if (!pressed && _buttonPressed) {

-        if (millis() - _buttonPressTime >= BUTTON_LONG_PRESS_MS) {

+        if (millis() - _buttonPressTime >= BUTTON_LONG_PRESS_MS) return true;

-    if (!LittleFS.exists(CONFIG_FILE_DEVICE)) {

+    if (!LittleFS.exists(CONFIG_FILE_DEVICE)) return false;

File file = LittleFS.open(CONFIG_FILE_DEVICE, "r");

DeserializationError err = deserializeJson(doc, file);

Serial.printf("[WiFi] JSON 解析失败: %s\n", err.c_str());

-    strncpy(_config.mqtt_server, doc["mqtt_server"] | MQTT_DEFAULT_SERVER, sizeof(_config.mqtt_server) - 1);

-    strncpy(_config.mqtt_port,   doc["mqtt_port"]   | MQTT_DEFAULT_PORT_STR, sizeof(_config.mqtt_port) - 1);

-    strncpy(_config.mqtt_user,   doc["mqtt_user"]   | MQTT_DEFAULT_USER,     sizeof(_config.mqtt_user) - 1);

-    strncpy(_config.mqtt_pass,   doc["mqtt_pass"]   | MQTT_DEFAULT_PASS,     sizeof(_config.mqtt_pass) - 1);

-    strncpy(_config.device_id,   doc["device_id"]   | MQTT_DEFAULT_DEVICE,   sizeof(_config.device_id) - 1);

-    strncpy(_config.env_note,    doc["env_note"]    | MQTT_DEFAULT_ENV,      sizeof(_config.env_note) - 1);

+    strncpy(_config.mqtt_server, doc["mqtt_server"] | MQTT_DEFAULT_SERVER, sizeof(_config.mqtt_server)-1);

+    strncpy(_config.mqtt_port,   doc["mqtt_port"]   | MQTT_DEFAULT_PORT_STR, sizeof(_config.mqtt_port)-1);

+    strncpy(_config.mqtt_user,   doc["mqtt_user"]   | MQTT_DEFAULT_USER,     sizeof(_config.mqtt_user)-1);

+    strncpy(_config.mqtt_pass,   doc["mqtt_pass"]   | MQTT_DEFAULT_PASS,     sizeof(_config.mqtt_pass)-1);

+    strncpy(_config.device_id,   doc["device_id"]   | MQTT_DEFAULT_DEVICE,   sizeof(_config.device_id)-1);

+    strncpy(_config.env_note,    doc["env_note"]    | MQTT_DEFAULT_ENV,      sizeof(_config.env_note)-1);

-    Serial.printf("  设备名: %s, 环境: %s\n", _config.device_id, _config.env_note);

File file = LittleFS.open(CONFIG_FILE_DEVICE, "w");

-        Serial.println(F("[WiFi] ❌ 无法创建配置文件"));

-    if (serializeJson(doc, file) == 0) {

-        Serial.println(F("[WiFi] ❌ 配置写入失败"));

+    if (!file) { Serial.println(F("[WiFi] ❌ 无法创建配置文件")); return false; }

+    if (serializeJson(doc, file) == 0) { Serial.println(F("[WiFi] ❌ 配置写入失败")); file.close(); return false; }

+bool WiFiMgmt::isConnected() const { return _connected && (WiFi.status() == WL_CONNECTED); }

+DeviceConfig WiFiMgmt::getConfig() const { return _config; }

+const char* WiFiMgmt::getDeviceId() const { return _config.device_id; }

+const char* WiFiMgmt::getEnvNote() const { return _config.env_note; }

-void WiFiMgmt::_apCallback(WiFiManager* wm) {

-    Serial.println(F("[WiFi] 🔧 进入配网模式"));

-    Serial.printf("  AP WiFi名称: %s\n", WIFI_AP_SSID);

-    Serial.printf("  AP 密码: %s\n", WIFI_AP_PASSWORD);

-    Serial.println(F("  手机连接此 WiFi 即可打开配网页面 (自动弹出)"));

-    Serial.println(F("[WiFi] 💾 参数已保存"));

-    strncpy(_instance->_config.mqtt_server, _instance->_param_mqtt_server->getValue(),

-            sizeof(_instance->_config.mqtt_server) - 1);

-    strncpy(_instance->_config.mqtt_port,   _instance->_param_mqtt_port->getValue(),

-            sizeof(_instance->_config.mqtt_port) - 1);

-    strncpy(_instance->_config.mqtt_user,   _instance->_param_mqtt_user->getValue(),

-            sizeof(_instance->_config.mqtt_user) - 1);

-    strncpy(_instance->_config.mqtt_pass,   _instance->_param_mqtt_pass->getValue(),

-            sizeof(_instance->_config.mqtt_pass) - 1);

-    strncpy(_instance->_config.device_id,   _instance->_param_device_id->getValue(),

-            sizeof(_instance->_config.device_id) - 1);

-    strncpy(_instance->_config.env_note,    _instance->_param_env_note->getValue(),

-            sizeof(_instance->_config.env_note) - 1);

-    Serial.println(F("[WiFi] 📋 新配置:"));

-                  _instance->_config.mqtt_server,

-                  _instance->_config.mqtt_port);

-    Serial.printf("  设备: %s\n", _instance->_config.device_id);

-    Serial.printf("  环境: %s\n", _instance->_config.env_note);

-    return _connected && (WiFi.status() == WL_CONNECTED);

+void WiFiMgmt::_setLEDMode(uint8_t mode) { _ledMode = mode; }

-DeviceConfig WiFiMgmt::getConfig() const {

-const char* WiFiMgmt::getDeviceId() const {

-const char* WiFiMgmt::getEnvNote() const {

-// LED 模式控制 (线程安全, 仅写模式变量，由独立任务执行)

-void WiFiMgmt::_setLEDMode(uint8_t mode) {

-    _ledMode = mode;  // uint8_t 赋值在 ESP32 上是原子的

void WiFiMgmt::_ledTaskFunc(void* param) {

-    const TickType_t fastMs = pdMS_TO_TICKS(300);   // 快闪 = 配网模式

-    const TickType_t slowMs = pdMS_TO_TICKS(1000);  // 慢闪 = 未连接

+    const TickType_t fastMs = pdMS_TO_TICKS(300);

+    const TickType_t slowMs = pdMS_TO_TICKS(1000);

+        if (!_instance) { vTaskDelay(slowMs); continue; }

-            digitalWrite(LED_WIFI_PIN, LOW);

-            digitalWrite(LED_WIFI_PIN, HIGH);

+        case 0: digitalWrite(LED_WIFI_PIN, LOW); vTaskDelay(slowMs); break;

+        case 1: digitalWrite(LED_WIFI_PIN, HIGH); vTaskDelay(slowMs);

+                digitalWrite(LED_WIFI_PIN, LOW);  vTaskDelay(slowMs); break;

+        case 2: digitalWrite(LED_WIFI_PIN, HIGH); vTaskDelay(fastMs);

+                digitalWrite(LED_WIFI_PIN, LOW);  vTaskDelay(fastMs); break;

+        case 3: digitalWrite(LED_WIFI_PIN, HIGH); vTaskDelay(slowMs); break;


Now I need to remove the WiFiManager dependency from platformio.ini and remove the old wm_strings_zh.h line since we no longer need it. Also need to add the WM_STRINGS_FILE build flag we added earlier.

Wait, I already removed the WiFiManager includes from wifi_manager.h. Let me update platformio.ini to remove the WiFiManager library dependency.

Also I should remove the `-DWM_STRINGS_FILE` line I just added since we no longer use WiFiManager.

And remove the `#define WM_STRINGS_FILE` from the old wifi_manager.h (which I already overwrote).

Let me update platformio.ini.


现在去掉 WiFiManager 库依赖：


Edit

    -DBOARD_HAS_PSRAM
    -DWM_STRINGS_FILE='\"wm_strings_zh.h\"'


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

; PlatformIO Project Configuration File
; ESP32-S3 + R60ABD1 60G 毫米波雷达 健康监测系统

[env:esp32-s3-devkitc-1]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino

; 串口监视器
monitor_speed = 115200
monitor_filters = direct
monitor_rts = 0
monitor_dtr = 0

; Flash 配置
board_build.flash_mode = qio
board_build.filesystem = littlefs
board_build.partitions = default_8MB.csv
board_build.arduino.memory_type = qio_opi

; 编译选项 - 优化速度
build_flags =
    -Os
    -DCORE_DEBUG_LEVEL=0
    -DARDUINO_USB_CDC_ON_BOOT=1
    -DBOARD_HAS_PSRAM
    -DWM_STRINGS_FILE='\"wm_strings_zh.h\"'

; 库依赖
lib_deps =
    ; WiFi 配网 (自带 captive portal)
    tzapu/WiFiManager@^2.0.17

    ; MQTT 客户端
    knolleary/PubSubClient@^2.8

    ; JSON 解析 (更省内存)
    bblanchon/ArduinoJson@^7.1.0

    ; DHT11 温湿度传感器
    adafruit/DHT sensor library@^1.4.6
    adafruit/Adafruit Unified Sensor@^1.1.14

    ; OLED 显示屏 (SSD1306 128x64 I2C)
    olikraus/U8g2@^2.35.30

; 上传配置
upload_speed = 921600
upload_port = COM16

-    -DWM_STRINGS_FILE='\"wm_strings_zh.h\"'


Now let me remove the WiFiManager library dependency from platformio.ini.


lib_deps =
    ; WiFi 配网 (自带 captive portal)
    tzapu/WiFiManager@^2.0.17

    ; MQTT 客户端
    knolleary/PubSubClient@^2.8

lib_deps =
    ; MQTT 客户端
    knolleary/PubSubClient@^2.8


lib_deps =
    ; MQTT 客户端
    knolleary/PubSubClient@^2.8

; PlatformIO Project Configuration File
; ESP32-S3 + R60ABD1 60G 毫米波雷达 健康监测系统

[env:esp32-s3-devkitc-1]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino

; 串口监视器
monitor_speed = 115200
monitor_filters = direct
monitor_rts = 0
monitor_dtr = 0

; Flash 配置
board_build.flash_mode = qio
board_build.filesystem = littlefs
board_build.partitions = default_8MB.csv
board_build.arduino.memory_type = qio_opi

; 编译选项 - 优化速度
build_flags =
    -Os
    -DCORE_DEBUG_LEVEL=0
    -DARDUINO_USB_CDC_ON_BOOT=1
    -DBOARD_HAS_PSRAM

; 库依赖
lib_deps =
    ; WiFi 配网 (自带 captive portal)
    tzapu/WiFiManager@^2.0.17

    ; MQTT 客户端
    knolleary/PubSubClient@^2.8

    ; JSON 解析 (更省内存)
    bblanchon/ArduinoJson@^7.1.0

    ; DHT11 温湿度传感器
    adafruit/DHT sensor library@^1.4.6
    adafruit/Adafruit Unified Sensor@^1.1.14

    ; OLED 显示屏 (SSD1306 128x64 I2C)
    olikraus/U8g2@^2.35.30

; 上传配置
upload_speed = 921600
upload_port = COM16

; 库依赖
lib_deps =
-    ; WiFi 配网 (自带 captive portal)
-    tzapu/WiFiManager@^2.0.17
; MQTT 客户端
knolleary/PubSubClient@^2.8


Now let me verify everything is consistent. Let me check:
1. `wifi_manager.h` — no WiFiManager include, uses WebServer.h and DNSServer.h ✓
2. `wifi_manager.cpp` — no WiFiManager usage ✓
3. `platformio.ini` — no WiFiManager dependency, no WM_STRINGS_FILE ✓
4. `main.cpp` — uses the same public API (begin, loop, isConnected, getConfig, getDeviceId, startConfigPortal, resetSettings) ✓

Let me also verify that the `_saveParamsCallback` and `_apCallback` references in `main.cpp` or elsewhere are gone since we no longer have those.

Wait, `_saveParamsCallback` was a static method in the old `WiFiMgmt` class that was set as a WiFiManager callback. The new class doesn't have it. Let me check if anything references it... Actually, let me also check if `wifi_manager.h` is still included anywhere that might cause issues.

Actually, I already wrote the new wifi_manager.h and wifi_manager.cpp. The old references to `_saveParamsCallback` and `_apCallback` are gone since they were in the old wifi_manager files.

But wait, in main.cpp there's a line around line 470:
```cpp
DeviceConfig cfg = g_wifi.getConfig();
```
This still works with our new API. ✓

Let me also check if there's a `_saveParamsCallback` reference anywhere else.


验证一下改动是否完整：


No matches found


No references to old WiFiManager APIs remain. Let me also check in the include directory.


include\config.h:86:#define MQTT_DEFAULT_PORT_STR "8000"         // WiFiManager 参数用字符串
include\wm_strings_zh.h:3: * 中文语言包 for WiFiManager
include\wm_strings_zh.h:6: * WiFiManager, a library for the ESP8266/Arduino platform
include\wm_strings_zh.h:136: "
GitHub https://github.com/tzapu/WiFiManager.";
include\wm_strings_zh.h:206:const char HTTP_INFO_aboutver[]     PROGMEM = "
WiFiManager 版本

{1}
";


These are just comments and the unused wm_strings_zh.h file. No actual code references. Clean enough.

Let me also verify the main.cpp doesn't have any issues with the new API. Let me quickly check.


干净了。来看看新旧对比：


1	; PlatformIO Project Configuration File
2	; ESP32-S3 + R60ABD1 60G 毫米波雷达 健康监测系统
3
4	[env:esp32-s3-devkitc-1]
5	platform = espressif32
6	board = esp32-s3-devkitc-1
7	framework = arduino
8
9	; 串口监视器
10	monitor_speed = 115200
11	monitor_filters = direct
12	monitor_rts = 0
13	monitor_dtr = 0
14
15	; Flash 配置
16	board_build.flash_mode = qio
17	board_build.filesystem = littlefs
18	board_build.partitions = default_8MB.csv
19	board_build.arduino.memory_type = qio_opi
20
21	; 编译选项 - 优化速度
22	build_flags =
23	    -Os
24	    -DCORE_DEBUG_LEVEL=0
25	    -DARDUINO_USB_CDC_ON_BOOT=1
26	    -DBOARD_HAS_PSRAM
27
28	; 库依赖
29	lib_deps =
30	    ; MQTT 客户端
31	    knolleary/PubSubClient@^2.8
32
33	    ; JSON 解析 (更省内存)
34	    bblanchon/ArduinoJson@^7.1.0
35
36	    ; DHT11 温湿度传感器
37	    adafruit/DHT sensor library@^1.4.6
38	    adafruit/Adafruit Unified Sensor@^1.1.14
39
40	    ; OLED 显示屏 (SSD1306 128x64 I2C)
41	    olikraus/U8g2@^2.35.30
42
43	; 上传配置
44	upload_speed = 921600
45	upload_port = COM16
46

; PlatformIO Project Configuration File
; ESP32-S3 + R60ABD1 60G 毫米波雷达 健康监测系统

[env:esp32-s3-devkitc-1]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino

; 串口监视器
monitor_speed = 115200
monitor_filters = direct
monitor_rts = 0
monitor_dtr = 0

; Flash 配置
board_build.flash_mode = qio
board_build.filesystem = littlefs
board_build.partitions = default_8MB.csv
board_build.arduino.memory_type = qio_opi

; 编译选项 - 优化速度
build_flags =
    -Os
    -DCORE_DEBUG_LEVEL=0
    -DARDUINO_USB_CDC_ON_BOOT=1
    -DBOARD_HAS_PSRAM

; 库依赖
lib_deps =
    ; MQTT 客户端
    knolleary/PubSubClient@^2.8

    ; JSON 解析 (更省内存)
    bblanchon/ArduinoJson@^7.1.0

    ; DHT11 温湿度传感器
    adafruit/DHT sensor library@^1.4.6
    adafruit/Adafruit Unified Sensor@^1.1.14

    ; OLED 显示屏 (SSD1306 128x64 I2C)
    olikraus/U8g2@^2.35.30

; 上传配置
upload_speed = 921600
upload_port = COM16


Everything looks clean. Let me summarize for the user.


搞定。完全不再用 WiFiManager 了。

### 改了什么

| 旧 | 新 |
|----|----|
| `tzapu/WiFiManager@^2.0.17` | ❌ **删掉** |
| 依赖 WiFiManager 的 HTML 渲染 | **原生** `WebServer` + `DNSServer` |
| `setCustomHeadElement` / `setCustomMenuHTML` | `_htmlPage()` 直接输出完整 HTML |
| WiFiManager 控制表单提交 | `_handleSave()` 直接解析 POST 参数 |
| 中文语言包 hack | 所有文字直接写中文 |

### 工作流程

```
开机
  └─ _connectWiFi()     ← 用 WiFi.begin() 尝试连
       └─ 失败?
            └─ 启动 AP + Captive Portal (60s)
                 └─ 手机连 AP → 打开任何网页 → 配网页面
                 └─ 填表提交 → /save
                      └─ 存 LittleFS → 连新 WiFi
                      └─ 成功? 常亮
                      └─ 失败? 重新开配网
```

### 好处

- HTML/CSS/JS **100% 可控**，不会出现 WiFiManager 的干扰
- 页面完全匹配 `data/index.html` 设计
- 库依赖少一个，编译更快
- `main.cpp` 不用改，API 完全兼容

编译上传试试！


<ide_opened_file>The user opened the file d:\PlatformIO1\Projects\Healthv2.0\src\wifi_manager.cpp in the IDE. This may or may not be related to the current task.</ide_opened_file>

WebServer + LittleFS方式，结合我给你发软件组写的文件，去写配置页面


The user wants me to combine the WebServer + LittleFS approach (which I just wrote) with the software team's design file (`硬件配网.txt`). Let me re-read the software team's design file to get the exact CSS and HTML structure they want.

Looking at `硬件配网.txt`, the software team's design has:
1. Green theme (#12a889)
2. Specific CSS with BEM naming
3. Specific HTML structure with header, cards, help modal, guide tip
4. JavaScript for modal toggle, guide tooltip

The user wants me to use this EXACT design as the provisioning page.

Let me update `_htmlPage()` in wifi_manager.cpp to match the software team's design from `硬件配网.txt`.

Key differences from what I currently have:
- Green theme instead of blue
- "观照-五劳无感 · 设备配网" instead of "健康监测系统"
- Help modal with 4-step guide
- Guide tooltip
- Gateway info bar
- Only 2 visible fields (device_id, env_note) — MQTT fields hidden
- Different card structure with icons
- Different button styling (round, green)

Let me rewrite the `_htmlPage()` method using the software team's exact design from `硬件配网.txt`.


让我重读软件组的设计文件，把 `_htmlPage()` 完全按它的结构来写：


WebServer + LittleFS方式，结合我给你发软件组写的文件，去写配置页面


118	  // ====================== 1. Head 头部：CSS样式+主题色 ======================
119	  String customHead = R"HTML(
120	<meta name='theme-color' content='#12a889'>
121	<style>
122	:root{--color-primary:#12a889;--color-primary-light:#35c9aa;--color-primary-dark:#08745f;--color-success:#00b42a;--color-warning:#ff7d00;--color-error:#f53f3f;--color-bg-page:#f3faf8;--color-bg-card:#ffffff;--color-bg-hover:#eef8f5;--color-card-green:#e9fff8;--color-text-primary:#15201d;--color-text-secondary:#4e6660;--color-text-tertiary:#8aa09a;--color-border:#dcebe7;--color-divider:#edf5f2;}
123	*{box-sizing:border-box;margin:0;padding:0}
124	body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'PingFang SC','Hiragino Sans GB','Microsoft YaHei',sans-serif;background:var(--color-bg-page);min-height:100vh;padding:12px 16px 24px;color:var(--color-text-primary);font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased;}
125	.wrap{max-width:420px;margin:0 auto;}
126	.header{display:flex;align-items:center;justify-content:center;position:relative;padding:20px 0 16px;}
127	.headerIcon{width:56px;height:56px;border-radius:50%;background:var(--color-card-green);display:flex;align-items:center;justify-content:center;font-size:28px;margin-right:14px;box-shadow:0 4px 12px rgba(18,168,137,0.12);}
128	.headerText{text-align:left;}
129	h1{font-size:20px;font-weight:700;color:var(--color-text-primary);letter-spacing:0.3px;}
130	h3{font-size:12px;font-weight:400;color:var(--color-text-secondary);margin-top:4px;}
131	.helpIcon{position:absolute;top:24px;right:4px;width:40px;height:40px;border-radius:50%;background:var(--color-card-green);color:var(--color-primary);font-size:20px;font-weight:700;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:transform 0.2s,background 0.2s;z-index:5;}
132	.helpIcon:hover{background:#d4f7ec;}
133	.helpIcon:active{transform:scale(0.92);}
134	.helpIcon.hint{animation:hintPulse 1.6s ease-in-out infinite;}
135	@keyframes hintPulse{0%,100%{box-shadow:0 0 0 0 rgba(18,168,137,0.5);transform:translateY(0);}50%{box-shadow:0 0 0 10px rgba(18,168,137,0);transform:translateY(-3px);}}
136	.card{background:var(--color-bg-card);border-radius:16px;padding:20px;margin-bottom:16px;box-shadow:0 2px 12px rgba(0,0,0,0.06);}
137	.cardHeader{display:flex;align-items:center;gap:12px;margin-bottom:18px;}
138	.cardHeaderIcon{width:36px;height:36px;border-radius:10px;background:var(--color-card-green);display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0;}
139	.cardTitle{font-size:16px;font-weight:600;color:var(--color-text-primary);}
140	.cardSub{font-size:12px;color:var(--color-text-tertiary);margin-top:2px;}
141	.field{margin-top:14px;}.field:first-child{margin-top:0;}
142	label{display:block;font-size:14px;font-weight:500;color:var(--color-text-secondary);margin-bottom:8px;}
143	.inputWrap{position:relative;display:flex;align-items:center;background:var(--color-bg-hover);border-radius:10px;border:1px solid transparent;transition:border-color 0.2s,background 0.2s;}
144	.inputIcon{padding:0 10px;font-size:16px;color:var(--color-text-tertiary);}
145	input,select{flex:1;width:100%;padding:12px 14px;font-size:14px;border:none;background:transparent;color:var(--color-text-primary);outline:none;font-family:inherit;}
146	input::placeholder{color:var(--color-text-tertiary);}
147	.inputWrap:focus-within{border-color:var(--color-primary);background:#fff;}
148	select{appearance:none;-webkit-appearance:none;background-image:url('data:image/svg+xml;charset=UTF-8,%3csvg xmlns=''http://www.w3.org/2000/svg'' width=''12'' height=''12'' viewBox=''0 0 12 12''%3e%3cpath fill=''%238aa09a'' d=''M6 9L1 3h10z''/%3e%3c/svg%3e');background-repeat:no-repeat;background-position:right 14px center;background-size:10px 10px;padding-right:30px;}
149	.gateInfo{display:flex;align-items:center;justify-content:space-between;padding:12px 14px;border-radius:12px;background:var(--color-card-green);margin-bottom:14px;}
150	.gateLabel{font-size:13px;color:var(--color-text-secondary);}
151	.gateId{font-size:14px;color:var(--color-primary);font-weight:600;}
152	.buttonGroup{display:flex;flex-direction:column;gap:12px;margin-top:8px;}
153	button{width:100%;height:48px;border:none;border-radius:24px;font-size:15px;font-weight:600;color:#fff;background:var(--color-primary);box-shadow:0 2px 6px rgba(18,168,137,0.2);cursor:pointer;transition:transform 0.15s,opacity 0.15s;font-family:inherit;}
154	button:active{transform:scale(0.98);opacity:0.92;}
155	button.secondary{background:var(--color-bg-hover);color:var(--color-text-secondary);box-shadow:none;}
156	button.danger{background:#fff;color:var(--color-error);border:1px solid #ffd6d6;box-shadow:none;}
157	.footer{text-align:center;font-size:11px;color:var(--color-text-tertiary);padding:20px 0 6px;}
158	.modalMask{position:fixed;inset:0;background:rgba(15,31,28,0.55);backdrop-filter:blur(2px);display:flex;align-items:center;justify-content:center;opacity:0;visibility:hidden;transition:opacity 0.25s ease,visibility 0.25s ease;z-index:100;padding:20px;}
159	.modalMask.show{opacity:1;visibility:visible;}
160	.modal{width:100%;max-width:360px;background:#fff;border-radius:20px;padding:24px 22px 18px;transform:scale(0.85) translateY(20px);opacity:0;transition:transform 0.3s cubic-bezier(0.34,1.56,0.64,1),opacity 0.25s ease;box-shadow:0 12px 40px rgba(0,0,0,0.2);max-height:80vh;overflow-y:auto;}
161	.modalMask.show .modal{transform:scale(1) translateY(0);opacity:1;}
162	.modalHeader{display:flex;align-items:center;gap:12px;padding-bottom:18px;border-bottom:1px solid var(--color-divider);}
163	.modalHeaderIcon{width:40px;height:40px;border-radius:12px;background:var(--color-card-green);display:flex;align-items:center;justify-content:center;font-size:20px;flex-shrink:0;}
164	.modalHeaderText{flex:1;}
165	.modalTitle{font-size:16px;font-weight:700;color:var(--color-text-primary);}
166	.modalSub{font-size:12px;color:var(--color-text-tertiary);margin-top:2px;}
167	.modalClose{width:28px;height:28px;border-radius:50%;background:var(--color-bg-hover);color:var(--color-text-tertiary);font-size:16px;display:flex;align-items:center;justify-content:center;cursor:pointer;}
168	.modalSteps{display:flex;flex-direction:column;gap:12px;padding:20px 0 22px;}
169	.modalStep{display:flex;align-items:flex-start;gap:14px;padding:12px;border-radius:12px;background:var(--color-bg-hover);}
170	.modalStepNum{width:30px;height:30px;border-radius:50%;background:var(--color-primary);color:#fff;font-size:13px;font-weight:700;display:flex;align-items:center;justify-content:center;flex-shrink:0;}
171	.modalStepContent{flex:1;}
172	.modalStepTitle{font-size:14px;font-weight:600;color:var(--color-text-primary);}
173	.modalStepDesc{font-size:12px;color:var(--color-text-tertiary);margin-top:3px;line-height:1.6;}
174	.modalFooter{display:flex;gap:10px;padding-top:6px;border-top:1px solid var(--color-divider);padding-top:14px;}
175	.modalFooter button{flex:1;height:42px;font-size:14px;}
176	.modalFooter button.primary{background:var(--color-primary);color:#fff;}
177	.modalFooter button.ghost{background:var(--color-bg-hover);color:var(--color-text-secondary);}
178	.guideTip{position:absolute;top:60px;right:0;transform:translateY(-8px) scale(0.9);transform-origin:top right;background:var(--color-primary);color:#fff;font-size:12px;padding:8px 14px;border-radius:999px;box-shadow:0 4px 12px rgba(18,168,137,0.35);opacity:0;transition:opacity 0.3s ease,transform 0.3s ease;z-index:6;pointer-events:none;white-space:nowrap;}
179	.guideTip::after{content:'';position:absolute;top:-5px;right:12px;width:10px;height:10px;background:var(--color-primary);transform:rotate(45deg);}
180	.guideTip.show{opacity:1;transform:translateY(0) scale(1);}
181	@media(prefers-color-scheme:dark){body{background:#0f1f1c;color:#e6f0ee;}.card{background:#182b27;box-shadow:0 2px 12px rgba(0,0,0,0.4);}.cardHeaderIcon,.modalStep,.inputWrap{background:#1f3530;}.inputWrap:focus-within{background:#243d38;border-color:var(--color-primary-light);}label{color:#a8bdb8;}h1{color:#e6f0ee;}.gateInfo{background:#143530;}button.secondary{background:#1f3530;color:#a8bdb8;}button.danger{background:#2a1a1a;color:#ff8a8a;border-color:#5a2828;}.modal{background:#182b27;}.modalHeader{border-bottom-color:#243d38;}.modalFooter{border-top-color:#243d38;}.modalClose{background:#1f3530;color:#a8bdb8;}}
182	</style>
183	)HTML";
184	  wm.setCustomHeadElement(customHead.c_str());
185
186	  // ====================== 2. Body页面结构 + JS弹窗 ======================
187	  String customBody = R"HTML(

  // ====================== 1. Head 头部：CSS样式+主题色 ======================
  String customHead = R"HTML(
<meta name='theme-color' content='#12a889'>
<style>
:root{--color-primary:#12a889;--color-primary-light:#35c9aa;--color-primary-dark:#08745f;--color-success:#00b42a;--color-warning:#ff7d00;--color-error:#f53f3f;--color-bg-page:#f3faf8;--color-bg-card:#ffffff;--color-bg-hover:#eef8f5;--color-card-green:#e9fff8;--color-text-primary:#15201d;--color-text-secondary:#4e6660;--color-text-tertiary:#8aa09a;--color-border:#dcebe7;--color-divider:#edf5f2;}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'PingFang SC','Hiragino Sans GB','Microsoft YaHei',sans-serif;background:var(--color-bg-page);min-height:100vh;padding:12px 16px 24px;color:var(--color-text-primary);font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased;}
.wrap{max-width:420px;margin:0 auto;}
.header{display:flex;align-items:center;justify-content:center;position:relative;padding:20px 0 16px;}
.headerIcon{width:56px;height:56px;border-radius:50%;background:var(--color-card-green);display:flex;align-items:center;justify-content:center;font-size:28px;margin-right:14px;box-shadow:0 4px 12px rgba(18,168,137,0.12);}
.headerText{text-align:left;}
h1{font-size:20px;font-weight:700;color:var(--color-text-primary);letter-spacing:0.3px;}
h3{font-size:12px;font-weight:400;color:var(--color-text-secondary);margin-top:4px;}
.helpIcon{position:absolute;top:24px;right:4px;width:40px;height:40px;border-radius:50%;background:var(--color-card-green);color:var(--color-primary);font-size:20px;font-weight:700;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:transform 0.2s,background 0.2s;z-index:5;}
.helpIcon:hover{background:#d4f7ec;}
.helpIcon:active{transform:scale(0.92);}
.helpIcon.hint{animation:hintPulse 1.6s ease-in-out infinite;}
@keyframes hintPulse{0%,100%{box-shadow:0 0 0 0 rgba(18,168,137,0.5);transform:translateY(0);}50%{box-shadow:0 0 0 10px rgba(18,168,137,0);transform:translateY(-3px);}}
.card{background:var(--color-bg-card);border-radius:16px;padding:20px;margin-bottom:16px;box-shadow:0 2px 12px rgba(0,0,0,0.06);}
.cardHeader{display:flex;align-items:center;gap:12px;margin-bottom:18px;}
.cardHeaderIcon{width:36px;height:36px;border-radius:10px;background:var(--color-card-green);display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0;}
.cardTitle{font-size:16px;font-weight:600;color:var(--color-text-primary);}
.cardSub{font-size:12px;color:var(--color-text-tertiary);margin-top:2px;}
.field{margin-top:14px;}.field:first-child{margin-top:0;}
label{display:block;font-size:14px;font-weight:500;color:var(--color-text-secondary);margin-bottom:8px;}
.inputWrap{position:relative;display:flex;align-items:center;background:var(--color-bg-hover);border-radius:10px;border:1px solid transparent;transition:border-color 0.2s,background 0.2s;}
.inputIcon{padding:0 10px;font-size:16px;color:var(--color-text-tertiary);}
input,select{flex:1;width:100%;padding:12px 14px;font-size:14px;border:none;background:transparent;color:var(--color-text-primary);outline:none;font-family:inherit;}
input::placeholder{color:var(--color-text-tertiary);}
.inputWrap:focus-within{border-color:var(--color-primary);background:#fff;}
select{appearance:none;-webkit-appearance:none;background-image:url('data:image/svg+xml;charset=UTF-8,%3csvg xmlns=''http://www.w3.org/2000/svg'' width=''12'' height=''12'' viewBox=''0 0 12 12''%3e%3cpath fill=''%238aa09a'' d=''M6 9L1 3h10z''/%3e%3c/svg%3e');background-repeat:no-repeat;background-position:right 14px center;background-size:10px 10px;padding-right:30px;}
.gateInfo{display:flex;align-items:center;justify-content:space-between;padding:12px 14px;border-radius:12px;background:var(--color-card-green);margin-bottom:14px;}
.gateLabel{font-size:13px;color:var(--color-text-secondary);}
.gateId{font-size:14px;color:var(--color-primary);font-weight:600;}
.buttonGroup{display:flex;flex-direction:column;gap:12px;margin-top:8px;}
button{width:100%;height:48px;border:none;border-radius:24px;font-size:15px;font-weight:600;color:#fff;background:var(--color-primary);box-shadow:0 2px 6px rgba(18,168,137,0.2);cursor:pointer;transition:transform 0.15s,opacity 0.15s;font-family:inherit;}
button:active{transform:scale(0.98);opacity:0.92;}
button.secondary{background:var(--color-bg-hover);color:var(--color-text-secondary);box-shadow:none;}
button.danger{background:#fff;color:var(--color-error);border:1px solid #ffd6d6;box-shadow:none;}
.footer{text-align:center;font-size:11px;color:var(--color-text-tertiary);padding:20px 0 6px;}
.modalMask{position:fixed;inset:0;background:rgba(15,31,28,0.55);backdrop-filter:blur(2px);display:flex;align-items:center;justify-content:center;opacity:0;visibility:hidden;transition:opacity 0.25s ease,visibility 0.25s ease;z-index:100;padding:20px;}
.modalMask.show{opacity:1;visibility:visible;}
.modal{width:100%;max-width:360px;background:#fff;border-radius:20px;padding:24px 22px 18px;transform:scale(0.85) translateY(20px);opacity:0;transition:transform 0.3s cubic-bezier(0.34,1.56,0.64,1),opacity 0.25s ease;box-shadow:0 12px 40px rgba(0,0,0,0.2);max-height:80vh;overflow-y:auto;}
.modalMask.show .modal{transform:scale(1) translateY(0);opacity:1;}
.modalHeader{display:flex;align-items:center;gap:12px;padding-bottom:18px;border-bottom:1px solid var(--color-divider);}
.modalHeaderIcon{width:40px;height:40px;border-radius:12px;background:var(--color-card-green);display:flex;align-items:center;justify-content:center;font-size:20px;flex-shrink:0;}
.modalHeaderText{flex:1;}
.modalTitle{font-size:16px;font-weight:700;color:var(--color-text-primary);}
.modalSub{font-size:12px;color:var(--color-text-tertiary);margin-top:2px;}
.modalClose{width:28px;height:28px;border-radius:50%;background:var(--color-bg-hover);color:var(--color-text-tertiary);font-size:16px;display:flex;align-items:center;justify-content:center;cursor:pointer;}
.modalSteps{display:flex;flex-direction:column;gap:12px;padding:20px 0 22px;}
.modalStep{display:flex;align-items:flex-start;gap:14px;padding:12px;border-radius:12px;background:var(--color-bg-hover);}
.modalStepNum{width:30px;height:30px;border-radius:50%;background:var(--color-primary);color:#fff;font-size:13px;font-weight:700;display:flex;align-items:center;justify-content:center;flex-shrink:0;}
.modalStepContent{flex:1;}
.modalStepTitle{font-size:14px;font-weight:600;color:var(--color-text-primary);}
.modalStepDesc{font-size:12px;color:var(--color-text-tertiary);margin-top:3px;line-height:1.6;}
.modalFooter{display:flex;gap:10px;padding-top:6px;border-top:1px solid var(--color-divider);padding-top:14px;}
.modalFooter button{flex:1;height:42px;font-size:14px;}
.modalFooter button.primary{background:var(--color-primary);color:#fff;}
.modalFooter button.ghost{background:var(--color-bg-hover);color:var(--color-text-secondary);}
.guideTip{position:absolute;top:60px;right:0;transform:translateY(-8px) scale(0.9);transform-origin:top right;background:var(--color-primary);color:#fff;font-size:12px;padding:8px 14px;border-radius:999px;box-shadow:0 4px 12px rgba(18,168,137,0.35);opacity:0;transition:opacity 0.3s ease,transform 0.3s ease;z-index:6;pointer-events:none;white-space:nowrap;}
.guideTip::after{content:'';position:absolute;top:-5px;right:12px;width:10px;height:10px;background:var(--color-primary);transform:rotate(45deg);}
.guideTip.show{opacity:1;transform:translateY(0) scale(1);}
@media(prefers-color-scheme:dark){body{background:#0f1f1c;color:#e6f0ee;}.card{background:#182b27;box-shadow:0 2px 12px rgba(0,0,0,0.4);}.cardHeaderIcon,.modalStep,.inputWrap{background:#1f3530;}.inputWrap:focus-within{background:#243d38;border-color:var(--color-primary-light);}label{color:#a8bdb8;}h1{color:#e6f0ee;}.gateInfo{background:#143530;}button.secondary{background:#1f3530;color:#a8bdb8;}button.danger{background:#2a1a1a;color:#ff8a8a;border-color:#5a2828;}.modal{background:#182b27;}.modalHeader{border-bottom-color:#243d38;}.modalFooter{border-top-color:#243d38;}.modalClose{background:#1f3530;color:#a8bdb8;}}
</style>
)HTML";
  wm.setCustomHeadElement(customHead.c_str());

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


OK I now need to update the `_htmlPage()` method in wifi_manager.cpp to exactly match the software team's design from `硬件配网.txt`. The key differences:

1. Green theme (#12a889) - not blue
2. "观照-五劳无感 · 设备配网" - not "健康监测系统"
3. Header with 📡 icon and help button
4. Gateway info bar
5. Help modal with 4 steps
6. Guide tooltip
7. Only 2 visible params (device_id, env_note) - MQTT are hidden
8. Different card structure with `.cardHeader`, `.cardHeaderIcon`, `.cardTitle`, `.cardSub`
9. `.inputWrap` with `.inputIcon` for form styling
10. Round buttons
11. Dark mode support
12. Footer

But I also need to:
- Keep MQTT params as hidden inputs
- Make the form POST to `/save`
- Include WiFi SSID `<select>` for scanned networks (ESP32 needs to populate this)

For the SSID scan — since we're not using WiFiManager anymore, I need to either:
1. Do a WiFi scan in C++ and inject the results into the HTML
2. Use JavaScript to fetch scan results via AJAX
3. Let the user type the SSID manually

The simplest approach: do a WiFi scan in `_htmlPage()` and inject the SSID options into the `<select>`.

Let me update the `_htmlPage()` method now. I'll replace the entire method with the software team's design.

Actually, looking at the software team's design more carefully... They have:

```html
<div class='field'><label>WiFi 名称</label><div class='inputWrap'><span class='inputIcon'>📡</span><select></select></div></div>
<div class='field'><label>WiFi 密码</label><div class='inputWrap'><span class='inputIcon'>🔒</span><input type='password' placeholder='请输入 WiFi 密码'></div></div>
```

The `<select>` for SSID and `<input type='password'>` for password. In WiFiManager, the `<select>` gets populated by WiFiManager's JS. But in our native approach, we need to populate it ourselves.

I'll do the WiFi scan in C++ when building the page:
```cpp
int n = WiFi.scanNetworks();
for (int i = 0; i < n; i++) {
    // add <option> for each network
}
```

Also, the form needs `name='s'` for SSID and `name='p'` for password (matching our save handler).

Let me also add the MQTT fields as hidden inputs.

For the modal steps, the software team's Step 1 says "选择 WiFi 并输入密码" and "确保手机已连接目标 2.4G 网络". But our actual flow is: user connects to the device's AP first, THEN selects WiFi. So the modal should reflect this — Step 1: connect to the device's AP.

Let me include the AP SSID/password info in the modal.

Let me write this now.


现在把 `_htmlPage()` 完全按照软件组的设计文件来写——绿色主题，完整弹窗引导，WiFi 扫描注入：


220	    if (_server) { _server->stop(); delete _server; _server = nullptr; }
221	    if (_dns)    { _dns->stop();    delete _dns;    _dns = nullptr; }
222
223	    WiFi.softAPdisconnect(true);
224	    WiFi.mode(WIFI_STA);

    if (_server) { _server->stop(); delete _server; _server = nullptr; }
    if (_dns)    { _dns->stop();    delete _dns;    _dns = nullptr; }

    WiFi.softAPdisconnect(true);
    WiFi.mode(WIFI_STA);


Let me find the _htmlPage() method start and end to replace it.


296:// 配网 HTML 页面 (完全匹配 data/index.html 设计)
299:String WiFiMgmt::_htmlPage() {


I need to find the end of the _htmlPage() method. It likely ends with `return html; }` Let me find where it ends.


429:    return html;

429:    return html;


So the `_htmlPage()` method spans lines 299-429. Let me read the full method, then replace it with the software team's design.

Actually, I already wrote this method recently. Let me just replace it completely with the software team's design. Let me read lines 296-429 to get the exact replacement boundaries.


296	// 配网 HTML 页面 (完全匹配 data/index.html 设计)
297	// ============================================================
298
299	String WiFiMgmt::_htmlPage() {
300	    String html = String(R"HTML(<!DOCTYPE html>
301
302
303	<meta charset='UTF-8'>
304	<meta name='viewport' content='width=device-width,initial-scale=1,user-scalable=no'>
305	<meta name='theme-color' content='#4A90D9'>
306	<title>健康监测系统 - 配网设置</title>
307
342
343
344

345

346
🏥

347
健康监测系统

348
ESP32-S3 + 毫米波雷达 · 配网设置

349

350
351	<form method='POST' action='/save'>
352

353
📶 WiFi 设置

354

355	WiFi 名称 (SSID)
356	<select id='s' name='s'></select>
357

358

359	WiFi 密码
360	<input type='password' name='p' placeholder='请输入 WiFi 密码'>
361

362

363
364

365
☁ MQTT 云平台设置

366

367	MQTT 服务器地址
368	<input type='text' name='mqtt_server' value=')HTML");
369	    html += _config.mqtt_server;
370	    html += R"HTML('>
371

372

373	MQTT 端口
374	<input type='number' name='mqtt_port' value=')HTML";
375	    html += _config.mqtt_port;
376	    html += R"HTML('>
377

378

379	MQTT 用户名 (可选)
380	<input type='text' name='mqtt_user' value=')HTML";
381	    html += _config.mqtt_user;
382	    html += R"HTML('>
383

384

385	MQTT 密码 (可选)
386	<input type='password' name='mqtt_pass' value=')HTML";
387	    html += _config.mqtt_pass;
388	    html += R"HTML('>
389

390

391
392

393
🏠 环境备注

394

395	设备位置 / 房间名称
396	<input type='text' name='env_note' placeholder='例如: 主卧 / 客厅 / 老人房 / 婴儿房' value=')HTML");
397	    html += _config.env_note;
398	    html += R"HTML('>
399

400
💡 请备注此设备安装的环境位置，方便在云平台上区分不同房间的数据

401

402
403

404
📱 设备设置

405

406	设备名称 / ID
407	<input type='text' name='device_id' value=')HTML");
408	    html += _config.device_id;
409	    html += R"HTML('>
410

411

412
413	<button class='btn-submit' type='submit'>💾 保存配置并连接</button>
414	</form>
415	<button class='btn-reset' onclick="if(confirm('确定要恢复出厂设置吗?'))location.href='/reset'">🔄 恢复出厂设置</button>
416
417

418	 等待配置...
419

420

421
422
428	");
429	    return html;
430	}

// 配网 HTML 页面 (完全匹配 data/index.html 设计)
// ============================================================

String WiFiMgmt::_htmlPage() {
    String html = String(R"HTML(<!DOCTYPE html>

<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width,initial-scale=1,user-scalable=no'>
<meta name='theme-color' content='#4A90D9'>
<title>健康监测系统 - 配网设置</title>

🏥

健康监测系统

ESP32-S3 + 毫米波雷达 · 配网设置

<form method='POST' action='/save'>

📶 WiFi 设置

WiFi 名称 (SSID)
<select id='s' name='s'></select>

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

☁ MQTT 云平台设置

MQTT 服务器地址
<input type='text' name='mqtt_server' value=')HTML");
    html += _config.mqtt_server;
    html += R"HTML('>

MQTT 端口
<input type='number' name='mqtt_port' value=')HTML";
    html += _config.mqtt_port;
    html += R"HTML('>

MQTT 用户名 (可选)
<input type='text' name='mqtt_user' value=')HTML";
    html += _config.mqtt_user;
    html += R"HTML('>

MQTT 密码 (可选)
<input type='password' name='mqtt_pass' value=')HTML";
    html += _config.mqtt_pass;
    html += R"HTML('>

🏠 环境备注

设备位置 / 房间名称
<input type='text' name='env_note' placeholder='例如: 主卧 / 客厅 / 老人房 / 婴儿房' value=')HTML");
    html += _config.env_note;
    html += R"HTML('>

💡 请备注此设备安装的环境位置，方便在云平台上区分不同房间的数据

📱 设备设置

设备名称 / ID
<input type='text' name='device_id' value=')HTML");
    html += _config.device_id;
    html += R"HTML('>

<button class='btn-submit' type='submit'>💾 保存配置并连接</button>
</form>
<button class='btn-reset' onclick="if(confirm('确定要恢复出厂设置吗?'))location.href='/reset'">🔄 恢复出厂设置</button>

 等待配置...

");
    return html;
}


Now I need to replace this entire method with the software team's design from `硬件配网.txt`. Key changes:

1. Green theme (#12a889)
2. Header with 📡 "观照-五劳无感 · 设备配网"
3. Help button with pulse animation
4. Guide tooltip
5. Gateway info bar
6. WiFi card with scanned networks injected
7. Device basic info card (only device_id + env_note visible)
8. MQTT as hidden inputs
9. Help modal with 4 steps
10. Save + Reset buttons
11. Footer
12. JavaScript for modal, tooltip, scan

For the WiFi scan, I'll do the scan in C++ and inject `<option>` elements into the `<select>`.


// ============================================================
// 配网 HTML 页面 (完全匹配 data/index.html 设计)
// ============================================================

String WiFiMgmt::_htmlPage() {
    String html = String(R"HTML(<!DOCTYPE html>

<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width,initial-scale=1,user-scalable=no'>
<meta name='theme-color' content='#4A90D9'>
<title>健康监测系统 - 配网设置</title>

🏥

健康监测系统

ESP32-S3 + 毫米波雷达 · 配网设置

<form method='POST' action='/save'>

📶 WiFi 设置

WiFi 名称 (SSID)
<select id='s' name='s'></select>

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

☁ MQTT 云平台设置

MQTT 服务器地址
<input type='text' name='mqtt_server' value=')HTML");
    html += _config.mqtt_server;
    html += R"HTML('>

MQTT 端口
<input type='number' name='mqtt_port' value=')HTML";
    html += _config.mqtt_port;
    html += R"HTML('>

MQTT 用户名 (可选)
<input type='text' name='mqtt_user' value=')HTML";
    html += _config.mqtt_user;
    html += R"HTML('>

MQTT 密码 (可选)
<input type='password' name='mqtt_pass' value=')HTML";
    html += _config.mqtt_pass;
    html += R"HTML('>

🏠 环境备注

设备位置 / 房间名称
<input type='text' name='env_note' placeholder='例如: 主卧 / 客厅 / 老人房 / 婴儿房' value=')HTML");
    html += _config.env_note;
    html += R"HTML('>

💡 请备注此设备安装的环境位置，方便在云平台上区分不同房间的数据

📱 设备设置

设备名称 / ID
<input type='text' name='device_id' value=')HTML");
    html += _config.device_id;
    html += R"HTML('>

<button class='btn-submit' type='submit'>💾 保存配置并连接</button>
</form>
<button class='btn-reset' onclick="if(confirm('确定要恢复出厂设置吗?'))location.href='/reset'">🔄 恢复出厂设置</button>

 等待配置...

");
    return html;
}

// ============================================================
// 配网 HTML 页面 (完全匹配 软件组设计稿 硬件配网.txt)
// ============================================================

String WiFiMgmt::_htmlPage() {
    // 扫描 WiFi 并构建 SSID 选项
    String ssidOptions;
    int n = WiFi.scanNetworks(false, true);  // async=false, showHidden=false
    for (int i = 0; i < n; i++) {
        ssidOptions += "<option value='" + WiFi.SSID(i) + "'>" + WiFi.SSID(i) + " (" + String(WiFi.RSSI(i)) + "dBm)</option>";
    }
    if (n == 0) {
        ssidOptions = "<option value=''>未发现 WiFi, 请刷新</option>";
    }
    WiFi.scanDelete();

    String html = String(R"HTML(<!DOCTYPE html>

<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width,initial-scale=1,user-scalable=no'>
<meta name='theme-color' content='#12a889'>
<title>健康监测系统 - 配网设置</title>

📡

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

WiFi 联网与参数配置

?

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

📍 当前网关Gateway_A8F3

<form method='POST' action='/save'>

📶

无线网络

请选择 2.4GHz WiFi

WiFi 名称
📡<select name='s'>)HTML")
    + ssidOptions +
    R"HTML(</select>

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

⚙

设备基础信息

唯一标识与安装位置

🔖 设备编号
#<input type='text' name='device_id' value=')HTML")
    + String(_config.device_id) +
    R"HTML('>

📍 安装环境
🏠<input type='text' name='env_note' placeholder='如: 主卧 / 客厅 / 老人房' value=')HTML")
    + String(_config.env_note) +
    R"HTML('>

)HTML");

    // MQTT 隐藏域
    html += "<input type='hidden' name='mqtt_server' value='" + String(_config.mqtt_server) + "'>";
    html += "<input type='hidden' name='mqtt_port' value='"   + String(_config.mqtt_port)   + "'>";
    html += "<input type='hidden' name='mqtt_user' value='"   + String(_config.mqtt_user)   + "'>";
    html += "<input type='hidden' name='mqtt_pass' value='"   + String(_config.mqtt_pass)   + "'>";

    html += String(R"HTML(

<button type='submit'>保存配置并重启连接</button>

</form>
<button class='danger' type='button' onclick="if(confirm('确定要恢复出厂设置吗?'))location.href='/reset'">恢复出厂配置</button>

Health Monitor · Device Provisioning

📋

配网流程

按顺序完成 4 步操作

×

1

连接设备热点

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

2

选择 WiFi 并输入密码

选择您的 2.4GHz WiFi 网络

3

填写设备基础信息

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

4

保存并等待连接

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

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

)HTML");
    return html;
}


// ============================================================
// 配网 HTML 页面 (完全匹配 data/index.html 设计)
// ============================================================

String WiFiMgmt::_htmlPage() {
    String html = String(R"HTML(<!DOCTYPE html>

<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width,initial-scale=1,user-scalable=no'>
<meta name='theme-color' content='#4A90D9'>
<title>健康监测系统 - 配网设置</title>

🏥

健康监测系统

ESP32-S3 + 毫米波雷达 · 配网设置

<form method='POST' action='/save'>

📶 WiFi 设置

WiFi 名称 (SSID)
<select id='s' name='s'></select>

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

☁ MQTT 云平台设置

MQTT 服务器地址
<input type='text' name='mqtt_server' value=')HTML");
    html += _config.mqtt_server;
    html += R"HTML('>

MQTT 端口
<input type='number' name='mqtt_port' value=')HTML";
    html += _config.mqtt_port;
    html += R"HTML('>

MQTT 用户名 (可选)
<input type='text' name='mqtt_user' value=')HTML";
    html += _config.mqtt_user;
    html += R"HTML('>

MQTT 密码 (可选)
<input type='password' name='mqtt_pass' value=')HTML";
    html += _config.mqtt_pass;
    html += R"HTML('>

🏠 环境备注

设备位置 / 房间名称
<input type='text' name='env_note' placeholder='例如: 主卧 / 客厅 / 老人房 / 婴儿房' value=')HTML");
    html += _config.env_note;
    html += R"HTML('>

💡 请备注此设备安装的环境位置，方便在云平台上区分不同房间的数据

📱 设备设置

设备名称 / ID
<input type='text' name='device_id' value=')HTML");
    html += _config.device_id;
    html += R"HTML('>

<button class='btn-submit' type='submit'>💾 保存配置并连接</button>
</form>
<button class='btn-reset' onclick="if(confirm('确定要恢复出厂设置吗?'))location.href='/reset'">🔄 恢复出厂设置</button>

 等待配置...

");
    return html;
}

// ============================================================
// 配网 HTML 页面 (完全匹配 软件组设计稿 硬件配网.txt)
// ============================================================

String WiFiMgmt::_htmlPage() {
    // 扫描 WiFi 并构建 SSID 选项
    String ssidOptions;
    int n = WiFi.scanNetworks(false, true);  // async=false, showHidden=false
    for (int i = 0; i < n; i++) {
        ssidOptions += "<option value='" + WiFi.SSID(i) + "'>" + WiFi.SSID(i) + " (" + String(WiFi.RSSI(i)) + "dBm)</option>";
    }
    if (n == 0) {
        ssidOptions = "<option value=''>未发现 WiFi, 请刷新</option>";
    }
    WiFi.scanDelete();

    String html = String(R"HTML(<!DOCTYPE html>

<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width,initial-scale=1,user-scalable=no'>
<meta name='theme-color' content='#12a889'>
<title>健康监测系统 - 配网设置</title>

📡

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

WiFi 联网与参数配置

?

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

📍 当前网关Gateway_A8F3

<form method='POST' action='/save'>

📶

无线网络

请选择 2.4GHz WiFi

WiFi 名称
📡<select name='s'>)HTML")
    + ssidOptions +
    R"HTML(</select>

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

⚙

设备基础信息

唯一标识与安装位置

🔖 设备编号
#<input type='text' name='device_id' value=')HTML")
    + String(_config.device_id) +
    R"HTML('>

📍 安装环境
🏠<input type='text' name='env_note' placeholder='如: 主卧 / 客厅 / 老人房' value=')HTML")
    + String(_config.env_note) +
    R"HTML('>

)HTML");

    // MQTT 隐藏域
    html += "<input type='hidden' name='mqtt_server' value='" + String(_config.mqtt_server) + "'>";
    html += "<input type='hidden' name='mqtt_port' value='"   + String(_config.mqtt_port)   + "'>";
    html += "<input type='hidden' name='mqtt_user' value='"   + String(_config.mqtt_user)   + "'>";
    html += "<input type='hidden' name='mqtt_pass' value='"   + String(_config.mqtt_pass)   + "'>";

    html += String(R"HTML(

<button type='submit'>保存配置并重启连接</button>

</form>
<button class='danger' type='button' onclick="if(confirm('确定要恢复出厂设置吗?'))location.href='/reset'">恢复出厂配置</button>

Health Monitor · Device Provisioning

📋

配网流程

按顺序完成 4 步操作

×

1

连接设备热点

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

2

选择 WiFi 并输入密码

选择您的 2.4GHz WiFi 网络

3

填写设备基础信息

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

4

保存并等待连接

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

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

)HTML");
    return html;
}

-// 配网 HTML 页面 (完全匹配 data/index.html 设计)

+// 配网 HTML 页面 (完全匹配 软件组设计稿 硬件配网.txt)

+    int n = WiFi.scanNetworks(false, true);  // async=false, showHidden=false

+        ssidOptions += "<option value='" + WiFi.SSID(i) + "'>" + WiFi.SSID(i) + " (" + String(WiFi.RSSI(i)) + "dBm)</option>";

+        ssidOptions = "<option value=''>未发现 WiFi, 请刷新</option>";

-.form-group input,.form-group select{width:100%;height:44px;padding:0 14px;border:2px solid var(--border);border-radius:8px;font-size:15px;color:var(--text);background:var(--bg);transition:border-color 0.2s,box-shadow 0.2s;outline:none}

-.form-group input:focus,.form-group select:focus{border-color:var(--primary);box-shadow:0 0 0 3px rgba(74,144,217,0.15);background:var(--card-bg)}

-.form-group select{appearance:none;-webkit-appearance:none;background-image:url('data:image/svg+xml;charset=UTF-8,%3csvg xmlns=%27http://www.w3.org/2000/svg%27 width=%2712%27 height=%2712%27 viewBox=%270 0 12 12%27%3e%3cpath fill=%27%23BDC3C7%27 d=%27M6 9L1 3h10z%27/%3e%3c/svg%3e');background-repeat:no-repeat;background-position:right 14px center;background-size:10px 10px;padding-right:30px}

+.helpIcon.hint{animation:hintPulse 1.6s ease-in-out infinite;}

+@keyframes hintPulse{0%,100%{box-shadow:0 0 0 0 rgba(18,168,137,0.5);transform:translateY(0);}50%{box-shadow:0 0 0 10px rgba(18,168,137,0);transform:translateY(-3px);}}

+select{appearance:none;-webkit-appearance:none;background-image:url('data:image/svg+xml;charset=UTF-8,%3csvg xmlns=''http://www.w3.org/2000/svg'' width=''12'' height=''12'' viewBox=''0 0 12 12''%3e%3cpath fill=''%238aa09a'' d=''M6 9L1 3h10z''/%3e%3c/svg%3e');background-repeat:no-repeat;background-position:right 14px center;background-size:10px 10px;padding-right:30px;}

+.modal{width:100%;max-width:360px;background:#fff;border-radius:20px;padding:24px 22px 18px;transform:scale(0.85) translateY(20px);opacity:0;transition:transform 0.3s cubic-bezier(0.34,1.56,0.64,1),opacity 0.25s ease;box-shadow:0 12px 40px rgba(0,0,0,0.2);max-height:80vh;overflow-y:auto;}

+.guideTip{position:absolute;top:60px;right:0;transform:translateY(-8px) scale(0.9);transform-origin:top right;background:var(--color-primary);color:#fff;font-size:12px;padding:8px 14px;border-radius:999px;box-shadow:0 4px 12px rgba(18,168,137,0.35);opacity:0;transition:opacity 0.3s ease,transform 0.3s ease;z-index:6;pointer-events:none;white-space:nowrap;}

-
ESP32-S3 + 毫米波雷达 · 配网设置

+

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

WiFi 联网与参数配置

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

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

-<input type='password' name='p' placeholder='请输入 WiFi 密码'>

-
☁ MQTT 云平台设置

-<input type='text' name='mqtt_server' value=')HTML");

+

⚙

设备基础信息

唯一标识与安装位置

+
🔖 设备编号
#<input type='text' name='device_id' value=')HTML")

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

-<input type='number' name='mqtt_port' value=')HTML";

+    html += "<input type='hidden' name='mqtt_server' value='" + String(_config.mqtt_server) + "'>";

+    html += "<input type='hidden' name='mqtt_port' value='"   + String(_config.mqtt_port)   + "'>";

+    html += "<input type='hidden' name='mqtt_user' value='"   + String(_config.mqtt_user)   + "'>";

+    html += "<input type='hidden' name='mqtt_pass' value='"   + String(_config.mqtt_pass)   + "'>";

-<input type='text' name='mqtt_user' value=')HTML";

-<input type='password' name='mqtt_pass' value=')HTML";

+<button class='danger' type='button' onclick="if(confirm('确定要恢复出厂设置吗?'))location.href='/reset'">恢复出厂配置</button>

-<input type='text' name='env_note' placeholder='例如: 主卧 / 客厅 / 老人房 / 婴儿房' value=')HTML");

-
💡 请备注此设备安装的环境位置，方便在云平台上区分不同房间的数据

-<input type='text' name='device_id' value=')HTML");

+

配网流程

按顺序完成 4 步操作

+

1

连接设备热点

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

+

2

选择 WiFi 并输入密码

选择您的 2.4GHz WiFi 网络

+

3

填写设备基础信息

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

+

4

保存并等待连接

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

-<button class='btn-submit' type='submit'>💾 保存配置并连接</button>

-<button class='btn-reset' onclick="if(confirm('确定要恢复出厂设置吗?'))location.href='/reset'">🔄 恢复出厂设置</button>


Let me verify the result is correct by reading a key section.


完成。验证一下关键部分：


296	// 配网 HTML 页面 (完全匹配 软件组设计稿 硬件配网.txt)
297	// ============================================================
298
299	String WiFiMgmt::_htmlPage() {
300	    // 扫描 WiFi 并构建 SSID 选项
301	    String ssidOptions;
302	    int n = WiFi.scanNetworks(false, true);  // async=false, showHidden=false
303	    for (int i = 0; i < n; i++) {
304	        ssidOptions += "<option value='" + WiFi.SSID(i) + "'>" + WiFi.SSID(i) + " (" + String(WiFi.RSSI(i)) + "dBm)</option>";
305	    }
306	    if (n == 0) {
307	        ssidOptions = "<option value=''>未发现 WiFi, 请刷新</option>";
308	    }
309	    WiFi.scanDelete();
310
311	    String html = String(R"HTML(<!DOCTYPE html>
312	<html lang='zh-CN'>
313	<head>
314	<meta charset='UTF-8'>
315	<meta name='viewport' content='width=device-width,initial-scale=1,user-scalable=no'>
316	<meta name='theme-color' content='#12a889'>
317	<title>健康监测系统 - 配网设置</title>
318	<style>
319	:root{--color-primary:#12a889;--color-primary-light:#35c9aa;--color-primary-dark:#08745f;--color-success:#00b42a;--color-warning:#ff7d00;--color-error:#f53f3f;--color-bg-page:#f3faf8;--color-bg-card:#ffffff;--color-bg-hover:#eef8f5;--color-card-green:#e9fff8;--color-text-primary:#15201d;--color-text-secondary:#4e6660;--color-text-tertiary:#8aa09a;--color-border:#dcebe7;--color-divider:#edf5f2;}
320	*{box-sizing:border-box;margin:0;padding:0}
321	body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'PingFang SC','Hiragino Sans GB','Microsoft YaHei',sans-serif;background:var(--color-bg-page);min-height:100vh;padding:12px 16px 24px;color:var(--color-text-primary);font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased;}
322	.wrap{max-width:420px;margin:0 auto;}
323	.header{display:flex;align-items:center;justify-content:center;position:relative;padding:20px 0 16px;}
324	.headerIcon{width:56px;height:56px;border-radius:50%;background:var(--color-card-green);display:flex;align-items:center;justify-content:center;font-size:28px;margin-right:14px;box-shadow:0 4px 12px rgba(18,168,137,0.12);}
325	.headerText{text-align:left;}

// 配网 HTML 页面 (完全匹配 软件组设计稿 硬件配网.txt)
// ============================================================

String WiFiMgmt::_htmlPage() {
    // 扫描 WiFi 并构建 SSID 选项
    String ssidOptions;
    int n = WiFi.scanNetworks(false, true);  // async=false, showHidden=false
    for (int i = 0; i < n; i++) {
        ssidOptions += "<option value='" + WiFi.SSID(i) + "'>" + WiFi.SSID(i) + " (" + String(WiFi.RSSI(i)) + "dBm)</option>";
    }
    if (n == 0) {
        ssidOptions = "<option value=''>未发现 WiFi, 请刷新</option>";
    }
    WiFi.scanDelete();

    String html = String(R"HTML(<!DOCTYPE html>
<html lang='zh-CN'>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width,initial-scale=1,user-scalable=no'>
<meta name='theme-color' content='#12a889'>
<title>健康监测系统 - 配网设置</title>
<style>
:root{--color-primary:#12a889;--color-primary-light:#35c9aa;--color-primary-dark:#08745f;--color-success:#00b42a;--color-warning:#ff7d00;--color-error:#f53f3f;--color-bg-page:#f3faf8;--color-bg-card:#ffffff;--color-bg-hover:#eef8f5;--color-card-green:#e9fff8;--color-text-primary:#15201d;--color-text-secondary:#4e6660;--color-text-tertiary:#8aa09a;--color-border:#dcebe7;--color-divider:#edf5f2;}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'PingFang SC','Hiragino Sans GB','Microsoft YaHei',sans-serif;background:var(--color-bg-page);min-height:100vh;padding:12px 16px 24px;color:var(--color-text-primary);font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased;}
.wrap{max-width:420px;margin:0 auto;}
.header{display:flex;align-items:center;justify-content:center;position:relative;padding:20px 0 16px;}
.headerIcon{width:56px;height:56px;border-radius:50%;background:var(--color-card-green);display:flex;align-items:center;justify-content:center;font-size:28px;margin-right:14px;box-shadow:0 4px 12px rgba(18,168,137,0.12);}
.headerText{text-align:left;}


Looks good. Let me verify the modal and the end of the page.