// CPageGlassList.cpp: 实现文件
|
//
|
|
#include "stdafx.h"
|
#include "Servo.h"
|
#include "CPageGlassList.h"
|
#include "afxdialogex.h"
|
#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), "}" })
|
});
|
}
|
}
|
|
|
// ---- 做分页切片(可按需加更严格的 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)
|
{
|
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<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);
|
}
|
|
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(), 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();
|
}
|
}
|
|
void CPageGlassList::OnBnClickedButtonNextPage()
|
{
|
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();
|
}
|