feat(panels): 异常列表卡片化(色条+类型标签+显隐眼睛,真实数据)(规范§6.3)

This commit is contained in:
gaozheng 2026-06-10 16:44:35 +08:00
parent b26dcc1ca7
commit 8f31f043df
3 changed files with 151 additions and 17 deletions

View File

@ -563,6 +563,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
// 右上 dock异常列表 / 对象属性 合并为带 Tab 表头的面板(对齐原型上半)。
auto* anomalyList = new QListWidget();
applyListSelection(anomalyList);
geopro::app::applyAnomalyCardDelegate(anomalyList);
auto* objAttrLabel = new QLabel(QStringLiteral("(选中对象后显示其属性)"));
objAttrLabel->setWordWrap(true);
objAttrLabel->setAlignment(Qt::AlignTop | Qt::AlignLeft);

View File

@ -3,22 +3,26 @@
#include <cmath>
#include <cstddef>
#include <QAbstractItemModel>
#include <QColor>
#include <QIcon>
#include <QEvent>
#include <QListWidget>
#include <QListWidgetItem>
#include <QPixmap>
#include <QMouseEvent>
#include <QObject>
#include <QPainter>
#include <QPainterPath>
#include <QPen>
#include <QString>
#include <QStyledItemDelegate>
#include "Theme.hpp"
#include "model/ColorScale.hpp"
namespace geopro::app {
namespace {
// 颜色块图标边长(像素)。
constexpr int kSwatch = 12;
// 由 localPts 算「位置(质心x)·深(质心y)·尺寸(包络对角)」摘要文本。
// 异常坐标在剖面距离/深度空间(x=距离米, y=深度米)。
QString summarize(const geopro::core::Anomaly& a)
@ -43,15 +47,128 @@ QString summarize(const geopro::core::Anomaly& a)
.arg(span, 0, 'f', 0);
}
// lineColor 字符串("#RRGGBB"/"rgba(...)") → 颜色块 QPixmap
QPixmap swatch(const std::string& colorStr)
// lineColor 字符串 → QColor兼容 "#RRGGBB" 与 "rgba(...)"
QColor barColor(const QString& s)
{
const auto c = geopro::core::parseColor(colorStr, geopro::core::AlphaScale::Bit255);
QPixmap pm(kSwatch, kSwatch);
pm.fill(QColor(c.r, c.g, c.b));
return pm;
const auto c = geopro::core::parseColor(s.toStdString(), geopro::core::AlphaScale::Bit255);
return QColor(c.r, c.g, c.b);
}
// 右侧眼睛命中区(卡片右端,竖直居中)。
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<QMouseEvent*>(e);
if (anomalyEyeRect(opt.rect).contains(me->position().toPoint())) {
const auto cur = static_cast<Qt::CheckState>(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;
// 卡底hover/选中高亮)
if (selected || hover) {
QPainterPath path; path.addRoundedRect(r, 6, 6);
p->fillPath(path, geopro::app::tokenColor(selected ? "bg/selected" : "bg/hover"));
}
// 左 3px 状态色竖条(取异常自身 lineColor
p->fillRect(QRect(r.left(), r.top() + 4, 3, r.height() - 8),
barColor(idx.data(kAnomalyColorRole).toString()));
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;
// 第一行:名称(加粗)
QFont nf = opt.font; nf.setPixelSize(geopro::app::scaledPx(13)); nf.setWeight(QFont::DemiBold);
p->setFont(nf);
p->setPen(geopro::app::tokenColor("text/primary"));
const QRect nameR(left, r.top() + 8, rowW, 20);
p->drawText(nameR, Qt::AlignLeft | Qt::AlignVCenter,
p->fontMetrics().elidedText(name, Qt::ElideRight, rowW));
// 第二行:类型胶囊 + 摘要
int x = left;
const int cy = r.top() + 38;
if (!type.isEmpty()) {
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, geopro::app::tokenColor("bg/hover"));
p->setPen(geopro::app::tokenColor("text/secondary"));
p->drawText(pill, Qt::AlignCenter, type);
x = pill.right() + 8;
}
if (!summary.isEmpty()) {
QFont sf = opt.font; 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<Qt::CheckState>(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<geopro::core::Anomaly>& anomalies)
@ -61,16 +178,24 @@ void populateAnomalyList(QListWidget* list, const std::vector<geopro::core::Anom
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);
const QString type = QString::fromStdString(a.typeName);
QString text = name;
if (!type.isEmpty()) text += QStringLiteral("%1").arg(type);
text += QStringLiteral("\n%1").arg(summarize(a));
auto* item = new QListWidgetItem(QIcon(swatch(a.lineColor)), text, list);
auto* item = new QListWidgetItem(name, list);
item->setData(kAnomalyIndexRole, static_cast<int>(i));
item->setData(kAnomalyColorRole, QString::fromStdString(a.lineColor));
item->setData(kAnomalyTypeRole, QString::fromStdString(a.typeName));
item->setData(kAnomalySummaryRole, summarize(a));
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
item->setCheckState(Qt::Checked); // 默认显示
}
}
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

View File

@ -10,6 +10,14 @@ namespace geopro::app {
// 异常索引存于条目的 Qt::UserRole= 在原异常 vector 中的下标,用于显隐映射)。
constexpr int kAnomalyIndexRole = 0x0100; // Qt::UserRole
// 卡片委托读取的结构化角色(避免把数据塞进显示文本)。
constexpr int kAnomalyColorRole = 0x0101; // lineColor 字符串
constexpr int kAnomalyTypeRole = 0x0102; // typeName
constexpr int kAnomalySummaryRole = 0x0103; // 位置·深·尺寸 摘要
// 给异常列表套用卡片委托(左色条+名称+类型标签+摘要+右侧显隐眼睛规范§6.3)。
void applyAnomalyCardDelegate(QListWidget* list);
// 用异常填充 QListWidget对齐原型右上「异常列表」每条目 = 颜色块图标 + 名称 +
// 派生「位置 Xm · 深 Ym · 尺寸 Zm」(由 location.coordinate 质心/包络算)。
// 条目可勾选:勾=显示(默认全勾);勾选状态变化由调用方连接驱动该异常 actor 显隐。