mrDarker
2025-09-12 0fb528df2c1f05ef7d52827432bd934ce6f9d8cd
SourceCode/Bond/Servo/CPageGlassList.cpp
@@ -8,576 +8,1325 @@
#include "GlassJson.h"
#include "CServoUtilsTool.h"
#include "ToolUnits.h"
#include <optional>
#include <unordered_set>
#include <unordered_map>
#include <vector>
#include <string>
#define PAGE_SIZE                       50
#define PAGE_BACKGROUND_COLOR           RGB(252, 252, 255)
// WIP 颜色:父(根/无 buddy)= 基础绿;子(buddy)= 更浅
static const COLORREF kWipText = RGB(0, 0, 0);
static const COLORREF kWipParentBk = RGB(201, 228, 180); // 基础绿
static const COLORREF kWipChildBk = RGB(221, 241, 208); // 更浅一点
// ===== 放在 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 0
#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 < 806; ++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), "}" })
            });
    }
}
#define PAGE_SIZE                  10
#define PAGE_BACKGROUND_COLOR         RGB(252, 252, 255)
// ---- 做分页切片(可按需加更严格的 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 对话框
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)
    ON_NOTIFY(ELCN_SHOWFULLTEXT, IDC_LIST_ALARM, &CPageGlassList::OnShowFullText)
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;
    // 放在任何清空/重建动作之前:记录展开的父节点 key(ClassID)
    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(), std::to_string(r.id).c_str());
      m_listCtrl.SetItemText(index, 1, std::to_string(r.cassetteSeqNo).c_str());
      m_listCtrl.SetItemText(index, 2, std::to_string(r.jobSeqNo).c_str());
      m_listCtrl.SetItemText(index, 3, r.classId.c_str());
      m_listCtrl.SetItemText(index, 4, SERVO::CServoUtilsTool::getMaterialsTypeText((SERVO::MaterialsType)r.materialType).c_str());
      m_listCtrl.SetItemText(index, 5, SERVO::CServoUtilsTool::getGlassStateText((SERVO::GlsState)r.state).c_str());
      m_listCtrl.SetItemText(index, 6, r.tStart.c_str());
      m_listCtrl.SetItemText(index, 7, r.tEnd.c_str());
      m_listCtrl.SetItemText(index, 8, r.buddyId.c_str());
      m_listCtrl.SetItemText(index, 9, SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)r.aoiResult).c_str());
      m_listCtrl.SetItemText(index, 10, r.path.c_str());
      m_listCtrl.SetItemText(index, 11, r.params.c_str());
      m_listCtrl.SetItemColor(index, RGB(0, 0, 0), RGB(255, 255, 0));
    // —— 双保险:先清掉可见项,再清树结构 ——
    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) { m_rebuilding = false; 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) {
                // 按你的约定:g 是父,buddy 是子
                SERVO::CGlass* parent = g;
                SERVO::CGlass* child = b;
                // parent
                std::vector<CString> pcols(colCount);
                pcols[1] = _T("");
                pcols[2] = std::to_string(parent->getCassetteSequenceNo()).c_str();
                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, kWipText, kWipParentBk);   // 父:基础绿
                // child
                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, kWipText, kWipChildBk);     // 子:更浅
                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, kWipText, kWipParentBk);         // 仍用基础绿
                usedWip.insert(g);
            }
        }
        for (auto* item : tempGlasses) item->release();
    }
    // ==================== 2) DB 当前页(两阶段构建,处理单向 buddy) ====================
#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
    // 建索引:classId -> index
    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> consumed;
    int zebra = 0;
    auto zebraBk = [&](int z) -> COLORREF {
        return (z % 2 == 0) ? RGB(255, 255, 255) : RGB(235, 235, 235);
    };
    // -------- Phase 1: 先处理“有 buddyId 的记录”(能配就配;单向也配) ----------
    for (size_t i = 0; i < page.items.size(); ++i) {
        const auto& r = page.items[i];
        if (consumed.count(r.classId)) continue;
        if (r.buddyId.empty()) continue;
        COLORREF bk = zebraBk(zebra);
        auto it = idxById.find(r.buddyId);
        if (it != idxById.end()) {
            const auto& br = page.items[it->second];
            if (!consumed.count(br.classId)) {
                // —— 以“有 buddyId 的这条 r”为父,buddy 作为子(单向也能配)——
                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[1] = std::to_string(br.id).c_str();
                ccols[2] = std::to_string(br.cassetteSeqNo).c_str();
                ccols[3] = std::to_string(br.jobSeqNo).c_str();
                ccols[4] = br.classId.c_str();
                ccols[5] = SERVO::CServoUtilsTool::getMaterialsTypeText((SERVO::MaterialsType)br.materialType).c_str();
                ccols[6] = SERVO::CServoUtilsTool::getGlassStateText((SERVO::GlsState)br.state).c_str();
                ccols[7] = br.tStart.c_str();
                ccols[8] = br.tEnd.c_str();
                ccols[9] = br.buddyId.c_str();
                ccols[10] = SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)br.aoiResult).c_str();
                ccols[11] = br.path.c_str();
                ccols[12] = br.params.c_str();
                auto* nChild = m_listCtrl.InsertChild(nParent, ccols);
                m_listCtrl.SetNodeColor(nChild, RGB(0, 0, 0), bk);
                consumed.insert(r.classId);
                consumed.insert(br.classId);
                ++zebra;
                continue;
            }
        }
        // 同页没找到 buddy(或已被消费)→ 插占位子行
        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); // 占位只写 ClassID
        ccols[4] = r.buddyId.c_str();
        auto* nChild = m_listCtrl.InsertChild(nParent, ccols);
        m_listCtrl.SetNodeColor(nChild, RGB(0, 0, 0), bk);
        consumed.insert(r.classId);
        ++zebra;
    }
    // -------- Phase 2: 剩余未消费的,作为“单条根行” ----------
    for (size_t i = 0; i < page.items.size(); ++i) {
        const auto& r = page.items[i];
        if (consumed.count(r.classId)) continue;
        COLORREF bk = zebraBk(zebra);
        std::vector<CString> cols(colCount);
        cols[1] = std::to_string(r.id).c_str();
        cols[2] = std::to_string(r.cassetteSeqNo).c_str();
        cols[3] = std::to_string(r.jobSeqNo).c_str();
        cols[4] = r.classId.c_str();
        cols[5] = SERVO::CServoUtilsTool::getMaterialsTypeText((SERVO::MaterialsType)r.materialType).c_str();
        cols[6] = SERVO::CServoUtilsTool::getGlassStateText((SERVO::GlsState)r.state).c_str();
        cols[7] = r.tStart.c_str();
        cols[8] = r.tEnd.c_str();
        cols[9] = r.buddyId.c_str();
        cols[10] = SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)r.aoiResult).c_str();
        cols[11] = r.path.c_str();
        cols[12] = r.params.c_str();
        auto* n = m_listCtrl.InsertRoot(cols);
        m_listCtrl.SetNodeColor(n, RGB(0, 0, 0), bk);
        consumed.insert(r.classId);
        ++zebra;
    }
    // 一次性重绘
    m_listCtrl.RebuildVisible();
    // 上一页 / 下一页
    UpdatePageControls();
    m_rebuilding = false;
}
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();
   }
}
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("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[] = { 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], 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);
    m_listCtrl.SetPopupFullTextColumns({ 11, 12 });
   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::OnShowFullText(NMHDR* pNMHDR, LRESULT* pResult)
{
    auto* p = reinterpret_cast<NMC_ELC_SHOWFULLTEXT*>(pNMHDR);
    // 这里暂时用消息框显示;后续可换成你的详情页
    CString strNewMsg = p->text;
    strNewMsg.Replace(_T(","), _T("\n"));
    MessageBox(strNewMsg, _T("详细信息"), MB_OK | MB_ICONINFORMATION);
    *pResult = 0;
}
void CPageGlassList::UpdateWipData()
{
    // 只在第 1 页刷新 WIP;其它页不动
    if (m_nCurPage != 1) return;
    // 若刚好在 UpdatePageData() 重建期间,跳过这轮增量,避免互相干扰
    if (m_rebuilding) return;
    const int colCount = m_listCtrl.GetHeaderCtrl() ? m_listCtrl.GetHeaderCtrl()->GetItemCount() : 0;
    if (colCount <= 0) return;
    // 1) 收集当前可见里的“WIP 行”(第1列 id 为空)
    //    a) wipRowById:classId -> (row, node*),收集根+子,便于判断“buddy 是否已在可见表中”
    //    b) wipRootById:classId -> node*,仅收集根节点,便于只对“根”补子项
    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
    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;
    };
    // 2.1 构建“最新 WIP 键集”(含 buddy,且命中过滤),用于检测“缺失/删除”
    std::unordered_set<std::string> newWipKeys;
    for (auto* g : wipGlasses) {
        if (!GlassMatchesFilters(*g, m_filters)) continue;
#ifdef _UNICODE
        newWipKeys.insert(CT2A(CString(g->getID().c_str())));
#else
        newWipKeys.insert(g->getID());
#endif
        if (auto* b = g->getBuddy()) {
            if (GlassMatchesFilters(*b, m_filters)) {
#ifdef _UNICODE
                newWipKeys.insert(CT2A(CString(b->getID().c_str())));
#else
                newWipKeys.insert(b->getID());
#endif
            }
        }
    }
    bool needRebuildRemoval = false; // WIP 变少/清空:需要整页重建
    bool needRebuildChildren = false; // 结构变化:新增子
    bool needRebuildAllForNewRoot = false; // 新增根(保证 WIP 仍在顶部)
    std::vector<int> rowsToRedraw;         // 仅文本变化
    // 可见集中有但新数据里没有 -> 触发“删除/减少”的整页重建
    for (const auto& kv : wipRowById) {
        if (newWipKeys.find(kv.first) == newWipKeys.end()) { needRebuildRemoval = true; break; }
    }
    // UI 状态(当需要重建时使用)
    std::vector<CExpandableListCtrl::Node*> savedSel;
    CExpandableListCtrl::Node* savedTop = nullptr;
    // 3) 逐个处理 WIP:已存在 -> 就地更新;必要时“只对根补子项”
    //                 不存在 -> 优先挂到 buddy 容器;否则触发整页重建(新根保持顶部)
    for (auto* g : wipGlasses) {
        if (!GlassMatchesFilters(*g, m_filters)) continue;
#ifdef _UNICODE
        std::string cid = CT2A(CString(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()) {
#ifdef _UNICODE
                std::string bid = CT2A(CString(b->getID().c_str()));
#else
                std::string bid = b->getID();
#endif
                auto itChildAny = wipRowById.find(bid);
                if (itChildAny != wipRowById.end()) {
                    int crow = itChildAny->second.first;
                    auto bcols = makeColsFromWip(b);
                    ApplyColsToRow(m_listCtrl, crow, bcols);
                    rowsToRedraw.push_back(crow);
                }
            }
            // —— 只对“根节点”补子项,且仅当 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 不同,或有子但现在没 buddy)
                    bool relationChanged =
                        (!oldChildCid.IsEmpty() && newBuddyCid.IsEmpty()) ||
                        (oldChildCid.IsEmpty() && !newBuddyCid.IsEmpty()) ||
                        (!oldChildCid.IsEmpty() && !newBuddyCid.IsEmpty() &&
                            oldChildCid.CompareNoCase(newBuddyCid) != 0);
                    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, kWipText, kWipChildBk);  // 子:浅色
                            m_listCtrl.SetNodeColor(container, kWipText, kWipParentBk); // 父:基础绿
                        }
                        // 若已有子:同步刷新子行文本与颜色
                        else if (hasChildAlready) {
                            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;
                                        }
                                    }
                                    m_listCtrl.SetNodeColor(ch.get(), kWipText, kWipChildBk);
                                    m_listCtrl.SetNodeColor(container, kWipText, kWipParentBk);
                                    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) 不存在:新增
            //   先尝试“挂到 buddy 的容器根”下面;
            //   若 buddy 不在当前可见表,则触发全量重建(保证 WIP 顶部)。
            SERVO::CGlass* b = g->getBuddy();
            CExpandableListCtrl::Node* container = nullptr;
            if (b) {
#ifdef _UNICODE
                std::string bid = CT2A(CString(b->getID().c_str()));
#else
                std::string bid = b->getID();
#endif
                auto itB = wipRowById.find(bid);
                if (itB != wipRowById.end()) {
                    CExpandableListCtrl::Node* buddyNode = itB->second.second;
                    container = buddyNode ? (buddyNode->parent ? buddyNode->parent : buddyNode) : nullptr;
                }
            }
            if (container) {
                // buddy 容器存在:把 g 作为“子行”挂上去(避免重复)
                CString cidCs = g->getID().c_str();
                if (!NodeHasChildWithClassId(container, cidCs)) {
                    if (!needRebuildChildren) { CaptureUiState(m_listCtrl, savedSel, savedTop); }
                    needRebuildChildren = true;
                    auto cols = makeColsFromWip(g);
                    auto* ch = m_listCtrl.InsertChild(container, cols);
                    // 子:更浅;父:基础绿(兜底)
                    m_listCtrl.SetNodeColor(ch, kWipText, kWipChildBk);
                    m_listCtrl.SetNodeColor(container, kWipText, kWipParentBk);
                }
            }
            else {
                // buddy 不在可见表:为了保持“WIP 永远在顶部”,触发一次全量重建
                needRebuildAllForNewRoot = true;
            }
        }
    }
    // 4) 应用 UI 更新 —— 把“删除/减少”的情况并入全量重建分支
    if (needRebuildAllForNewRoot || needRebuildRemoval) {
        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。
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 相同:NULL 不会命中范围)
      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 相同:NULL 不会命中范围)
        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, "");
   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.SetItemColor(index, RGB(0, 0, 0), RGB(255, 255, 255));
   m_listCtrl.SetItemText(index, 1, std::to_string(pGlass->getCassetteSequenceNo()).c_str());
   m_listCtrl.SetItemText(index, 2, std::to_string(pGlass->getJobSequenceNo()).c_str());
   m_listCtrl.SetItemText(index, 3, pGlass->getID().c_str());
   m_listCtrl.SetItemText(index, 4, SERVO::CServoUtilsTool::getMaterialsTypeText(pGlass->getType()).c_str());
   m_listCtrl.SetItemText(index, 5, SERVO::CServoUtilsTool::getGlassStateText(pGlass->state()).c_str());
   m_listCtrl.SetItemText(index, 6, CToolUnits::TimePointToLocalString(pGlass->tStart()).c_str());
   m_listCtrl.SetItemText(index, 7, CToolUnits::TimePointToLocalString(pGlass->tEnd()).c_str());
   m_listCtrl.SetItemText(index, 8, pGlass->getBuddyId().c_str());
   m_listCtrl.SetItemText(index, 9, SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)pGlass->getAOIInspResult()).c_str());
   m_listCtrl.SetItemText(index, 10, pGlass->getPathDescription().c_str());
   m_listCtrl.SetItemText(index, 11, 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;
}