// ============================================================================
//  qt_file_io_demo —— Qt5 文件操作教学综合示例（非 wiki 生成，手工维护）
//
//  覆盖 5 个主题，每组按钮对应一个场景；所有演示文件落到可执行文件同目录的
//  demo_output/ 子目录，statusLabel 始终显示绝对路径，方便课堂手动打开查看：
//    ① 基础文件操作：QFile 写/读/追加、QFileInfo、QDir 列目录
//    ② 字符集编码：UTF-8 / Latin-1 / GBK / UTF-16LE + BOM + 「错码读回」乱码对比
//    ③ 文本方式 vs 二进制方式：QTextStream 文本、QDataStream 二进制、QIODevice::Text
//    ④ 异常与错误处理：Qt 返回值式（open/errorString）+ C++ try/catch（ifstream 异常）两范式
//    ⑤ QJson 文件读写：构造→落盘 / 读盘→解析 / 改写 / 损坏解析
//
//  形态：一个 QWidget 布满按钮（仿 qt_dialog_showcase），纯代码、单文件。
//  离屏验证：QT_QPA_PLATFORM=offscreen 时 200ms 自动退出（仿 p03 风格，不点按钮
//            不触发任何文件写入）。
//
//  编码演示设计要点：优先用 QString::toUtf8()/toLatin1()（Qt 内置、不依赖 CJK codec
//    插件），GBK 通过 QTextCodec 尝试，未安装 codec 则给出明确提示并优雅降级 —— 这
//    本身就是 Linux 上精简 Qt 安装的真实坑点。
// ============================================================================
#include <utility>

#include <QApplication>
#include <QWidget>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGroupBox>
#include <QGridLayout>
#include <QPushButton>
#include <QLabel>
#include <QFrame>
#include <QFile>
#include <QTextStream>
#include <QDataStream>
#include <QFileInfo>
#include <QDir>
#include <QList>
#include <QDateTime>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonValue>
#include <QJsonParseError>
#include <QTextCodec>
#include <QDesktopServices>
#include <QUrl>
#include <QTimer>

#include <fstream>       // std::ifstream（C++ 异常演示）
#include <ios>           // std::ios_base::failure
#include <stdexcept>     // std::runtime_error
#include <exception>

class FileIoDemo : public QWidget {
public:
    explicit FileIoDemo(QWidget *parent = nullptr) : QWidget(parent) {
        // demo_output 放在可执行文件同目录，路径稳定可预测；statusLabel 始终显示绝对路径。
        m_dir = QCoreApplication::applicationDirPath() + QStringLiteral("/demo_output");
        QDir().mkpath(m_dir);

        auto *root = new QVBoxLayout(this);

        m_status = new QLabel(
            QStringLiteral("演示文件目录：%1\n点击下方按钮，每个按钮产生一个文件，可手动打开查看。")
                .arg(m_dir));
        m_status->setWordWrap(true);
        m_status->setMinimumHeight(54);
        m_status->setFrameShape(QFrame::Box);
        root->addWidget(m_status);

        buildBasicGroup(root);     // ①
        buildEncodingGroup(root);  // ②
        buildTextBinGroup(root);   // ③
        buildErrorGroup(root);     // ④
        buildJsonGroup(root);      // ⑤

        // 底部：打开 / 清空 demo_output
        auto *bottom = new QHBoxLayout;
        auto *btnOpen = new QPushButton(QStringLiteral("在文件管理器中打开 demo_output"), this);
        auto *btnClear = new QPushButton(QStringLiteral("清空 demo_output"), this);
        bottom->addWidget(btnOpen);
        bottom->addWidget(btnClear);
        root->addLayout(bottom);
        connect(btnOpen, &QPushButton::clicked, this, &FileIoDemo::openDemoDir);
        connect(btnClear, &QPushButton::clicked, this, &FileIoDemo::clearDemoDir);

        setWindowTitle(QStringLiteral("Qt5 文件操作教学示例（5 主题 × 多场景）"));
        resize(700, 760);
    }

private:
    QLabel *m_status = nullptr;
    QString m_dir;  // demo_output 绝对路径

    // ---- 通用 helpers --------------------------------------------------------
    template <typename F>
    void addBtn(QGridLayout *g, int row, int col, const QString &text, F &&slot) {
        auto *btn = new QPushButton(text, this);
        g->addWidget(btn, row, col);
        connect(btn, &QPushButton::clicked, this, std::forward<F>(slot));
    }

    void setStatus(const QString &s) { m_status->setText(s); }

    QString path(const QString &fname) const { return m_dir + QLatin1Char('/') + fname; }

    // 写原始字节；extra 可叠加 QIODevice::Text / QIODevice::Append
    bool writeBytes(const QString &fname, const QByteArray &bytes,
                    QIODevice::OpenMode extra = QIODevice::NotOpen) {
        QFile f(path(fname));
        if (!f.open(QIODevice::WriteOnly | extra)) {
            setStatus(QStringLiteral("写入失败：%1\n错误：%2").arg(path(fname), f.errorString()));
            return false;
        }
        f.write(bytes);
        f.close();
        return true;
    }

    // 读原始字节
    bool readBytes(const QString &fname, QByteArray *out,
                   QIODevice::OpenMode extra = QIODevice::NotOpen) {
        QFile f(path(fname));
        if (!f.open(QIODevice::ReadOnly | extra)) {
            setStatus(QStringLiteral("读取失败：%1\n错误：%2").arg(path(fname), f.errorString()));
            return false;
        }
        *out = f.readAll();
        f.close();
        return true;
    }

    // 汇报某文件信息（名称/大小/绝对路径），可选附注，用于 statusLabel
    QString report(const QString &fname, const QString &note = QString()) {
        QFileInfo fi(path(fname));
        const QString base = QStringLiteral("✅ %1（%2 字节）\n绝对路径：%3")
                                 .arg(fname).arg(fi.size()).arg(fi.absoluteFilePath());
        return note.isEmpty() ? base : base + QStringLiteral("\n") + note;
    }

    // ---- ① 基础文件操作 ------------------------------------------------------
    void buildBasicGroup(QVBoxLayout *root) {
        auto *g = new QGroupBox(QStringLiteral("① 基础文件操作（QFile / QTextStream / QFileInfo / QDir）"));
        auto *l = new QGridLayout(g);
        addBtn(l, 0, 0, QStringLiteral("QFile 写文本"),  [this] { basicWriteText(); });
        addBtn(l, 0, 1, QStringLiteral("QFile 读文本"),  [this] { basicReadText(); });
        addBtn(l, 0, 2, QStringLiteral("追加 vs 覆盖"),  [this] { basicAppend(); });
        addBtn(l, 1, 0, QStringLiteral("QFileInfo 信息"), [this] { basicFileInfo(); });
        addBtn(l, 1, 1, QStringLiteral("QDir 列目录"),    [this] { basicDirList(); });
        root->addWidget(g);
    }
    void basicWriteText() {
        // 坑点：Qt5 的 QTextStream 默认 codec 是「系统区域编码」（中文 Windows 常为 GBK），
        // 不是 UTF-8。写中文必须显式 setCodec("UTF-8")，否则文件编码随系统而变。
        QFile f(path("01_qfile_write.txt"));
        if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) {
            setStatus(QStringLiteral("打开失败：%1").arg(f.errorString())); return;
        }
        QTextStream out(&f);
        out.setCodec(QTextCodec::codecForName("UTF-8"));
        out << QStringLiteral("第一行：Qt5 文件操作\n");
        out << QStringLiteral("第二行：当前时间 ") << QDateTime::currentDateTime().toString(Qt::ISODate) << '\n';
        out << QStringLiteral("第三行：中文测试 —— 你好，世界\n");
        f.close();
        setStatus(report("01_qfile_write.txt", QStringLiteral("用 QTextStream 写，显式 setCodec UTF-8。")));
    }
    void basicReadText() {
        QFile f(path("01_qfile_write.txt"));
        if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
            setStatus(QStringLiteral("读取失败（请先点「QFile 写文本」）：%1").arg(f.errorString())); return;
        }
        QTextStream in(&f);
        in.setCodec(QTextCodec::codecForName("UTF-8"));
        const QString all = in.readAll();
        f.close();
        setStatus(QStringLiteral("读到内容（%1 个字符）：\n%2").arg(all.length()).arg(all));
    }
    void basicAppend() {
        // WriteOnly 每次清空覆盖；Append 在末尾追加、保留原内容
        QFile f(path("01_append.txt"));
        if (!f.open(QIODevice::WriteOnly | QIODevice::Append)) { setStatus(f.errorString()); return; }
        QTextStream out(&f);
        out.setCodec(QTextCodec::codecForName("UTF-8"));
        out << QStringLiteral("append @ ")
            << QDateTime::currentDateTime().toString(Qt::ISODateWithMs) << '\n';
        f.close();

        QFile r(path("01_append.txt"));
        r.open(QIODevice::ReadOnly | QIODevice::Text);
        setStatus(report("01_append.txt",
                         QStringLiteral("当前全部内容：\n") + QString::fromUtf8(r.readAll())));
    }
    void basicFileInfo() {
        writeBytes("01_fileinfo.txt", "hello\n");
        const QFileInfo fi(path("01_fileinfo.txt"));
        setStatus(QStringLiteral("文件名：%1\n存在：%2  大小：%3 字节\n绝对路径：%4\n所在目录：%5")
                      .arg(fi.fileName())
                      .arg(fi.exists() ? QStringLiteral("是") : QStringLiteral("否"))
                      .arg(fi.size()).arg(fi.absoluteFilePath()).arg(fi.absolutePath()));
    }
    void basicDirList() {
        const QDir d(m_dir);
        const QStringList files = d.entryList(QDir::Files, QDir::Name);
        setStatus(QStringLiteral("demo_output 内文件数：%1\n%2")
                      .arg(files.size()).arg(files.join('\n')));
    }

    // ---- ② 字符集编码 --------------------------------------------------------
    void buildEncodingGroup(QVBoxLayout *root) {
        auto *g = new QGroupBox(QStringLiteral("② 字符集编码（UTF-8 / Latin-1 / GBK / UTF-16LE + BOM + 乱码）"));
        auto *l = new QGridLayout(g);
        addBtn(l, 0, 0, QStringLiteral("写 UTF-8"),       [this] { encUtf8(); });
        addBtn(l, 0, 1, QStringLiteral("写 UTF-8 + BOM"), [this] { encUtf8Bom(); });
        addBtn(l, 0, 2, QStringLiteral("写 Latin-1"),     [this] { encLatin1(); });
        addBtn(l, 1, 0, QStringLiteral("写 GBK"),          [this] { encGbk(); });
        addBtn(l, 1, 1, QStringLiteral("写 UTF-16LE"),     [this] { encUtf16Le(); });
        addBtn(l, 1, 2, QStringLiteral("乱码：错码读回"),   [this] { encMismatch(); });
        root->addWidget(g);
    }
    void encUtf8() {
        const QString s = QStringLiteral("你好，Qt\tASCII: Hello\n");  // 含中文 + ASCII + 制表符
        writeBytes("02_utf8.txt", s.toUtf8());
        setStatus(report("02_utf8.txt", QStringLiteral("toUtf8()：中文每字 3 字节，ASCII 每字 1 字节。")));
    }
    void encUtf8Bom() {
        QByteArray b;
        b.append((char)0xEF).append((char)0xBB).append((char)0xBF);  // UTF-8 BOM = EF BB BF
        b.append(QStringLiteral("带 BOM 的 UTF-8\n").toUtf8());
        writeBytes("02_utf8_bom.txt", b);
        setStatus(report("02_utf8_bom.txt", QStringLiteral("头部 3 字节 EF BB BF 即 UTF-8 BOM（十六进制可见）。")));
    }
    void encLatin1() {
        // Latin-1（ISO-8859-1）只能表示 U+0000~U+00FF；中文超出范围，toLatin1() 会替换成 '?'
        const QString s = QStringLiteral("Hello 世界");
        writeBytes("02_latin1.txt", s.toLatin1());
        setStatus(report("02_latin1.txt",
            QStringLiteral("toLatin1() 把超出 0xFF 的「世界」替换成 '?'，文件里只剩 ASCII 部分。")));
    }
    void encGbk() {
        // GBK 是中文 Windows 默认编码，需 Qt 的 CJK codec 插件（plugins/codecs）。
        // 精简 Qt 安装常不含该插件，codecForName 返回 nullptr —— 这里优雅降级，
        // 并列出当前实际可用的编码，让学生看清「为什么 GBK 用不了」。
        QTextCodec *gbk = QTextCodec::codecForName("GBK");
        if (!gbk) {
            const QList<QByteArray> codecs = QTextCodec::availableCodecs();
            QStringList names;
            for (const QByteArray &c : codecs) names << QString::fromLatin1(c);
            setStatus(QStringLiteral("⚠️ 当前 Qt 构建未提供 GBK codec 插件\n"
                                      "（QTextCodec::codecForName(\"GBK\") 返回 nullptr）。\n"
                                      "这是真实坑点：精简 Qt 安装常不含 CJK codec 插件。\n"
                                      "当前可用编码（%1 个）：%2\n"
                                      "可改用 toUtf8()/toLatin1()（内置）演示编码原理，见其余按钮。")
                          .arg(names.size()).arg(names.join(QStringLiteral(", "))));
            return;
        }
        const QString s = QStringLiteral("你好，GBK 编码\n");
        writeBytes("02_gbk.txt", gbk->fromUnicode(s));
        setStatus(report("02_gbk.txt",
            QStringLiteral("GBK：中文每字 2 字节。用编辑器以 GBK 打开才正常，以 UTF-8 打开会乱码。")));
    }
    void encUtf16Le() {
        const QString s = QStringLiteral("UTF-16LE 你好\n");
        QByteArray b;
        b.append((char)0xFF).append((char)0xFE);  // UTF-16LE BOM = FF FE
        const QChar *p = s.unicode();
        for (int i = 0; i < s.length(); ++i) {
            const ushort u = p[i].unicode();
            b.append((char)(u & 0xFF));           // 低字节在前 = Little Endian
            b.append((char)((u >> 8) & 0xFF));
        }
        writeBytes("02_utf16le.txt", b);
        setStatus(report("02_utf16le.txt", QStringLiteral("FF FE 为 UTF-16LE BOM；之后每字符固定 2 字节。")));
    }
    void encMismatch() {
        if (!QFile::exists(path("02_utf8.txt"))) encUtf8();
        QByteArray raw;
        if (!readBytes("02_utf8.txt", &raw)) return;
        // 故意用 Latin-1 解码 UTF-8 字节：多字节中文被拆成多个单字节字符 = 乱码（mojibake）
        const QString mojibake = QString::fromLatin1(raw);
        setStatus(QStringLiteral("02_utf8.txt 用 Latin-1 强行解码（编码不匹配）→ 乱码：\n%1\n"
                                  "结论：读文件必须用与写入一致的编码。").arg(mojibake));
    }

    // ---- ③ 文本方式 vs 二进制方式 -------------------------------------------
    void buildTextBinGroup(QVBoxLayout *root) {
        auto *g = new QGroupBox(
            QStringLiteral("③ 文本方式 vs 二进制方式（QTextStream / QIODevice::Text / QDataStream）"));
        auto *l = new QGridLayout(g);
        addBtn(l, 0, 0, QStringLiteral("文本写结构"),          [this] { tbStructText(); });
        addBtn(l, 0, 1, QStringLiteral("二进制写结构"),        [this] { tbStructBin(); });
        addBtn(l, 0, 2, QStringLiteral("对比大小"),            [this] { tbCompare(); });
        addBtn(l, 1, 0, QStringLiteral("QIODevice::Text 换行"), [this] { tbNewline(); });
        root->addWidget(g);
    }
    void tbStructText() {
        // 文本方式：人可读，但格式松散、解析需约定分隔符、浮点精度受 << 默认值影响
        QFile f(path("03_struct_text.txt"));
        if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) { setStatus(f.errorString()); return; }
        QTextStream out(&f);
        out.setCodec(QTextCodec::codecForName("UTF-8"));
        out << "name=张三\nage=20\nscore=95.5\n";
        f.close();
        setStatus(report("03_struct_text.txt", QStringLiteral("文本：可读，但程序解析需自己处理格式。")));
    }
    void tbStructBin() {
        // 二进制方式（QDataStream）：紧凑、带类型长度、可精确还原；是 Qt 推荐的跨平台二进制格式
        QFile f(path("03_struct_bin.dat"));
        if (!f.open(QIODevice::WriteOnly)) { setStatus(f.errorString()); return; }
        QDataStream out(&f);
        out.setVersion(QDataStream::Qt_5_14);          // 固定版本，避免 Qt 升级后格式不兼容
        out.setByteOrder(QDataStream::LittleEndian);
        out << qint32(20) << double(95.5) << QStringLiteral("张三");
        f.close();

        // 立即读回验证二进制可精确还原
        QFile r(path("03_struct_bin.dat"));
        r.open(QIODevice::ReadOnly);
        QDataStream in(&r);
        in.setVersion(QDataStream::Qt_5_14);
        in.setByteOrder(QDataStream::LittleEndian);
        qint32 age = 0; double score = 0; QString rname;
        in >> age >> score >> rname;
        r.close();
        setStatus(report("03_struct_bin.dat",
            QStringLiteral("读回验证 → age=%1 score=%2 name=%3（二进制可精确还原）")
                .arg(age).arg(score, 0, 'f', 1).arg(rname)));
    }
    void tbCompare() {
        if (!QFile::exists(path("03_struct_text.txt"))) tbStructText();
        if (!QFile::exists(path("03_struct_bin.dat"))) tbStructBin();
        const QFileInfo ft(path("03_struct_text.txt")), fb(path("03_struct_bin.dat"));
        setStatus(QStringLiteral("文本 03_struct_text.txt：%1 字节\n二进制 03_struct_bin.dat：%2 字节\n"
                                  "用十六进制工具看 .dat：int=4B、double=8B、QString 带长度前缀 + UTF-16。")
                      .arg(ft.size()).arg(fb.size()));
    }
    void tbNewline() {
        // QIODevice::Text：Windows 上写 '\n' 会被转成 "\r\n"；Linux 上不转换。
        // 不加 Text 标志（纯二进制写法）则始终原样 '\n'。这是跨平台文本文件的经典差异。
        const QByteArray body = "AAAA\nBBBB\nCCCC\n";
        writeBytes("03_nl_text.txt", body, QIODevice::Text);  // 文本模式
        writeBytes("03_nl_bin.txt", body);                    // 二进制模式（无转换）
        setStatus(QStringLiteral("已写 03_nl_text.txt（QIODevice::Text）与 03_nl_bin.txt（无 Text）。\n"
                                  "Windows 下前者含 \\r\\n、后者只 \\n；Linux 下两者相同（平台差异，见注释）。"));
    }

    // ---- ④ 异常与错误处理 ----------------------------------------------------
    void buildErrorGroup(QVBoxLayout *root) {
        auto *g = new QGroupBox(
            QStringLiteral("④ 异常与错误处理（Qt 返回值式 ｜ C++ try/catch 两范式对照）"));
        auto *l = new QGridLayout(g);
        addBtn(l, 0, 0, QStringLiteral("Qt: 打开不存在文件"), [this] { errQtOpenMissing(); });
        addBtn(l, 0, 1, QStringLiteral("Qt: 只读模式写"),     [this] { errQtReadOnlyWrite(); });
        addBtn(l, 0, 2, QStringLiteral("C++: ifstream 异常"), [this] { errCppIfstream(); });
        addBtn(l, 1, 0, QStringLiteral("C++: throw + catch"), [this] { errCppThrow(); });
        addBtn(l, 1, 1, QStringLiteral("JSON: 损坏解析"),      [this] { errJsonBroken(); });
        root->addWidget(g);
    }
    void errQtOpenMissing() {
        // Qt 范式：不抛异常，靠 open() 返回值 + errorString()
        QFile f(path("这个文件不存在.txt"));
        if (f.open(QIODevice::ReadOnly)) {
            f.close();
            setStatus(QStringLiteral("意外：文件竟存在。"));
        } else {
            setStatus(QStringLiteral("【Qt 返回值式】open() 返回 false\n错误码：%1\n错误描述：%2\n"
                                      "（Qt 设计上几乎不用 C++ 异常，错误经返回值/API 暴露）")
                          .arg(f.error()).arg(f.errorString()));
        }
    }
    void errQtReadOnlyWrite() {
        writeBytes("04_readonly.txt", "data\n");
        QFile f(path("04_readonly.txt"));
        f.open(QIODevice::ReadOnly);   // 故意只读
        const qint64 n = f.write("try overwrite");  // 只读模式下写
        const QString es = f.errorString();
        f.close();
        setStatus(QStringLiteral("【Qt 返回值式】只读模式下 write() 返回 %1（-1 表示失败）\nerrorString()：%2")
                      .arg(n).arg(es));
    }
    void errCppIfstream() {
        // C++ 范式：标准库流默认不抛异常；调用 exceptions() 开启后，打开失败抛 std::ios_base::failure
        std::ifstream in;
        in.exceptions(std::ios::failbit | std::ios::badbit);
        try {
            in.open(path("这个文件不存在.txt").toStdString());  // 不存在 → failbit → 抛异常
            setStatus(QStringLiteral("意外：文件竟存在。"));
        } catch (const std::ios_base::failure &e) {
            setStatus(QStringLiteral("【C++ 异常式】捕获 std::ios_base::failure\nwhat()：%1\n"
                                      "（对比：Qt 的 QFile 同样情况只返回 false，不抛异常）")
                          .arg(QString::fromStdString(e.what())));
        } catch (const std::exception &e) {
            setStatus(QStringLiteral("捕获 std::exception：%1").arg(QString::fromStdString(e.what())));
        }
    }
    void errCppThrow() {
        // 演示自定义异常 + 多层 catch 顺序（派生类在前、基类在后）
        try {
            throw std::runtime_error("演示：文件内容校验失败，主动抛出");
        } catch (const std::runtime_error &e) {
            setStatus(QStringLiteral("【C++ 异常式】捕获 std::runtime_error：\n%1\n"
                                      "要点：catch 顺序从派生类到基类，否则被基类提前吞掉。")
                          .arg(QString::fromStdString(e.what())));
        }
    }
    void errJsonBroken() {
        // 先写一段语法错误的 JSON（键未加引号、对象未闭合）
        writeBytes("04_broken.json", "{ name: \"张三\", score: 95 ");
        QByteArray raw;
        if (!readBytes("04_broken.json", &raw)) return;
        QJsonParseError err;
        const QJsonDocument doc = QJsonDocument::fromJson(raw, &err);
        if (doc.isNull()) {
            setStatus(QStringLiteral("【JSON 解析】失败（QJsonDocument 不抛异常）。\n"
                                      "错误偏移：%1\n错误描述：%2\n"
                                      "（JSON 解析用 QJsonParseError 拿精确出错位置，而非 try/catch）")
                          .arg(err.offset).arg(err.errorString()));
        } else {
            setStatus(QStringLiteral("意外：JSON 竟合法。"));
        }
    }

    // ---- ⑤ QJson 文件读写 ----------------------------------------------------
    void buildJsonGroup(QVBoxLayout *root) {
        auto *g = new QGroupBox(QStringLiteral("⑤ QJson 文件读写（QJsonDocument / Object / Array）"));
        auto *l = new QGridLayout(g);
        addBtn(l, 0, 0, QStringLiteral("写 JSON 文件"), [this] { jsonWrite(); });
        addBtn(l, 0, 1, QStringLiteral("读 JSON 文件"), [this] { jsonRead(); });
        addBtn(l, 1, 0, QStringLiteral("修改后回写"),   [this] { jsonModify(); });
        root->addWidget(g);
    }
    void jsonWrite() {
        QJsonObject student;
        student["name"] = QStringLiteral("张三");
        student["score"] = 92;
        student["enrolled"] = true;
        QJsonArray tags;
        tags.append(QStringLiteral("Qt"));
        tags.append(QStringLiteral("C++"));
        tags.append(QStringLiteral("多线程"));
        student["tags"] = tags;
        const QJsonDocument doc(student);
        writeBytes("05_student.json", doc.toJson(QJsonDocument::Indented));
        setStatus(report("05_student.json",
            QStringLiteral("QJsonObject → QJsonDocument → toJson(Indented) 缩进美化。")));
    }
    void jsonRead() {
        if (!QFile::exists(path("05_student.json"))) jsonWrite();
        QByteArray raw;
        if (!readBytes("05_student.json", &raw)) return;
        const QJsonDocument doc = QJsonDocument::fromJson(raw);
        if (doc.isNull() || !doc.isObject()) {
            setStatus(QStringLiteral("解析失败，请先点「写 JSON 文件」。")); return;
        }
        const QJsonObject obj = doc.object();
        const QJsonArray tags = obj["tags"].toArray();
        QStringList tagList;
        for (const QJsonValue &v : tags) tagList << v.toString();
        setStatus(QStringLiteral("读 05_student.json：\nname = %1\nscore = %2\nenrolled = %3\ntags = [%4]")
                      .arg(obj["name"].toString())
                      .arg(obj["score"].toInt())
                      .arg(obj["enrolled"].toBool() ? QStringLiteral("true") : QStringLiteral("false"))
                      .arg(tagList.join(", ")));
    }
    void jsonModify() {
        if (!QFile::exists(path("05_student.json"))) jsonWrite();
        QByteArray raw;
        if (!readBytes("05_student.json", &raw)) return;
        QJsonObject obj = QJsonDocument::fromJson(raw).object();
        obj["score"] = 100;                       // 改
        obj["grade"] = QStringLiteral("A+");      // 增
        obj.remove("enrolled");                   // 删
        writeBytes("05_student.json", QJsonDocument(obj).toJson(QJsonDocument::Indented));
        setStatus(report("05_student.json",
            QStringLiteral("已改 score=100、新增 grade=A+、删除 enrolled，覆盖回写。")));
    }

    // ---- 底部按钮 ------------------------------------------------------------
    void openDemoDir() {
        QDesktopServices::openUrl(QUrl::fromLocalFile(m_dir));
    }
    void clearDemoDir() {
        const QDir d(m_dir);
        int n = 0;
        for (const QFileInfo &fi : d.entryInfoList(QDir::Files)) {
            if (QFile::remove(fi.absoluteFilePath())) ++n;
        }
        setStatus(QStringLiteral("已清空 demo_output，删除 %1 个文件。").arg(n));
    }
};

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    FileIoDemo w;
    w.show();
    // 离屏自动化验证时 200ms 后自动退出（仿 p03 风格）；
    // 正常运行窗口保持、不自动退出；不点按钮则不触发任何文件写入。
    if (qgetenv("QT_QPA_PLATFORM") == "offscreen") {
        QTimer::singleShot(200, &app, &QApplication::quit);
    }
    return app.exec();
}
