LAPTOP-SNT8I5JK\Boounion
2025-09-11 eaf39ceeef11c5685ddbb840541460e5dc5fccaa
SourceCode/Bond/Servo/CPageGlassList.cpp
@@ -8,11 +8,333 @@
#include "GlassJson.h"
#include "CServoUtilsTool.h"
#include "ToolUnits.h"
#include <optional>
#include <optional>
#include <unordered_set>
#include <unordered_map>
#include <vector>
#include <string>
#define PAGE_SIZE                  10
#define PAGE_BACKGROUND_COLOR         RGB(252, 252, 255)
// ===== 放在 CPageGlassList.cpp 顶部的匿名工具(文件内静态) =====
// 把当前“已展开”的父行,用它们的 classId(第4列文本)做 key 记录下来
static std::unordered_set<std::string> SnapshotExpandedKeys(CExpandableListCtrl& lv) {
    std::unordered_set<std::string> keys;
    for (int i = 0; i < lv.GetItemCount(); ++i) {
        auto* n = lv.GetNodeByVisibleIndex(i);
        if (!n || n->children.empty() || !n->expanded) continue;
        if ((int)n->cols.size() > 4) {
#ifdef _UNICODE
            keys.insert(CT2A(n->cols[4]));
#else
            keys.insert(n->cols[4].GetString());
#endif
        }
    }
    return keys;
}
// 根据快照恢复展开状态(在你新建完每个“父节点”时调用一次)
static void MaybeRestoreExpandByKey(CExpandableListCtrl::Node* n,
    const std::unordered_set<std::string>& keys) {
    if (!n || (int)n->cols.size() <= 4) return;
#ifdef _UNICODE
    std::string k = CT2A(n->cols[4]);
#else
    std::string k = n->cols[4].GetString();
#endif
    if (keys.find(k) != keys.end())
        n->expanded = true;
}
static void CaptureUiState(CExpandableListCtrl& lv,
    std::vector<CExpandableListCtrl::Node*>& outSel,
    CExpandableListCtrl::Node*& outTopNode)
{
    outSel.clear(); outTopNode = nullptr;
    const int top = lv.GetTopIndex();
    if (top >= 0 && top < lv.GetItemCount())
        outTopNode = lv.GetNodeByVisibleIndex(top);
    for (int i = 0; i < lv.GetItemCount(); ++i) {
        if ((lv.GetItemState(i, LVIS_SELECTED) & LVIS_SELECTED) != 0) {
            auto* n = lv.GetNodeByVisibleIndex(i);
            if (n) outSel.push_back(n);
        }
    }
}
static void RestoreUiState(CExpandableListCtrl& lv,
    const std::vector<CExpandableListCtrl::Node*>& sel,
    CExpandableListCtrl::Node* topNode)
{
    // 清掉现有选择
    for (int i = 0; i < lv.GetItemCount(); ++i)
        lv.SetItemState(i, 0, LVIS_SELECTED);
    // 恢复选择
    for (auto* n : sel) {
        for (int i = 0; i < lv.GetItemCount(); ++i) {
            if (lv.GetNodeByVisibleIndex(i) == n) {
                lv.SetItemState(i, LVIS_SELECTED, LVIS_SELECTED);
                break;
            }
        }
    }
    // 尽量把之前的顶行滚回可见
    if (topNode) {
        for (int i = 0; i < lv.GetItemCount(); ++i) {
            if (lv.GetNodeByVisibleIndex(i) == topNode) {
                lv.EnsureVisible(i, FALSE);
                break;
            }
        }
    }
}
// ====== 开关:1=启用假数据(只替换 DB 查询);0=用真实 DB ======
#define USE_FAKE_DB_DEMO 1
#if USE_FAKE_DB_DEMO
#include <ctime>
#include <atlconv.h>   // CStringA
#include <initializer_list>
#include <string>
#include <vector>
// ---- 模拟记录/分页结构(字段与你现有代码一致)----
struct FakeDbRecord {
    int         id;
    int         cassetteSeqNo;
    int         jobSeqNo;
    std::string classId;
    int         materialType;
    int         state;
    std::string tStart;
    std::string tEnd;
    std::string buddyId;
    int         aoiResult;
    std::string path;
    std::string params;
};
struct FakeDbPage { std::vector<FakeDbRecord> items; };
// ---- CString -> std::string(ANSI,本地代码页;仅用于测试模拟)----
static std::string toAnsi(const CString& s) {
#ifdef _UNICODE
    CStringA a(s);
    return std::string(a.GetString());
#else
    return std::string(s.GetString());
#endif
}
// ---- 安全拼接工具:不使用运算符 +(避免重载/转换问题)----
static std::string sjoin(std::initializer_list<std::string> parts) {
    std::string out;
    size_t total = 0; for (const auto& p : parts) total += p.size();
    out.reserve(total);
    for (const auto& p : parts) out.append(p);
    return out;
}
// ---- 时间格式工具:now + days/minutes 偏移 ----
static std::string _fmt_time(int daysOff, int minutesOff) {
    using namespace std::chrono;
    auto now = system_clock::now() + hours(24 * daysOff) + minutes(minutesOff);
    std::time_t tt = system_clock::to_time_t(now);
    std::tm tm{};
#ifdef _WIN32
    localtime_s(&tm, &tt);
#else
    tm = *std::localtime(&tt);
#endif
    char buf[32];
    std::snprintf(buf, sizeof(buf), "%04d-%02d-%02d %02d:%02d:%02d",
        tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec);
    return std::string(buf);
}
// ---- 造全量假数据(含同页配对/跨页配对/单条)----
static void _make_all_fake(std::vector<FakeDbRecord>& outAll) {
    outAll.clear();
    int id = 1000;
    // ===== 先造几条“没有 Buddy”的记录(保证出现在第 1 页开头)=====
    for (int n = 1; n <= 4; ++n) {
        CString cid; cid.Format(_T("NB%03d"), n);
        outAll.push_back(FakeDbRecord{
            id++, 700 + n, 800 + n, toAnsi(cid),
            n % 3, n % 5,
            _fmt_time(0, -n * 5),           // 开始时间稍早一点
            _fmt_time(0, -n * 5 + 1),
            std::string(),                // buddyId 为空
            n % 4,
            sjoin({ "path/", toAnsi(cid) }),
            sjoin({ "{\"noBuddy\":", std::to_string(n), "}" })
            });
    }
    // ===== 造配对数据的工具 =====
    auto mkPair = [&](int k, bool crossPage) {
        // 互为 buddy 的两条
        CString a; a.Format(_T("G%04dA"), k);
        CString b; b.Format(_T("G%04dB"), k);
        FakeDbRecord A{
            id++, 100 + k, 200 + k, toAnsi(a),
            k % 3, k % 5, _fmt_time(0, k * 3), _fmt_time(0, k * 3 + 2),
            toAnsi(b), k % 4,
            sjoin({ "path/", toAnsi(a) }),
            sjoin({ "{\"k\":\"", toAnsi(a), "\"}" })
        };
        FakeDbRecord B{
            id++, 110 + k, 210 + k, toAnsi(b),
            (k + 1) % 3, (k + 2) % 5, _fmt_time(0, k * 3 + 1), _fmt_time(0, k * 3 + 4),
            toAnsi(a), (k + 1) % 4,
            sjoin({ "path/", toAnsi(b) }),
            sjoin({ "{\"k\":\"", toAnsi(b), "\"}" })
        };
        if (crossPage) {
            // 先放 A,再插 3 条“单条”把 B 挤到后页,最后放 B
            outAll.push_back(A);
            for (int s = 0; s < 3; ++s) {
                CString sid; sid.Format(_T("S%04d_%d"), k, s);
                outAll.push_back(FakeDbRecord{
                    id++, 300 + k * 10 + s, 400 + k * 10 + s, toAnsi(sid),
                    (k + s) % 3, (k + s) % 5, _fmt_time(0, k * 2 + s), _fmt_time(0, k * 2 + s + 1),
                    std::string(), (k + s) % 4,
                    sjoin({ "path/", toAnsi(sid) }),
                    sjoin({ "{\"single\":", std::to_string(s), "}" })
                    });
            }
            outAll.push_back(B);
        }
        else {
            // 同页紧挨着
            outAll.push_back(A);
            outAll.push_back(B);
        }
    };
    // ===== 然后按原逻辑追加:同页配对 / 跨页配对 / 单条 =====
    // 6 组同页配对(12 条)
    for (int k = 1; k <= 6; ++k) mkPair(k, false);
    // 4 组跨页配对(每组中间插 3 条“单条”)
    for (int k = 101; k <= 104; ++k) mkPair(k, true);
    // 若干“单条”
    for (int u = 201; u < 206; ++u) {
        CString cid; cid.Format(_T("U%04d"), u);
        outAll.push_back(FakeDbRecord{
            id++, 500 + u, 600 + u, toAnsi(cid),
            u % 3, u % 5, _fmt_time(0, u % 17), _fmt_time(0, (u % 17) + 1),
            std::string(), u % 4,
            sjoin({ "path/", toAnsi(cid) }),
            sjoin({ "{\"u\":", std::to_string(u), "}" })
            });
    }
}
// ---- 做分页切片(可按需加更严格的 filters)----
static FakeDbPage _make_page_fake(const GlassLogDb::Filters& /*f*/, int pageSize, int offset) {
    std::vector<FakeDbRecord> all;
    _make_all_fake(all);
    FakeDbPage page;
    int n = (int)all.size();
    int beg = min(max(0, offset), n);
    int end = min(beg + max(0, pageSize), n);
    page.items.insert(page.items.end(), all.begin() + beg, all.begin() + end);
    return page;
}
static int _fake_total_count() {
    std::vector<FakeDbRecord> all;
    _make_all_fake(all);
    return (int)all.size();
}
#endif // USE_FAKE_DB_DEMO
// 判断某 parent 下是否已存在 classId == cid 的子节点(忽略大小写)
static bool NodeHasChildWithClassId(CExpandableListCtrl::Node* parent, const CString& cid)
{
    if (!parent) return false;
    for (auto& ch : parent->children) {
        if (ch && ch->cols.size() > 4) {
            if (ch->cols[4].CompareNoCase(cid) == 0)
                return true;
        }
    }
    return false;
}
// 把 cols 写回到某一行(从第1列开始;第0列我们没用)
static void ApplyColsToRow(CListCtrl& lv, int row, const std::vector<CString>& cols) {
    int colCount = 0;
    if (auto* hdr = lv.GetHeaderCtrl()) colCount = hdr->GetItemCount();
    colCount = min(colCount, (int)cols.size());
    for (int c = 1; c < colCount; ++c) {
        lv.SetItemText(row, c, cols[c]);
    }
}
// 选中行的 ClassID 快照(用于重建后恢复)
static std::unordered_set<std::string> SnapshotSelectedKeys(CListCtrl& lv) {
    std::unordered_set<std::string> keys;
    int n = lv.GetItemCount();
    for (int i = 0; i < n; ++i) {
        if ((lv.GetItemState(i, LVIS_SELECTED) & LVIS_SELECTED) == 0) continue;
        CString cls = lv.GetItemText(i, 4);
#ifdef _UNICODE
        keys.insert(CT2A(cls));
#else
        keys.insert(cls.GetString());
#endif
    }
    return keys;
}
// 顶行(TopIndex)对应的 ClassID
static std::optional<std::string> SnapshotTopKey(CListCtrl& lv) {
    int top = lv.GetTopIndex();
    if (top < 0 || top >= lv.GetItemCount()) return std::nullopt;
    CString cls = lv.GetItemText(top, 4);
#ifdef _UNICODE
    return std::optional<std::string>(CT2A(cls));
#else
    return std::optional<std::string>(cls.GetString());
#endif
}
// 用 ClassID 集合恢复选中
static void RestoreSelectionByKeys(CListCtrl& lv, const std::unordered_set<std::string>& keys) {
    int n = lv.GetItemCount();
    for (int i = 0; i < n; ++i) lv.SetItemState(i, 0, LVIS_SELECTED);
    for (int i = 0; i < n; ++i) {
        CString cls = lv.GetItemText(i, 4);
#ifdef _UNICODE
        if (keys.count(CT2A(cls))) lv.SetItemState(i, LVIS_SELECTED, LVIS_SELECTED);
#else
        if (keys.count(cls.GetString())) lv.SetItemState(i, LVIS_SELECTED, LVIS_SELECTED);
#endif
    }
}
// 尽量把某个 ClassID 滚回可见
static void RestoreTopByKey(CListCtrl& lv, const std::optional<std::string>& key) {
    if (!key) return;
    int n = lv.GetItemCount();
    for (int i = 0; i < n; ++i) {
        CString cls = lv.GetItemText(i, 4);
#ifdef _UNICODE
        if (CT2A(cls) == *key) { lv.EnsureVisible(i, FALSE); break; }
#else
        if (cls.GetString() == *key) { lv.EnsureVisible(i, FALSE); break; }
#endif
    }
}
// CPageGlassList 对话框
@@ -26,7 +348,7 @@
   m_pObserver = nullptr;
   m_strStatus = "";
   m_nCurPage = 0;
    m_nCurPage = 1;
   m_nTotalPages = 1;
   memset(m_szTimeStart, 0, sizeof(m_szTimeStart));
@@ -39,6 +361,7 @@
{
   if (m_hbrBkgnd != nullptr) {
      ::DeleteObject(m_hbrBkgnd);
        m_hbrBkgnd = nullptr;
   }
   if (m_pObserver != nullptr) {
      m_pObserver->unsubscribe();
@@ -54,7 +377,6 @@
   DDX_Control(pDX, IDC_LIST_ALARM, m_listCtrl);
}
BEGIN_MESSAGE_MAP(CPageGlassList, CDialogEx)
   ON_WM_CTLCOLOR()
   ON_WM_DESTROY()
@@ -68,11 +390,16 @@
   ON_BN_CLICKED(IDC_BUTTON_NEXT_PAGE, &CPageGlassList::OnBnClickedButtonNextPage)
END_MESSAGE_MAP()
// ===== 私有小工具 =====
static int GetColumnCount(CListCtrl& lv)
{
    CHeaderCtrl* pHdr = lv.GetHeaderCtrl();
    return pHdr ? pHdr->GetItemCount() : 0;
}
// CPageGlassList 消息处理程序
// ===== CPageGlassList 消息处理程序 =====
void CPageGlassList::InitRxWindow()
{
   /* code */
   // 订阅数据
   IRxWindows* pRxWindows = RX_GetRxWindows();
   pRxWindows->enableLog(5);
@@ -83,14 +410,14 @@
         int code = pAny->getCode();
         if (RX_CODE_EQ_ROBOT_TASK == code) {
            UpdatePageData();
                UpdateWipData();   // 只更新,不重建,不改变展开/选择
         }
         pAny->release();
         }, [&]() -> void {
            // onComplete
         }, [&](IThrowable* pThrowable) -> void {
            // onErrorm
                // onError
            pThrowable->printf();
         });
@@ -122,16 +449,6 @@
   CWnd* pLabelPage = GetDlgItem(IDC_LABEL_PAGE_NUMBER);
   if (pBtnNext && pBtnPrev && pLabelPage) {
      // 获取分页文本宽度估算
      //CString strLabel;
      //GetDlgItemText(IDC_LABEL_PAGE_NUMBER, strLabel);
      //if (strLabel.IsEmpty()) {
      //   strLabel = _T("第 1 / 1 页");
      //}
      //int nCharWidth = 8;
      //int nLabelWidth = strLabel.GetLength() * nCharWidth + 20;
      // 设置按钮和标签位置
      pBtnNext->MoveWindow(xRight - nButtonWidth, yBottom, nButtonWidth, nButtonHeight);
      xRight -= nButtonWidth + nSpacing;
@@ -183,21 +500,9 @@
   if (m_dateTimeStart.m_hWnd == nullptr || m_dateTimeEnd.m_hWnd == nullptr) {
      return;
   }
   // 禁用初始状态
    // 自定义范围时才可编辑
   m_dateTimeStart.EnableWindow(FALSE);
   m_dateTimeEnd.EnableWindow(FALSE);
   // 设置格式:显示日期 + 时间
   //m_dateTimeStart.SetFormat(_T("yyyy/MM/dd HH:mm:ss"));
   //m_dateTimeEnd.SetFormat(_T("yyyy/MM/dd HH:mm:ss"));
   // 修改样式以支持时间格式
   //DWORD dwStyleStart = m_dateTimeStart.GetStyle();
   //DWORD dwStyleEnd = m_dateTimeEnd.GetStyle();
   //m_dateTimeStart.ModifyStyle(0, DTS_TIMEFORMAT | DTS_UPDOWN);
   //m_dateTimeEnd.ModifyStyle(0, DTS_TIMEFORMAT);
}
void CPageGlassList::LoadData()
@@ -208,78 +513,214 @@
void CPageGlassList::UpdatePageData()
{
   // 如果为第1页, 取出缓存Glass, 符合条件则显示;
    m_rebuilding = true;
    // 放在任何清空/重建动作之前:
    auto expandedKeys = SnapshotExpandedKeys(m_listCtrl);
    // —— 双保险:先清掉可见项,再清树结构 ——
    m_listCtrl.SetRedraw(FALSE);
   m_listCtrl.DeleteAllItems();
   UpdateWipData();
    m_listCtrl.SetRedraw(TRUE);
    // —— 清空树(依赖 CExpandableListCtrl::ClearTree())——
    m_listCtrl.ClearTree();
   // 查询
    const int colCount = m_listCtrl.GetHeaderCtrl() ? m_listCtrl.GetHeaderCtrl()->GetItemCount() : 0;
    if (colCount <= 0) return;
    // ==================== 1) WIP:仅第 1 页构建,且放在最顶部 ====================
    if (m_nCurPage == 1) {
        std::vector<SERVO::CGlass*> wipGlasses;
        theApp.m_model.m_master.getWipGlasses(wipGlasses);
        std::vector<SERVO::CGlass*> tempGlasses = wipGlasses; // 待释放
        auto glassHit = [&](SERVO::CGlass* g) -> bool {
            return g && GlassMatchesFilters(*g, m_filters);
        };
        std::unordered_set<SERVO::CGlass*> usedWip;
        for (auto* g : wipGlasses) {
            if (!glassHit(g) || usedWip.count(g)) continue;
            SERVO::CGlass* b = g->getBuddy();
            if (b) {
                SERVO::CGlass* parent = g;
                SERVO::CGlass* child = b;
                std::vector<CString> pcols(colCount);
                pcols[1] = _T("");
                pcols[2] = std::to_string(parent->getCassetteSequenceNo()).c_str();
                pcols[3] = std::to_string(parent->getJobSequenceNo()).c_str();
                pcols[4] = parent->getID().c_str();
                pcols[5] = SERVO::CServoUtilsTool::getMaterialsTypeText(parent->getType()).c_str();
                pcols[6] = SERVO::CServoUtilsTool::getGlassStateText(parent->state()).c_str();
                pcols[7] = CToolUnits::TimePointToLocalString(parent->tStart()).c_str();
                pcols[8] = CToolUnits::TimePointToLocalString(parent->tEnd()).c_str();
                pcols[9] = parent->getBuddyId().c_str();
                pcols[10] = SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)parent->getAOIInspResult()).c_str();
                pcols[11] = parent->getPathDescription().c_str();
                pcols[12] = parent->getParamsDescription().c_str();
                auto* nParent = m_listCtrl.InsertRoot(pcols);
                MaybeRestoreExpandByKey(nParent, expandedKeys);
                m_listCtrl.SetNodeColor(nParent, RGB(0, 0, 0), RGB(201, 228, 180));
                std::vector<CString> ccols(colCount);
                ccols[1] = _T("");
                ccols[2] = std::to_string(child->getCassetteSequenceNo()).c_str();
                ccols[3] = std::to_string(child->getJobSequenceNo()).c_str();
                ccols[4] = child->getID().c_str();
                ccols[5] = SERVO::CServoUtilsTool::getMaterialsTypeText(child->getType()).c_str();
                ccols[6] = SERVO::CServoUtilsTool::getGlassStateText(child->state()).c_str();
                ccols[7] = CToolUnits::TimePointToLocalString(child->tStart()).c_str();
                ccols[8] = CToolUnits::TimePointToLocalString(child->tEnd()).c_str();
                ccols[9] = child->getBuddyId().c_str();
                ccols[10] = SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)child->getAOIInspResult()).c_str();
                ccols[11] = child->getPathDescription().c_str();
                ccols[12] = child->getParamsDescription().c_str();
                auto* nChild = m_listCtrl.InsertChild(nParent, ccols);
                m_listCtrl.SetNodeColor(nChild, RGB(0, 0, 0), RGB(201, 228, 180));
                usedWip.insert(parent);
                usedWip.insert(child);
            }
            else {
                std::vector<CString> cols(colCount);
                cols[1] = _T("");
                cols[2] = std::to_string(g->getCassetteSequenceNo()).c_str();
                cols[3] = std::to_string(g->getJobSequenceNo()).c_str();
                cols[4] = g->getID().c_str();
                cols[5] = SERVO::CServoUtilsTool::getMaterialsTypeText(g->getType()).c_str();
                cols[6] = SERVO::CServoUtilsTool::getGlassStateText(g->state()).c_str();
                cols[7] = CToolUnits::TimePointToLocalString(g->tStart()).c_str();
                cols[8] = CToolUnits::TimePointToLocalString(g->tEnd()).c_str();
                cols[9] = g->getBuddyId().c_str();
                cols[10] = SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)g->getAOIInspResult()).c_str();
                cols[11] = g->getPathDescription().c_str();
                cols[12] = g->getParamsDescription().c_str();
                auto* n = m_listCtrl.InsertRoot(cols);
                m_listCtrl.SetNodeColor(n, RGB(0, 0, 0), RGB(201, 228, 180));
                usedWip.insert(g);
            }
        }
        for (auto* item : tempGlasses) item->release();
    }
    // ==================== 2) DB 当前页(无论第几页都构建;排在 WIP 之后) ====================
#if USE_FAKE_DB_DEMO
    auto page = _make_page_fake(m_filters, PAGE_SIZE, PAGE_SIZE * (m_nCurPage - 1));
#else
   auto& db = GlassLogDb::Instance();
   auto page = db.queryPaged(m_filters, PAGE_SIZE, PAGE_SIZE * (m_nCurPage - 1));
   for (const auto& r : page.items) {
      int index = m_listCtrl.InsertItem(m_listCtrl.GetItemCount(), "");
      m_listCtrl.SetItemText(index, 1, std::to_string(r.id).c_str());
      m_listCtrl.SetItemText(index, 2, std::to_string(r.cassetteSeqNo).c_str());
      m_listCtrl.SetItemText(index, 3, std::to_string(r.jobSeqNo).c_str());
      m_listCtrl.SetItemText(index, 4, r.classId.c_str());
      m_listCtrl.SetItemText(index, 5, SERVO::CServoUtilsTool::getMaterialsTypeText((SERVO::MaterialsType)r.materialType).c_str());
      m_listCtrl.SetItemText(index, 6, SERVO::CServoUtilsTool::getGlassStateText((SERVO::GlsState)r.state).c_str());
      m_listCtrl.SetItemText(index, 7, r.tStart.c_str());
      m_listCtrl.SetItemText(index, 8, r.tEnd.c_str());
      m_listCtrl.SetItemText(index, 9, r.buddyId.c_str());
      m_listCtrl.SetItemText(index, 10, SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)r.aoiResult).c_str());
      m_listCtrl.SetItemText(index, 11, r.path.c_str());
      m_listCtrl.SetItemText(index, 12, r.params.c_str());
      m_listCtrl.SetItemColor(index, RGB(0, 0, 0), index % 2 == 0 ? RGB(255, 255, 255) : RGB(235, 235, 235));
#endif
      // 测试反序列化
      /*
      SERVO::CGlass g2;
      std::string err;
      if (GlassJson::FromString(r.pretty, g2, &err)) {
         AfxMessageBox(r.pretty.c_str());
    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;
    std::unordered_set<std::string> usedDb;
    int zebra = 0;
    for (size_t i = 0; i < page.items.size(); ++i) {
        const auto& r = page.items[i];
        if (usedDb.count(r.classId)) continue;
        COLORREF bk = (zebra % 2 == 0) ? RGB(255, 255, 255) : RGB(235, 235, 235);
        bool paired = false;
        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;
      }
      */
   }
        }
        if (!paired && !r.buddyId.empty()) {
            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* 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
            auto* nChild = m_listCtrl.InsertChild(nParent, ccols);
            m_listCtrl.SetNodeColor(nChild, RGB(0, 0, 0), bk);
            usedDb.insert(r.classId);
            paired = true;
        }
        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();
            auto* n = m_listCtrl.InsertRoot(cols);
            m_listCtrl.SetNodeColor(n, RGB(0, 0, 0), bk);
            usedDb.insert(r.classId);
        }
        ++zebra;
    }
    // 一次性重绘
    m_listCtrl.RebuildVisible();
   // 上一页 / 下一页
   UpdatePageControls();
}
void CPageGlassList::UpdateWipData()
{
   if (m_nCurPage != 1) return;
   // 取出缓存Glass, 符合条件则显示;
   // 但要删除旧的数据
   std::vector<SERVO::CGlass*> wipGlasses;
   theApp.m_model.m_master.getWipGlasses(wipGlasses);
   std::vector<SERVO::CGlass*> tempGlasses = wipGlasses;
   int count = m_listCtrl.GetItemCount();
   if (count > 0) {
      for (int i = count - 1; i >= 0; i--) {
         SERVO::CGlass* pGlass = (SERVO::CGlass*)m_listCtrl.GetItemData(i);
         if (eraseGlassInVector(pGlass, wipGlasses)
            && GlassMatchesFilters(*pGlass, m_filters)) {
            // 更新
            UpdateWipRow(i, pGlass);
         }
         else {
            // 删除
            m_listCtrl.DeleteItem(i);
         }
      }
   }
   // 剩下的如符号插入
   for (auto* item : wipGlasses) {
      if (GlassMatchesFilters(*item, m_filters)) {
         InsertWipRow(item);
      }
   }
   for (auto* item : tempGlasses) {
      item->release();
   }
    m_rebuilding = false;
}
void CPageGlassList::UpdatePageControls()
@@ -291,13 +732,11 @@
   GetDlgItem(IDC_BUTTON_NEXT_PAGE)->EnableWindow(m_nCurPage < m_nTotalPages);
}
// CPageTransferLog 消息处理程序
BOOL CPageGlassList::OnInitDialog()
{
   CDialogEx::OnInitDialog();
   // TODO:  在此添加额外的初始化
    // 定时器:1=初始化订阅,2=周期刷新(只增量)
   SetTimer(1, 3000, nullptr);
   SetTimer(2, 2000, nullptr);
@@ -315,6 +754,7 @@
   DWORD dwStyle = m_listCtrl.GetExtendedStyle();
   dwStyle |= LVS_EX_FULLROWSELECT;
   dwStyle |= LVS_EX_GRIDLINES;
    dwStyle |= LVS_EX_DOUBLEBUFFER;
   m_listCtrl.SetExtendedStyle(dwStyle);
   HIMAGELIST imageList = ImageList_Create(24, 24, ILC_COLOR24, 1, 1);
@@ -335,18 +775,21 @@
      _T("路径"),
      _T("工艺参数") 
   };
   int widths[] = { 0, 80, 80, 80, 100, 120, 120, 120, 120, 200, 200, 200, 200 };
    int widths[] = { 24, 80, 80, 80, 100, 120, 120, 120, 120, 200, 200, 200, 200 };
   for (int i = 0; i < _countof(headers); ++i) {
      strItem.Format(_T("Col_%d_Width"), i);
      widths[i] = GetPrivateProfileInt("GlassListCtrl", strItem, widths[i], strIniFile);
        int def = widths[i];
        widths[i] = GetPrivateProfileInt("GlassListCtrl", strItem, def, strIniFile);
        if (i == 0 && widths[i] < 16) widths[i] = 24; // 让三角图标有空间展示
      m_listCtrl.InsertColumn(i, headers[i], i == 0 ? LVCFMT_RIGHT : LVCFMT_LEFT, widths[i]);
   }
    // 二次兜底,防止 ini 写进了 0
    if (m_listCtrl.GetColumnWidth(0) < 16) m_listCtrl.SetColumnWidth(0, 24);
   Resize();
   OnBnClickedButtonSearch();
    OnBnClickedButtonSearch(); // 触发一次查询与首屏填充
   return TRUE;  // return TRUE unless you set the focus to a control
   // 异常: OCX 属性页应返回 FALSE
}
HBRUSH CPageGlassList::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
@@ -354,11 +797,9 @@
   if (nCtlColor == CTLCOLOR_STATIC) {
      pDC->SetBkColor(m_crBkgnd);
   }
   if (m_hbrBkgnd == nullptr) {
      m_hbrBkgnd = CreateSolidBrush(m_crBkgnd);
   }
   return m_hbrBkgnd;
}
@@ -374,16 +815,20 @@
      m_pObserver = nullptr;
   }
   // 保存列宽
    // 保存列宽(首列兜底,避免把 0 写回去)
   CString strIniFile, strItem, strTemp;
   strIniFile.Format(_T("%s\\configuration.ini"), (LPTSTR)(LPCTSTR)theApp.m_strAppDir);
   CHeaderCtrl* pHeader = m_listCtrl.GetHeaderCtrl();
    if (pHeader) {
   for (int i = 0; i < pHeader->GetItemCount(); i++) {
      RECT rect;
      pHeader->GetItemRect(i, &rect);
      strItem.Format(_T("Col_%d_Width"), i);
      strTemp.Format(_T("%d"), rect.right - rect.left);
            int w = rect.right - rect.left;
            if (i == 0 && w < 16) w = 24;
            strTemp.Format(_T("%d"), w);
      WritePrivateProfileString("GlassListCtrl", strItem, strTemp, strIniFile);
        }
   }
}
@@ -399,9 +844,8 @@
      KillTimer(1);
      InitRxWindow();
   }
   else if (nIDEvent == 2) {
      UpdateWipData();
        UpdateWipData();  // 只做增量,不重建
   }
   CDialogEx::OnTimer(nIDEvent);
@@ -414,10 +858,6 @@
   int nCount = pComboBox->GetCount();
   m_dateTimeStart.EnableWindow(nIndex == nCount - 1);
   m_dateTimeEnd.EnableWindow(nIndex == nCount - 1);
   // 更新日期过滤器和页面数据
   // UpdateDateFilter();
   // LoadTransfers();
}
void CPageGlassList::OnCbnSelchangeComboStatusFilter()
@@ -432,7 +872,6 @@
      pComboBox->GetLBText(nIndex, cstrText);
      m_strStatus = CT2A(cstrText);
   }
   // LoadTransfers();
}
void CPageGlassList::OnBnClickedButtonSearch()
@@ -476,8 +915,12 @@
      if (CToolUnits::GetCtrlDateRangeUtc_EndOfDay(m_dateTimeEnd, tp))   m_filters.tStartTo = tp;
   }
#if USE_FAKE_DB_DEMO
    long long total = _fake_total_count();
#else
   auto& db = GlassLogDb::Instance();
   long long total = db.count(m_filters);
#endif
   m_nTotalPages = (PAGE_SIZE > 0) ? int((total + PAGE_SIZE - 1) / PAGE_SIZE) : 1;
   LoadData();
@@ -491,8 +934,6 @@
   }
   // 导出 CSV:导出符合 filters 的“全部记录”(不受分页限制)
      // 返回导出的行数(不含表头)
      // csvPath:目标文件路径(UTF-8)
   auto& db = GlassLogDb::Instance();
   std::string csvPath((LPTSTR)(LPCTSTR)fileDialog.GetPathName());
   if (db.exportCsv(csvPath, m_filters) > 0) {
@@ -515,6 +956,255 @@
      UpdatePageData();
   }
}
void CPageGlassList::UpdateWipData()
{
    // 只在第 1 页刷新 WIP;其它页不动
    if (m_nCurPage != 1) 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*,仅收集“根节点”,便于只对根节点补子项
    std::unordered_map<std::string, std::pair<int, CExpandableListCtrl::Node*>> wipRowById;
    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 行,跳过
        auto* node = m_listCtrl.GetNodeByVisibleIndex(row);
        CString cls = m_listCtrl.GetItemText(row, 4);  // 第4列 Class ID
#ifdef _UNICODE
        std::string key = CT2A(cls);
#else
        std::string key = cls.GetString();
#endif
        wipRowById[key] = { row, node };
        if (node && node->parent == nullptr) {
            wipRootById[key] = node;                   // 仅根节点进入这个表
        }
    }
    // 2) 拉当前 WIP 列表
    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
        cols[2] = std::to_string(g->getCassetteSequenceNo()).c_str();
        cols[3] = std::to_string(g->getJobSequenceNo()).c_str();
        cols[4] = g->getID().c_str();
        cols[5] = SERVO::CServoUtilsTool::getMaterialsTypeText(g->getType()).c_str();
        cols[6] = SERVO::CServoUtilsTool::getGlassStateText(g->state()).c_str();
        cols[7] = CToolUnits::TimePointToLocalString(g->tStart()).c_str();
        cols[8] = CToolUnits::TimePointToLocalString(g->tEnd()).c_str();
        cols[9] = g->getBuddyId().c_str();
        cols[10] = SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)g->getAOIInspResult()).c_str();
        cols[11] = g->getPathDescription().c_str();
        cols[12] = g->getParamsDescription().c_str();
        return cols;
    };
    bool needRebuildChildren = false;     // 本次是否新增了子节点(结构变化)
    bool needRebuildAllForNewRoot = false;// 本次是否发现了“新增根节点”的需求(为保证 WIP 在顶部)
    std::vector<int> rowsToRedraw;        // 仅文本变化的行
    // UI 状态(当需要重建时使用)
    std::vector<CExpandableListCtrl::Node*> savedSel;
    CExpandableListCtrl::Node* savedTop = nullptr;
    // 3) 逐个处理 WIP:已存在 -> 就地更新;必要时“只对根补子项”
    //                 不存在 -> 触发“全量重建”,以保证新 WIP 根行出现在列表顶部
    for (auto* g : wipGlasses) {
        if (!GlassMatchesFilters(*g, m_filters)) continue;
#ifdef _UNICODE
        std::string cid = CT2A(g->getID().c_str());
#else
        std::string cid = g->getID();
#endif
        auto itAny = wipRowById.find(cid);
        if (itAny != wipRowById.end()) {
            // (A) 已存在:仅更新文案 & 重绘该行
            int row = itAny->second.first;
            m_listCtrl.SetItemText(row, 2, std::to_string(g->getCassetteSequenceNo()).c_str());
            m_listCtrl.SetItemText(row, 3, std::to_string(g->getJobSequenceNo()).c_str());
            m_listCtrl.SetItemText(row, 4, g->getID().c_str());
            m_listCtrl.SetItemText(row, 5, SERVO::CServoUtilsTool::getMaterialsTypeText(g->getType()).c_str());
            m_listCtrl.SetItemText(row, 6, SERVO::CServoUtilsTool::getGlassStateText(g->state()).c_str());
            m_listCtrl.SetItemText(row, 7, CToolUnits::TimePointToLocalString(g->tStart()).c_str());
            m_listCtrl.SetItemText(row, 8, CToolUnits::TimePointToLocalString(g->tEnd()).c_str());
            m_listCtrl.SetItemText(row, 9, g->getBuddyId().c_str());
            m_listCtrl.SetItemText(row, 10, SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)g->getAOIInspResult()).c_str());
            m_listCtrl.SetItemText(row, 11, g->getPathDescription().c_str());
            m_listCtrl.SetItemText(row, 12, g->getParamsDescription().c_str());
            rowsToRedraw.push_back(row);
            // —— 顺带刷新 buddy 子行(如果它已在可见表里)——
            if (SERVO::CGlass* b = g->getBuddy()) {
                CString buddyCidCs = b->getID().c_str();
#ifdef _UNICODE
                std::string bid = CT2A(buddyCidCs);
#else
                std::string bid = buddyCidCs.GetString();
#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;
                    CString newBuddyCid = b->getID().c_str();
#ifdef _UNICODE
                    std::string newBid = CT2A(newBuddyCid);
#else
                    std::string newBid = newBuddyCid.GetString();
#endif
                    // 现有容器下的“第一个子 classId”(如果有的话)
                    CString oldChildCid;
                    if (!container->children.empty() && container->children[0] && container->children[0]->cols.size() > 4)
                        oldChildCid = container->children[0]->cols[4];
                    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
                    if (relationChanged) {
                        // 关系变更走“结构重建”,避免重复或反向挂载
                        needRebuildAllForNewRoot = true;
                    }
                    else {
                        // 关系未变:若 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, RGB(0, 0, 0), RGB(201, 228, 180));
                        }
                        // 若已有子:顺带把子行文本刷新一下(比如 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; // 底层数据
                                    // 可见行刷新
                                    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)
                                                m_listCtrl.SetItemText(r, c, cols[c]);
                                            rowsToRedraw.push_back(r);
                                            break;
                                        }
                                    }
                                    break;
                                }
                            }
                        }
                    }
                }
                // 当前是“子节点”的情况:一律不挂子,交给重建(若父变更)
            }
            else {
                // 没有 buddy:如果容器下现在有子,也算关系变化,触发重建
                auto itRoot = wipRootById.find(cid);
                if (itRoot != wipRootById.end()) {
                    CExpandableListCtrl::Node* container = itRoot->second;
                    if (!container->children.empty())
                        needRebuildAllForNewRoot = true;
                }
            }
        }
        else {
            // (B) 不存在:新增根行——为保证“WIP 永远在顶部”,触发全量重建
            needRebuildAllForNewRoot = true;
        }
    }
    // 4) 应用 UI 更新
    if (needRebuildAllForNewRoot) {
        // 用 key(ClassID)保存并恢复,避免 Node* 失效
        auto selKeys = SnapshotSelectedKeys(m_listCtrl);
        auto topKey = SnapshotTopKey(m_listCtrl);
        UpdatePageData();                      // 全量重建(WIP 顶部)
        RestoreSelectionByKeys(m_listCtrl, selKeys);
        RestoreTopByKey(m_listCtrl, topKey);
    }
    else if (needRebuildChildren) {
        auto selKeys = SnapshotSelectedKeys(m_listCtrl);
        auto topKey = SnapshotTopKey(m_listCtrl);
        m_listCtrl.RebuildVisible();           // 仅结构变化(加子)
        RestoreSelectionByKeys(m_listCtrl, selKeys);
        RestoreTopByKey(m_listCtrl, topKey);
    }
    else {
        for (int row : rowsToRedraw)           // 仅文本变化
            m_listCtrl.RedrawItems(row, row);
    }
    // 5) 释放 retain
    for (auto* g : tempRetain) g->release();
}
void CPageGlassList::InsertWipRow(SERVO::CGlass* /*pGlass*/)
{
    // 不再使用
}
void CPageGlassList::UpdateWipRow(unsigned int /*index*/, SERVO::CGlass* /*pGlass*/)
{
    // 不再使用
}
bool CPageGlassList::eraseGlassInVector(SERVO::CGlass* /*pGlass*/, std::vector<SERVO::CGlass*>& /*glasses*/)
{
    return false;
}
// ===== 过滤逻辑(原样保留) =====
// 核心:WIP 的 CGlass 是否命中当前 Filters
// useEndTime=true 时用 tEnd 判时间(比如“完成列表”用 t_end),默认按 tStart。
@@ -547,39 +1237,4 @@
   }
   return true;
}
void CPageGlassList::InsertWipRow(SERVO::CGlass* pGlass)
{
   int index = m_listCtrl.InsertItem(0, "");
   m_listCtrl.SetItemColor(index, RGB(0, 0, 0), RGB(201, 228, 180));
   UpdateWipRow(index, pGlass);
}
void CPageGlassList::UpdateWipRow(unsigned int index, SERVO::CGlass* pGlass)
{
   ASSERT(index < m_listCtrl.GetItemCount());
   m_listCtrl.SetItemData(index, (DWORD_PTR)pGlass);
   m_listCtrl.SetItemText(index, 2, std::to_string(pGlass->getCassetteSequenceNo()).c_str());
   m_listCtrl.SetItemText(index, 3, std::to_string(pGlass->getJobSequenceNo()).c_str());
   m_listCtrl.SetItemText(index, 4, pGlass->getID().c_str());
   m_listCtrl.SetItemText(index, 5, SERVO::CServoUtilsTool::getMaterialsTypeText(pGlass->getType()).c_str());
   m_listCtrl.SetItemText(index, 6, SERVO::CServoUtilsTool::getGlassStateText(pGlass->state()).c_str());
   m_listCtrl.SetItemText(index, 7, CToolUnits::TimePointToLocalString(pGlass->tStart()).c_str());
   m_listCtrl.SetItemText(index, 8, CToolUnits::TimePointToLocalString(pGlass->tEnd()).c_str());
   m_listCtrl.SetItemText(index, 9, pGlass->getBuddyId().c_str());
   m_listCtrl.SetItemText(index, 10, SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)pGlass->getAOIInspResult()).c_str());
   m_listCtrl.SetItemText(index, 11, pGlass->getPathDescription().c_str());
   m_listCtrl.SetItemText(index, 12, pGlass->getParamsDescription().c_str());
}
bool CPageGlassList::eraseGlassInVector(SERVO::CGlass* pGlass, std::vector<SERVO::CGlass*>& glasses)
{
   auto iter = std::find(glasses.begin(), glasses.end(), pGlass);
   if (iter != glasses.end()) {
      glasses.erase(iter);
      return true;
   }
   return false;
}