mrDarker
2025-09-12 0fb528df2c1f05ef7d52827432bd934ce6f9d8cd
Merge branch 'clh' into liuyang
已修改8个文件
441 ■■■■ 文件已修改
SourceCode/Bond/Servo/CEquipment.cpp 30 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CExpandableListCtrl.cpp 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CExpandableListCtrl.h 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CMaster.cpp 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CPageGlassList.cpp 319 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CPageGlassList.h 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CServoUtilsTool.cpp 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/Model.cpp 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
SourceCode/Bond/Servo/CEquipment.cpp
@@ -5,6 +5,7 @@
#include "CArm.h"
#include "CGlassPool.h"
#include "Servo.h"
#include "GlassJson.h"
namespace SERVO {
@@ -336,10 +337,15 @@
                m_slot[i].serialize(ar);
                CGlass* pGlass = (CGlass *)m_slot[i].getContext();
                if (pGlass != nullptr) {                    
                    pGlass->serialize(ar);
                    const std::string pretty = GlassJson::ToPrettyString(*pGlass);
                    CString strPretty = CString(pretty.c_str());
                    ar << strPretty;
                    CGlass* pBuddy = pGlass->getBuddy();
                    if (pBuddy != nullptr) {
                        pBuddy->serialize(ar);
                        const std::string prettyBuddy = GlassJson::ToPrettyString(*pBuddy);
                        CString strPrettyBuddy = CString(prettyBuddy.c_str());
                        ar << strPrettyBuddy;
                    }
                }
            }
@@ -349,14 +355,26 @@
            for (int i = 0; i < SLOT_MAX; i++) {
                m_slot[i].serialize(ar);
                if (m_slot[i].getTempContext() != nullptr) {
                    CString strPretty;
                    std::string pretty;
                    ar >> strPretty;
                    pretty = (LPTSTR)(LPCTSTR)strPretty;
                    if (!pretty.empty()) {
                    CGlass* pGlass = theApp.m_model.m_glassPool.allocaGlass();
                    pGlass->serialize(ar);
                        GlassJson::FromString(pretty, *pGlass);
                    m_slot[i].setContext(pGlass);
                    if (pGlass->getBuddy() != nullptr) {
                        if (!pGlass->getBuddyId().empty()) {
                        CGlass* pBuddy = theApp.m_model.m_glassPool.allocaGlass();
                        pBuddy->serialize(ar);
                            CString strPrettyBuddy;
                            std::string prettyBuddy;
                            ar >> strPrettyBuddy;
                            prettyBuddy = (LPTSTR)(LPCTSTR)strPrettyBuddy;
                            GlassJson::FromString(prettyBuddy, *pBuddy);
                        pGlass->forceSetBuddy(pBuddy);
                    }
                    }
                }
            }
            
@@ -1568,7 +1586,9 @@
        
        // 关联的Glass也要更新
        CGlass* pBuddy = pGlass->getBuddy();
        LOGI("<Equipment-%s>decodeProcessDataReport pBuddy=%x %s", getName().c_str(), pBuddy, pGlass->getID().c_str());
        if (pBuddy != nullptr) {
            LOGI("<Equipment-%s>decodeProcessDataReport addParams pBuddy=%x %s", getName().c_str(), pBuddy, pGlass->getID().c_str());
            pBuddy->addParams(params);
        }
SourceCode/Bond/Servo/CExpandableListCtrl.cpp
@@ -3,7 +3,11 @@
IMPLEMENT_DYNAMIC(CExpandableListCtrl, CListCtrl)
CExpandableListCtrl::CExpandableListCtrl() {}
CExpandableListCtrl::CExpandableListCtrl()
{
    m_popupCols = { };
}
CExpandableListCtrl::~CExpandableListCtrl() {}
BEGIN_MESSAGE_MAP(CExpandableListCtrl, CListCtrl)
@@ -249,6 +253,38 @@
            }
        }
    }
    // —— 若点击到需要“全文显示”的列,则向父窗口发送自定义通知 —— //
    if (!m_popupCols.empty()) {
        LPNMITEMACTIVATE pia = reinterpret_cast<LPNMITEMACTIVATE>(pNMHDR);
        // 用 SubItemHitTest 更精准拿到列
        LVHITTESTINFO ht{};
        ht.pt = pia->ptAction;
        int hit = SubItemHitTest(&ht);
        if (hit >= 0 && ht.iItem >= 0 && ht.iSubItem >= 0) {
            const int row = ht.iItem;
            const int col = ht.iSubItem;
            if (m_popupCols.count(col)) {
                CString full = GetItemText(row, col);
                if (!full.IsEmpty() && _IsCellTruncated(row, col, full)) {
                    NMC_ELC_SHOWFULLTEXT nm{};
                    nm.hdr.hwndFrom = m_hWnd;
                    nm.hdr.idFrom = GetDlgCtrlID();
                    nm.hdr.code = ELCN_SHOWFULLTEXT;
                    nm.iItem = row;
                    nm.iSubItem = col;
                    nm.text = full;
                    if (CWnd* pParent = GetParent()) {
                        pParent->SendMessage(WM_NOTIFY, nm.hdr.idFrom, reinterpret_cast<LPARAM>(&nm));
                    }
                }
            }
        }
    }
    *pResult = 0;
}
@@ -441,4 +477,29 @@
    Invalidate();
}
void CExpandableListCtrl::SetPopupFullTextColumns(const std::vector<int>& cols)
{
    m_popupCols.clear();
    for (int c : cols) m_popupCols.insert(c);
}
bool CExpandableListCtrl::_IsCellTruncated(int row, int col, const CString& text) const
{
    if (text.IsEmpty()) return false;
    // 单元格显示区域宽度
    CRect rcCell;
    if (!const_cast<CExpandableListCtrl*>(this)->GetSubItemRect(row, col, LVIR_BOUNDS, rcCell))
        return false;
    // 用控件字体测量文本像素宽
    CClientDC dc(const_cast<CExpandableListCtrl*>(this));
    CFont* pOld = dc.SelectObject(const_cast<CExpandableListCtrl*>(this)->GetFont());
    CSize sz = dc.GetTextExtent(text);
    dc.SelectObject(pOld);
    const int kPadding = 8; // 预留一点边距/省略号余量
    return sz.cx > (rcCell.Width() - kPadding);
}
SourceCode/Bond/Servo/CExpandableListCtrl.h
@@ -2,6 +2,19 @@
#include <vector>
#include <memory>
#include <unordered_map>
#include <set>
// ===== 自定义通知:点击需要弹出全文的单元格 =====
#ifndef ELCN_SHOWFULLTEXT
#define ELCN_SHOWFULLTEXT (NM_FIRST - 201)
struct NMC_ELC_SHOWFULLTEXT {
    NMHDR   hdr;       // hwndFrom / idFrom / code = ELCN_SHOWFULLTEXT
    int     iItem;     // 行
    int     iSubItem;  // 列(0-based)
    CString text;      // 完整文本
};
#endif
class CExpandableListCtrl : public CListCtrl
{
@@ -46,6 +59,12 @@
    // 清除树
    void ClearTree();
    // 设置哪些列需要“被截断则通知父窗口显示全文”(0-based列号)
    void SetPopupFullTextColumns(const std::vector<int>& cols);
    std::set<int> m_popupCols; // 需要通知的列集合
    bool _IsCellTruncated(int row, int col, const CString& text) const;
protected:
    virtual void PreSubclassWindow();
    afx_msg int  OnCreate(LPCREATESTRUCT lpCreateStruct);
SourceCode/Bond/Servo/CMaster.cpp
@@ -774,6 +774,7 @@
                                continue;
                            }
                            pGlass->start();
                            pEFEM->setContext(m_pActiveRobotTask->getContext());
                            goto PORT_GET;
                        }
SourceCode/Bond/Servo/CPageGlassList.cpp
@@ -98,7 +98,7 @@
}
// ====== 开关:1=启用假数据(只替换 DB 查询);0=用真实 DB ======
#define USE_FAKE_DB_DEMO 1
#define USE_FAKE_DB_DEMO 0
#if USE_FAKE_DB_DEMO
#include <ctime>
@@ -392,6 +392,7 @@
    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()
// ===== 私有小工具 =====
@@ -519,7 +520,7 @@
{
    m_rebuilding = true;
    // 放在任何清空/重建动作之前:
    // 放在任何清空/重建动作之前:记录展开的父节点 key(ClassID)
    auto expandedKeys = SnapshotExpandedKeys(m_listCtrl);
    // —— 双保险:先清掉可见项,再清树结构 ——
@@ -531,7 +532,7 @@
    m_listCtrl.ClearTree();
    const int colCount = m_listCtrl.GetHeaderCtrl() ? m_listCtrl.GetHeaderCtrl()->GetItemCount() : 0;
    if (colCount <= 0) return;
    if (colCount <= 0) { m_rebuilding = false; return; }
    // ==================== 1) WIP:仅第 1 页构建,且放在最顶部 ====================
    if (m_nCurPage == 1) {
@@ -550,9 +551,11 @@
            SERVO::CGlass* b = g->getBuddy();
            if (b) {
                // 按你的约定:g 是父,buddy 是子
                SERVO::CGlass* parent = g;
                SERVO::CGlass* child = b;
                // parent
                std::vector<CString> pcols(colCount);
                pcols[1] = _T("");
                pcols[2] = std::to_string(parent->getCassetteSequenceNo()).c_str();
@@ -571,6 +574,7 @@
                MaybeRestoreExpandByKey(nParent, expandedKeys);
                m_listCtrl.SetNodeColor(nParent, kWipText, kWipParentBk);   // 父:基础绿
                // child
                std::vector<CString> ccols(colCount);
                ccols[1] = _T("");
                ccols[2] = std::to_string(child->getCassetteSequenceNo()).c_str();
@@ -614,7 +618,7 @@
        for (auto* item : tempGlasses) item->release();
    }
    // ==================== 2) DB 当前页(无论第几页都构建;排在 WIP 之后) ====================
    // ==================== 2) DB 当前页(两阶段构建,处理单向 buddy) ====================
#if USE_FAKE_DB_DEMO
    auto page = _make_page_fake(m_filters, PAGE_SIZE, PAGE_SIZE * (m_nCurPage - 1));
#else
@@ -622,99 +626,128 @@
    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;
    for (size_t i = 0; i < page.items.size(); ++i) {
        idxById[page.items[i].classId] = i;
    }
    std::unordered_set<std::string> usedDb;
    // 已消费(已插入为父或子)
    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 (usedDb.count(r.classId)) continue;
        if (consumed.count(r.classId)) continue;
        if (r.buddyId.empty()) continue;
        COLORREF bk = (zebra % 2 == 0) ? RGB(255, 255, 255) : RGB(235, 235, 235);
        bool paired = false;
        COLORREF bk = zebraBk(zebra);
        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;
            if (!consumed.count(br.classId)) {
                // —— 以“有 buddyId 的这条 r”为父,buddy 作为子(单向也能配)——
                    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[1] = std::to_string(r.id).c_str();
                pcols[2] = std::to_string(r.cassetteSeqNo).c_str();
                pcols[3] = std::to_string(r.jobSeqNo).c_str();
                pcols[4] = r.classId.c_str();
            pcols[5] = SERVO::CServoUtilsTool::getMaterialsTypeText((SERVO::MaterialsType)r.materialType).c_str();
            pcols[6] = SERVO::CServoUtilsTool::getGlassStateText((SERVO::GlsState)r.state).c_str();
            pcols[7] = r.tStart.c_str(); pcols[8] = r.tEnd.c_str(); pcols[9] = r.buddyId.c_str();
                pcols[7] = r.tStart.c_str();
                pcols[8] = r.tEnd.c_str();
                pcols[9] = r.buddyId.c_str();
            pcols[10] = SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)r.aoiResult).c_str();
            pcols[11] = r.path.c_str(); pcols[12] = r.params.c_str();
                pcols[11] = r.path.c_str();
                pcols[12] = r.params.c_str();
            auto* nParent = m_listCtrl.InsertRoot(pcols);
            MaybeRestoreExpandByKey(nParent, expandedKeys);
            m_listCtrl.SetNodeColor(nParent, RGB(0, 0, 0), bk);
            std::vector<CString> ccols(colCount);
            ccols[4] = r.buddyId.c_str(); // 占位子行:显示 buddy 的 classId
                ccols[1] = std::to_string(br.id).c_str();
                ccols[2] = std::to_string(br.cassetteSeqNo).c_str();
                ccols[3] = std::to_string(br.jobSeqNo).c_str();
                ccols[4] = br.classId.c_str();
                ccols[5] = SERVO::CServoUtilsTool::getMaterialsTypeText((SERVO::MaterialsType)br.materialType).c_str();
                ccols[6] = SERVO::CServoUtilsTool::getGlassStateText((SERVO::GlsState)br.state).c_str();
                ccols[7] = br.tStart.c_str();
                ccols[8] = br.tEnd.c_str();
                ccols[9] = br.buddyId.c_str();
                ccols[10] = SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)br.aoiResult).c_str();
                ccols[11] = br.path.c_str();
                ccols[12] = br.params.c_str();
            auto* nChild = m_listCtrl.InsertChild(nParent, ccols);
            m_listCtrl.SetNodeColor(nChild, RGB(0, 0, 0), bk);
            usedDb.insert(r.classId);
            paired = true;
                consumed.insert(r.classId);
                consumed.insert(br.classId);
                ++zebra;
                continue;
            }
        }
        if (!paired) {
        // 同页没找到 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[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[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();
        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);
        }
        consumed.insert(r.classId);
        ++zebra;
    }
@@ -726,6 +759,7 @@
    m_rebuilding = false;
}
void CPageGlassList::UpdatePageControls()
{
@@ -789,6 +823,7 @@
    }
    // 二次兜底,防止 ini 写进了 0
    if (m_listCtrl.GetColumnWidth(0) < 16) m_listCtrl.SetColumnWidth(0, 24);
    m_listCtrl.SetPopupFullTextColumns({ 11, 12 });
    Resize();
    OnBnClickedButtonSearch(); // 触发一次查询与首屏填充
@@ -961,17 +996,30 @@
    }
}
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*,仅收集“根节点”,便于只对根节点补子项
    //    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) {
@@ -995,26 +1043,7 @@
    std::vector<SERVO::CGlass*> wipGlasses;
    theApp.m_model.m_master.getWipGlasses(wipGlasses);
    std::vector<SERVO::CGlass*> tempRetain = wipGlasses; // 稍后统一 release
    /*
    static int i = 0;
    i++;
    if (i == 8) {
        for (auto item : wipGlasses) {
            if (item->getBuddy() != nullptr) {
                item->setInspResult(EQ_ID_MEASUREMENT, 0, SERVO::InspResult::Fail);
                item->getBuddy()->setID("11111");
            }
        }
    }
    if (i == 16) {
        for (auto item : wipGlasses) {
            if (item->getBuddy() != nullptr) {
                item->setInspResult(EQ_ID_MEASUREMENT, 0, SERVO::InspResult::Pass);
                item->getBuddy()->setID("22222");
            }
        }
    }
    */
    auto makeColsFromWip = [&](SERVO::CGlass* g) {
        std::vector<CString> cols(colCount);
        cols[1] = _T(""); // WIP 没 DB id
@@ -1032,21 +1061,47 @@
        return cols;
    };
    bool needRebuildChildren = false;     // 本次是否新增了子节点(结构变化)
    bool needRebuildAllForNewRoot = false;// 本次是否发现了“新增根节点”的需求(为保证 WIP 在顶部)
    std::vector<int> rowsToRedraw;        // 仅文本变化的行
    // 2.1 构建“最新 WIP 键集”(含 buddy,且命中过滤),用于检测“缺失/删除”
    std::unordered_set<std::string> newWipKeys;
    for (auto* g : wipGlasses) {
        if (!GlassMatchesFilters(*g, m_filters)) continue;
#ifdef _UNICODE
        newWipKeys.insert(CT2A(CString(g->getID().c_str())));
#else
        newWipKeys.insert(g->getID());
#endif
        if (auto* b = g->getBuddy()) {
            if (GlassMatchesFilters(*b, m_filters)) {
#ifdef _UNICODE
                newWipKeys.insert(CT2A(CString(b->getID().c_str())));
#else
                newWipKeys.insert(b->getID());
#endif
            }
        }
    }
    bool needRebuildRemoval = false; // WIP 变少/清空:需要整页重建
    bool needRebuildChildren = false; // 结构变化:新增子
    bool needRebuildAllForNewRoot = false; // 新增根(保证 WIP 仍在顶部)
    std::vector<int> rowsToRedraw;         // 仅文本变化
    // 可见集中有但新数据里没有 -> 触发“删除/减少”的整页重建
    for (const auto& kv : wipRowById) {
        if (newWipKeys.find(kv.first) == newWipKeys.end()) { needRebuildRemoval = true; break; }
    }
    // UI 状态(当需要重建时使用)
    std::vector<CExpandableListCtrl::Node*> savedSel;
    CExpandableListCtrl::Node* savedTop = nullptr;
    // 3) 逐个处理 WIP:已存在 -> 就地更新;必要时“只对根补子项”
    //                 不存在 -> 触发“全量重建”,以保证新 WIP 根行出现在列表顶部
    //                 不存在 -> 优先挂到 buddy 容器;否则触发整页重建(新根保持顶部)
    for (auto* g : wipGlasses) {
        if (!GlassMatchesFilters(*g, m_filters)) continue;
#ifdef _UNICODE
        std::string cid = CT2A(g->getID().c_str());
        std::string cid = CT2A(CString(g->getID().c_str()));
#else
        std::string cid = g->getID();
#endif
@@ -1070,27 +1125,23 @@
            // —— 顺带刷新 buddy 子行(如果它已在可见表里)——
            if (SERVO::CGlass* b = g->getBuddy()) {
                CString buddyCidCs = b->getID().c_str();
#ifdef _UNICODE
                std::string bid = CT2A(buddyCidCs);
                std::string bid = CT2A(CString(b->getID().c_str()));
#else
                std::string bid = buddyCidCs.GetString();
                std::string bid = b->getID();
#endif
                auto itChildAny = wipRowById.find(bid);
                if (itChildAny != wipRowById.end()) {
                    int crow = itChildAny->second.first;
                    // 生成 buddy 的列文本并一次性写回
                    auto bcols = makeColsFromWip(b);
                    ApplyColsToRow(m_listCtrl, crow, bcols);
                    rowsToRedraw.push_back(crow);
                }
                // 如果 buddy 行当前不存在,可保留你原来的“只在根下、且 buddy 不在可见表时补子项”的逻辑
            }
            // —— 只对“根节点”补子项,且仅当 buddy 尚未出现在可见表,且根下也没有该 buddy —— 
            SERVO::CGlass* b = g->getBuddy();
            if (b) {
                // 当前根容器?(子节点不作为容器)
                auto itRoot = wipRootById.find(cid);
                if (itRoot != wipRootById.end()) {
                    CExpandableListCtrl::Node* container = itRoot->second;
@@ -1110,52 +1161,50 @@
                    bool buddyExistsAnywhere = (wipRowById.find(newBid) != wipRowById.end());
                    bool hasChildAlready = NodeHasChildWithClassId(container, newBuddyCid);
                    // —— 关键:关系是否发生变化?(oldChildCid 与 newBuddyCid 不同)
                    // 关系是否发生变化?(oldChildCid 与 newBuddyCid 不同,或有子但现在没 buddy)
                    bool relationChanged =
                        (!oldChildCid.IsEmpty() && newBuddyCid.IsEmpty()) ||                             // 之前有子,现在没 buddy
                        (oldChildCid.IsEmpty() && !newBuddyCid.IsEmpty()) ||                             // 之前没子,现在有 buddy
                        (!oldChildCid.IsEmpty() && !newBuddyCid.IsEmpty() && oldChildCid.CompareNoCase(newBuddyCid) != 0); // 改 buddy
                        (!oldChildCid.IsEmpty() && newBuddyCid.IsEmpty()) ||
                        (oldChildCid.IsEmpty() && !newBuddyCid.IsEmpty()) ||
                        (!oldChildCid.IsEmpty() && !newBuddyCid.IsEmpty() &&
                            oldChildCid.CompareNoCase(newBuddyCid) != 0);
                    if (relationChanged) {
                        // 关系变更走“结构重建”,避免重复或反向挂载
                        needRebuildAllForNewRoot = true;
                        needRebuildAllForNewRoot = true; // 避免重复或反向挂载
                    }
                    else {
                        // 关系未变:若 buddy 还不在可见表且容器下也没有,则补子
                        // 关系未变:若 buddy 不在可见表且容器下也没有,则补子
                        if (!buddyExistsAnywhere && !hasChildAlready) {
                            if (!needRebuildChildren) { CaptureUiState(m_listCtrl, savedSel, savedTop); }
                            needRebuildChildren = true;
                            auto cols = makeColsFromWip(b);
                            auto* ch = m_listCtrl.InsertChild(container, cols);
                            m_listCtrl.SetNodeColor(ch, kWipText, kWipChildBk);         // 子:更浅
                            m_listCtrl.SetNodeColor(container, kWipText, kWipParentBk); // 父:基础绿(兜底纠正)
                            m_listCtrl.SetNodeColor(ch, kWipText, kWipChildBk);  // 子:浅色
                            m_listCtrl.SetNodeColor(container, kWipText, kWipParentBk); // 父:基础绿
                        }
                        // 若已有子:顺带把子行文本刷新一下(比如 AOI 更新)
                        // 若已有子:同步刷新子行文本与颜色
                        else if (hasChildAlready) {
                            // 找到对应子并更新文本/cols,避免后续 Rebuild 倒回旧值
                            for (auto& ch : container->children) {
                                if (ch && ch->cols.size() > 4 && ch->cols[4].CompareNoCase(newBuddyCid) == 0) {
                                    auto cols = makeColsFromWip(b);
                                    ch->cols = cols; // 底层数据
                                    ch->cols = cols; // 更新底层数据
                                    // 可见行刷新
                                    for (int r = 0; r < m_listCtrl.GetItemCount(); ++r) {
                                        if (m_listCtrl.GetNodeByVisibleIndex(r) == ch.get()) {
                                            for (int c = 1; c < (int)cols.size(); ++c) {
                                            for (int c = 1; c < (int)cols.size(); ++c)
                                                m_listCtrl.SetItemText(r, c, cols[c]);
                                                m_listCtrl.SetNodeColor(ch.get(), kWipText, kWipChildBk);   // 保证子行是浅色
                                                m_listCtrl.SetNodeColor(container, kWipText, kWipParentBk); // 父保持基础绿
                                            }
                                            rowsToRedraw.push_back(r);
                                            break;
                                        }
                                    }
                                    m_listCtrl.SetNodeColor(ch.get(), kWipText, kWipChildBk);
                                    m_listCtrl.SetNodeColor(container, kWipText, kWipParentBk);
                                    break;
                                }
                            }
                        }
                    }
                }
                // 当前是“子节点”的情况:一律不挂子,交给重建(若父变更)
                // 若当前是“子节点”,不在这里调整父子关系;让“关系变化”走全量重建
            }
            else {
                // 没有 buddy:如果容器下现在有子,也算关系变化,触发重建
@@ -1168,17 +1217,51 @@
            }
        }
        else {
            // (B) 不存在:新增根行——为保证“WIP 永远在顶部”,触发全量重建
            needRebuildAllForNewRoot = true;
            // (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;
        }
    }
    // 4) 应用 UI 更新
    if (needRebuildAllForNewRoot) {
        // 用 key(ClassID)保存并恢复,避免 Node* 失效
            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 顶部)
        UpdatePageData();                      // 全量重建(WIP 顶部 & 删除无效项)
        RestoreSelectionByKeys(m_listCtrl, selKeys);
        RestoreTopByKey(m_listCtrl, topKey);
    }
SourceCode/Bond/Servo/CPageGlassList.h
@@ -74,5 +74,6 @@
    afx_msg void OnBnClickedButtonExport();
    afx_msg void OnBnClickedButtonPrevPage();
    afx_msg void OnBnClickedButtonNextPage();
    afx_msg void OnShowFullText(NMHDR* pNMHDR, LRESULT* pResult);
    DECLARE_MESSAGE_MAP()
};
SourceCode/Bond/Servo/CServoUtilsTool.cpp
@@ -121,8 +121,8 @@
            if (slot == 0) return "后烘烤A腔";
            if (slot == 1) return "冷却A";
            if (slot == 0) return "后烘烤B腔";
            if (slot == 1) return "冷却B";
            if (slot == 2) return "后烘烤B腔";
            if (slot == 3) return "冷却B";
        }
        if (eqid == EQ_ID_MEASUREMENT) {
SourceCode/Bond/Servo/Model.cpp
@@ -437,6 +437,10 @@
        m_hsmsPassive.requestEventReportSend_Panel_End();
        auto& db = GlassLogDb::Instance();
        db.insertFromCGlass((*(SERVO::CGlass*)pPanel));
        SERVO::CGlass* pBuddy = ((SERVO::CGlass*)pPanel)->getBuddy();
        if (pBuddy != nullptr) {
            db.insertFromCGlass(*pBuddy);
        }
    };
    m_master.setListener(masterListener);
    m_master.setContinuousTransferCount(m_configuration.getContinuousTransferCount());