// CPageGlassList.cpp: 实现文件 // #include "stdafx.h" #include "Servo.h" #include "CPageGlassList.h" #include "afxdialogex.h" #include "GlassJson.h" #include "CServoUtilsTool.h" #include "ToolUnits.h" #include #include #include #include #include #include #include "CProcessDataListDlg.h" #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 SnapshotExpandedKeys(CExpandableListCtrl& lv) { std::unordered_set 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& 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& 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& 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 // ====== 开关:1=启用模拟传感器数据生成;0=使用真实数据 ====== #define USE_MOCK_SENSOR_DATA 0 #if USE_FAKE_DB_DEMO #include #include // CStringA #include #include #include // ---- 模拟记录/分页结构(字段与你现有代码一致)---- 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 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 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& 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), "}" }) }); } } // ---- 做分页切片(可按需加更严格的 filters)---- static FakeDbPage _make_page_fake(const GlassLogDb::Filters& /*f*/, int pageSize, int offset) { std::vector 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 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& 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 SnapshotSelectedKeys(CListCtrl& lv) { std::unordered_set 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 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(CT2A(cls)); #else return std::optional(cls.GetString()); #endif } // 用 ClassID 集合恢复选中 static void RestoreSelectionByKeys(CListCtrl& lv, const std::unordered_set& 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& 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 } } bool CopyUtf8ToClipboard(const std::string& utf8) { // 1) UTF-8 -> UTF-16 长度(含结尾 '\0') int wlen = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, nullptr, 0); if (wlen <= 0) return false; // 2) 为剪贴板分配全局可移动内存(必须 GMEM_MOVEABLE) SIZE_T bytes = static_cast(wlen) * sizeof(wchar_t); HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, bytes); if (!hMem) return false; // 3) 填充 UTF-16 文本 wchar_t* wbuf = static_cast(GlobalLock(hMem)); if (!wbuf) { GlobalFree(hMem); return false; } MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, wbuf, wlen); GlobalUnlock(hMem); // 4) 打开剪贴板并设置数据(CF_UNICODETEXT) if (!OpenClipboard(nullptr)) { GlobalFree(hMem); return false; } if (!EmptyClipboard()) { CloseClipboard(); GlobalFree(hMem); return false; } // 成功后,内存所有权交给剪贴板,不能再 GlobalFree if (!SetClipboardData(CF_UNICODETEXT, hMem)) { CloseClipboard(); GlobalFree(hMem); return false; } CloseClipboard(); return true; } // 辅助函数:将 ANSI CString 写入文件为 UTF-8 编码 bool CPageGlassList::WriteAnsiStringAsUtf8ToFile(const CString& ansiContent, const CString& filePath) { CFile file; if (!file.Open(filePath, CFile::modeCreate | CFile::modeWrite)) { return false; } // 写入 UTF-8 BOM const unsigned char bom[] = { 0xEF, 0xBB, 0xBF }; file.Write(bom, 3); // 将 ANSI 转换为 Unicode int unicodeLength = MultiByteToWideChar(CP_ACP, 0, ansiContent, ansiContent.GetLength(), NULL, 0); if (unicodeLength <= 0) { file.Close(); return false; } wchar_t* unicodeBuffer = new wchar_t[unicodeLength + 1]; MultiByteToWideChar(CP_ACP, 0, ansiContent, ansiContent.GetLength(), unicodeBuffer, unicodeLength); unicodeBuffer[unicodeLength] = 0; // 将 Unicode 转换为 UTF-8 int utf8Length = WideCharToMultiByte(CP_UTF8, 0, unicodeBuffer, unicodeLength, NULL, 0, NULL, NULL); bool success = false; if (utf8Length > 0) { char* utf8Buffer = new char[utf8Length]; WideCharToMultiByte(CP_UTF8, 0, unicodeBuffer, unicodeLength, utf8Buffer, utf8Length, NULL, NULL); file.Write(utf8Buffer, utf8Length); delete[] utf8Buffer; success = true; } delete[] unicodeBuffer; file.Close(); return success; } // CPageGlassList 对话框 IMPLEMENT_DYNAMIC(CPageGlassList, CDialogEx) CPageGlassList::CPageGlassList(CWnd* pParent /*=nullptr*/) : CDialogEx(IDD_PAGE_GLASS_LIST, pParent) { m_crBkgnd = PAGE_BACKGROUND_COLOR; m_hbrBkgnd = nullptr; m_pObserver = nullptr; 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'; } CPageGlassList::~CPageGlassList() { 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); } 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_NOTIFY(ELCN_SHOWFULLTEXT, IDC_LIST_ALARM, &CPageGlassList::OnShowFullText) ON_BN_CLICKED(IDC_BUTTON_EXPORT_ROW, &CPageGlassList::OnBnClickedButtonExportRow) END_MESSAGE_MAP() // ===== 私有小工具 ===== static int GetColumnCount(CListCtrl& lv) { CHeaderCtrl* pHdr = lv.GetHeaderCtrl(); return pHdr ? pHdr->GetItemCount() : 0; } // ===== CPageGlassList 消息处理程序 ===== void CPageGlassList::InitRxWindows() { // 订阅数据 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) { UpdateWipData(); // 只更新,不重建,不改变展开/选择 } pAny->release(); }, [&]() -> void { // onComplete }, [&](IThrowable* pThrowable) -> void { // onError pThrowable->printf(); }); theApp.m_model.getObservable()->observeOn(pRxWindows->mainThread())->subscribe(m_pObserver); } } void CPageGlassList::Resize() { 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; // ===== 分页控件布局 ===== 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); if (pBtnNext && pBtnPrev && pLabelPage) { pBtnNext->MoveWindow(xRight - nButtonWidth, yBottom, nButtonWidth, nButtonHeight); xRight -= nButtonWidth + nSpacing; pLabelPage->MoveWindow(xRight - nLabelWidth, yBottom, nLabelWidth, nButtonHeight); xRight -= nLabelWidth + nSpacing; 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); } } 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); } } 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); } } void CPageGlassList::InitDateTimeControls() { 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(); } void CPageGlassList::UpdatePageData() { m_rebuilding = true; // 放在任何清空/重建动作之前:记录展开的父节点 key(ClassID) auto expandedKeys = SnapshotExpandedKeys(m_listCtrl); // —— 双保险:先清掉可见项,再清树结构 —— m_listCtrl.SetRedraw(FALSE); m_listCtrl.DeleteAllItems(); m_listCtrl.SetRedraw(TRUE); // —— 清空树(依赖 CExpandableListCtrl::ClearTree())—— m_listCtrl.ClearTree(); 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 wipGlasses; theApp.m_model.m_master.getWipGlasses(wipGlasses); std::vector tempGlasses = wipGlasses; // 待释放 auto glassHit = [&](SERVO::CGlass* g) -> bool { return g && GlassMatchesFilters(*g, m_filters); }; std::unordered_set 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 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 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 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) ==================== const int rawLimit = PAGE_SIZE + 1; const int rawOffset = PAGE_SIZE * (m_nCurPage - 1); #if USE_FAKE_DB_DEMO auto page = _make_page_fake(m_filters, rawLimit, rawOffset); #else auto& db = GlassLogDb::Instance(); auto pageFull = db.queryPaged(m_filters, rawLimit, rawOffset); #endif #if !USE_FAKE_DB_DEMO // —— 三元键工具:|C|J —— // // —— 三元键工具:|C|J —— // auto makeKey = [](const std::string& cls, int csn, int jsn) -> std::string { std::string k; k.reserve(cls.size() + 32); k.append(cls); k.push_back('|'); k.push_back('C'); k.append(std::to_string(csn)); k.push_back('|'); k.push_back('J'); k.append(std::to_string(jsn)); return k; }; // ★★★ 这里是关键修复:接收“const Row&”,不要非 const 引用 using RowT = std::decay::type; auto makeKeyR = [&](const RowT& r) -> std::string { return makeKey(r.classId, r.cassetteSeqNo, r.jobSeqNo); }; // 不区分大小写 classId 相等 auto iEquals = [](const std::string& a, const std::string& b) { #ifdef _WIN32 return _stricmp(a.c_str(), b.c_str()) == 0; #else return strcasecmp(a.c_str(), b.c_str()) == 0; #endif }; // —— lookahead 预读:若超出 1 条,尝试把“最后一条”与“预读”判为一对(严格优先)—— std::optional lookahead; if (pageFull.items.size() == rawLimit) { const auto& last = pageFull.items[PAGE_SIZE - 1]; const auto& extra = pageFull.items[PAGE_SIZE]; bool strictPair = (!last.buddyId.empty() && iEquals(last.buddyId, extra.classId) && last.cassetteSeqNo == extra.cassetteSeqNo && last.jobSeqNo == extra.jobSeqNo) || (!extra.buddyId.empty() && iEquals(extra.buddyId, last.classId) && extra.cassetteSeqNo == last.cassetteSeqNo && extra.jobSeqNo == last.jobSeqNo); bool loosePair = (!last.buddyId.empty() && iEquals(last.buddyId, extra.classId)) || (!extra.buddyId.empty() && iEquals(extra.buddyId, last.classId)); if (strictPair || loosePair) { lookahead = extra; } // 预读不算入本页 pageFull.items.pop_back(); } // 之后正常按 page 构建 auto& pageRef = pageFull; // —— 建两个索引 —— // // A) byTriple: 三元键 -> index(唯一/已消费依据) // B) byClass : classId -> indices(buddy 候选池,允许多个) std::unordered_map byTriple; std::unordered_map> byClass; byTriple.reserve(pageRef.items.size()); byClass.reserve(pageRef.items.size()); for (size_t i = 0; i < pageRef.items.size(); ++i) { const auto& r = pageRef.items[i]; byTriple[makeKeyR(r)] = i; byClass[r.classId].push_back(i); } // —— 已消费集合(用三元键)—— std::unordered_set consumed; consumed.reserve(pageRef.items.size()); int zebra = 0; auto zebraBk = [&](int z) -> COLORREF { return (z % 2 == 0) ? RGB(255, 255, 255) : RGB(235, 235, 235); }; // -------- Phase 1: 先处理“有 buddyId 的记录” ---------- for (size_t i = 0; i < pageRef.items.size(); ++i) { const auto& r = pageRef.items[i]; if (consumed.count(makeKeyR(r))) continue; if (r.buddyId.empty()) continue; // 在同页里为 r 找 buddy 候选 size_t buddyIdx = (size_t)-1; auto itVec = byClass.find(r.buddyId); if (itVec != byClass.end()) { const auto& vec = itVec->second; // 1) 严格匹配:Cassette/Job 一致 for (size_t j : vec) { const auto& br = pageRef.items[j]; if (br.cassetteSeqNo == r.cassetteSeqNo && br.jobSeqNo == r.jobSeqNo) { if (!consumed.count(makeKeyR(br))) { buddyIdx = j; break; } } } // 2) 宽松匹配:同 classId 未消费的任意一条 if (buddyIdx == (size_t)-1) { for (size_t j : vec) { const auto& br = pageRef.items[j]; if (!consumed.count(makeKeyR(br))) { buddyIdx = j; break; } } } } COLORREF bk = zebraBk(zebra); if (buddyIdx != (size_t)-1) { const auto& br = pageRef.items[buddyIdx]; // 父:r(有 buddyId),子:br std::vector 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 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(makeKeyR(r)); consumed.insert(makeKeyR(br)); ++zebra; continue; } // 没找到 buddy → 插占位子行(只写 ClassID) std::vector 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), zebraBk(zebra)); std::vector ccols(colCount); ccols[4] = r.buddyId.c_str(); // 占位 auto* nChild = m_listCtrl.InsertChild(nParent, ccols); m_listCtrl.SetNodeColor(nChild, RGB(0, 0, 0), zebraBk(zebra)); consumed.insert(makeKeyR(r)); ++zebra; } // -------- Phase 2: 剩余未消费的,作为“单条根行” ---------- for (size_t i = 0; i < pageRef.items.size(); ++i) { const auto& r = pageRef.items[i]; if (consumed.count(makeKeyR(r))) continue; COLORREF bk = zebraBk(zebra); std::vector cols(colCount); cols[1] = std::to_string(r.id).c_str(); cols[2] = std::to_string(r.cassetteSeqNo).c_str(); cols[3] = std::to_string(r.jobSeqNo).c_str(); cols[4] = r.classId.c_str(); cols[5] = SERVO::CServoUtilsTool::getMaterialsTypeText((SERVO::MaterialsType)r.materialType).c_str(); cols[6] = SERVO::CServoUtilsTool::getGlassStateText((SERVO::GlsState)r.state).c_str(); cols[7] = r.tStart.c_str(); cols[8] = r.tEnd.c_str(); cols[9] = r.buddyId.c_str(); cols[10] = SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)r.aoiResult).c_str(); cols[11] = r.path.c_str(); cols[12] = r.params.c_str(); auto* n = m_listCtrl.InsertRoot(cols); m_listCtrl.SetNodeColor(n, RGB(0, 0, 0), bk); consumed.insert(makeKeyR(r)); ++zebra; } // 一次性重绘 m_listCtrl.RebuildVisible(); #else // ===== DEMO 分支(保持原样;若要演示同样逻辑,可仿照上面改造)===== // 如果多出一条,看看它是否是“本页最后一条”的 buddy std::optional lookahead; auto iEquals = [](const std::string& a, const std::string& b) { #ifdef _WIN32 return _stricmp(a.c_str(), b.c_str()) == 0; #else return strcasecmp(a.c_str(), b.c_str()) == 0; #endif }; if (page.items.size() == rawLimit) { const auto& last = page.items[PAGE_SIZE - 1]; const auto& extra = page.items[PAGE_SIZE]; bool pair = (!last.buddyId.empty() && iEquals(last.buddyId, extra.classId)) || (!extra.buddyId.empty() && iEquals(extra.buddyId, last.classId)); if (pair) lookahead = extra; page.items.pop_back(); } // 你可以把 DEMO 分支也切到三元键逻辑;这里从略 auto& pageRef = page; std::unordered_map idxById; idxById.reserve(pageRef.items.size()); for (size_t i = 0; i < pageRef.items.size(); ++i) idxById[pageRef.items[i].classId] = i; std::unordered_set consumed; int zebra = 0; auto zebraBk = [&](int z) -> COLORREF { return (z % 2 == 0) ? RGB(255, 255, 255) : RGB(235, 235, 235); }; for (size_t i = 0; i < pageRef.items.size(); ++i) { const auto& r = pageRef.items[i]; if (consumed.count(r.classId)) continue; if (!r.buddyId.empty()) { auto it = idxById.find(r.buddyId); if (it != idxById.end()) { const auto& br = pageRef.items[it->second]; if (!consumed.count(br.classId)) { COLORREF bk = zebraBk(zebra); std::vector pcols(colCount), ccols(colCount); pcols[1] = std::to_string(r.id).c_str(); pcols[2] = std::to_string(r.cassetteSeqNo).c_str(); pcols[3] = std::to_string(r.jobSeqNo).c_str(); pcols[4] = r.classId.c_str(); pcols[5] = SERVO::CServoUtilsTool::getMaterialsTypeText((SERVO::MaterialsType)r.materialType).c_str(); pcols[6] = SERVO::CServoUtilsTool::getGlassStateText((SERVO::GlsState)r.state).c_str(); pcols[7] = r.tStart.c_str(); pcols[8] = r.tEnd.c_str(); pcols[9] = r.buddyId.c_str(); pcols[10] = SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)r.aoiResult).c_str(); pcols[11] = r.path.c_str(); pcols[12] = r.params.c_str(); auto* nParent = m_listCtrl.InsertRoot(pcols); MaybeRestoreExpandByKey(nParent, expandedKeys); m_listCtrl.SetNodeColor(nParent, RGB(0, 0, 0), bk); ccols[1] = std::to_string(br.id).c_str(); ccols[2] = std::to_string(br.cassetteSeqNo).c_str(); ccols[3] = std::to_string(br.jobSeqNo).c_str(); ccols[4] = br.classId.c_str(); ccols[5] = SERVO::CServoUtilsTool::getMaterialsTypeText((SERVO::MaterialsType)br.materialType).c_str(); ccols[6] = SERVO::CServoUtilsTool::getGlassStateText((SERVO::GlsState)br.state).c_str(); ccols[7] = br.tStart.c_str(); ccols[8] = br.tEnd.c_str(); ccols[9] = br.buddyId.c_str(); ccols[10] = SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)br.aoiResult).c_str(); ccols[11] = br.path.c_str(); ccols[12] = br.params.c_str(); auto* nChild = m_listCtrl.InsertChild(nParent, ccols); m_listCtrl.SetNodeColor(nChild, RGB(0, 0, 0), bk); consumed.insert(r.classId); consumed.insert(br.classId); ++zebra; continue; } } // 插占位子 COLORREF bk = zebraBk(zebra); std::vector pcols(colCount), ccols(colCount); pcols[1] = std::to_string(r.id).c_str(); pcols[2] = std::to_string(r.cassetteSeqNo).c_str(); pcols[3] = std::to_string(r.jobSeqNo).c_str(); pcols[4] = r.classId.c_str(); pcols[5] = SERVO::CServoUtilsTool::getMaterialsTypeText((SERVO::MaterialsType)r.materialType).c_str(); pcols[6] = SERVO::CServoUtilsTool::getGlassStateText((SERVO::GlsState)r.state).c_str(); pcols[7] = r.tStart.c_str(); pcols[8] = r.tEnd.c_str(); pcols[9] = r.buddyId.c_str(); pcols[10] = SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)r.aoiResult).c_str(); pcols[11] = r.path.c_str(); pcols[12] = r.params.c_str(); auto* nParent = m_listCtrl.InsertRoot(pcols); MaybeRestoreExpandByKey(nParent, expandedKeys); m_listCtrl.SetNodeColor(nParent, RGB(0, 0, 0), bk); ccols[4] = r.buddyId.c_str(); auto* nChild = m_listCtrl.InsertChild(nParent, ccols); m_listCtrl.SetNodeColor(nChild, RGB(0, 0, 0), bk); consumed.insert(r.classId); ++zebra; } } for (size_t i = 0; i < pageRef.items.size(); ++i) { const auto& r = pageRef.items[i]; if (consumed.count(r.classId)) continue; COLORREF bk = zebraBk(zebra); std::vector cols(colCount); cols[1] = std::to_string(r.id).c_str(); cols[2] = std::to_string(r.cassetteSeqNo).c_str(); cols[3] = std::to_string(r.jobSeqNo).c_str(); cols[4] = r.classId.c_str(); cols[5] = SERVO::CServoUtilsTool::getMaterialsTypeText((SERVO::MaterialsType)r.materialType).c_str(); cols[6] = SERVO::CServoUtilsTool::getGlassStateText((SERVO::GlsState)r.state).c_str(); cols[7] = r.tStart.c_str(); cols[8] = r.tEnd.c_str(); cols[9] = r.buddyId.c_str(); cols[10] = SERVO::CServoUtilsTool::getInspResultText((SERVO::InspResult)r.aoiResult).c_str(); cols[11] = r.path.c_str(); cols[12] = r.params.c_str(); auto* n = m_listCtrl.InsertRoot(cols); m_listCtrl.SetNodeColor(n, RGB(0, 0, 0), bk); consumed.insert(r.classId); ++zebra; } m_listCtrl.RebuildVisible(); #endif // 上一页 / 下一页 UpdatePageControls(); m_rebuilding = false; } void CPageGlassList::UpdatePageControls() { 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); } BOOL CPageGlassList::OnInitDialog() { CDialogEx::OnInitDialog(); // 定时器:1=初始化订阅,2=周期刷新(只增量) SetTimer(1, 3000, nullptr); SetTimer(2, 2000, nullptr); // 下拉框控件 InitStatusCombo(); InitTimeRangeCombo(); // 日期控件 InitDateTimeControls(); // 报表控件 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; 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); CString headers[] = { _T(""), _T("id"), _T("Cassette SN"), _T("Job SN"), _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(); // 触发一次查询与首屏填充 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; } 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; } // 保存列宽(首列兜底,避免把 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(); } void CPageGlassList::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent == 1) { KillTimer(1); InitRxWindows(); } else if (nIDEvent == 2) { UpdateWipData(); // 只做增量,不重建 } 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); } 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); } } void CPageGlassList::OnBnClickedButtonSearch() { // 获取关键字输入框内容 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; } #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(); } void CPageGlassList::OnBnClickedButtonExport() { CFileDialog fileDialog(FALSE, _T("csv"), NULL, OFN_HIDEREADONLY, _T("CSV Files (*.csv)|*.csv||")); if (fileDialog.DoModal() != IDOK) { return; } // 导出 CSV:导出符合 filters 的“全部记录”(不受分页限制) auto& db = GlassLogDb::Instance(); std::string csvPath((LPTSTR)(LPCTSTR)fileDialog.GetPathName()); if (db.exportCsv(csvPath, m_filters) > 0) { AfxMessageBox("导出CSV成功!"); } } void CPageGlassList::OnBnClickedButtonExportRow() { int nSelected = m_listCtrl.GetSelectionMark(); if (nSelected == -1) { AfxMessageBox(_T("请先选择一行记录!")); return; } // 直接从第一列获取 ID CString strId = m_listCtrl.GetItemText(nSelected, 1); if (strId.IsEmpty()) { AfxMessageBox(_T("WIP记录暂不支持保存")); return; } // 数据库记录 long long recordId = _ttoi64(strId); // 从数据库查询完整记录 auto& db = GlassLogDb::Instance(); auto row = db.queryById(recordId); if (!row) { AfxMessageBox(_T("查询记录失败")); return; } // 使用 Glass ID 构建默认文件名 CString strDefaultFileName; CString strGlassId = row->classId.c_str(); // 移除文件名中的非法字符 CString strSanitizedGlassId = strGlassId; strSanitizedGlassId.Remove('\\'); strSanitizedGlassId.Remove('/'); strSanitizedGlassId.Remove(':'); strSanitizedGlassId.Remove('*'); strSanitizedGlassId.Remove('?'); strSanitizedGlassId.Remove('"'); strSanitizedGlassId.Remove('<'); strSanitizedGlassId.Remove('>'); strSanitizedGlassId.Remove('|'); strDefaultFileName.Format(_T("Glass_%s.csv"), strSanitizedGlassId); // 文件保存对话框,设置默认文件名 CFileDialog fileDialog(FALSE, _T("csv"), strDefaultFileName, OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT, _T("CSV Files (*.csv)|*.csv|JSON Files (*.json)|*.json||")); if (fileDialog.DoModal() != IDOK) return; CString filePath = fileDialog.GetPathName(); CString fileExt = fileDialog.GetFileExt(); if (fileExt.CompareNoCase(_T("json")) == 0) { ExportToJson(*row, filePath); } else { ExportToCsv(*row, filePath); } } void CPageGlassList::ExportToJson(const GlassLogDb::Row& row, const CString& filePath) { // 保存为 JSON if (!row.pretty.empty()) { CFile file; if (file.Open(filePath, CFile::modeCreate | CFile::modeWrite)) { file.Write(row.pretty.c_str(), (UINT)row.pretty.length()); file.Close(); CString strSuccess; strSuccess.Format(_T("记录已保存为JSON文件:\n%s"), filePath); AfxMessageBox(strSuccess); } else { AfxMessageBox(_T("保存文件失败")); } } else { AfxMessageBox(_T("该记录没有JSON数据")); } } void CPageGlassList::ExportToCsv(const GlassLogDb::Row& row, const CString& filePath) { CString csvContent; // === 第一部分:基础信息 === ExportBasicInfo(csvContent, row); // === 第二部分:工艺参数 === ExportProcessParams(csvContent, row); // === 第三部分:传感器数据详情 === ExportSensorData(csvContent, row); // 使用辅助函数保存为 UTF-8 编码 if (WriteAnsiStringAsUtf8ToFile(csvContent, filePath)) { CString strSuccess; strSuccess.Format(_T("记录已保存为CSV文件:\n%s"), filePath); AfxMessageBox(strSuccess); } else { AfxMessageBox(_T("保存文件失败")); } } void CPageGlassList::ExportBasicInfo(CString& csvContent, const GlassLogDb::Row& row) { csvContent += _T("=== 基础信息 ===\n"); csvContent += _T("ID,Cassette序列号,Job序列号,Glass ID,物料类型,状态,开始时间,结束时间,绑定Glass ID,AOI结果,路径\n"); CString baseInfoRow; baseInfoRow.Format(_T("%lld,%d,%d,%s,%d,%d,%s,%s,%s,%d,%s\n"), row.id, row.cassetteSeqNo, row.jobSeqNo, CString(row.classId.c_str()), row.materialType, row.state, CString(row.tStart.c_str()), CString(row.tEnd.c_str()), CString(row.buddyId.c_str()), row.aoiResult, CString(row.path.c_str())); csvContent += baseInfoRow; } void CPageGlassList::ExportProcessParams(CString& csvContent, const GlassLogDb::Row& row) { csvContent += _T("\n=== 工艺参数 ===\n"); // 如果有 pretty 字段,解析工艺参数 if (!row.pretty.empty()) { SERVO::CGlass tempGlass; if (GlassJson::FromString(row.pretty, tempGlass)) { auto& params = tempGlass.getParams(); if (!params.empty()) { // 工艺参数表头 csvContent += _T("参数名称,参数ID,数值,机器单元\n"); // 工艺参数数据 for (auto& param : params) { CString paramRow; CString valueStr; // 根据参数类型格式化数值 if (param.getValueType() == PVT_INT) { valueStr.Format(_T("%d"), param.getIntValue()); } else { valueStr.Format(_T("%.3f"), param.getDoubleValue()); } paramRow.Format(_T("%s,%s,%s,%s\n"), CString(param.getName().c_str()), CString(param.getId().c_str()), valueStr, CString(param.getUnit().c_str())); csvContent += paramRow; } } else { csvContent += _T("无工艺参数数据\n"); } } else { csvContent += _T("无法解析工艺参数\n"); } } else { csvContent += _T("无工艺参数数据\n"); } } void CPageGlassList::ExportSensorData(CString& csvContent, const GlassLogDb::Row& row) { csvContent += _T("\n=== 传感器数据详情 ===\n"); // 如果有 pretty 字段,解析传感器数据 if (!row.pretty.empty()) { SERVO::CGlass tempGlass; if (GlassJson::FromString(row.pretty, tempGlass)) { #if USE_MOCK_SENSOR_DATA // 生成模拟的SVData用于测试 GenerateMockSVData(tempGlass); #endif // 对每个机器生成表格 for (const auto& machinePair : tempGlass.getAllSVData()) { int machineId = machinePair.first; const auto& dataByType = machinePair.second; CString machineName = CString(SERVO::CServoUtilsTool::getEqName(machineId).c_str()); csvContent += _T("\n[") + machineName + _T("]\n"); if (dataByType.empty()) { csvContent += _T("No sensor data\n"); continue; } auto columnOrder = getMachineColumnOrder(machineId, &dataByType); if (columnOrder.empty()) { csvContent += _T("No exportable columns\n"); continue; } CString header = _T("Timestamp(ms),LocalTime"); for (const auto& dataType : columnOrder) { header += _T(","); header += CString(dataType.c_str()); } header += _T("\n"); csvContent += header; auto baselineIt = std::find_if(columnOrder.begin(), columnOrder.end(), [&](const std::string& type) { auto dataIt = dataByType.find(type); return dataIt != dataByType.end() && !dataIt->second.empty(); }); if (baselineIt == columnOrder.end()) { csvContent += _T("No usable time series\n"); continue; } const auto& timeSeries = dataByType.at(*baselineIt); for (size_t i = 0; i < timeSeries.size(); ++i) { auto timestamp = timeSeries[i].timestamp; auto ms = timePointToMs(timestamp); CString row; row.Format(_T("%lld,"), ms); CString localTime = CString(timePointToString(timestamp).c_str()); row += localTime; for (const auto& dataType : columnOrder) { row += _T(","); auto dataTypeIt = dataByType.find(dataType); if (dataTypeIt != dataByType.end() && i < dataTypeIt->second.size()) { CString valueStr; valueStr.Format(_T("%.3f"), dataTypeIt->second[i].value); row += valueStr; } else { row += _T("N/A"); } } row += _T("\n"); csvContent += row; } } } else { csvContent += _T("无法解析传感器数据\n"); } } else { csvContent += _T("无传感器数据\n"); } } void CPageGlassList::OnBnClickedButtonPrevPage() { if (m_nCurPage > 1) { m_nCurPage--; UpdatePageData(); } } void CPageGlassList::OnBnClickedButtonNextPage() { if (m_nCurPage < m_nTotalPages) { m_nCurPage++; UpdatePageData(); } } void CPageGlassList::OnShowFullText(NMHDR* pNMHDR, LRESULT* pResult) { auto* p = reinterpret_cast(pNMHDR); // 对话框显示工艺参数 if (p->iSubItem == 12) { CProcessDataListDlg dlg; dlg.setRawText(p->text); dlg.DoModal(); } else { AfxMessageBox(p->text); } *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> wipRowById; std::unordered_map 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 wipGlasses; theApp.m_model.m_master.getWipGlasses(wipGlasses); std::vector tempRetain = wipGlasses; // 稍后统一 release auto makeColsFromWip = [&](SERVO::CGlass* g) { std::vector 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 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 rowsToRedraw; // 仅文本变化 // 可见集中有但新数据里没有 -> 触发“删除/减少”的整页重建 for (const auto& kv : wipRowById) { if (newWipKeys.find(kv.first) == newWipKeys.end()) { needRebuildRemoval = true; break; } } // UI 状态(当需要重建时使用) std::vector savedSel; CExpandableListCtrl::Node* savedTop = nullptr; // 3) 逐个处理 WIP:已存在 -> 就地更新;必要时“只对根补子项” // 不存在 -> 挂到 buddy 容器;若 buddy 不在可见表,触发全量重建(保证 WIP 顶部) 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); } } // —— 只对“根节点”补子项 —— 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); // 关系是否发生变化? 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) 不存在:新增 SERVO::CGlass* b = g->getBuddy(); CExpandableListCtrl::Node* container = nullptr; if (b) { #ifdef _UNICODE std::string bid = CT2A(CString(b->getID().c_str())); #else std::string bid = b->getID(); #endif auto itB = wipRowById.find(bid); if (itB != wipRowById.end()) { CExpandableListCtrl::Node* buddyNode = itB->second.second; container = buddyNode ? (buddyNode->parent ? buddyNode->parent : buddyNode) : nullptr; } } if (container) { CString cidCs = g->getID().c_str(); if (!NodeHasChildWithClassId(container, cidCs)) { if (!needRebuildChildren) { CaptureUiState(m_listCtrl, savedSel, savedTop); } needRebuildChildren = true; auto cols = makeColsFromWip(g); auto* ch = m_listCtrl.InsertChild(container, cols); // 子:更浅;父:基础绿(兜底) m_listCtrl.SetNodeColor(ch, kWipText, kWipChildBk); m_listCtrl.SetNodeColor(container, kWipText, kWipParentBk); } } else { // buddy 不在可见表:为了保持“WIP 永远在顶部”,触发一次全量重建 needRebuildAllForNewRoot = true; } } } // 4) 应用 UI 更新 —— 把“删除/减少”的情况并入全量重建分支 if (needRebuildAllForNewRoot || 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& /*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*/) { // 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; } // 3) 时间(与 DB 保持一致:默认按 t_start 过滤;需要可切到 t_end) if (f.tStartFrom || f.tStartTo) { std::optional 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; } BOOL CPageGlassList::PreTranslateMessage(MSG* pMsg) { if (pMsg->wParam == VK_RETURN || pMsg->wParam == VK_ESCAPE) { return TRUE; } return CDialogEx::PreTranslateMessage(pMsg); } // 获取机器预定义的列顺序 std::vector CPageGlassList::getMachineColumnOrder(int machineId, const std::unordered_map>* actualData) { std::vector columnOrder; auto dataTypes = SERVO::CServoUtilsTool::getEqDataTypes(); auto it = dataTypes.find(machineId); if (actualData != nullptr) { if (it != dataTypes.end()) { for (const auto& name : it->second) { if (actualData->find(name) != actualData->end()) { columnOrder.push_back(name); } } } for (const auto& kv : *actualData) { if (std::find(columnOrder.begin(), columnOrder.end(), kv.first) == columnOrder.end()) { columnOrder.push_back(kv.first); } } return columnOrder; } if (it != dataTypes.end()) { columnOrder = it->second; } return columnOrder; } // 时间戳转换为字符串 std::string CPageGlassList::timePointToString(const std::chrono::system_clock::time_point& tp) { auto time_t = std::chrono::system_clock::to_time_t(tp); std::tm tm; localtime_s(&tm, &time_t); char buffer[20]; std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &tm); return buffer; } // 时间戳转换为毫秒 int64_t CPageGlassList::timePointToMs(const std::chrono::system_clock::time_point& tp) { return std::chrono::duration_cast(tp.time_since_epoch()).count(); } // 生成模拟的SVData用于测试 void CPageGlassList::GenerateMockSVData(SERVO::CGlass& glass) { // 获取设备数据类型配置 auto& dataTypes = SERVO::CServoUtilsTool::getEqDataTypes(); // 为每个设备生成模拟数据 for (const auto& machinePair : dataTypes) { int machineId = machinePair.first; const auto& dataTypeList = machinePair.second; std::vector filteredTypes; if (machineId == EQ_ID_VACUUMBAKE || machineId == EQ_ID_BAKE_COOLING) { const char activePrefix = 'A'; for (const auto& dataType : dataTypeList) { if (!dataType.empty() && dataType[0] == activePrefix) { filteredTypes.push_back(dataType); } } } const auto& typeList = filteredTypes.empty() ? dataTypeList : filteredTypes; // 生成时间序列:从当前时间往前推10分钟,每1秒一个数据点 auto now = std::chrono::system_clock::now(); auto startTime = now - std::chrono::minutes(10); // 为每个数据类型生成模拟数据 for (const auto& dataType : typeList) { std::vector mockData; // 生成600个数据点(10分钟 * 60个点/分钟) for (int i = 0; i < 600; ++i) { auto timestamp = startTime + std::chrono::seconds(i * 1); // 根据设备类型和数据类型生成不同的模拟值 double value = GenerateMockValue(machineId, dataType, i); mockData.emplace_back(timestamp, value); } // 将模拟数据添加到glass对象中 glass.addSVData(machineId, dataType, mockData); } } } // 根据设备类型和数据类型生成模拟数值 double CPageGlassList::GenerateMockValue(int machineId, const std::string& dataType, int index) { // 基础值范围 double baseValue = 0.0; double variation = 0.0; // 根据设备类型设置基础值 switch (machineId) { case EQ_ID_Bonder1: case EQ_ID_Bonder2: if (dataType.find("压力") != std::string::npos) { baseValue = 50.0; // 压力基础值 variation = 10.0; // 压力变化范围 } else if (dataType.find("温度") != std::string::npos) { baseValue = 180.0; // 温度基础值 variation = 5.0; // 温度变化范围 } else if (dataType.find("扩展值") != std::string::npos) { baseValue = 100.0; // 扩展值基础值 variation = 15.0; // 扩展值变化范围 } break; case EQ_ID_VACUUMBAKE: if (dataType.find("扩展值") != std::string::npos) { baseValue = 80.0; variation = 12.0; } else if (dataType.find("温度") != std::string::npos) { baseValue = 200.0; variation = 8.0; } break; case EQ_ID_BAKE_COOLING: if (dataType.find("温度") != std::string::npos) { baseValue = 25.0; // 冷却温度 variation = 3.0; } break; default: baseValue = 50.0; variation = 5.0; break; } // 添加时间相关的趋势和随机变化 double timeTrend = sin(index * 0.1) * 2.0; // 正弦波趋势 double randomNoise = (rand() % 100 - 50) / 100.0 * variation * 0.3; // 随机噪声 return baseValue + timeTrend + randomNoise; }