#include "panels/AnomalyListPanel.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Theme.hpp" #include "model/ColorScale.hpp" namespace geopro::app { namespace { // 由 localPts 算「位置(质心x)·深(质心y)·尺寸(包络对角)」摘要文本。 // 异常坐标在剖面距离/深度空间(x=距离米, y=深度米)。 QString summarize(const geopro::core::Anomaly& a) { if (a.localPts.empty()) return QStringLiteral("(无几何)"); double cx = 0.0, cy = 0.0; double minx = a.localPts[0].x, maxx = a.localPts[0].x; double miny = a.localPts[0].y, maxy = a.localPts[0].y; for (const auto& p : a.localPts) { cx += p.x; cy += p.y; if (p.x < minx) minx = p.x; if (p.x > maxx) maxx = p.x; if (p.y < miny) miny = p.y; if (p.y > maxy) maxy = p.y; } const auto n = static_cast(a.localPts.size()); cx /= n; cy /= n; const double span = std::hypot(maxx - minx, maxy - miny); return QStringLiteral("位置 %1m · 深 %2m · 尺寸 %3m") .arg(cx, 0, 'f', 0) .arg(cy, 0, 'f', 0) .arg(span, 0, 'f', 0); } // lineColor 字符串 → QColor(兼容 "#RRGGBB" 与 "rgba(...)")。 QColor barColor(const QString& s) { const auto c = geopro::core::parseColor(s.toStdString(), geopro::core::AlphaScale::Bit255); return QColor(c.r, c.g, c.b); } // 异常分级 → 状态语义键(规范 §1.4/§6.3/§8.3:高=Danger 中=Warning 低=Info 未知=Neutral)。 // Anomaly 数据模型不带显式 level 字段,故按可得信号稳健推断: // 1) typeName/name 含「高/中/低」字样 → 直接定级; // 2) 否则按 lineColor 色相归类(红→高 橙/黄→中 蓝→低,与右栏列表/三维标注牌同色); // 3) 仍无法判定 → Neutral(停用/未知),避免乱给状态色。 // 返回值为状态 token 前缀("danger"/"warning"/"info"/"neutral"),调用方据此拼 token 名。 QString anomalyStatus(const geopro::core::Anomaly& a) { const QString tag = QString::fromStdString(a.typeName + a.name); if (tag.contains(QStringLiteral("高"))) return QStringLiteral("danger"); if (tag.contains(QStringLiteral("中"))) return QStringLiteral("warning"); if (tag.contains(QStringLiteral("低"))) return QStringLiteral("info"); // 按 lineColor 色相归类(HSV 色相环:红≈0/360 橙黄≈20–70 蓝≈190–260)。 const QColor c = barColor(QString::fromStdString(a.lineColor)); if (c.isValid() && c.saturationF() > 0.25) { const int h = c.hue(); // -1=无色相(灰) if (h >= 0) { if (h < 20 || h >= 330) return QStringLiteral("danger"); if (h < 75) return QStringLiteral("warning"); if (h >= 185 && h < 265) return QStringLiteral("info"); } } return QStringLiteral("neutral"); } // 状态键 + 后缀 → 主题 token 名(如 status + "danger" + "-bg")。neutral 无 -bg,回落中性面。 QColor statusColor(const QString& status, bool bg) { if (status == QStringLiteral("neutral")) return geopro::app::tokenColor(bg ? "bg/panel-subtle" : "status/neutral"); const QString name = QStringLiteral("status/%1%2").arg(status, bg ? QStringLiteral("-bg") : QString()); return geopro::app::tokenColor(name.toUtf8().constData()); } // 右侧眼睛命中区(卡片右端,竖直居中)。 QRect anomalyEyeRect(const QRect& itemRect) { const QRect r = itemRect.adjusted(4, 2, -4, -2); const int sz = 22; return QRect(r.right() - sz - 8, r.center().y() - sz / 2, sz, sz); } class AnomalyCardDelegate : public QStyledItemDelegate { public: using QStyledItemDelegate::QStyledItemDelegate; QSize sizeHint(const QStyleOptionViewItem&, const QModelIndex&) const override { return QSize(0, 58); } bool editorEvent(QEvent* e, QAbstractItemModel* model, const QStyleOptionViewItem& opt, const QModelIndex& idx) override { if (e->type() == QEvent::MouseButtonRelease) { auto* me = static_cast(e); if (anomalyEyeRect(opt.rect).contains(me->position().toPoint())) { const auto cur = static_cast(idx.data(Qt::CheckStateRole).toInt()); model->setData(idx, cur == Qt::Checked ? Qt::Unchecked : Qt::Checked, Qt::CheckStateRole); return true; // 吃掉点击:只切显隐,不改选中 } } return QStyledItemDelegate::editorEvent(e, model, opt, idx); } void paint(QPainter* p, const QStyleOptionViewItem& opt, const QModelIndex& idx) const override { p->save(); p->setRenderHint(QPainter::Antialiasing, true); const QRect r = opt.rect.adjusted(4, 3, -4, -3); const bool selected = opt.state & QStyle::State_Selected; const bool hover = opt.state & QStyle::State_MouseOver; // 分级状态键(§6.3):高=Danger 中=Warning 低=Info 未知=Neutral。 const QString status = idx.data(kAnomalyStatusRole).toString(); // 卡底(§6.3 规范§3.2 radius/md=6):静止态 = 该分级状态浅底;选中/hover 叠一档 // 交互态(选中=bg/selected 强调底,hover=bg/hover),让交互可辨又不丢分级语义。 QPainterPath path; path.addRoundedRect(r, geopro::app::radius::kMd, geopro::app::radius::kMd); const QColor cardBg = selected ? geopro::app::tokenColor("bg/selected") : hover ? geopro::app::tokenColor("bg/hover") : statusColor(status, /*bg=*/true); p->fillPath(path, cardBg); // 左 3px 状态色竖条(取分级状态主色,与卡底/标签同源;§6.3) p->fillRect(QRect(r.left(), r.top() + 4, 3, r.height() - 8), statusColor(status, /*bg=*/false)); const QString name = idx.data(Qt::DisplayRole).toString(); const QString type = idx.data(kAnomalyTypeRole).toString(); const QString summary = idx.data(kAnomalySummaryRole).toString(); const int left = r.left() + 14; const int right = anomalyEyeRect(opt.rect).left() - 8; // 给眼睛留位 const int rowW = right - left; // 第一行:状态圆点 + 名称(text/body-strong 600)。圆点 8px,色 = 分级状态主色(§6.3)。 const int dot = 8; const QRect nameR(left + dot + 6, r.top() + 8, rowW - dot - 6, 20); p->setBrush(statusColor(status, /*bg=*/false)); p->setPen(Qt::NoPen); p->drawEllipse(QPointF(left + dot / 2.0, nameR.center().y()), dot / 2.0, dot / 2.0); QFont nf = opt.font; nf.setPixelSize(geopro::app::scaledPx(13)); nf.setWeight(QFont::DemiBold); p->setFont(nf); p->setPen(geopro::app::tokenColor("text/primary")); p->setBrush(Qt::NoBrush); p->drawText(nameR, Qt::AlignLeft | Qt::AlignVCenter, p->fontMetrics().elidedText(name, Qt::ElideRight, nameR.width())); // 第二行:分级胶囊标签 + 摘要 int x = left; const int cy = r.top() + 38; if (!type.isEmpty()) { // 等级标签(§6.8):胶囊 radius/pill(按高度算半径),底 = 状态浅底,文字 = 状态主色。 QFont pf = opt.font; pf.setPixelSize(geopro::app::scaledPx(11)); p->setFont(pf); const QFontMetrics fm(pf); const int tw = fm.horizontalAdvance(type); const int ph = fm.height() + 2; const QRect pill(x, cy - ph / 2, tw + 12, ph); QPainterPath pp; pp.addRoundedRect(pill, ph / 2.0, ph / 2.0); p->fillPath(pp, statusColor(status, /*bg=*/true)); p->setPen(statusColor(status, /*bg=*/false)); p->drawText(pill, Qt::AlignCenter, type); x = pill.right() + 8; } if (!summary.isEmpty()) { // 属性行数值(§2.1/§6.3):用等宽字族,保证「140m · 18m / 32 Ω·m」逐列对齐。 QFont sf = opt.font; sf.setFamilies(QString::fromLatin1(geopro::app::type::kMonoFamily).split(QStringLiteral(", "))); sf.setPixelSize(geopro::app::scaledPx(11)); p->setFont(sf); p->setPen(geopro::app::tokenColor("text/secondary")); const QRect sumR(x, cy - 10, right - x, 20); p->drawText(sumR, Qt::AlignLeft | Qt::AlignVCenter, p->fontMetrics().elidedText(summary, Qt::ElideRight, sumR.width())); } // 右侧眼睛(显隐):可见=次要色睁眼;隐藏=禁用色 + 斜杠 const bool visible = static_cast(idx.data(Qt::CheckStateRole).toInt()) == Qt::Checked; const QColor eyeCol = geopro::app::tokenColor(visible ? "text/secondary" : "text/disabled"); const QRectF eb = anomalyEyeRect(opt.rect); const QPointF c = eb.center(); const double w = eb.width() * 0.42, h = eb.height() * 0.24; p->setPen(QPen(eyeCol, 1.4)); p->setBrush(Qt::NoBrush); QPainterPath eye; eye.moveTo(c.x() - w, c.y()); eye.quadTo(c.x(), c.y() - h * 2.0, c.x() + w, c.y()); eye.quadTo(c.x(), c.y() + h * 2.0, c.x() - w, c.y()); p->drawPath(eye); p->setBrush(eyeCol); p->drawEllipse(c, h * 0.95, h * 0.95); p->setBrush(Qt::NoBrush); if (!visible) p->drawLine(QPointF(c.x() - w, c.y() + h * 1.6), QPointF(c.x() + w, c.y() - h * 1.6)); p->restore(); } }; } // namespace void populateAnomalyList(QListWidget* list, const std::vector& anomalies) { if (!list) return; list->clear(); for (std::size_t i = 0; i < anomalies.size(); ++i) { const auto& a = anomalies[i]; const QString name = QString::fromStdString(a.name.empty() ? "异常" : a.name); auto* item = new QListWidgetItem(name, list); item->setData(kAnomalyIndexRole, static_cast(i)); item->setData(kAnomalyColorRole, QString::fromStdString(a.lineColor)); item->setData(kAnomalyTypeRole, QString::fromStdString(a.typeName)); item->setData(kAnomalySummaryRole, summarize(a)); item->setData(kAnomalyStatusRole, anomalyStatus(a)); // 分级状态键(驱动卡底/标签/竖条同色) item->setFlags(item->flags() | Qt::ItemIsUserCheckable); item->setCheckState(Qt::Checked); // 默认显示 } } QString anomalyVisibleCountText(QListWidget* list) { if (!list) return QStringLiteral("0/0"); int total = 0, visible = 0; for (int i = 0; i < list->count(); ++i) { const QListWidgetItem* it = list->item(i); if (!it) continue; ++total; if (it->checkState() == Qt::Checked) ++visible; } return QStringLiteral("%1/%2").arg(visible).arg(total); } void applyAnomalyCardDelegate(QListWidget* list) { if (!list) return; list->setItemDelegate(new AnomalyCardDelegate(list)); list->setMouseTracking(true); list->setSpacing(0); QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, list, [list]() { list->viewport()->update(); }); } } // namespace geopro::app