LAPTOP-SNT8I5JK\Boounion
2025-10-14 cfcab53b0e7d5918c79cc77f0f447730682f94b1
SourceCode/Bond/Servo/CPageGlassList.cpp
@@ -8,12 +8,12 @@
#include "GlassJson.h"
#include "CServoUtilsTool.h"
#include "ToolUnits.h"
#include <optional>
#include <unordered_set>
#include <unordered_map>
#include <vector>
#include <string>
#include "CProcessDataListDlg.h"
#define PAGE_SIZE                       50
#define PAGE_BACKGROUND_COLOR           RGB(252, 252, 255)
@@ -98,7 +98,7 @@
}
// ====== 开关:1=启用假数据(只替换 DB 查询);0=用真实 DB ======
#define USE_FAKE_DB_DEMO 1
#define USE_FAKE_DB_DEMO 0
#if USE_FAKE_DB_DEMO
#include <ctime>
@@ -340,6 +340,88 @@
    }
}
bool CopyUtf8ToClipboard(const std::string& utf8)
{
    // 1) UTF-8 -> UTF-16 长度(含结尾 '\0')
    int wlen = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, nullptr, 0);
    if (wlen <= 0) return false;
    // 2) 为剪贴板分配全局可移动内存(必须 GMEM_MOVEABLE)
    SIZE_T bytes = static_cast<SIZE_T>(wlen) * sizeof(wchar_t);
    HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, bytes);
    if (!hMem) return false;
    // 3) 填充 UTF-16 文本
    wchar_t* wbuf = static_cast<wchar_t*>(GlobalLock(hMem));
    if (!wbuf) { GlobalFree(hMem); return false; }
    MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, wbuf, wlen);
    GlobalUnlock(hMem);
    // 4) 打开剪贴板并设置数据(CF_UNICODETEXT)
    if (!OpenClipboard(nullptr)) { GlobalFree(hMem); return false; }
    if (!EmptyClipboard()) { CloseClipboard(); GlobalFree(hMem); return false; }
    // 成功后,内存所有权交给剪贴板,不能再 GlobalFree
    if (!SetClipboardData(CF_UNICODETEXT, hMem)) {
        CloseClipboard();
        GlobalFree(hMem);
        return false;
    }
    CloseClipboard();
    return true;
}
// 辅助函数:将 ANSI CString 写入文件为 UTF-8 编码
bool CPageGlassList::WriteAnsiStringAsUtf8ToFile(const CString& ansiContent, const CString& filePath)
{
    CFile file;
    if (!file.Open(filePath, CFile::modeCreate | CFile::modeWrite)) {
        return false;
    }
    // 写入 UTF-8 BOM
    const unsigned char bom[] = { 0xEF, 0xBB, 0xBF };
    file.Write(bom, 3);
    // 将 ANSI 转换为 Unicode
    int unicodeLength = MultiByteToWideChar(CP_ACP, 0,
        ansiContent, ansiContent.GetLength(),
        NULL, 0);
    if (unicodeLength <= 0) {
        file.Close();
        return false;
    }
    wchar_t* unicodeBuffer = new wchar_t[unicodeLength + 1];
    MultiByteToWideChar(CP_ACP, 0,
        ansiContent, ansiContent.GetLength(),
        unicodeBuffer, unicodeLength);
    unicodeBuffer[unicodeLength] = 0;
    // 将 Unicode 转换为 UTF-8
    int utf8Length = WideCharToMultiByte(CP_UTF8, 0,
        unicodeBuffer, unicodeLength,
        NULL, 0, NULL, NULL);
    bool success = false;
    if (utf8Length > 0) {
        char* utf8Buffer = new char[utf8Length];
        WideCharToMultiByte(CP_UTF8, 0,
            unicodeBuffer, unicodeLength,
            utf8Buffer, utf8Length, NULL, NULL);
        file.Write(utf8Buffer, utf8Length);
        delete[] utf8Buffer;
        success = true;
    }
    delete[] unicodeBuffer;
    file.Close();
    return success;
}
// CPageGlassList 对话框
IMPLEMENT_DYNAMIC(CPageGlassList, CDialogEx)
@@ -392,6 +474,8 @@
    ON_BN_CLICKED(IDC_BUTTON_EXPORT, &CPageGlassList::OnBnClickedButtonExport)
    ON_BN_CLICKED(IDC_BUTTON_PREV_PAGE, &CPageGlassList::OnBnClickedButtonPrevPage)
    ON_BN_CLICKED(IDC_BUTTON_NEXT_PAGE, &CPageGlassList::OnBnClickedButtonNextPage)
    ON_NOTIFY(ELCN_SHOWFULLTEXT, IDC_LIST_ALARM, &CPageGlassList::OnShowFullText)
    ON_BN_CLICKED(IDC_BUTTON_EXPORT_ROW, &CPageGlassList::OnBnClickedButtonExportRow)
END_MESSAGE_MAP()
// ===== 私有小工具 =====
@@ -402,7 +486,7 @@
}
// ===== CPageGlassList 消息处理程序 =====
void CPageGlassList::InitRxWindow()
void CPageGlassList::InitRxWindows()
{
    // 订阅数据
    IRxWindows* pRxWindows = RX_GetRxWindows();
@@ -519,10 +603,10 @@
{
    m_rebuilding = true;
    // 放在任何清空/重建动作之前:
    // 放在任何清空/重建动作之前:记录展开的父节点 key(ClassID)
    auto expandedKeys = SnapshotExpandedKeys(m_listCtrl);
    // —— 双保险:先清掉可见项,再清树结构 ——
    // —— 双保险:先清掉可见项,再清树结构 ——
    m_listCtrl.SetRedraw(FALSE);
    m_listCtrl.DeleteAllItems();
    m_listCtrl.SetRedraw(TRUE);
@@ -531,7 +615,7 @@
    m_listCtrl.ClearTree();
    const int colCount = m_listCtrl.GetHeaderCtrl() ? m_listCtrl.GetHeaderCtrl()->GetItemCount() : 0;
    if (colCount <= 0) return;
    if (colCount <= 0) { m_rebuilding = false; return; }
    // ==================== 1) WIP:仅第 1 页构建,且放在最顶部 ====================
    if (m_nCurPage == 1) {
@@ -550,9 +634,11 @@
            SERVO::CGlass* b = g->getBuddy();
            if (b) {
                // 按你的约定:g 是父,buddy 是子
                SERVO::CGlass* parent = g;
                SERVO::CGlass* child = b;
                // parent
                std::vector<CString> pcols(colCount);
                pcols[1] = _T("");
                pcols[2] = std::to_string(parent->getCassetteSequenceNo()).c_str();
@@ -571,6 +657,7 @@
                MaybeRestoreExpandByKey(nParent, expandedKeys);
                m_listCtrl.SetNodeColor(nParent, kWipText, kWipParentBk);   // 父:基础绿
                // child
                std::vector<CString> ccols(colCount);
                ccols[1] = _T("");
                ccols[2] = std::to_string(child->getCassetteSequenceNo()).c_str();
@@ -614,118 +701,375 @@
        for (auto* item : tempGlasses) item->release();
    }
    // ==================== 2) DB 当前页(无论第几页都构建;排在 WIP 之后) ====================
    // ==================== 2) DB 当前页(两阶段构建,处理单向 buddy) ====================
    const int rawLimit = PAGE_SIZE + 1;
    const int rawOffset = PAGE_SIZE * (m_nCurPage - 1);
#if USE_FAKE_DB_DEMO
    auto page = _make_page_fake(m_filters, PAGE_SIZE, PAGE_SIZE * (m_nCurPage - 1));
    auto page = _make_page_fake(m_filters, rawLimit, rawOffset);
#else
    auto& db = GlassLogDb::Instance();
    auto page = db.queryPaged(m_filters, PAGE_SIZE, PAGE_SIZE * (m_nCurPage - 1));
    auto pageFull = db.queryPaged(m_filters, rawLimit, rawOffset);
#endif
    std::unordered_map<std::string, size_t> idxById;
    idxById.reserve(page.items.size());
    for (size_t i = 0; i < page.items.size(); ++i) idxById[page.items[i].classId] = i;
#if !USE_FAKE_DB_DEMO
    // —— 三元键工具:<classId>|C<cassette>|J<job> —— //
// —— 三元键工具:<classId>|C<cassette>|J<job> —— //
    auto makeKey = [](const std::string& cls, int csn, int jsn) -> std::string {
        std::string k;
        k.reserve(cls.size() + 32);
        k.append(cls);
        k.push_back('|'); k.push_back('C');
        k.append(std::to_string(csn));
        k.push_back('|'); k.push_back('J');
        k.append(std::to_string(jsn));
        return k;
    };
    std::unordered_set<std::string> usedDb;
    // ★★★ 这里是关键修复:接收“const Row&”,不要非 const 引用
    using RowT = std::decay<decltype(pageFull.items.front())>::type;
    auto makeKeyR = [&](const RowT& r) -> std::string {
        return makeKey(r.classId, r.cassetteSeqNo, r.jobSeqNo);
    };
    // 不区分大小写 classId 相等
    auto iEquals = [](const std::string& a, const std::string& b) {
#ifdef _WIN32
        return _stricmp(a.c_str(), b.c_str()) == 0;
#else
        return strcasecmp(a.c_str(), b.c_str()) == 0;
#endif
};
    // —— lookahead 预读:若超出 1 条,尝试把“最后一条”与“预读”判为一对(严格优先)——
    std::optional<decltype(pageFull.items)::value_type> lookahead;
    if (pageFull.items.size() == rawLimit) {
        const auto& last = pageFull.items[PAGE_SIZE - 1];
        const auto& extra = pageFull.items[PAGE_SIZE];
        bool strictPair =
            (!last.buddyId.empty() && iEquals(last.buddyId, extra.classId)
                && last.cassetteSeqNo == extra.cassetteSeqNo
                && last.jobSeqNo == extra.jobSeqNo)
            || (!extra.buddyId.empty() && iEquals(extra.buddyId, last.classId)
                && extra.cassetteSeqNo == last.cassetteSeqNo
                && extra.jobSeqNo == last.jobSeqNo);
        bool loosePair =
            (!last.buddyId.empty() && iEquals(last.buddyId, extra.classId)) ||
            (!extra.buddyId.empty() && iEquals(extra.buddyId, last.classId));
        if (strictPair || loosePair) {
            lookahead = extra;
        }
        // 预读不算入本页
        pageFull.items.pop_back();
    }
    // 之后正常按 page 构建
    auto& pageRef = pageFull;
    // —— 建两个索引 —— //
    // A) byTriple: 三元键 -> index(唯一/已消费依据)
    // B) byClass : classId -> indices(buddy 候选池,允许多个)
    std::unordered_map<std::string, size_t> byTriple;
    std::unordered_map<std::string, std::vector<size_t>> byClass;
    byTriple.reserve(pageRef.items.size());
    byClass.reserve(pageRef.items.size());
    for (size_t i = 0; i < pageRef.items.size(); ++i) {
        const auto& r = pageRef.items[i];
        byTriple[makeKeyR(r)] = i;
        byClass[r.classId].push_back(i);
    }
    // —— 已消费集合(用三元键)——
    std::unordered_set<std::string> consumed;
    consumed.reserve(pageRef.items.size());
    int zebra = 0;
    auto zebraBk = [&](int z) -> COLORREF {
        return (z % 2 == 0) ? RGB(255, 255, 255) : RGB(235, 235, 235);
    };
    for (size_t i = 0; i < page.items.size(); ++i) {
        const auto& r = page.items[i];
        if (usedDb.count(r.classId)) continue;
    // -------- Phase 1: 先处理“有 buddyId 的记录” ----------
    for (size_t i = 0; i < pageRef.items.size(); ++i) {
        const auto& r = pageRef.items[i];
        if (consumed.count(makeKeyR(r))) continue;
        if (r.buddyId.empty()) continue;
        COLORREF bk = (zebra % 2 == 0) ? RGB(255, 255, 255) : RGB(235, 235, 235);
        bool paired = false;
        // 在同页里为 r 找 buddy 候选
        size_t buddyIdx = (size_t)-1;
        auto itVec = byClass.find(r.buddyId);
        if (itVec != byClass.end()) {
            const auto& vec = itVec->second;
        if (!r.buddyId.empty()) {
            auto it = idxById.find(r.buddyId);
            if (it != idxById.end()) {
                const auto& br = page.items[it->second];
                if (!usedDb.count(br.classId)) {
                    bool rIsParent = (r.classId <= br.classId);
                    const auto& parentRec = rIsParent ? r : br;
                    const auto& childRec = rIsParent ? br : r;
                    std::vector<CString> pcols(colCount);
                    pcols[1] = std::to_string(parentRec.id).c_str(); pcols[2] = std::to_string(parentRec.cassetteSeqNo).c_str();
                    pcols[3] = std::to_string(parentRec.jobSeqNo).c_str(); pcols[4] = parentRec.classId.c_str();
                    pcols[5] = SERVO::CServoUtilsTool::getMaterialsTypeText((SERVO::MaterialsType)parentRec.materialType).c_str();
                    pcols[6] = SERVO::CServoUtilsTool::getGlassStateText((SERVO::GlsState)parentRec.state).c_str();
                    pcols[7] = parentRec.tStart.c_str(); pcols[8] = parentRec.tEnd.c_str(); pcols[9] = parentRec.buddyId.c_str();
                    pcols[10] = SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)parentRec.aoiResult).c_str();
                    pcols[11] = parentRec.path.c_str(); pcols[12] = parentRec.params.c_str();
                    auto* nParent = m_listCtrl.InsertRoot(pcols);
                    MaybeRestoreExpandByKey(nParent, expandedKeys);
                    m_listCtrl.SetNodeColor(nParent, RGB(0, 0, 0), bk);
                    std::vector<CString> ccols(colCount);
                    ccols[1] = std::to_string(childRec.id).c_str(); ccols[2] = std::to_string(childRec.cassetteSeqNo).c_str();
                    ccols[3] = std::to_string(childRec.jobSeqNo).c_str(); ccols[4] = childRec.classId.c_str();
                    ccols[5] = SERVO::CServoUtilsTool::getMaterialsTypeText((SERVO::MaterialsType)childRec.materialType).c_str();
                    ccols[6] = SERVO::CServoUtilsTool::getGlassStateText((SERVO::GlsState)childRec.state).c_str();
                    ccols[7] = childRec.tStart.c_str(); ccols[8] = childRec.tEnd.c_str(); ccols[9] = childRec.buddyId.c_str();
                    ccols[10] = SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)childRec.aoiResult).c_str();
                    ccols[11] = childRec.path.c_str(); ccols[12] = childRec.params.c_str();
                    auto* nChild = m_listCtrl.InsertChild(nParent, ccols);
                    m_listCtrl.SetNodeColor(nChild, RGB(0, 0, 0), bk);
                    usedDb.insert(parentRec.classId);
                    usedDb.insert(childRec.classId);
                    paired = true;
            // 1) 严格匹配:Cassette/Job 一致
            for (size_t j : vec) {
                const auto& br = pageRef.items[j];
                if (br.cassetteSeqNo == r.cassetteSeqNo && br.jobSeqNo == r.jobSeqNo) {
                    if (!consumed.count(makeKeyR(br))) { buddyIdx = j; break; }
                }
            }
            // 2) 宽松匹配:同 classId 未消费的任意一条
            if (buddyIdx == (size_t)-1) {
                for (size_t j : vec) {
                    const auto& br = pageRef.items[j];
                    if (!consumed.count(makeKeyR(br))) { buddyIdx = j; break; }
                }
            }
        }
        if (!paired && !r.buddyId.empty()) {
        COLORREF bk = zebraBk(zebra);
        if (buddyIdx != (size_t)-1) {
            const auto& br = pageRef.items[buddyIdx];
            // 父:r(有 buddyId),子:br
            std::vector<CString> pcols(colCount);
            pcols[1] = std::to_string(r.id).c_str(); pcols[2] = std::to_string(r.cassetteSeqNo).c_str();
            pcols[3] = std::to_string(r.jobSeqNo).c_str(); pcols[4] = r.classId.c_str();
            pcols[1] = std::to_string(r.id).c_str();
            pcols[2] = std::to_string(r.cassetteSeqNo).c_str();
            pcols[3] = std::to_string(r.jobSeqNo).c_str();
            pcols[4] = r.classId.c_str();
            pcols[5] = SERVO::CServoUtilsTool::getMaterialsTypeText((SERVO::MaterialsType)r.materialType).c_str();
            pcols[6] = SERVO::CServoUtilsTool::getGlassStateText((SERVO::GlsState)r.state).c_str();
            pcols[7] = r.tStart.c_str(); pcols[8] = r.tEnd.c_str(); pcols[9] = r.buddyId.c_str();
            pcols[7] = r.tStart.c_str();
            pcols[8] = r.tEnd.c_str();
            pcols[9] = r.buddyId.c_str();
            pcols[10] = SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)r.aoiResult).c_str();
            pcols[11] = r.path.c_str(); pcols[12] = r.params.c_str();
            pcols[11] = r.path.c_str();
            pcols[12] = r.params.c_str();
            auto* nParent = m_listCtrl.InsertRoot(pcols);
            MaybeRestoreExpandByKey(nParent, expandedKeys);
            m_listCtrl.SetNodeColor(nParent, RGB(0, 0, 0), bk);
            std::vector<CString> ccols(colCount);
            ccols[4] = r.buddyId.c_str(); // 占位子行:显示 buddy 的 classId
            ccols[1] = std::to_string(br.id).c_str();
            ccols[2] = std::to_string(br.cassetteSeqNo).c_str();
            ccols[3] = std::to_string(br.jobSeqNo).c_str();
            ccols[4] = br.classId.c_str();
            ccols[5] = SERVO::CServoUtilsTool::getMaterialsTypeText((SERVO::MaterialsType)br.materialType).c_str();
            ccols[6] = SERVO::CServoUtilsTool::getGlassStateText((SERVO::GlsState)br.state).c_str();
            ccols[7] = br.tStart.c_str();
            ccols[8] = br.tEnd.c_str();
            ccols[9] = br.buddyId.c_str();
            ccols[10] = SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)br.aoiResult).c_str();
            ccols[11] = br.path.c_str();
            ccols[12] = br.params.c_str();
            auto* nChild = m_listCtrl.InsertChild(nParent, ccols);
            m_listCtrl.SetNodeColor(nChild, RGB(0, 0, 0), bk);
            usedDb.insert(r.classId);
            paired = true;
            consumed.insert(makeKeyR(r));
            consumed.insert(makeKeyR(br));
            ++zebra;
            continue;
        }
        if (!paired) {
            std::vector<CString> cols(colCount);
            cols[1] = std::to_string(r.id).c_str(); cols[2] = std::to_string(r.cassetteSeqNo).c_str();
            cols[3] = std::to_string(r.jobSeqNo).c_str(); cols[4] = r.classId.c_str();
            cols[5] = SERVO::CServoUtilsTool::getMaterialsTypeText((SERVO::MaterialsType)r.materialType).c_str();
            cols[6] = SERVO::CServoUtilsTool::getGlassStateText((SERVO::GlsState)r.state).c_str();
            cols[7] = r.tStart.c_str(); cols[8] = r.tEnd.c_str(); cols[9] = r.buddyId.c_str();
            cols[10] = SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)r.aoiResult).c_str();
            cols[11] = r.path.c_str(); cols[12] = r.params.c_str();
        // 没找到 buddy → 插占位子行(只写 ClassID)
        std::vector<CString> pcols(colCount);
        pcols[1] = std::to_string(r.id).c_str();
        pcols[2] = std::to_string(r.cassetteSeqNo).c_str();
        pcols[3] = std::to_string(r.jobSeqNo).c_str();
        pcols[4] = r.classId.c_str();
        pcols[5] = SERVO::CServoUtilsTool::getMaterialsTypeText((SERVO::MaterialsType)r.materialType).c_str();
        pcols[6] = SERVO::CServoUtilsTool::getGlassStateText((SERVO::GlsState)r.state).c_str();
        pcols[7] = r.tStart.c_str();
        pcols[8] = r.tEnd.c_str();
        pcols[9] = r.buddyId.c_str();
        pcols[10] = SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)r.aoiResult).c_str();
        pcols[11] = r.path.c_str();
        pcols[12] = r.params.c_str();
            auto* n = m_listCtrl.InsertRoot(cols);
            m_listCtrl.SetNodeColor(n, RGB(0, 0, 0), bk);
            usedDb.insert(r.classId);
        }
        auto* nParent = m_listCtrl.InsertRoot(pcols);
        MaybeRestoreExpandByKey(nParent, expandedKeys);
        m_listCtrl.SetNodeColor(nParent, RGB(0, 0, 0), zebraBk(zebra));
        std::vector<CString> ccols(colCount);
        ccols[4] = r.buddyId.c_str(); // 占位
        auto* nChild = m_listCtrl.InsertChild(nParent, ccols);
        m_listCtrl.SetNodeColor(nChild, RGB(0, 0, 0), zebraBk(zebra));
        consumed.insert(makeKeyR(r));
        ++zebra;
    }
    // -------- Phase 2: 剩余未消费的,作为“单条根行” ----------
    for (size_t i = 0; i < pageRef.items.size(); ++i) {
        const auto& r = pageRef.items[i];
        if (consumed.count(makeKeyR(r))) continue;
        COLORREF bk = zebraBk(zebra);
        std::vector<CString> cols(colCount);
        cols[1] = std::to_string(r.id).c_str();
        cols[2] = std::to_string(r.cassetteSeqNo).c_str();
        cols[3] = std::to_string(r.jobSeqNo).c_str();
        cols[4] = r.classId.c_str();
        cols[5] = SERVO::CServoUtilsTool::getMaterialsTypeText((SERVO::MaterialsType)r.materialType).c_str();
        cols[6] = SERVO::CServoUtilsTool::getGlassStateText((SERVO::GlsState)r.state).c_str();
        cols[7] = r.tStart.c_str();
        cols[8] = r.tEnd.c_str();
        cols[9] = r.buddyId.c_str();
        cols[10] = SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)r.aoiResult).c_str();
        cols[11] = r.path.c_str();
        cols[12] = r.params.c_str();
        auto* n = m_listCtrl.InsertRoot(cols);
        m_listCtrl.SetNodeColor(n, RGB(0, 0, 0), bk);
        consumed.insert(makeKeyR(r));
        ++zebra;
    }
    // 一次性重绘
    m_listCtrl.RebuildVisible();
#else
    // ===== DEMO 分支(保持原样;若要演示同样逻辑,可仿照上面改造)=====
    // 如果多出一条,看看它是否是“本页最后一条”的 buddy
    std::optional<decltype(page.items)::value_type> lookahead;
    auto iEquals = [](const std::string& a, const std::string& b) {
#ifdef _WIN32
        return _stricmp(a.c_str(), b.c_str()) == 0;
#else
        return strcasecmp(a.c_str(), b.c_str()) == 0;
#endif
    };
    if (page.items.size() == rawLimit) {
        const auto& last = page.items[PAGE_SIZE - 1];
        const auto& extra = page.items[PAGE_SIZE];
        bool pair =
            (!last.buddyId.empty() && iEquals(last.buddyId, extra.classId)) ||
            (!extra.buddyId.empty() && iEquals(extra.buddyId, last.classId));
        if (pair) lookahead = extra;
        page.items.pop_back();
    }
    // 你可以把 DEMO 分支也切到三元键逻辑;这里从略
    auto& pageRef = page;
    std::unordered_map<std::string, size_t> idxById;
    idxById.reserve(pageRef.items.size());
    for (size_t i = 0; i < pageRef.items.size(); ++i) idxById[pageRef.items[i].classId] = i;
    std::unordered_set<std::string> consumed;
    int zebra = 0;
    auto zebraBk = [&](int z) -> COLORREF {
        return (z % 2 == 0) ? RGB(255, 255, 255) : RGB(235, 235, 235);
    };
    for (size_t i = 0; i < pageRef.items.size(); ++i) {
        const auto& r = pageRef.items[i];
        if (consumed.count(r.classId)) continue;
        if (!r.buddyId.empty()) {
            auto it = idxById.find(r.buddyId);
            if (it != idxById.end()) {
                const auto& br = pageRef.items[it->second];
                if (!consumed.count(br.classId)) {
                    COLORREF bk = zebraBk(zebra);
                    std::vector<CString> pcols(colCount), ccols(colCount);
                    pcols[1] = std::to_string(r.id).c_str();
                    pcols[2] = std::to_string(r.cassetteSeqNo).c_str();
                    pcols[3] = std::to_string(r.jobSeqNo).c_str();
                    pcols[4] = r.classId.c_str();
                    pcols[5] = SERVO::CServoUtilsTool::getMaterialsTypeText((SERVO::MaterialsType)r.materialType).c_str();
                    pcols[6] = SERVO::CServoUtilsTool::getGlassStateText((SERVO::GlsState)r.state).c_str();
                    pcols[7] = r.tStart.c_str();
                    pcols[8] = r.tEnd.c_str();
                    pcols[9] = r.buddyId.c_str();
                    pcols[10] = SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)r.aoiResult).c_str();
                    pcols[11] = r.path.c_str();
                    pcols[12] = r.params.c_str();
                    auto* nParent = m_listCtrl.InsertRoot(pcols);
                    MaybeRestoreExpandByKey(nParent, expandedKeys);
                    m_listCtrl.SetNodeColor(nParent, RGB(0, 0, 0), bk);
                    ccols[1] = std::to_string(br.id).c_str();
                    ccols[2] = std::to_string(br.cassetteSeqNo).c_str();
                    ccols[3] = std::to_string(br.jobSeqNo).c_str();
                    ccols[4] = br.classId.c_str();
                    ccols[5] = SERVO::CServoUtilsTool::getMaterialsTypeText((SERVO::MaterialsType)br.materialType).c_str();
                    ccols[6] = SERVO::CServoUtilsTool::getGlassStateText((SERVO::GlsState)br.state).c_str();
                    ccols[7] = br.tStart.c_str();
                    ccols[8] = br.tEnd.c_str();
                    ccols[9] = br.buddyId.c_str();
                    ccols[10] = SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)br.aoiResult).c_str();
                    ccols[11] = br.path.c_str();
                    ccols[12] = br.params.c_str();
                    auto* nChild = m_listCtrl.InsertChild(nParent, ccols);
                    m_listCtrl.SetNodeColor(nChild, RGB(0, 0, 0), bk);
                    consumed.insert(r.classId);
                    consumed.insert(br.classId);
                    ++zebra;
                    continue;
                }
            }
            // 插占位子
            COLORREF bk = zebraBk(zebra);
            std::vector<CString> pcols(colCount), ccols(colCount);
            pcols[1] = std::to_string(r.id).c_str();
            pcols[2] = std::to_string(r.cassetteSeqNo).c_str();
            pcols[3] = std::to_string(r.jobSeqNo).c_str();
            pcols[4] = r.classId.c_str();
            pcols[5] = SERVO::CServoUtilsTool::getMaterialsTypeText((SERVO::MaterialsType)r.materialType).c_str();
            pcols[6] = SERVO::CServoUtilsTool::getGlassStateText((SERVO::GlsState)r.state).c_str();
            pcols[7] = r.tStart.c_str();
            pcols[8] = r.tEnd.c_str();
            pcols[9] = r.buddyId.c_str();
            pcols[10] = SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)r.aoiResult).c_str();
            pcols[11] = r.path.c_str();
            pcols[12] = r.params.c_str();
            auto* nParent = m_listCtrl.InsertRoot(pcols);
            MaybeRestoreExpandByKey(nParent, expandedKeys);
            m_listCtrl.SetNodeColor(nParent, RGB(0, 0, 0), bk);
            ccols[4] = r.buddyId.c_str();
            auto* nChild = m_listCtrl.InsertChild(nParent, ccols);
            m_listCtrl.SetNodeColor(nChild, RGB(0, 0, 0), bk);
            consumed.insert(r.classId);
            ++zebra;
        }
    }
    for (size_t i = 0; i < pageRef.items.size(); ++i) {
        const auto& r = pageRef.items[i];
        if (consumed.count(r.classId)) continue;
        COLORREF bk = zebraBk(zebra);
        std::vector<CString> cols(colCount);
        cols[1] = std::to_string(r.id).c_str();
        cols[2] = std::to_string(r.cassetteSeqNo).c_str();
        cols[3] = std::to_string(r.jobSeqNo).c_str();
        cols[4] = r.classId.c_str();
        cols[5] = SERVO::CServoUtilsTool::getMaterialsTypeText((SERVO::MaterialsType)r.materialType).c_str();
        cols[6] = SERVO::CServoUtilsTool::getGlassStateText((SERVO::GlsState)r.state).c_str();
        cols[7] = r.tStart.c_str();
        cols[8] = r.tEnd.c_str();
        cols[9] = r.buddyId.c_str();
        cols[10] = SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)r.aoiResult).c_str();
        cols[11] = r.path.c_str();
        cols[12] = r.params.c_str();
        auto* n = m_listCtrl.InsertRoot(cols);
        m_listCtrl.SetNodeColor(n, RGB(0, 0, 0), bk);
        consumed.insert(r.classId);
        ++zebra;
    }
    m_listCtrl.RebuildVisible();
#endif
    // 上一页 / 下一页
    UpdatePageControls();
    m_rebuilding = false;
}
void CPageGlassList::UpdatePageControls()
{
@@ -767,8 +1111,8 @@
    CString headers[] = {
        _T(""),
        _T("id"),
        _T("Cassette Sequence No"),
        _T("Job Sequence No"),
        _T("Cassette SN"),
        _T("Job SN"),
        _T("Class ID"),
        _T("物料类型"),
        _T("状态"),
@@ -789,6 +1133,7 @@
    }
    // 二次兜底,防止 ini 写进了 0
    if (m_listCtrl.GetColumnWidth(0) < 16) m_listCtrl.SetColumnWidth(0, 24);
    m_listCtrl.SetPopupFullTextColumns({ 11, 12 });
    Resize();
    OnBnClickedButtonSearch(); // 触发一次查询与首屏填充
@@ -846,7 +1191,7 @@
{
    if (nIDEvent == 1) {
        KillTimer(1);
        InitRxWindow();
        InitRxWindows();
    }
    else if (nIDEvent == 2) {
        UpdateWipData();  // 只做增量,不重建
@@ -945,6 +1290,270 @@
    }
}
void CPageGlassList::OnBnClickedButtonExportRow()
{
    int nSelected = m_listCtrl.GetSelectionMark();
    if (nSelected == -1) {
        AfxMessageBox(_T("请先选择一行记录!"));
        return;
    }
    // 直接从第一列获取 ID
    CString strId = m_listCtrl.GetItemText(nSelected, 1);
    if (strId.IsEmpty()) {
        AfxMessageBox(_T("WIP记录暂不支持保存"));
        return;
    }
    // 数据库记录
    long long recordId = _ttoi64(strId);
    // 从数据库查询完整记录
    auto& db = GlassLogDb::Instance();
    auto row = db.queryById(recordId);
    if (!row) {
        AfxMessageBox(_T("查询记录失败"));
        return;
    }
    // 使用 Glass ID 构建默认文件名
    CString strDefaultFileName;
    CString strGlassId = row->classId.c_str();
    // 移除文件名中的非法字符
    CString strSanitizedGlassId = strGlassId;
    strSanitizedGlassId.Remove('\\');
    strSanitizedGlassId.Remove('/');
    strSanitizedGlassId.Remove(':');
    strSanitizedGlassId.Remove('*');
    strSanitizedGlassId.Remove('?');
    strSanitizedGlassId.Remove('"');
    strSanitizedGlassId.Remove('<');
    strSanitizedGlassId.Remove('>');
    strSanitizedGlassId.Remove('|');
    strDefaultFileName.Format(_T("Glass_%s.json"), strSanitizedGlassId);
    // 文件保存对话框,设置默认文件名
    CFileDialog fileDialog(FALSE, _T("json"), strDefaultFileName,
        OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT,
        _T("JSON Files (*.json)|*.json|CSV Files (*.csv)|*.csv||"));
    if (fileDialog.DoModal() != IDOK) return;
    CString filePath = fileDialog.GetPathName();
    CString fileExt = fileDialog.GetFileExt();
    if (fileExt.CompareNoCase(_T("json")) == 0) {
        ExportToJson(*row, filePath);
    }
    else {
        ExportToCsv(*row, filePath);
    }
}
void CPageGlassList::ExportToJson(const GlassLogDb::Row& row, const CString& filePath)
{
    // 保存为 JSON
    if (!row.pretty.empty()) {
        CFile file;
        if (file.Open(filePath, CFile::modeCreate | CFile::modeWrite)) {
            file.Write(row.pretty.c_str(), row.pretty.length());
            file.Close();
            CString strSuccess;
            strSuccess.Format(_T("记录已保存为JSON文件:\n%s"), filePath);
            AfxMessageBox(strSuccess);
        }
        else {
            AfxMessageBox(_T("保存文件失败"));
        }
    }
    else {
        AfxMessageBox(_T("该记录没有JSON数据"));
    }
}
void CPageGlassList::ExportToCsv(const GlassLogDb::Row& row, const CString& filePath)
{
    CString csvContent;
    // === 第一部分:基础信息 ===
    ExportBasicInfo(csvContent, row);
    // === 第二部分:工艺参数 ===
    ExportProcessParams(csvContent, row);
    // === 第三部分:传感器数据详情 ===
    ExportSensorData(csvContent, row);
    // 使用辅助函数保存为 UTF-8 编码
    if (WriteAnsiStringAsUtf8ToFile(csvContent, filePath)) {
        CString strSuccess;
        strSuccess.Format(_T("记录已保存为CSV文件:\n%s"), filePath);
        AfxMessageBox(strSuccess);
    }
    else {
        AfxMessageBox(_T("保存文件失败"));
    }
}
void CPageGlassList::ExportBasicInfo(CString& csvContent, const GlassLogDb::Row& row)
{
    csvContent += _T("=== 基础信息 ===\n");
    csvContent += _T("ID,Cassette序列号,Job序列号,Glass ID,物料类型,状态,开始时间,结束时间,绑定Glass ID,AOI结果,路径\n");
    CString baseInfoRow;
    baseInfoRow.Format(_T("%lld,%d,%d,%s,%d,%d,%s,%s,%s,%d,%s\n"),
        row.id, row.cassetteSeqNo, row.jobSeqNo,
        CString(row.classId.c_str()), row.materialType, row.state,
        CString(row.tStart.c_str()), CString(row.tEnd.c_str()),
        CString(row.buddyId.c_str()), row.aoiResult,
        CString(row.path.c_str()));
    csvContent += baseInfoRow;
}
void CPageGlassList::ExportProcessParams(CString& csvContent, const GlassLogDb::Row& row)
{
    csvContent += _T("\n=== 工艺参数 ===\n");
    // 如果有 pretty 字段,解析工艺参数
    if (!row.pretty.empty()) {
        SERVO::CGlass tempGlass;
        if (GlassJson::FromString(row.pretty, tempGlass)) {
            auto& params = tempGlass.getParams();
            if (!params.empty()) {
                // 工艺参数表头
                csvContent += _T("参数名称,参数ID,数值,机器单元\n");
                // 工艺参数数据
                for (auto& param : params) {
                    CString paramRow;
                    CString valueStr;
                    // 根据参数类型格式化数值
                    if (param.getValueType() == PVT_INT) {
                        valueStr.Format(_T("%d"), param.getIntValue());
                    }
                    else {
                        valueStr.Format(_T("%.3f"), param.getDoubleValue());
                    }
                    paramRow.Format(_T("%s,%s,%s,%s\n"),
                        CString(param.getName().c_str()),
                        CString(param.getId().c_str()),
                        valueStr,
                        CString(param.getUnit().c_str()));
                    csvContent += paramRow;
                }
            }
            else {
                csvContent += _T("无工艺参数数据\n");
            }
        }
        else {
            csvContent += _T("无法解析工艺参数\n");
        }
    }
    else {
        csvContent += _T("无工艺参数数据\n");
    }
}
void CPageGlassList::ExportSensorData(CString& csvContent, const GlassLogDb::Row& row)
{
    csvContent += _T("\n=== 传感器数据详情 ===\n");
    // 如果有 pretty 字段,解析传感器数据
    if (!row.pretty.empty()) {
        SERVO::CGlass tempGlass;
        if (GlassJson::FromString(row.pretty, tempGlass)) {
            // 对每个机器生成表格
            for (const auto& machinePair : tempGlass.getAllSVData()) {
                int machineId = machinePair.first;
                CString machineName = CString(SERVO::CServoUtilsTool::getEqName(machineId).c_str());
                csvContent += _T("\n[") + machineName + _T("]\n");
                // 获取该机器的预定义列顺序
                auto columnOrder = getMachineColumnOrder(machineId);
                if (columnOrder.empty()) {
                    csvContent += _T("无预定义列配置\n");
                    continue;
                }
                // 构建表头 - 直接使用中文列名
                CString header = _T("时间戳(ms),本地时间");
                for (const auto& dataType : columnOrder) {
                    header += _T(",");
                    header += CString(dataType.c_str()); // 直接使用中文列名
                }
                header += _T("\n");
                csvContent += header;
                // 检查是否有数据
                if (machinePair.second.empty()) {
                    csvContent += _T("无传感器数据\n");
                    continue;
                }
                // 使用第一个数据类型的时间序列作为基准
                const std::string& firstDataType = columnOrder[0];
                auto firstDataTypeIt = machinePair.second.find(firstDataType);
                if (firstDataTypeIt == machinePair.second.end() || firstDataTypeIt->second.empty()) {
                    csvContent += _T("无基准数据类型数据\n");
                    continue;
                }
                const auto& timeSeries = firstDataTypeIt->second;
                // 对于每个时间点,输出一行数据
                for (size_t i = 0; i < timeSeries.size(); i++) {
                    auto timestamp = timeSeries[i].timestamp;
                    // 时间戳(毫秒)
                    auto ms = timePointToMs(timestamp);
                    CString row;
                    row.Format(_T("%lld,"), ms);
                    // 本地时间字符串
                    CString localTime = CString(timePointToString(timestamp).c_str());
                    row += localTime;
                    // 按照预定义的列顺序输出数据
                    for (const auto& dataType : columnOrder) {
                        row += _T(",");
                        auto dataTypeIt = machinePair.second.find(dataType);
                        if (dataTypeIt != machinePair.second.end() && i < dataTypeIt->second.size()) {
                            // 直接按索引获取数据
                            CString valueStr;
                            valueStr.Format(_T("%.3f"), dataTypeIt->second[i].value);
                            row += valueStr;
                        }
                        else {
                            // 理论上不应该发生,因为您说没有空值
                            row += _T("N/A");
                        }
                    }
                    row += _T("\n");
                    csvContent += row;
                }
            }
        }
        else {
            csvContent += _T("无法解析传感器数据\n");
        }
    }
    else {
        csvContent += _T("无传感器数据\n");
    }
}
void CPageGlassList::OnBnClickedButtonPrevPage()
{
    if (m_nCurPage > 1) {
@@ -961,19 +1570,33 @@
    }
}
void CPageGlassList::OnShowFullText(NMHDR* pNMHDR, LRESULT* pResult)
{
    auto* p = reinterpret_cast<NMC_ELC_SHOWFULLTEXT*>(pNMHDR);
    // 对话框显示工艺参数
    CProcessDataListDlg dlg;
    dlg.setRawText(p->text);
    dlg.DoModal();
    *pResult = 0;
}
void CPageGlassList::UpdateWipData()
{
    // 只在第 1 页刷新 WIP;其它页不动
    if (m_nCurPage != 1) return;
    // 若刚好在 UpdatePageData() 重建期间,跳过这轮增量,避免互相干扰
    if (m_rebuilding) return;
    const int colCount = m_listCtrl.GetHeaderCtrl() ? m_listCtrl.GetHeaderCtrl()->GetItemCount() : 0;
    if (colCount <= 0) return;
    // 1) 收集当前可见里的“WIP 行”(第1列 id 为空)
    //    a) wipRowById:classId -> (row, node*),收集“根+子”的全部,便于判断“buddy 是否已在可见表中”
    //    b) wipRootById:classId -> node*,仅收集“根节点”,便于只对根节点补子项
    //    a) wipRowById:classId -> (row, node*),收集根+子,便于判断“buddy 是否已在可见表中”
    //    b) wipRootById:classId -> node*,仅收集根节点,便于只对“根”补子项
    std::unordered_map<std::string, std::pair<int, CExpandableListCtrl::Node*>> wipRowById;
    std::unordered_map<std::string, CExpandableListCtrl::Node*> wipRootById;
    std::unordered_map<std::string, CExpandableListCtrl::Node*>                 wipRootById;
    for (int row = 0; row < m_listCtrl.GetItemCount(); ++row) {
        CString idDb = m_listCtrl.GetItemText(row, 1); // 第1列是 DB id
        if (!idDb.IsEmpty()) continue;                 // 有 id 的是 DB 行,跳过
@@ -995,26 +1618,7 @@
    std::vector<SERVO::CGlass*> wipGlasses;
    theApp.m_model.m_master.getWipGlasses(wipGlasses);
    std::vector<SERVO::CGlass*> tempRetain = wipGlasses; // 稍后统一 release
    /*
    static int i = 0;
    i++;
    if (i == 8) {
        for (auto item : wipGlasses) {
            if (item->getBuddy() != nullptr) {
                item->setInspResult(EQ_ID_MEASUREMENT, 0, SERVO::InspResult::Fail);
                item->getBuddy()->setID("11111");
            }
        }
    }
    if (i == 16) {
        for (auto item : wipGlasses) {
            if (item->getBuddy() != nullptr) {
                item->setInspResult(EQ_ID_MEASUREMENT, 0, SERVO::InspResult::Pass);
                item->getBuddy()->setID("22222");
            }
        }
    }
    */
    auto makeColsFromWip = [&](SERVO::CGlass* g) {
        std::vector<CString> cols(colCount);
        cols[1] = _T(""); // WIP 没 DB id
@@ -1032,21 +1636,47 @@
        return cols;
    };
    bool needRebuildChildren = false;     // 本次是否新增了子节点(结构变化)
    bool needRebuildAllForNewRoot = false;// 本次是否发现了“新增根节点”的需求(为保证 WIP 在顶部)
    std::vector<int> rowsToRedraw;        // 仅文本变化的行
    // 2.1 构建“最新 WIP 键集”(含 buddy,且命中过滤),用于检测“缺失/删除”
    std::unordered_set<std::string> newWipKeys;
    for (auto* g : wipGlasses) {
        if (!GlassMatchesFilters(*g, m_filters)) continue;
#ifdef _UNICODE
        newWipKeys.insert(CT2A(CString(g->getID().c_str())));
#else
        newWipKeys.insert(g->getID());
#endif
        if (auto* b = g->getBuddy()) {
            if (GlassMatchesFilters(*b, m_filters)) {
#ifdef _UNICODE
                newWipKeys.insert(CT2A(CString(b->getID().c_str())));
#else
                newWipKeys.insert(b->getID());
#endif
            }
        }
    }
    bool needRebuildRemoval = false; // WIP 变少/清空:需要整页重建
    bool needRebuildChildren = false; // 结构变化:新增子
    bool needRebuildAllForNewRoot = false; // 新增根(保证 WIP 仍在顶部)
    std::vector<int> rowsToRedraw;         // 仅文本变化
    // 可见集中有但新数据里没有 -> 触发“删除/减少”的整页重建
    for (const auto& kv : wipRowById) {
        if (newWipKeys.find(kv.first) == newWipKeys.end()) { needRebuildRemoval = true; break; }
    }
    // UI 状态(当需要重建时使用)
    std::vector<CExpandableListCtrl::Node*> savedSel;
    CExpandableListCtrl::Node* savedTop = nullptr;
    // 3) 逐个处理 WIP:已存在 -> 就地更新;必要时“只对根补子项”
    //                 不存在 -> 触发“全量重建”,以保证新 WIP 根行出现在列表顶部
    //                 不存在 -> 挂到 buddy 容器;若 buddy 不在可见表,触发全量重建(保证 WIP 顶部)
    for (auto* g : wipGlasses) {
        if (!GlassMatchesFilters(*g, m_filters)) continue;
#ifdef _UNICODE
        std::string cid = CT2A(g->getID().c_str());
        std::string cid = CT2A(CString(g->getID().c_str()));
#else
        std::string cid = g->getID();
#endif
@@ -1070,27 +1700,23 @@
            // —— 顺带刷新 buddy 子行(如果它已在可见表里)——
            if (SERVO::CGlass* b = g->getBuddy()) {
                CString buddyCidCs = b->getID().c_str();
#ifdef _UNICODE
                std::string bid = CT2A(buddyCidCs);
                std::string bid = CT2A(CString(b->getID().c_str()));
#else
                std::string bid = buddyCidCs.GetString();
                std::string bid = b->getID();
#endif
                auto itChildAny = wipRowById.find(bid);
                if (itChildAny != wipRowById.end()) {
                    int crow = itChildAny->second.first;
                    // 生成 buddy 的列文本并一次性写回
                    auto bcols = makeColsFromWip(b);
                    ApplyColsToRow(m_listCtrl, crow, bcols);
                    rowsToRedraw.push_back(crow);
                }
                // 如果 buddy 行当前不存在,可保留你原来的“只在根下、且 buddy 不在可见表时补子项”的逻辑
            }
            // —— 只对“根节点”补子项,且仅当 buddy 尚未出现在可见表,且根下也没有该 buddy ——
            // —— 只对“根节点”补子项 ——
            SERVO::CGlass* b = g->getBuddy();
            if (b) {
                // 当前根容器?(子节点不作为容器)
                auto itRoot = wipRootById.find(cid);
                if (itRoot != wipRootById.end()) {
                    CExpandableListCtrl::Node* container = itRoot->second;
@@ -1110,55 +1736,52 @@
                    bool buddyExistsAnywhere = (wipRowById.find(newBid) != wipRowById.end());
                    bool hasChildAlready = NodeHasChildWithClassId(container, newBuddyCid);
                    // —— 关键:关系是否发生变化?(oldChildCid 与 newBuddyCid 不同)
                    // 关系是否发生变化?
                    bool relationChanged =
                        (!oldChildCid.IsEmpty() && newBuddyCid.IsEmpty()) ||                             // 之前有子,现在没 buddy
                        (oldChildCid.IsEmpty() && !newBuddyCid.IsEmpty()) ||                             // 之前没子,现在有 buddy
                        (!oldChildCid.IsEmpty() && !newBuddyCid.IsEmpty() && oldChildCid.CompareNoCase(newBuddyCid) != 0); // 改 buddy
                        (!oldChildCid.IsEmpty() && newBuddyCid.IsEmpty()) ||
                        (oldChildCid.IsEmpty() && !newBuddyCid.IsEmpty()) ||
                        (!oldChildCid.IsEmpty() && !newBuddyCid.IsEmpty() &&
                            oldChildCid.CompareNoCase(newBuddyCid) != 0);
                    if (relationChanged) {
                        // 关系变更走“结构重建”,避免重复或反向挂载
                        needRebuildAllForNewRoot = true;
                        needRebuildAllForNewRoot = true; // 避免重复或反向挂载
                    }
                    else {
                        // 关系未变:若 buddy 还不在可见表且容器下也没有,则补子
                        // 关系未变:若 buddy 不在可见表且容器下也没有,则补子
                        if (!buddyExistsAnywhere && !hasChildAlready) {
                            if (!needRebuildChildren) { CaptureUiState(m_listCtrl, savedSel, savedTop); }
                            needRebuildChildren = true;
                            auto cols = makeColsFromWip(b);
                            auto* ch = m_listCtrl.InsertChild(container, cols);
                            m_listCtrl.SetNodeColor(ch, kWipText, kWipChildBk);         // 子:更浅
                            m_listCtrl.SetNodeColor(container, kWipText, kWipParentBk); // 父:基础绿(兜底纠正)
                            m_listCtrl.SetNodeColor(ch, kWipText, kWipChildBk);  // 子:浅色
                            m_listCtrl.SetNodeColor(container, kWipText, kWipParentBk); // 父:基础绿
                        }
                        // 若已有子:顺带把子行文本刷新一下(比如 AOI 更新)
                        // 若已有子:同步刷新子行文本与颜色
                        else if (hasChildAlready) {
                            // 找到对应子并更新文本/cols,避免后续 Rebuild 倒回旧值
                            for (auto& ch : container->children) {
                                if (ch && ch->cols.size() > 4 && ch->cols[4].CompareNoCase(newBuddyCid) == 0) {
                                    auto cols = makeColsFromWip(b);
                                    ch->cols = cols; // 底层数据
                                    ch->cols = cols; // 更新底层数据
                                    // 可见行刷新
                                    for (int r = 0; r < m_listCtrl.GetItemCount(); ++r) {
                                        if (m_listCtrl.GetNodeByVisibleIndex(r) == ch.get()) {
                                            for (int c = 1; c < (int)cols.size(); ++c) {
                                            for (int c = 1; c < (int)cols.size(); ++c)
                                                m_listCtrl.SetItemText(r, c, cols[c]);
                                                m_listCtrl.SetNodeColor(ch.get(), kWipText, kWipChildBk);   // 保证子行是浅色
                                                m_listCtrl.SetNodeColor(container, kWipText, kWipParentBk); // 父保持基础绿
                                            }
                                            rowsToRedraw.push_back(r);
                                            break;
                                        }
                                    }
                                    m_listCtrl.SetNodeColor(ch.get(), kWipText, kWipChildBk);
                                    m_listCtrl.SetNodeColor(container, kWipText, kWipParentBk);
                                    break;
                                }
                            }
                        }
                    }
                }
                // 当前是“子节点”的情况:一律不挂子,交给重建(若父变更)
            }
            else {
                // 没有 buddy:如果容器下现在有子,也算关系变化,触发重建
                // 没 buddy 但容器下有子 -> 关系变化,触发全量重建
                auto itRoot = wipRootById.find(cid);
                if (itRoot != wipRootById.end()) {
                    CExpandableListCtrl::Node* container = itRoot->second;
@@ -1168,17 +1791,48 @@
            }
        }
        else {
            // (B) 不存在:新增根行——为保证“WIP 永远在顶部”,触发全量重建
            needRebuildAllForNewRoot = true;
            // (B) 不存在:新增
            SERVO::CGlass* b = g->getBuddy();
            CExpandableListCtrl::Node* container = nullptr;
            if (b) {
#ifdef _UNICODE
                std::string bid = CT2A(CString(b->getID().c_str()));
#else
                std::string bid = b->getID();
#endif
                auto itB = wipRowById.find(bid);
                if (itB != wipRowById.end()) {
                    CExpandableListCtrl::Node* buddyNode = itB->second.second;
                    container = buddyNode ? (buddyNode->parent ? buddyNode->parent : buddyNode) : nullptr;
                }
            }
            if (container) {
                CString cidCs = g->getID().c_str();
                if (!NodeHasChildWithClassId(container, cidCs)) {
                    if (!needRebuildChildren) { CaptureUiState(m_listCtrl, savedSel, savedTop); }
                    needRebuildChildren = true;
                    auto cols = makeColsFromWip(g);
                    auto* ch = m_listCtrl.InsertChild(container, cols);
                    // 子:更浅;父:基础绿(兜底)
                    m_listCtrl.SetNodeColor(ch, kWipText, kWipChildBk);
                    m_listCtrl.SetNodeColor(container, kWipText, kWipParentBk);
                }
            }
            else {
                // buddy 不在可见表:为了保持“WIP 永远在顶部”,触发一次全量重建
                needRebuildAllForNewRoot = true;
            }
        }
    }
    // 4) 应用 UI 更新
    if (needRebuildAllForNewRoot) {
        // 用 key(ClassID)保存并恢复,避免 Node* 失效
    // 4) 应用 UI 更新 —— 把“删除/减少”的情况并入全量重建分支
    if (needRebuildAllForNewRoot || needRebuildRemoval) {
        auto selKeys = SnapshotSelectedKeys(m_listCtrl);
        auto topKey = SnapshotTopKey(m_listCtrl);
        UpdatePageData();                      // 全量重建(WIP 顶部)
        UpdatePageData();                      // 全量重建(WIP 顶部 & 删除无效项)
        RestoreSelectionByKeys(m_listCtrl, selKeys);
        RestoreTopByKey(m_listCtrl, topKey);
    }
@@ -1247,3 +1901,37 @@
    return true;
}
BOOL CPageGlassList::PreTranslateMessage(MSG* pMsg)
{
    if (pMsg->wParam == VK_RETURN || pMsg->wParam == VK_ESCAPE) {
        return TRUE;
    }
    return CDialogEx::PreTranslateMessage(pMsg);
}
// 获取机器预定义的列顺序
std::vector<std::string> CPageGlassList::getMachineColumnOrder(int machineId)
{
    auto dataTypes = SERVO::CServoUtilsTool::getEqDataTypes();
    auto it = dataTypes.find(machineId);
    return it != dataTypes.end() ? it->second : std::vector<std::string>();
}
// 时间戳转换为字符串
std::string CPageGlassList::timePointToString(const std::chrono::system_clock::time_point& tp)
{
    auto time_t = std::chrono::system_clock::to_time_t(tp);
    std::tm tm;
    localtime_s(&tm, &time_t);
    char buffer[20];
    std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &tm);
    return buffer;
}
// 时间戳转换为毫秒
int64_t CPageGlassList::timePointToMs(const std::chrono::system_clock::time_point& tp)
{
    return std::chrono::duration_cast<std::chrono::milliseconds>(tp.time_since_epoch()).count();
}