| | |
| | | #include "GlassJson.h" |
| | | #include "CServoUtilsTool.h" |
| | | #include "ToolUnits.h" |
| | | #include <optional> |
| | | #include <unordered_set> |
| | | #include <unordered_map> |
| | | #include <vector> |
| | | #include <string> |
| | | #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<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 |
| | | } |
| | | } |
| | | |
| | | 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<SIZE_T>(wlen) * sizeof(wchar_t); |
| | | HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, bytes); |
| | | if (!hMem) return false; |
| | | |
| | | // 3) 填充 UTF-16 文本 |
| | | wchar_t* wbuf = static_cast<wchar_t*>(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) |
| | | : 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) |
| | | ON_BN_CLICKED(IDC_BUTTON_EXPORT_ROW, &CPageGlassList::OnBnClickedButtonExportRow) |
| | | END_MESSAGE_MAP() |
| | | |
| | | |
| | | // CPageGlassList 消息处理程序 |
| | | void CPageGlassList::InitRxWindow() |
| | | // ===== 私有小工具 ===== |
| | | static int GetColumnCount(CListCtrl& lv) |
| | | { |
| | | /* 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(); |
| | | CHeaderCtrl* pHdr = lv.GetHeaderCtrl(); |
| | | return pHdr ? pHdr->GetItemCount() : 0; |
| | | } |
| | | |
| | | if (RX_CODE_EQ_ROBOT_TASK == code) { |
| | | UpdatePageData(); |
| | | } |
| | | // ===== 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(); |
| | | |
| | | pAny->release(); |
| | | }, [&]() -> void { |
| | | // onComplete |
| | | }, [&](IThrowable* pThrowable) -> void { |
| | | // onErrorm |
| | | pThrowable->printf(); |
| | | }); |
| | | if (RX_CODE_EQ_ROBOT_TASK == code) { |
| | | UpdateWipData(); // 只更新,不重建,不改变展开/选择 |
| | | } |
| | | |
| | | theApp.m_model.getObservable()->observeOn(pRxWindows->mainThread())->subscribe(m_pObserver); |
| | | } |
| | | 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); |
| | | 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() |
| | | { |
| | | // 查询 |
| | | auto& db = GlassLogDb::Instance(); |
| | | auto page = db.queryPaged(m_filters, PAGE_SIZE, PAGE_SIZE * (m_nCurPage - 1)); |
| | | m_listCtrl.DeleteAllItems(); |
| | | 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_rebuilding = true; |
| | | |
| | | // 测试反序列化 |
| | | /* |
| | | SERVO::CGlass g2; |
| | | std::string err; |
| | | if (GlassJson::FromString(r.pretty, g2, &err)) { |
| | | AfxMessageBox(r.pretty.c_str()); |
| | | } |
| | | */ |
| | | } |
| | | // 放在任何清空/重建动作之前:记录展开的父节点 key(ClassID) |
| | | auto expandedKeys = SnapshotExpandedKeys(m_listCtrl); |
| | | |
| | | // 上一页 / 下一页 |
| | | UpdatePageControls(); |
| | | // —— 双保险:先清掉可见项,再清树结构 —— |
| | | 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<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) ==================== |
| | | 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 |
| | | // —— 三元键工具:<classId>|C<cassette>|J<job> —— // |
| | | // —— 三元键工具:<classId>|C<cassette>|J<job> —— // |
| | | 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<decltype(pageFull.items.front())>::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<decltype(pageFull.items)::value_type> 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<std::string, size_t> byTriple; |
| | | std::unordered_map<std::string, std::vector<size_t>> 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<std::string> 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<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(makeKeyR(r)); |
| | | consumed.insert(makeKeyR(br)); |
| | | ++zebra; |
| | | continue; |
| | | } |
| | | |
| | | // 没找到 buddy → 插占位子行(只写 ClassID) |
| | | 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), zebraBk(zebra)); |
| | | |
| | | std::vector<CString> 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<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(makeKeyR(r)); |
| | | ++zebra; |
| | | } |
| | | |
| | | // 一次性重绘 |
| | | m_listCtrl.RebuildVisible(); |
| | | |
| | | #else |
| | | // ===== DEMO 分支(保持原样;若要演示同样逻辑,可仿照上面改造)===== |
| | | // 如果多出一条,看看它是否是“本页最后一条”的 buddy |
| | | std::optional<decltype(page.items)::value_type> 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<std::string, size_t> 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<std::string> 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<CString> 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<CString> 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<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(); |
| | | #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); |
| | | 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); |
| | | // 定时器: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 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(); |
| | | 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(); |
| | | } |
| | | CDialogEx::OnTimer(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); |
| | | |
| | | // 更新日期过滤器和页面数据 |
| | | // 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::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.json"), strSanitizedGlassId); |
| | | |
| | | // 文件保存对话框,设置默认文件名 |
| | | CFileDialog fileDialog(FALSE, _T("json"), strDefaultFileName, |
| | | OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT, |
| | | _T("JSON Files (*.json)|*.json|CSV Files (*.csv)|*.csv||")); |
| | | |
| | | 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(), 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)) { |
| | | // 对每个机器生成表格 |
| | | for (const auto& machinePair : tempGlass.getAllSVData()) { |
| | | int machineId = machinePair.first; |
| | | CString machineName = CString(SERVO::CServoUtilsTool::getEqName(machineId).c_str()); |
| | | |
| | | csvContent += _T("\n[") + machineName + _T("]\n"); |
| | | |
| | | // 获取该机器的预定义列顺序 |
| | | auto columnOrder = getMachineColumnOrder(machineId); |
| | | |
| | | if (columnOrder.empty()) { |
| | | csvContent += _T("无预定义列配置\n"); |
| | | continue; |
| | | } |
| | | |
| | | // 构建表头 - 直接使用中文列名 |
| | | CString header = _T("时间戳(ms),本地时间"); |
| | | for (const auto& dataType : columnOrder) { |
| | | header += _T(","); |
| | | header += CString(dataType.c_str()); // 直接使用中文列名 |
| | | } |
| | | header += _T("\n"); |
| | | csvContent += header; |
| | | |
| | | // 检查是否有数据 |
| | | if (machinePair.second.empty()) { |
| | | csvContent += _T("无传感器数据\n"); |
| | | continue; |
| | | } |
| | | |
| | | // 使用第一个数据类型的时间序列作为基准 |
| | | const std::string& firstDataType = columnOrder[0]; |
| | | auto firstDataTypeIt = machinePair.second.find(firstDataType); |
| | | if (firstDataTypeIt == machinePair.second.end() || firstDataTypeIt->second.empty()) { |
| | | csvContent += _T("无基准数据类型数据\n"); |
| | | continue; |
| | | } |
| | | |
| | | const auto& timeSeries = firstDataTypeIt->second; |
| | | |
| | | // 对于每个时间点,输出一行数据 |
| | | 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 = machinePair.second.find(dataType); |
| | | if (dataTypeIt != machinePair.second.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(); |
| | | } |
| | | 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); |
| | | |
| | | // 对话框显示工艺参数 |
| | | CProcessDataListDlg dlg; |
| | | dlg.setRawText(p->text); |
| | | dlg.DoModal(); |
| | | |
| | | *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 容器;若 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<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*/) |
| | | { |
| | | // 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<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; |
| | | } |
| | | |
| | | BOOL CPageGlassList::PreTranslateMessage(MSG* pMsg) |
| | | { |
| | | if (pMsg->wParam == VK_RETURN || pMsg->wParam == VK_ESCAPE) { |
| | | return TRUE; |
| | | } |
| | | |
| | | return CDialogEx::PreTranslateMessage(pMsg); |
| | | } |
| | | |
| | | // 获取机器预定义的列顺序 |
| | | std::vector<std::string> CPageGlassList::getMachineColumnOrder(int machineId) |
| | | { |
| | | auto dataTypes = SERVO::CServoUtilsTool::getEqDataTypes(); |
| | | auto it = dataTypes.find(machineId); |
| | | return it != dataTypes.end() ? it->second : std::vector<std::string>(); |
| | | } |
| | | |
| | | // 时间戳转换为字符串 |
| | | 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<std::chrono::milliseconds>(tp.time_since_epoch()).count(); |
| | | } |