From eaf39ceeef11c5685ddbb840541460e5dc5fccaa Mon Sep 17 00:00:00 2001
From: LAPTOP-SNT8I5JK\Boounion <Chenluhua@qq.com>
Date: 星期四, 11 九月 2025 08:43:14 +0800
Subject: [PATCH] 1.可折叠ListCtrl应用到GlassList页,优化,模拟数据测试等;

---
 SourceCode/Bond/Servo/CPageGlassList.cpp | 1521 +++++++++++++++++++++++++++++++++++++++++----------------
 1 files changed, 1,088 insertions(+), 433 deletions(-)

diff --git a/SourceCode/Bond/Servo/CPageGlassList.cpp b/SourceCode/Bond/Servo/CPageGlassList.cpp
index 1a4cabb..2707729 100644
--- a/SourceCode/Bond/Servo/CPageGlassList.cpp
+++ b/SourceCode/Bond/Servo/CPageGlassList.cpp
@@ -8,578 +8,1233 @@
 #include "GlassJson.h"
 #include "CServoUtilsTool.h"
 #include "ToolUnits.h"
+
 #include <optional>
+#include <unordered_set>
+#include <unordered_map>
+#include <vector>
+#include <string>
+
+#define PAGE_SIZE                       10
+#define PAGE_BACKGROUND_COLOR           RGB(252, 252, 255)
 
 
-#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锛圓NSI锛屾湰鍦颁唬鐮侀〉锛涗粎鐢ㄤ簬娴嬭瘯妯℃嫙锛�----
+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;
+}
+
+// ---- 鏃堕棿鏍煎紡宸ュ叿锛歯ow + 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;
+}
+
+// 椤惰锛圱opIndex锛夊搴旂殑 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 瀵硅瘽妗�
 
 IMPLEMENT_DYNAMIC(CPageGlassList, CDialogEx)
 
 CPageGlassList::CPageGlassList(CWnd* pParent /*=nullptr*/)
-	: CDialogEx(IDD_PAGE_GLASS_LIST, pParent)
+    : CDialogEx(IDD_PAGE_GLASS_LIST, pParent)
 {
-	m_crBkgnd = PAGE_BACKGROUND_COLOR;
-	m_hbrBkgnd = nullptr;
-	m_pObserver = nullptr;
+    m_crBkgnd = PAGE_BACKGROUND_COLOR;
+    m_hbrBkgnd = nullptr;
+    m_pObserver = nullptr;
 
-	m_strStatus = "";
-	m_nCurPage = 0;
-	m_nTotalPages = 1;
+    m_strStatus = "";
+    m_nCurPage = 1;
+    m_nTotalPages = 1;
 
-	memset(m_szTimeStart, 0, sizeof(m_szTimeStart));
-	memset(m_szTimeEnd, 0, sizeof(m_szTimeEnd));
-	m_szTimeStart[0] = '\0';
-	m_szTimeEnd[0] = '\0';
+    memset(m_szTimeStart, 0, sizeof(m_szTimeStart));
+    memset(m_szTimeEnd, 0, sizeof(m_szTimeEnd));
+    m_szTimeStart[0] = '\0';
+    m_szTimeEnd[0] = '\0';
 }
 
 CPageGlassList::~CPageGlassList()
 {
-	if (m_hbrBkgnd != nullptr) {
-		::DeleteObject(m_hbrBkgnd);
-	}
-	if (m_pObserver != nullptr) {
-		m_pObserver->unsubscribe();
-		m_pObserver = nullptr;
-	}
+    if (m_hbrBkgnd != nullptr) {
+        ::DeleteObject(m_hbrBkgnd);
+        m_hbrBkgnd = nullptr;
+    }
+    if (m_pObserver != nullptr) {
+        m_pObserver->unsubscribe();
+        m_pObserver = nullptr;
+    }
 }
 
 void CPageGlassList::DoDataExchange(CDataExchange* pDX)
 {
-	CDialogEx::DoDataExchange(pDX);
-	DDX_Control(pDX, IDC_DATETIMEPICKER_START, m_dateTimeStart);
-	DDX_Control(pDX, IDC_DATETIMEPICKER_END, m_dateTimeEnd);
-	DDX_Control(pDX, IDC_LIST_ALARM, m_listCtrl);
+    CDialogEx::DoDataExchange(pDX);
+    DDX_Control(pDX, IDC_DATETIMEPICKER_START, m_dateTimeStart);
+    DDX_Control(pDX, IDC_DATETIMEPICKER_END, m_dateTimeEnd);
+    DDX_Control(pDX, IDC_LIST_ALARM, m_listCtrl);
 }
 
-
 BEGIN_MESSAGE_MAP(CPageGlassList, CDialogEx)
-	ON_WM_CTLCOLOR()
-	ON_WM_DESTROY()
-	ON_WM_SIZE()
-	ON_WM_TIMER()
-	ON_CBN_SELCHANGE(IDC_COMBO_DATETIME, &CPageGlassList::OnCbnSelchangeComboDatetime)
-	ON_CBN_SELCHANGE(IDC_COMBO_STATUS_FILTER, &CPageGlassList::OnCbnSelchangeComboStatusFilter)
-	ON_BN_CLICKED(IDC_BUTTON_SEARCH, &CPageGlassList::OnBnClickedButtonSearch)
-	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_WM_CTLCOLOR()
+    ON_WM_DESTROY()
+    ON_WM_SIZE()
+    ON_WM_TIMER()
+    ON_CBN_SELCHANGE(IDC_COMBO_DATETIME, &CPageGlassList::OnCbnSelchangeComboDatetime)
+    ON_CBN_SELCHANGE(IDC_COMBO_STATUS_FILTER, &CPageGlassList::OnCbnSelchangeComboStatusFilter)
+    ON_BN_CLICKED(IDC_BUTTON_SEARCH, &CPageGlassList::OnBnClickedButtonSearch)
+    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)
 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);
-	if (m_pObserver == NULL) {
-		m_pObserver = pRxWindows->allocObserver([&](IAny* pAny) -> void {
-			// onNext
-			pAny->addRef();
-			int code = pAny->getCode();
+    // 璁㈤槄鏁版嵁
+    IRxWindows* pRxWindows = RX_GetRxWindows();
+    pRxWindows->enableLog(5);
+    if (m_pObserver == NULL) {
+        m_pObserver = pRxWindows->allocObserver([&](IAny* pAny) -> void {
+            // onNext
+            pAny->addRef();
+            int code = pAny->getCode();
 
-			if (RX_CODE_EQ_ROBOT_TASK == code) {
-				UpdatePageData();
-			}
+            if (RX_CODE_EQ_ROBOT_TASK == code) {
+                UpdateWipData();   // 鍙洿鏂帮紝涓嶉噸寤猴紝涓嶆敼鍙樺睍寮�/閫夋嫨
+            }
 
-			pAny->release();
-			}, [&]() -> void {
-				// onComplete
-			}, [&](IThrowable* pThrowable) -> void {
-				// onErrorm
-				pThrowable->printf();
-			});
+            pAny->release();
+            }, [&]() -> void {
+                // onComplete
+            }, [&](IThrowable* pThrowable) -> void {
+                // onError
+                pThrowable->printf();
+            });
 
-		theApp.m_model.getObservable()->observeOn(pRxWindows->mainThread())->subscribe(m_pObserver);
-	}
+        theApp.m_model.getObservable()->observeOn(pRxWindows->mainThread())->subscribe(m_pObserver);
+    }
 }
 
 void CPageGlassList::Resize()
 {
-	CRect rcClient;
-	GetClientRect(&rcClient);
+    CRect rcClient;
+    GetClientRect(&rcClient);
 
-	// ===== 甯搁噺瀹氫箟 =====
-	const int nLeft = 12;
-	const int nRight = 12;
-	const int nTop = 58;
-	const int nButtonHeight = 28;
-	const int nButtonMarginBottom = 12;
-	const int nSpacing = 8;
-	const int nButtonWidth = 80;
-	const int nLabelWidth = 100;
+    // ===== 甯搁噺瀹氫箟 =====
+    const int nLeft = 12;
+    const int nRight = 12;
+    const int nTop = 58;
+    const int nButtonHeight = 28;
+    const int nButtonMarginBottom = 12;
+    const int nSpacing = 8;
+    const int nButtonWidth = 80;
+    const int nLabelWidth = 100;
 
-	// ===== 鍒嗛〉鎺т欢甯冨眬 =====
-	int yBottom = rcClient.bottom - nButtonMarginBottom - nButtonHeight;
-	int xRight = rcClient.Width() - nRight;
+    // ===== 鍒嗛〉鎺т欢甯冨眬 =====
+    int yBottom = rcClient.bottom - nButtonMarginBottom - nButtonHeight;
+    int xRight = rcClient.Width() - nRight;
 
-	CWnd* pBtnNext = GetDlgItem(IDC_BUTTON_NEXT_PAGE);
-	CWnd* pBtnPrev = GetDlgItem(IDC_BUTTON_PREV_PAGE);
-	CWnd* pLabelPage = GetDlgItem(IDC_LABEL_PAGE_NUMBER);
+    CWnd* pBtnNext = GetDlgItem(IDC_BUTTON_NEXT_PAGE);
+    CWnd* pBtnPrev = GetDlgItem(IDC_BUTTON_PREV_PAGE);
+    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;
+    if (pBtnNext && pBtnPrev && pLabelPage) {
+        pBtnNext->MoveWindow(xRight - nButtonWidth, yBottom, nButtonWidth, nButtonHeight);
+        xRight -= nButtonWidth + nSpacing;
 
-		// 璁剧疆鎸夐挳鍜屾爣绛句綅缃�
-		pBtnNext->MoveWindow(xRight - nButtonWidth, yBottom, nButtonWidth, nButtonHeight);
-		xRight -= nButtonWidth + nSpacing;
+        pLabelPage->MoveWindow(xRight - nLabelWidth, yBottom, nLabelWidth, nButtonHeight);
+        xRight -= nLabelWidth + nSpacing;
 
-		pLabelPage->MoveWindow(xRight - nLabelWidth, yBottom, nLabelWidth, nButtonHeight);
-		xRight -= nLabelWidth + nSpacing;
+        pBtnPrev->MoveWindow(xRight - nButtonWidth, yBottom, nButtonWidth, nButtonHeight);
+    }
 
-		pBtnPrev->MoveWindow(xRight - nButtonWidth, yBottom, nButtonWidth, nButtonHeight);
-	}
-
-	// ===== 琛ㄦ牸鍖哄煙甯冨眬 =====
-	if (nullptr != m_listCtrl.m_hWnd) {
-		int listHeight = yBottom - nTop - nSpacing;
-		m_listCtrl.MoveWindow(nLeft, nTop, rcClient.Width() - nLeft - nRight, listHeight);
-	}
+    // ===== 琛ㄦ牸鍖哄煙甯冨眬 =====
+    if (nullptr != m_listCtrl.m_hWnd) {
+        int listHeight = yBottom - nTop - nSpacing;
+        m_listCtrl.MoveWindow(nLeft, nTop, rcClient.Width() - nLeft - nRight, listHeight);
+    }
 }
 
 void CPageGlassList::InitStatusCombo()
 {
-	CComboBox* pComboBox = (CComboBox*)GetDlgItem(IDC_COMBO_STATUS_FILTER);
-	if (nullptr != pComboBox) {
-		pComboBox->ResetContent();
-		pComboBox->AddString(_T("鍏ㄩ儴"));
-		pComboBox->AddString(_T("Ready"));
-		pComboBox->AddString(_T("Running"));
-		pComboBox->AddString(_T("Error"));
-		pComboBox->AddString(_T("Abort"));
-		pComboBox->AddString(_T("Completed"));
-		pComboBox->SetCurSel(0);
-	}
+    CComboBox* pComboBox = (CComboBox*)GetDlgItem(IDC_COMBO_STATUS_FILTER);
+    if (nullptr != pComboBox) {
+        pComboBox->ResetContent();
+        pComboBox->AddString(_T("鍏ㄩ儴"));
+        pComboBox->AddString(_T("Ready"));
+        pComboBox->AddString(_T("Running"));
+        pComboBox->AddString(_T("Error"));
+        pComboBox->AddString(_T("Abort"));
+        pComboBox->AddString(_T("Completed"));
+        pComboBox->SetCurSel(0);
+    }
 }
 
 void CPageGlassList::InitTimeRangeCombo()
 {
-	CComboBox* pComboBox = (CComboBox*)GetDlgItem(IDC_COMBO_DATETIME);
-	if (nullptr != pComboBox) {
-		pComboBox->ResetContent();
-		pComboBox->AddString(_T("涓嶉檺"));
-		pComboBox->AddString(_T("浠婂ぉ"));
-		pComboBox->AddString(_T("涓冨ぉ鍐�"));
-		pComboBox->AddString(_T("鏈湀"));
-		pComboBox->AddString(_T("浠婂勾"));
-		pComboBox->AddString(_T("鑷畾涔�"));
-		pComboBox->SetCurSel(0);
-	}
+    CComboBox* pComboBox = (CComboBox*)GetDlgItem(IDC_COMBO_DATETIME);
+    if (nullptr != pComboBox) {
+        pComboBox->ResetContent();
+        pComboBox->AddString(_T("涓嶉檺"));
+        pComboBox->AddString(_T("浠婂ぉ"));
+        pComboBox->AddString(_T("涓冨ぉ鍐�"));
+        pComboBox->AddString(_T("鏈湀"));
+        pComboBox->AddString(_T("浠婂勾"));
+        pComboBox->AddString(_T("鑷畾涔�"));
+        pComboBox->SetCurSel(0);
+    }
 }
 
 void CPageGlassList::InitDateTimeControls()
 {
-	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);
+    if (m_dateTimeStart.m_hWnd == nullptr || m_dateTimeEnd.m_hWnd == nullptr) {
+        return;
+    }
+    // 鑷畾涔夎寖鍥存椂鎵嶅彲缂栬緫
+    m_dateTimeStart.EnableWindow(FALSE);
+    m_dateTimeEnd.EnableWindow(FALSE);
 }
 
 void CPageGlassList::LoadData()
 {
-	m_nCurPage = 1;
-	UpdatePageData();
+    m_nCurPage = 1;
+    UpdatePageData();
 }
 
 void CPageGlassList::UpdatePageData()
 {
-	// 濡傛灉涓虹1椤�, 鍙栧嚭缂撳瓨Glass, 绗﹀悎鏉′欢鍒欐樉绀猴紱
-	m_listCtrl.DeleteAllItems();
-	UpdateWipData();
+    m_rebuilding = true;
 
+    // 鏀惧湪浠讳綍娓呯┖/閲嶅缓鍔ㄤ綔涔嬪墠锛�
+    auto expandedKeys = SnapshotExpandedKeys(m_listCtrl);
 
-	// 鏌ヨ
-	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));
+    // 鈥斺�� 鍙屼繚闄╋細鍏堟竻鎺夊彲瑙侀」锛屽啀娓呮爲缁撴瀯 鈥斺��
+    m_listCtrl.SetRedraw(FALSE);
+    m_listCtrl.DeleteAllItems();
+    m_listCtrl.SetRedraw(TRUE);
 
-		// 娴嬭瘯鍙嶅簭鍒楀寲
-		/*
-		SERVO::CGlass g2;
-		std::string err;
-		if (GlassJson::FromString(r.pretty, g2, &err)) {
-			AfxMessageBox(r.pretty.c_str());
-		}
-		*/
-	}
+    // 鈥斺�� 娓呯┖鏍戯紙渚濊禆 CExpandableListCtrl::ClearTree()锛夆�斺��
+    m_listCtrl.ClearTree();
 
-	// 涓婁竴椤� / 涓嬩竴椤�
-	UpdatePageControls();
-}
+    const int colCount = m_listCtrl.GetHeaderCtrl() ? m_listCtrl.GetHeaderCtrl()->GetItemCount() : 0;
+    if (colCount <= 0) return;
 
-void CPageGlassList::UpdateWipData()
-{
-	if (m_nCurPage != 1) 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; // 寰呴噴鏀�
 
-	// 鍙栧嚭缂撳瓨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);
-			}
-		}
-	}
+        auto glassHit = [&](SERVO::CGlass* g) -> bool {
+            return g && GlassMatchesFilters(*g, m_filters);
+        };
 
-	// 鍓╀笅鐨勫绗﹀彿鎻掑叆
-	for (auto* item : wipGlasses) {
-		if (GlassMatchesFilters(*item, m_filters)) {
-			InsertWipRow(item);
-		}
-	}
-	for (auto* item : tempGlasses) {
-		item->release();
-	}
+        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));
+#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;
+
+    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();
+
+    m_rebuilding = false;
 }
 
 void CPageGlassList::UpdatePageControls()
 {
-	CString strPage;
-	strPage.Format(_T("绗� %d / %d 椤�"), m_nCurPage, m_nTotalPages);
-	SetDlgItemText(IDC_LABEL_PAGE_NUMBER, strPage);
-	GetDlgItem(IDC_BUTTON_PREV_PAGE)->EnableWindow(m_nCurPage > 1);
-	GetDlgItem(IDC_BUTTON_NEXT_PAGE)->EnableWindow(m_nCurPage < m_nTotalPages);
+    CString strPage;
+    strPage.Format(_T("绗� %d / %d 椤�"), m_nCurPage, m_nTotalPages);
+    SetDlgItemText(IDC_LABEL_PAGE_NUMBER, strPage);
+    GetDlgItem(IDC_BUTTON_PREV_PAGE)->EnableWindow(m_nCurPage > 1);
+    GetDlgItem(IDC_BUTTON_NEXT_PAGE)->EnableWindow(m_nCurPage < m_nTotalPages);
 }
-
-// CPageTransferLog 娑堟伅澶勭悊绋嬪簭
 
 BOOL CPageGlassList::OnInitDialog()
 {
-	CDialogEx::OnInitDialog();
+    CDialogEx::OnInitDialog();
 
-	// TODO:  鍦ㄦ娣诲姞棰濆鐨勫垵濮嬪寲
-	SetTimer(1, 3000, nullptr);
-	SetTimer(2, 2000, nullptr);
+    // 瀹氭椂鍣細1=鍒濆鍖栬闃咃紝2=鍛ㄦ湡鍒锋柊锛堝彧澧為噺锛�
+    SetTimer(1, 3000, nullptr);
+    SetTimer(2, 2000, nullptr);
 
-	// 涓嬫媺妗嗘帶浠�
-	InitStatusCombo();
-	InitTimeRangeCombo();
+    // 涓嬫媺妗嗘帶浠�
+    InitStatusCombo();
+    InitTimeRangeCombo();
 
-	// 鏃ユ湡鎺т欢
-	InitDateTimeControls();
+    // 鏃ユ湡鎺т欢
+    InitDateTimeControls();
 
-	// 鎶ヨ〃鎺т欢
-	CString strIniFile, strItem;
-	strIniFile.Format(_T("%s\\configuration.ini"), (LPTSTR)(LPCTSTR)theApp.m_strAppDir);
+    // 鎶ヨ〃鎺т欢
+    CString strIniFile, strItem;
+    strIniFile.Format(_T("%s\\configuration.ini"), (LPTSTR)(LPCTSTR)theApp.m_strAppDir);
 
-	DWORD dwStyle = m_listCtrl.GetExtendedStyle();
-	dwStyle |= LVS_EX_FULLROWSELECT;
-	dwStyle |= LVS_EX_GRIDLINES;
-	m_listCtrl.SetExtendedStyle(dwStyle);
+    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);
-	ListView_SetImageList(m_listCtrl.GetSafeHwnd(), imageList, LVSIL_SMALL);
+    HIMAGELIST imageList = ImageList_Create(24, 24, ILC_COLOR24, 1, 1);
+    ListView_SetImageList(m_listCtrl.GetSafeHwnd(), imageList, LVSIL_SMALL);
 
-	CString headers[] = { 
-		_T(""),
-		_T("id"), 
-		_T("Cassette Sequence No"),
-		_T("Job Sequence No"),
-		_T("Class ID"),
-		_T("鐗╂枡绫诲瀷"),
-		_T("鐘舵��"),
-		_T("宸ヨ壓寮�濮嬫椂闂�"), 
-		_T("宸ヨ壓缁撴潫鏃堕棿"),
-		_T("閭﹀畾Glass ID"),
-		_T("AOI妫�娴嬬粨鏋�"),
-		_T("璺緞"),
-		_T("宸ヨ壓鍙傛暟") 
-	};
-	int widths[] = { 0, 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);
-		m_listCtrl.InsertColumn(i, headers[i], i == 0 ? LVCFMT_RIGHT : LVCFMT_LEFT, widths[i]);
-	}
+    CString headers[] = {
+        _T(""),
+        _T("id"),
+        _T("Cassette Sequence No"),
+        _T("Job Sequence No"),
+        _T("Class ID"),
+        _T("鐗╂枡绫诲瀷"),
+        _T("鐘舵��"),
+        _T("宸ヨ壓寮�濮嬫椂闂�"),
+        _T("宸ヨ壓缁撴潫鏃堕棿"),
+        _T("閭﹀畾Glass ID"),
+        _T("AOI妫�娴嬬粨鏋�"),
+        _T("璺緞"),
+        _T("宸ヨ壓鍙傛暟")
+    };
+    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);
+        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();
+    Resize();
+    OnBnClickedButtonSearch(); // 瑙﹀彂涓�娆℃煡璇笌棣栧睆濉厖
 
-	return TRUE;  // return TRUE unless you set the focus to a control
-	// 寮傚父: OCX 灞炴�ч〉搴旇繑鍥� FALSE
+    return TRUE;  // return TRUE unless you set the focus to a control
 }
 
 HBRUSH CPageGlassList::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
 {
-	if (nCtlColor == CTLCOLOR_STATIC) {
-		pDC->SetBkColor(m_crBkgnd);
-	}
-
-	if (m_hbrBkgnd == nullptr) {
-		m_hbrBkgnd = CreateSolidBrush(m_crBkgnd);
-	}
-
-	return m_hbrBkgnd;
+    if (nCtlColor == CTLCOLOR_STATIC) {
+        pDC->SetBkColor(m_crBkgnd);
+    }
+    if (m_hbrBkgnd == nullptr) {
+        m_hbrBkgnd = CreateSolidBrush(m_crBkgnd);
+    }
+    return m_hbrBkgnd;
 }
 
 void CPageGlassList::OnDestroy()
 {
-	CDialogEx::OnDestroy();
-	if (m_hbrBkgnd != nullptr) {
-		::DeleteObject(m_hbrBkgnd);
-		m_hbrBkgnd = nullptr;
-	}
-	if (m_pObserver != nullptr) {
-		m_pObserver->unsubscribe();
-		m_pObserver = nullptr;
-	}
+    CDialogEx::OnDestroy();
+    if (m_hbrBkgnd != nullptr) {
+        ::DeleteObject(m_hbrBkgnd);
+        m_hbrBkgnd = nullptr;
+    }
+    if (m_pObserver != nullptr) {
+        m_pObserver->unsubscribe();
+        m_pObserver = nullptr;
+    }
 
-	// 淇濆瓨鍒楀
-	CString strIniFile, strItem, strTemp;
-	strIniFile.Format(_T("%s\\configuration.ini"), (LPTSTR)(LPCTSTR)theApp.m_strAppDir);
-	CHeaderCtrl* pHeader = m_listCtrl.GetHeaderCtrl();
-	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);
-		WritePrivateProfileString("GlassListCtrl", strItem, strTemp, strIniFile);
-	}
+    // 淇濆瓨鍒楀锛堥鍒楀厹搴曪紝閬垮厤鎶� 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);
+            int w = rect.right - rect.left;
+            if (i == 0 && w < 16) w = 24;
+            strTemp.Format(_T("%d"), w);
+            WritePrivateProfileString("GlassListCtrl", strItem, strTemp, strIniFile);
+        }
+    }
 }
 
 void CPageGlassList::OnSize(UINT nType, int cx, int cy)
 {
-	CDialogEx::OnSize(nType, cx, cy);
-	Resize();
+    CDialogEx::OnSize(nType, cx, cy);
+    Resize();
 }
 
 void CPageGlassList::OnTimer(UINT_PTR nIDEvent)
 {
-	if (nIDEvent == 1) {
-		KillTimer(1);
-		InitRxWindow();
-	}
+    if (nIDEvent == 1) {
+        KillTimer(1);
+        InitRxWindow();
+    }
+    else if (nIDEvent == 2) {
+        UpdateWipData();  // 鍙仛澧為噺锛屼笉閲嶅缓
+    }
 
-	else if (nIDEvent == 2) {
-		UpdateWipData();
-	}
-
-	CDialogEx::OnTimer(nIDEvent);
+    CDialogEx::OnTimer(nIDEvent);
 }
 
 void CPageGlassList::OnCbnSelchangeComboDatetime()
 {
-	CComboBox* pComboBox = (CComboBox*)GetDlgItem(IDC_COMBO_DATETIME);
-	int nIndex = pComboBox->GetCurSel();
-	int nCount = pComboBox->GetCount();
-	m_dateTimeStart.EnableWindow(nIndex == nCount - 1);
-	m_dateTimeEnd.EnableWindow(nIndex == nCount - 1);
-
-	// 鏇存柊鏃ユ湡杩囨护鍣ㄥ拰椤甸潰鏁版嵁
-	// UpdateDateFilter();
-	// LoadTransfers();
+    CComboBox* pComboBox = (CComboBox*)GetDlgItem(IDC_COMBO_DATETIME);
+    int nIndex = pComboBox->GetCurSel();
+    int nCount = pComboBox->GetCount();
+    m_dateTimeStart.EnableWindow(nIndex == nCount - 1);
+    m_dateTimeEnd.EnableWindow(nIndex == nCount - 1);
 }
 
 void CPageGlassList::OnCbnSelchangeComboStatusFilter()
 {
-	CComboBox* pComboBox = (CComboBox*)GetDlgItem(IDC_COMBO_STATUS_FILTER);
-	int nIndex = pComboBox->GetCurSel();
-	if (nIndex == 0) {
-		m_strStatus.clear();
-	}
-	else {
-		CString cstrText;
-		pComboBox->GetLBText(nIndex, cstrText);
-		m_strStatus = CT2A(cstrText);
-	}
-	// LoadTransfers();
+    CComboBox* pComboBox = (CComboBox*)GetDlgItem(IDC_COMBO_STATUS_FILTER);
+    int nIndex = pComboBox->GetCurSel();
+    if (nIndex == 0) {
+        m_strStatus.clear();
+    }
+    else {
+        CString cstrText;
+        pComboBox->GetLBText(nIndex, cstrText);
+        m_strStatus = CT2A(cstrText);
+    }
 }
 
 void CPageGlassList::OnBnClickedButtonSearch()
 {
-	// 鑾峰彇鍏抽敭瀛楄緭鍏ユ鍐呭
-	CString strKeyword;
-	GetDlgItemText(IDC_EDIT_KEYWORD, strKeyword);
-	m_filters.keyword = CT2A(strKeyword);
+    // 鑾峰彇鍏抽敭瀛楄緭鍏ユ鍐呭
+    CString strKeyword;
+    GetDlgItemText(IDC_EDIT_KEYWORD, strKeyword);
+    m_filters.keyword = CT2A(strKeyword);
 
-	CComboBox* pComboBox = (CComboBox*)GetDlgItem(IDC_COMBO_DATETIME);
-	int index = pComboBox->GetCurSel();
-	if (index == 0) {
-		// 涓嶉檺
-		m_filters.tStartFrom = std::nullopt;
-		m_filters.tStartTo = std::nullopt;
-	}
-	else if (index == 1) {
-		auto [fromUtc, toUtc] = CToolUnits::CalcQuickRangeUtc(QuickRange::Today);
-		m_filters.tStartFrom = fromUtc;
-		m_filters.tStartTo = toUtc;
-	}
-	else if (index == 2) {
-		auto [fromUtc, toUtc] = CToolUnits::CalcQuickRangeUtc(QuickRange::Last7Days);
-		m_filters.tStartFrom = fromUtc;
-		m_filters.tStartTo = toUtc;
-	}
-	else if (index == 3) {
-		auto [fromUtc, toUtc] = CToolUnits::CalcQuickRangeUtc(QuickRange::ThisMonth);
-		m_filters.tStartFrom = fromUtc;
-		m_filters.tStartTo = toUtc;
-	}
-	else if (index == 4) {
-		auto [fromUtc, toUtc] = CToolUnits::CalcQuickRangeUtc(QuickRange::ThisYear);
-		m_filters.tStartFrom = fromUtc;
-		m_filters.tStartTo = toUtc;
-	}
-	else if(index == 5){
-		// 鑷畾涔�
-		std::chrono::system_clock::time_point tp;
-		if (CToolUnits::GetCtrlDateRangeUtc_StartOfDay(m_dateTimeStart, tp)) m_filters.tStartFrom = tp;
-		if (CToolUnits::GetCtrlDateRangeUtc_EndOfDay(m_dateTimeEnd, tp))   m_filters.tStartTo = tp;
-	}
+    CComboBox* pComboBox = (CComboBox*)GetDlgItem(IDC_COMBO_DATETIME);
+    int index = pComboBox->GetCurSel();
+    if (index == 0) {
+        // 涓嶉檺
+        m_filters.tStartFrom = std::nullopt;
+        m_filters.tStartTo = std::nullopt;
+    }
+    else if (index == 1) {
+        auto [fromUtc, toUtc] = CToolUnits::CalcQuickRangeUtc(QuickRange::Today);
+        m_filters.tStartFrom = fromUtc;
+        m_filters.tStartTo = toUtc;
+    }
+    else if (index == 2) {
+        auto [fromUtc, toUtc] = CToolUnits::CalcQuickRangeUtc(QuickRange::Last7Days);
+        m_filters.tStartFrom = fromUtc;
+        m_filters.tStartTo = toUtc;
+    }
+    else if (index == 3) {
+        auto [fromUtc, toUtc] = CToolUnits::CalcQuickRangeUtc(QuickRange::ThisMonth);
+        m_filters.tStartFrom = fromUtc;
+        m_filters.tStartTo = toUtc;
+    }
+    else if (index == 4) {
+        auto [fromUtc, toUtc] = CToolUnits::CalcQuickRangeUtc(QuickRange::ThisYear);
+        m_filters.tStartFrom = fromUtc;
+        m_filters.tStartTo = toUtc;
+    }
+    else if (index == 5) {
+        // 鑷畾涔�
+        std::chrono::system_clock::time_point tp;
+        if (CToolUnits::GetCtrlDateRangeUtc_StartOfDay(m_dateTimeStart, tp)) m_filters.tStartFrom = tp;
+        if (CToolUnits::GetCtrlDateRangeUtc_EndOfDay(m_dateTimeEnd, tp))   m_filters.tStartTo = tp;
+    }
 
-	auto& db = GlassLogDb::Instance();
-	long long total = db.count(m_filters);
-	m_nTotalPages = (PAGE_SIZE > 0) ? int((total + PAGE_SIZE - 1) / PAGE_SIZE) : 1;
+#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();
+    LoadData();
 }
 
 void CPageGlassList::OnBnClickedButtonExport()
 {
-	CFileDialog fileDialog(FALSE, _T("csv"), NULL, OFN_HIDEREADONLY, _T("CSV Files (*.csv)|*.csv||"));
-	if (fileDialog.DoModal() != IDOK) {
-		return;
-	}
+    CFileDialog fileDialog(FALSE, _T("csv"), NULL, OFN_HIDEREADONLY, _T("CSV Files (*.csv)|*.csv||"));
+    if (fileDialog.DoModal() != IDOK) {
+        return;
+    }
 
-	// 瀵煎嚭 CSV锛氬鍑虹鍚� filters 鐨勨�滃叏閮ㄨ褰曗�濓紙涓嶅彈鍒嗛〉闄愬埗锛�
-		// 杩斿洖瀵煎嚭鐨勮鏁帮紙涓嶅惈琛ㄥご锛�
-		// csvPath锛氱洰鏍囨枃浠惰矾寰勶紙UTF-8锛�
-	auto& db = GlassLogDb::Instance();
-	std::string csvPath((LPTSTR)(LPCTSTR)fileDialog.GetPathName());
-	if (db.exportCsv(csvPath, m_filters) > 0) {
-		AfxMessageBox("瀵煎嚭CSV鎴愬姛锛�");
-	}
+    // 瀵煎嚭 CSV锛氬鍑虹鍚� filters 鐨勨�滃叏閮ㄨ褰曗�濓紙涓嶅彈鍒嗛〉闄愬埗锛�
+    auto& db = GlassLogDb::Instance();
+    std::string csvPath((LPTSTR)(LPCTSTR)fileDialog.GetPathName());
+    if (db.exportCsv(csvPath, m_filters) > 0) {
+        AfxMessageBox("瀵煎嚭CSV鎴愬姛锛�");
+    }
 }
 
 void CPageGlassList::OnBnClickedButtonPrevPage()
 {
-	if (m_nCurPage > 1) {
-		m_nCurPage--;
-		UpdatePageData();
-	}	
+    if (m_nCurPage > 1) {
+        m_nCurPage--;
+        UpdatePageData();
+    }
 }
 
 void CPageGlassList::OnBnClickedButtonNextPage()
 {
-	if (m_nCurPage < m_nTotalPages) {
-		m_nCurPage++;
-		UpdatePageData();
-	}
+    if (m_nCurPage < m_nTotalPages) {
+        m_nCurPage++;
+        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) 鏀堕泦褰撳墠鍙閲岀殑鈥淲IP 琛屸�濓紙绗�1鍒� id 涓虹┖锛�
+    //    a) wipRowById锛歝lassId -> (row, node*)锛屾敹闆嗏�滄牴+瀛愨�濈殑鍏ㄩ儴锛屼究浜庡垽鏂�渂uddy 鏄惁宸插湪鍙琛ㄤ腑鈥�
+    //    b) wipRootById锛歝lassId -> 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);
+
+                    // 鈥斺�� 鍏抽敭锛氬叧绯绘槸鍚﹀彂鐢熷彉鍖栵紵锛坥ldChildCid 涓� 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) 涓嶅瓨鍦細鏂板鏍硅鈥斺�斾负淇濊瘉鈥淲IP 姘歌繙鍦ㄩ《閮ㄢ�濓紝瑙﹀彂鍏ㄩ噺閲嶅缓
+            needRebuildAllForNewRoot = true;
+        }
+    }
+
+    // 4) 搴旂敤 UI 鏇存柊
+    if (needRebuildAllForNewRoot) {
+        // 鐢� key锛圕lassID锛変繚瀛樺苟鎭㈠锛岄伩鍏� Node* 澶辨晥
+        auto selKeys = SnapshotSelectedKeys(m_listCtrl);
+        auto topKey = SnapshotTopKey(m_listCtrl);
+        UpdatePageData();                      // 鍏ㄩ噺閲嶅缓锛圵IP 椤堕儴锛�
+        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;
+}
+
+// ===== 杩囨护閫昏緫锛堝師鏍蜂繚鐣欙級 =====
 
 // 鏍稿績锛歐IP 鐨� CGlass 鏄惁鍛戒腑褰撳墠 Filters
 // useEndTime=true 鏃剁敤 tEnd 鍒ゆ椂闂达紙姣斿鈥滃畬鎴愬垪琛ㄢ�濈敤 t_end锛夛紝榛樿鎸� tStart銆�
 bool CPageGlassList::GlassMatchesFilters(const SERVO::CGlass& g,
-	const GlassLogDb::Filters& f,
-	bool useEndTime/* = false*/)
+    const GlassLogDb::Filters& f,
+    bool useEndTime/* = false*/)
 {
-	// 1) 绮剧‘瀛楁
-	if (f.classId && g.getID() != *f.classId)      return false;
-	if (f.cassetteSeqNo && g.getCassetteSequenceNo() != *f.cassetteSeqNo)return false;
-	if (f.jobSeqNo && g.getJobSequenceNo() != *f.jobSeqNo)     return false;
+    // 1) 绮剧‘瀛楁
+    if (f.classId && g.getID() != *f.classId)      return false;
+    if (f.cassetteSeqNo && g.getCassetteSequenceNo() != *f.cassetteSeqNo) return false;
+    if (f.jobSeqNo && g.getJobSequenceNo() != *f.jobSeqNo)     return false;
 
-	// 2) 鍏抽敭瀛楋紙涓� DB 淇濇寔涓�鑷达細class_id / buddy_id / path / params / pretty锛�
-	if (f.keyword) {
-		const std::string& kw = *f.keyword;
-		if (!(CToolUnits::containsCI(g.getID(), kw)
-			|| CToolUnits::containsCI(g.getBuddyId(), kw)
-			|| CToolUnits::containsCI(g.getPathDescription(), kw)
-			|| CToolUnits::containsCI(g.getParamsDescription(), kw)))
-			return false;
-	}
+    // 2) 鍏抽敭瀛楋紙涓� DB 淇濇寔涓�鑷达細class_id / buddy_id / path / params / pretty锛�
+    if (f.keyword) {
+        const std::string& kw = *f.keyword;
+        if (!(CToolUnits::containsCI(g.getID(), kw)
+            || CToolUnits::containsCI(g.getBuddyId(), kw)
+            || CToolUnits::containsCI(g.getPathDescription(), kw)
+            || CToolUnits::containsCI(g.getParamsDescription(), kw)))
+            return false;
+    }
 
-	// 3) 鏃堕棿锛堜笌 DB 淇濇寔涓�鑷达細榛樿鎸� t_start 杩囨护锛涢渶瑕佸彲鍒囧埌 t_end锛�
-	if (f.tStartFrom || f.tStartTo) {
-		std::optional<std::chrono::system_clock::time_point> tp = useEndTime ? g.tEnd() : g.tStart();
-		// 绾﹀畾锛氳嫢娌℃湁瀵瑰簲鏃堕棿鎴筹紝鍒欒涓轰笉鍛戒腑锛堜笌 DB 鐩稿悓锛歂ULL 涓嶄細鍛戒腑鑼冨洿锛�
-		if (!tp) return false;
-		if (f.tStartFrom && *tp < *f.tStartFrom) return false;
-		if (f.tStartTo && *tp > *f.tStartTo)   return false;
-	}
+    // 3) 鏃堕棿锛堜笌 DB 淇濇寔涓�鑷达細榛樿鎸� t_start 杩囨护锛涢渶瑕佸彲鍒囧埌 t_end锛�
+    if (f.tStartFrom || f.tStartTo) {
+        std::optional<std::chrono::system_clock::time_point> tp = useEndTime ? g.tEnd() : g.tStart();
+        // 绾﹀畾锛氳嫢娌℃湁瀵瑰簲鏃堕棿鎴筹紝鍒欒涓轰笉鍛戒腑锛堜笌 DB 鐩稿悓锛歂ULL 涓嶄細鍛戒腑鑼冨洿锛�
+        if (!tp) return false;
+        if (f.tStartFrom && *tp < *f.tStartFrom) return false;
+        if (f.tStartTo && *tp > *f.tStartTo)   return false;
+    }
 
-	return true;
+    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;
-}
\ No newline at end of file

--
Gitblit v1.9.3