fix(detail): inversion 异常/自动标注/描述交互返工对齐原版 + 修 getExceptionName
- I9 文字标注:落点后弹 ExceptionTextDialog(字体/大小/颜色/不透明度/内容)写 customLegend;
补"新增异常类型"按钮(完整子流程标注待办);Anomaly 增 Text=4 + 文字字段
- 修 getExceptionName:原版 data 为纯字符串,客户端误当对象解析→名称回填失败;
改 wireString 解析,回调签名改 (bool,QString,QString);切类型每次回填
- I10 删除文案对齐原版 contourContentDelete
- I11 详情返工:380px 抽屉式双Tab(图例/坐标),线样式改只读,坐标系切换(图形/经纬度/投影)
+顶点数+导出txt(经纬度/投影无换算数据,标注;图形坐标可用),提交体仍 {id,exceptionName,remark}
- I13 自动标注返工:1400px,规则卡片(标题/折叠/删除),阈值模式 radio(切换清空),
右上统计(max/min/mean/median),预览表序号+逐条删除(等值线预览图高成本待办)
- I14 富文本补 背景色/对齐/字体族 工具栏 + QuillDelta 字体族往返;去下划线/列表(原版无)
build all 绿,339/339。GPR/金字塔 WIP 未碰。
This commit is contained in:
parent
5dbbb2576c
commit
6bc7c23a8c
|
|
@ -52,6 +52,7 @@ add_executable(geopro_desktop WIN32
|
|||
panels/chart/WhiteningDialog.cpp
|
||||
panels/chart/FilterDialog.cpp
|
||||
panels/chart/ExceptionDialog.cpp
|
||||
panels/chart/ExceptionTextDialog.cpp
|
||||
panels/chart/ExceptionDetailDialog.cpp
|
||||
panels/chart/AutoAnnotationDialog.cpp
|
||||
panels/chart/ContourSimplify.cpp
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@
|
|||
#include <QToolButton>
|
||||
#include <QVBoxLayout>
|
||||
namespace geopro::app {
|
||||
static QString markName(int t) { return t == 1 ? "点" : t == 3 ? "多边形" : "多段线"; }
|
||||
static QString markName(int t) {
|
||||
return t == 1 ? "点" : t == 3 ? "多边形" : t == 4 ? "文字" : "多段线";
|
||||
}
|
||||
|
||||
AnomalyTablePanel::AnomalyTablePanel(QWidget* parent) : QWidget(parent) {
|
||||
auto* lay = new QVBoxLayout(this); lay->setContentsMargins(0, 0, 0, 0);
|
||||
|
|
@ -53,9 +55,10 @@ void AnomalyTablePanel::setAnomalies(const std::vector<geopro::core::Anomaly>& l
|
|||
connect(btnDetail, &QToolButton::clicked, this, [this, i]() { emit detailRequested(i); });
|
||||
auto* btnDelete = new QToolButton(ops); btnDelete->setText(QStringLiteral("删除"));
|
||||
connect(btnDelete, &QToolButton::clicked, this, [this, i]() {
|
||||
// 原版 a-popconfirm 二次确认 → 这里用 QMessageBox 确认。
|
||||
// 原版 a-popconfirm 二次确认(contourContentDelete)→ 这里用 QMessageBox,文案对齐原版。
|
||||
if (QMessageBox::question(this, QStringLiteral("提示"),
|
||||
QStringLiteral("确定删除该异常?")) == QMessageBox::Yes)
|
||||
QStringLiteral("该操作会删除该异常标注数据,确认?")) ==
|
||||
QMessageBox::Yes)
|
||||
emit deleteRequested(i);
|
||||
});
|
||||
opLay->addWidget(eye);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
#include <QPushButton>
|
||||
#include <QTextCharFormat>
|
||||
#include <QTextEdit>
|
||||
#include <QTextListFormat>
|
||||
#include <QToolBar>
|
||||
#include <QToolButton>
|
||||
#include <QVBoxLayout>
|
||||
|
|
@ -20,6 +19,13 @@ namespace {
|
|||
// 字号下拉选项(px)——对照原版 ql-size 的 12~32px。
|
||||
const int kFontSizesPx[] = {12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32};
|
||||
|
||||
// 字体族——对照原版 ql-font:显示名 + Qt family。
|
||||
struct FontOption { const char* label; const char* family; };
|
||||
const FontOption kFontFamilies[] = {
|
||||
{"微软雅黑", "Microsoft YaHei"}, {"宋体", "SimSun"}, {"仿宋", "FangSong"},
|
||||
{"楷体", "KaiTi"}, {"黑体", "SimHei"}, {"仿宋_GB2312", "FangSong"},
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
DescriptionPanel::DescriptionPanel(QWidget* parent) : QWidget(parent) {
|
||||
|
|
@ -57,6 +63,8 @@ void DescriptionPanel::buildToolbar(QToolBar* tb) {
|
|||
connect(btn, &QToolButton::toggled, this, applier);
|
||||
return btn;
|
||||
};
|
||||
// 对照原版 Quill 工具栏:粗/斜 + 字色/背景色 + 对齐 + 标题 + 字号 + 字体族。
|
||||
// (原版无下划线/列表,故此处不设,以贴近原版。)
|
||||
addToggle(QStringLiteral("B"), [this](bool on) {
|
||||
QTextCharFormat f;
|
||||
f.setFontWeight(on ? QFont::Bold : QFont::Normal);
|
||||
|
|
@ -67,15 +75,11 @@ void DescriptionPanel::buildToolbar(QToolBar* tb) {
|
|||
f.setFontItalic(on);
|
||||
edit_->mergeCurrentCharFormat(f);
|
||||
});
|
||||
addToggle(QStringLiteral("U"), [this](bool on) {
|
||||
QTextCharFormat f;
|
||||
f.setFontUnderline(on);
|
||||
edit_->mergeCurrentCharFormat(f);
|
||||
});
|
||||
|
||||
// 字色:弹色板,作用于选区前景色。
|
||||
auto* colorBtn = new QToolButton(tb);
|
||||
colorBtn->setText(QStringLiteral("A"));
|
||||
colorBtn->setToolTip(QStringLiteral("字体颜色"));
|
||||
tb->addWidget(colorBtn);
|
||||
connect(colorBtn, &QToolButton::clicked, this, [this]() {
|
||||
const QColor c = QColorDialog::getColor(Qt::black, this, QStringLiteral("字体颜色"));
|
||||
|
|
@ -85,18 +89,33 @@ void DescriptionPanel::buildToolbar(QToolBar* tb) {
|
|||
edit_->mergeCurrentCharFormat(f);
|
||||
});
|
||||
|
||||
// 字号下拉(px)。
|
||||
auto* sizeBox = new QComboBox(tb);
|
||||
for (int px : kFontSizesPx) sizeBox->addItem(QStringLiteral("%1px").arg(px), px);
|
||||
sizeBox->setCurrentIndex(2); // 默认 16px(与原版一致)。
|
||||
tb->addWidget(sizeBox);
|
||||
connect(sizeBox, &QComboBox::currentIndexChanged, this, [this, sizeBox](int) {
|
||||
const int px = sizeBox->currentData().toInt();
|
||||
// 背景色:弹色板,作用于选区背景色(对照原版 ql-background)。
|
||||
auto* bgBtn = new QToolButton(tb);
|
||||
bgBtn->setText(QStringLiteral("▢"));
|
||||
bgBtn->setToolTip(QStringLiteral("背景颜色"));
|
||||
tb->addWidget(bgBtn);
|
||||
connect(bgBtn, &QToolButton::clicked, this, [this]() {
|
||||
const QColor c = QColorDialog::getColor(Qt::yellow, this, QStringLiteral("背景颜色"));
|
||||
if (!c.isValid()) return;
|
||||
QTextCharFormat f;
|
||||
f.setFontPointSize(px * 3.0 / 4.0); // px→pt。
|
||||
f.setBackground(c);
|
||||
edit_->mergeCurrentCharFormat(f);
|
||||
});
|
||||
|
||||
// 对齐:左/中/右/两端(对照原版 ql-align)——块级。
|
||||
auto addAlignBtn = [this, tb](const QString& label, Qt::Alignment align) {
|
||||
auto* btn = new QToolButton(tb);
|
||||
btn->setText(label);
|
||||
tb->addWidget(btn);
|
||||
connect(btn, &QToolButton::clicked, this, [this, align]() {
|
||||
edit_->setAlignment(align);
|
||||
});
|
||||
};
|
||||
addAlignBtn(QStringLiteral("⯇"), Qt::AlignLeft);
|
||||
addAlignBtn(QStringLiteral("≡"), Qt::AlignHCenter);
|
||||
addAlignBtn(QStringLiteral("⯈"), Qt::AlignRight);
|
||||
addAlignBtn(QStringLiteral("☰"), Qt::AlignJustify);
|
||||
|
||||
// 标题下拉(正文 / H1~H4)——块级,作用于当前段。
|
||||
auto* headerBox = new QComboBox(tb);
|
||||
headerBox->addItem(QStringLiteral("正文"), 0);
|
||||
|
|
@ -111,17 +130,29 @@ void DescriptionPanel::buildToolbar(QToolBar* tb) {
|
|||
edit_->setTextCursor(cur);
|
||||
});
|
||||
|
||||
// 有序 / 无序列表——块级。
|
||||
auto addListBtn = [this, tb](const QString& label, QTextListFormat::Style style) {
|
||||
auto* btn = new QToolButton(tb);
|
||||
btn->setText(label);
|
||||
tb->addWidget(btn);
|
||||
connect(btn, &QToolButton::clicked, this, [this, style]() {
|
||||
edit_->textCursor().createList(style);
|
||||
// 字号下拉(px)。
|
||||
auto* sizeBox = new QComboBox(tb);
|
||||
for (int px : kFontSizesPx) sizeBox->addItem(QStringLiteral("%1px").arg(px), px);
|
||||
sizeBox->setCurrentIndex(2); // 默认 16px(与原版一致)。
|
||||
tb->addWidget(sizeBox);
|
||||
connect(sizeBox, &QComboBox::currentIndexChanged, this, [this, sizeBox](int) {
|
||||
const int px = sizeBox->currentData().toInt();
|
||||
QTextCharFormat f;
|
||||
f.setFontPointSize(px * 3.0 / 4.0); // px→pt。
|
||||
edit_->mergeCurrentCharFormat(f);
|
||||
});
|
||||
|
||||
// 字体族下拉(对照原版 ql-font)。
|
||||
auto* fontBox = new QComboBox(tb);
|
||||
for (const auto& fo : kFontFamilies)
|
||||
fontBox->addItem(QString::fromUtf8(fo.label), QString::fromUtf8(fo.family));
|
||||
tb->addWidget(fontBox);
|
||||
connect(fontBox, &QComboBox::currentIndexChanged, this, [this, fontBox](int) {
|
||||
const QString family = fontBox->currentData().toString();
|
||||
QTextCharFormat f;
|
||||
f.setFontFamilies({family});
|
||||
edit_->mergeCurrentCharFormat(f);
|
||||
});
|
||||
};
|
||||
addListBtn(QStringLiteral("1."), QTextListFormat::ListDecimal);
|
||||
addListBtn(QStringLiteral("•"), QTextListFormat::ListDisc);
|
||||
}
|
||||
|
||||
void DescriptionPanel::setDelta(const QJsonArray& ops) {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,28 @@ namespace {
|
|||
// 颜色转 Quill 习惯的小写 #rrggbb(与原版 ql-color/ql-background 选项一致)。
|
||||
QString hexOf(const QColor& c) { return c.name(QColor::HexRgb); }
|
||||
|
||||
// 字体族:原版 ql-font 的 token ↔ Qt QFont family 名。
|
||||
// token(whitelist 值):Microsoft-YaHei / SimSun / SimHei / KaiTi / FangSong / FangSong_GB2312。
|
||||
// Delta 写出用 token;反序列化时把 token 转成 Qt 可识别的 family(连字符→空格等)。
|
||||
QString fontTokenToFamily(const QString& token) {
|
||||
if (token == QStringLiteral("Microsoft-YaHei")) return QStringLiteral("Microsoft YaHei");
|
||||
if (token == QStringLiteral("SimSun")) return QStringLiteral("SimSun");
|
||||
if (token == QStringLiteral("SimHei")) return QStringLiteral("SimHei");
|
||||
if (token == QStringLiteral("KaiTi")) return QStringLiteral("KaiTi");
|
||||
if (token == QStringLiteral("FangSong")) return QStringLiteral("FangSong");
|
||||
if (token == QStringLiteral("FangSong_GB2312")) return QStringLiteral("FangSong");
|
||||
return token; // Arial / sans-serif 等原样
|
||||
}
|
||||
|
||||
QString familyToFontToken(const QString& family) {
|
||||
if (family == QStringLiteral("Microsoft YaHei")) return QStringLiteral("Microsoft-YaHei");
|
||||
if (family == QStringLiteral("SimSun")) return QStringLiteral("SimSun");
|
||||
if (family == QStringLiteral("SimHei")) return QStringLiteral("SimHei");
|
||||
if (family == QStringLiteral("KaiTi")) return QStringLiteral("KaiTi");
|
||||
if (family == QStringLiteral("FangSong")) return QStringLiteral("FangSong");
|
||||
return family;
|
||||
}
|
||||
|
||||
// 行内样式 → attributes 对象(bold/italic/underline/color/background/size)。
|
||||
QJsonObject inlineAttrs(const QTextCharFormat& fmt) {
|
||||
QJsonObject a;
|
||||
|
|
@ -28,6 +50,9 @@ QJsonObject inlineAttrs(const QTextCharFormat& fmt) {
|
|||
const double pt = fmt.fontPointSize();
|
||||
if (pt > 0.0) // 以 px 表达(原版 ql-size 用 "NNpx";pt→px 约 *4/3 取整)。
|
||||
a[QStringLiteral("size")] = QStringLiteral("%1px").arg(qRound(pt * 4.0 / 3.0));
|
||||
const QStringList fams = fmt.fontFamilies().toStringList();
|
||||
if (!fams.isEmpty() && !fams.first().isEmpty()) // 字体族(原版 ql-font token)。
|
||||
a[QStringLiteral("font")] = familyToFontToken(fams.first());
|
||||
return a;
|
||||
}
|
||||
|
||||
|
|
@ -88,6 +113,8 @@ void applyInlineAttrs(const QJsonObject& attrs, QTextCharFormat& fmt) {
|
|||
const double px = size.chopped(2).toDouble();
|
||||
if (px > 0.0) fmt.setFontPointSize(px * 3.0 / 4.0); // px→pt。
|
||||
}
|
||||
const QString font = attrs.value(QStringLiteral("font")).toString();
|
||||
if (!font.isEmpty()) fmt.setFontFamilies({fontTokenToFamily(font)});
|
||||
}
|
||||
|
||||
// 把 block attributes 应用到块格式 / 列表(作用于换行前的当前块)。
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
#include "panels/chart/AutoAnnotationDialog.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <utility>
|
||||
|
||||
#include <QButtonGroup>
|
||||
#include <QComboBox>
|
||||
#include <QFormLayout>
|
||||
#include <QFrame>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
|
|
@ -12,8 +16,10 @@
|
|||
#include <QMessageBox>
|
||||
#include <QPointer>
|
||||
#include <QPushButton>
|
||||
#include <QRadioButton>
|
||||
#include <QSpinBox>
|
||||
#include <QTableWidget>
|
||||
#include <QToolButton>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "FormKit.hpp"
|
||||
|
|
@ -26,54 +32,88 @@ namespace {
|
|||
constexpr int kDefaultMinPoints = 4; // 原版 minPointCount 默认 4
|
||||
// 面异常类型固定 remarkSourceType="3"(原版自动标注仅支持面/polygon)。
|
||||
const QString kPolygonType = QStringLiteral("3");
|
||||
|
||||
// 从网格标量算 max/min/mean/median(过滤 NaN)。空 → 全 '-'。
|
||||
struct Stats { bool valid = false; double mx = 0, mn = 0, mean = 0, median = 0; };
|
||||
Stats computeStats(const std::vector<double>& raw) {
|
||||
std::vector<double> v;
|
||||
v.reserve(raw.size());
|
||||
for (double d : raw)
|
||||
if (!std::isnan(d)) v.push_back(d);
|
||||
if (v.empty()) return {};
|
||||
std::sort(v.begin(), v.end());
|
||||
Stats s;
|
||||
s.valid = true;
|
||||
s.mn = v.front();
|
||||
s.mx = v.back();
|
||||
double sum = 0;
|
||||
for (double d : v) sum += d;
|
||||
s.mean = sum / static_cast<double>(v.size());
|
||||
const size_t m = v.size() / 2;
|
||||
s.median = (v.size() % 2 == 0) ? (v[m - 1] + v[m]) / 2.0 : v[m];
|
||||
return s;
|
||||
}
|
||||
|
||||
QString fmt2(bool valid, double x) {
|
||||
return valid ? QString::number(x, 'f', 2) : QStringLiteral("-");
|
||||
}
|
||||
} // namespace
|
||||
|
||||
AutoAnnotationDialog::AutoAnnotationDialog(geopro::data::IDatasetCommandRepository* repo,
|
||||
QString dsObjectId, QString projectId, QWidget* parent)
|
||||
QString dsObjectId, QString projectId,
|
||||
std::vector<double> gridValues, QWidget* parent)
|
||||
: QDialog(parent),
|
||||
repo_(repo),
|
||||
dsObjectId_(std::move(dsObjectId)),
|
||||
projectId_(std::move(projectId)) {
|
||||
projectId_(std::move(projectId)),
|
||||
gridValues_(std::move(gridValues)) {
|
||||
setWindowTitle(QStringLiteral("自动标注"));
|
||||
setModal(true);
|
||||
resize(820, 520);
|
||||
resize(geopro::app::scaledPx(1400), geopro::app::scaledPx(600));
|
||||
|
||||
auto* root = formkit::dialogRoot(this);
|
||||
auto* split = new QHBoxLayout();
|
||||
|
||||
// ── 左:规则列表 ────────────────────────────────────────────────
|
||||
// ── 左:规则卡片列表(35%,对照原版左栏)────────────────────────────────
|
||||
auto* leftCol = new QVBoxLayout();
|
||||
leftCol->addWidget(new QLabel(QStringLiteral("标注规则:"), this));
|
||||
leftCol->addWidget(new QLabel(QStringLiteral("异常判定规则"), this));
|
||||
auto* ruleContainer = new QWidget(this);
|
||||
ruleHost_ = new QVBoxLayout(ruleContainer);
|
||||
ruleHost_->setContentsMargins(0, 0, 0, 0);
|
||||
leftCol->addWidget(ruleContainer);
|
||||
auto* addBtn = new QPushButton(QStringLiteral("加规则"), this);
|
||||
auto* addBtn = new QPushButton(QStringLiteral("添加规则"), this);
|
||||
connect(addBtn, &QPushButton::clicked, this, [this]() { addRule(); });
|
||||
leftCol->addWidget(addBtn);
|
||||
leftCol->addStretch();
|
||||
split->addLayout(leftCol, 1);
|
||||
auto* leftWrap = new QWidget(this);
|
||||
leftWrap->setLayout(leftCol);
|
||||
split->addWidget(leftWrap, 35);
|
||||
|
||||
// ── 右:预览表 ──────────────────────────────────────────────────
|
||||
// ── 右:上统计 + 下预览表 ───────────────────────────────────────────────
|
||||
auto* rightCol = new QVBoxLayout();
|
||||
rightCol->addWidget(new QLabel(QStringLiteral("预览:"), this));
|
||||
previewTable_ = new QTableWidget(0, 4, this);
|
||||
previewTable_->setHorizontalHeaderLabels(
|
||||
{QStringLiteral("异常名称"), QStringLiteral("异常类型"), QStringLiteral("阈值范围"),
|
||||
QStringLiteral("阈值模式")});
|
||||
buildStatsBar(rightCol);
|
||||
|
||||
detectedLabel_ = new QLabel(QStringLiteral("自动标注结果"), this);
|
||||
rightCol->addWidget(detectedLabel_);
|
||||
previewTable_ = new QTableWidget(0, 6, this);
|
||||
previewTable_->setHorizontalHeaderLabels({QStringLiteral("序号"), QStringLiteral("异常名称"),
|
||||
QStringLiteral("异常类型"), QStringLiteral("阈值范围"),
|
||||
QStringLiteral("阈值模式"), QStringLiteral("操作")});
|
||||
previewTable_->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
|
||||
previewTable_->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
rightCol->addWidget(previewTable_, 1);
|
||||
split->addLayout(rightCol, 1);
|
||||
auto* rightWrap = new QWidget(this);
|
||||
rightWrap->setLayout(rightCol);
|
||||
split->addWidget(rightWrap, 65);
|
||||
|
||||
root->addLayout(split, 1);
|
||||
|
||||
// ── 底部按钮 ────────────────────────────────────────────────────
|
||||
// ── 底部按钮:取消 / 执行自动标注 / 确认保存 ─────────────────────────────
|
||||
auto* btnLay = new QHBoxLayout();
|
||||
btnLay->addStretch();
|
||||
auto* cancelBtn = new QPushButton(QStringLiteral("取消"), this);
|
||||
auto* execBtn = new QPushButton(QStringLiteral("执行自动标注"), this);
|
||||
saveBtn_ = new QPushButton(QStringLiteral("确定保存"), this);
|
||||
saveBtn_ = new QPushButton(QStringLiteral("确认保存"), this);
|
||||
saveBtn_->setEnabled(false); // 必须先执行得到预览才能保存
|
||||
btnLay->addWidget(cancelBtn);
|
||||
btnLay->addWidget(execBtn);
|
||||
|
|
@ -88,6 +128,24 @@ AutoAnnotationDialog::AutoAnnotationDialog(geopro::data::IDatasetCommandReposito
|
|||
loadExceptionTypes(); // 拉面异常类型
|
||||
}
|
||||
|
||||
void AutoAnnotationDialog::buildStatsBar(QVBoxLayout* into) {
|
||||
// 数据统计(max/min/mean/median,从网格标量算)。预览图后置:当前以统计条替代,
|
||||
// 后续如复用 ContourPlotItem 渲染异常预览图可在此区追加。
|
||||
const Stats s = computeStats(gridValues_);
|
||||
auto* bar = new QFrame(this);
|
||||
bar->setFrameShape(QFrame::StyledPanel);
|
||||
auto* lay = new QHBoxLayout(bar);
|
||||
auto addStat = [&](const QString& name, double v) {
|
||||
lay->addWidget(new QLabel(QStringLiteral("%1:%2").arg(name, fmt2(s.valid, v)), bar));
|
||||
};
|
||||
addStat(QStringLiteral("最大值"), s.mx);
|
||||
addStat(QStringLiteral("最小值"), s.mn);
|
||||
addStat(QStringLiteral("均值"), s.mean);
|
||||
addStat(QStringLiteral("中位数"), s.median);
|
||||
lay->addStretch();
|
||||
into->addWidget(bar);
|
||||
}
|
||||
|
||||
void AutoAnnotationDialog::loadExceptionTypes() {
|
||||
if (!repo_) return;
|
||||
QPointer<AutoAnnotationDialog> self(this);
|
||||
|
|
@ -104,7 +162,7 @@ void AutoAnnotationDialog::loadExceptionTypes() {
|
|||
self->exceptionTypeOptions_.append(
|
||||
QJsonObject{{QStringLiteral("id"), id}, {QStringLiteral("name"), name}});
|
||||
}
|
||||
// 回填已存在的规则行下拉。
|
||||
// 回填已存在规则卡片下拉。
|
||||
for (auto& r : self->rules_) {
|
||||
r.type->clear();
|
||||
for (const QJsonValue& ov : self->exceptionTypeOptions_) {
|
||||
|
|
@ -119,39 +177,113 @@ void AutoAnnotationDialog::loadExceptionTypes() {
|
|||
void AutoAnnotationDialog::addRule() {
|
||||
auto* card = new QFrame(this);
|
||||
card->setFrameShape(QFrame::StyledPanel);
|
||||
auto* lay = new QHBoxLayout(card);
|
||||
lay->setContentsMargins(4, 4, 4, 4);
|
||||
auto* cardLay = new QVBoxLayout(card);
|
||||
cardLay->setContentsMargins(6, 6, 6, 6);
|
||||
|
||||
RuleRow row;
|
||||
row.mode = new QComboBox(card);
|
||||
row.mode->addItem(QStringLiteral("数值"), 1);
|
||||
row.mode->addItem(QStringLiteral("百分位"), 2);
|
||||
row.min = new QLineEdit(card);
|
||||
row.min->setPlaceholderText(QStringLiteral("min"));
|
||||
row.min->setFixedWidth(scaledPx(60));
|
||||
row.max = new QLineEdit(card);
|
||||
row.max->setPlaceholderText(QStringLiteral("max"));
|
||||
row.max->setFixedWidth(scaledPx(60));
|
||||
row.minPoints = new QSpinBox(card);
|
||||
row.minPoints->setRange(1, 100000);
|
||||
row.minPoints->setValue(kDefaultMinPoints);
|
||||
row.type = new QComboBox(card);
|
||||
RuleCard rc;
|
||||
rc.frame = card;
|
||||
|
||||
// 卡片头:折叠 + 「规则N」 + 删除。
|
||||
auto* header = new QHBoxLayout();
|
||||
auto* collapseBtn = new QToolButton(card);
|
||||
collapseBtn->setText(QStringLiteral("▼"));
|
||||
collapseBtn->setCheckable(true);
|
||||
rc.title = new QLabel(card);
|
||||
auto* delBtn = new QToolButton(card);
|
||||
delBtn->setText(QStringLiteral("删除"));
|
||||
header->addWidget(collapseBtn);
|
||||
header->addWidget(rc.title, 1);
|
||||
header->addWidget(delBtn);
|
||||
cardLay->addLayout(header);
|
||||
|
||||
// 卡片主体(可折叠)。
|
||||
rc.body = new QWidget(card);
|
||||
auto* form = formkit::makeEditForm();
|
||||
|
||||
// 阈值模式:radio-button 组(数值/百分位)。
|
||||
auto* modeRow = new QWidget(rc.body);
|
||||
auto* modeLay = new QHBoxLayout(modeRow);
|
||||
modeLay->setContentsMargins(0, 0, 0, 0);
|
||||
auto* rbNum = new QRadioButton(QStringLiteral("数值"), modeRow);
|
||||
auto* rbPct = new QRadioButton(QStringLiteral("百分位"), modeRow);
|
||||
rbNum->setChecked(true);
|
||||
rc.modeGroup = new QButtonGroup(rc.body);
|
||||
rc.modeGroup->addButton(rbNum, 1);
|
||||
rc.modeGroup->addButton(rbPct, 2);
|
||||
modeLay->addWidget(rbNum);
|
||||
modeLay->addWidget(rbPct);
|
||||
modeLay->addStretch();
|
||||
form->addRow(formkit::editLabel(QStringLiteral("阈值模式")), modeRow);
|
||||
|
||||
// 阈值范围:min - max。
|
||||
auto* rangeRow = new QWidget(rc.body);
|
||||
auto* rangeLay = new QHBoxLayout(rangeRow);
|
||||
rangeLay->setContentsMargins(0, 0, 0, 0);
|
||||
rc.min = new QLineEdit(rangeRow);
|
||||
rc.min->setPlaceholderText(QStringLiteral("最小"));
|
||||
rc.max = new QLineEdit(rangeRow);
|
||||
rc.max->setPlaceholderText(QStringLiteral("最大"));
|
||||
rangeLay->addWidget(rc.min);
|
||||
rangeLay->addWidget(new QLabel(QStringLiteral("-"), rangeRow));
|
||||
rangeLay->addWidget(rc.max);
|
||||
form->addRow(formkit::editLabel(QStringLiteral("阈值范围")), rangeRow);
|
||||
|
||||
// 切模式清空 min/max(对照原版 handleThresholdModeChange)。
|
||||
auto clearRange = [min = rc.min, max = rc.max]() { min->clear(); max->clear(); };
|
||||
connect(rbNum, &QRadioButton::toggled, rc.body, [clearRange](bool on) { if (on) clearRange(); });
|
||||
connect(rbPct, &QRadioButton::toggled, rc.body, [clearRange](bool on) { if (on) clearRange(); });
|
||||
|
||||
rc.minPoints = new QSpinBox(rc.body);
|
||||
rc.minPoints->setRange(1, 100000);
|
||||
rc.minPoints->setValue(kDefaultMinPoints);
|
||||
form->addRow(formkit::editLabel(QStringLiteral("最小点数")), rc.minPoints);
|
||||
|
||||
rc.type = new QComboBox(rc.body);
|
||||
for (const QJsonValue& ov : exceptionTypeOptions_) {
|
||||
const QJsonObject o = ov.toObject();
|
||||
row.type->addItem(o.value(QStringLiteral("name")).toString(),
|
||||
rc.type->addItem(o.value(QStringLiteral("name")).toString(),
|
||||
o.value(QStringLiteral("id")).toString());
|
||||
}
|
||||
form->addRow(formkit::editLabel(QStringLiteral("异常类型")), rc.type);
|
||||
|
||||
lay->addWidget(new QLabel(QStringLiteral("模式"), card));
|
||||
lay->addWidget(row.mode);
|
||||
lay->addWidget(row.min);
|
||||
lay->addWidget(row.max);
|
||||
lay->addWidget(new QLabel(QStringLiteral("最小点数"), card));
|
||||
lay->addWidget(row.minPoints);
|
||||
lay->addWidget(row.type, 1);
|
||||
rc.body->setLayout(form);
|
||||
cardLay->addWidget(rc.body);
|
||||
|
||||
rules_.push_back(row);
|
||||
// 折叠/展开。
|
||||
connect(collapseBtn, &QToolButton::toggled, rc.body, [rc, collapseBtn](bool collapsed) {
|
||||
rc.body->setVisible(!collapsed);
|
||||
collapseBtn->setText(collapsed ? QStringLiteral("▶") : QStringLiteral("▼"));
|
||||
});
|
||||
// 删除(至少保留一条)。
|
||||
connect(delBtn, &QToolButton::clicked, this, [this, card]() { removeRule(card); });
|
||||
|
||||
rules_.push_back(rc);
|
||||
ruleHost_->addWidget(card);
|
||||
renumberRules();
|
||||
}
|
||||
|
||||
void AutoAnnotationDialog::removeRule(QWidget* frame) {
|
||||
if (rules_.size() <= 1) { // 对照原版 atLeastOneRule。
|
||||
QMessageBox::warning(this, windowTitle(), QStringLiteral("至少保留一条规则"));
|
||||
return;
|
||||
}
|
||||
auto it = std::find_if(rules_.begin(), rules_.end(),
|
||||
[frame](const RuleCard& c) { return c.frame == frame; });
|
||||
if (it == rules_.end()) return;
|
||||
ruleHost_->removeWidget(frame);
|
||||
frame->deleteLater();
|
||||
rules_.erase(it);
|
||||
renumberRules();
|
||||
}
|
||||
|
||||
void AutoAnnotationDialog::renumberRules() {
|
||||
for (int i = 0; i < static_cast<int>(rules_.size()); ++i)
|
||||
rules_[i].title->setText(QStringLiteral("规则 %1").arg(i + 1));
|
||||
}
|
||||
|
||||
int AutoAnnotationDialog::currentMode(const RuleCard& c) const {
|
||||
const int id = c.modeGroup->checkedId();
|
||||
return id > 0 ? id : 1; // 默认数值
|
||||
}
|
||||
|
||||
QJsonArray AutoAnnotationDialog::buildRuleList() const {
|
||||
|
|
@ -159,7 +291,7 @@ QJsonArray AutoAnnotationDialog::buildRuleList() const {
|
|||
for (const auto& r : rules_) {
|
||||
QJsonObject rule{
|
||||
{QStringLiteral("exceptionTypeId"), r.type->currentData().toString()},
|
||||
{QStringLiteral("thresholdMode"), r.mode->currentData().toInt()},
|
||||
{QStringLiteral("thresholdMode"), currentMode(r)},
|
||||
{QStringLiteral("minPointCount"), r.minPoints->value()},
|
||||
};
|
||||
// min/max:空 → null(对照原版 Number(...) 或 null)。
|
||||
|
|
@ -176,11 +308,18 @@ QJsonArray AutoAnnotationDialog::buildRuleList() const {
|
|||
|
||||
void AutoAnnotationDialog::onExecute() {
|
||||
if (!repo_) return;
|
||||
const QJsonArray rules = buildRuleList();
|
||||
if (rules.isEmpty()) {
|
||||
QMessageBox::warning(this, windowTitle(), QStringLiteral("请至少添加一条规则"));
|
||||
// 校验:每条规则 min/max 至少填一个、异常类型必选(对照原版 handleExecute)。
|
||||
for (const auto& r : rules_) {
|
||||
if (r.min->text().trimmed().isEmpty() && r.max->text().trimmed().isEmpty()) {
|
||||
QMessageBox::warning(this, windowTitle(), QStringLiteral("阈值范围至少填写一项"));
|
||||
return;
|
||||
}
|
||||
if (r.type->currentData().toString().isEmpty()) {
|
||||
QMessageBox::warning(this, windowTitle(), QStringLiteral("请选择异常类型"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
const QJsonArray rules = buildRuleList();
|
||||
QJsonObject body{
|
||||
{QStringLiteral("dsObjectId"), dsObjectId_},
|
||||
{QStringLiteral("projectId"), projectId_},
|
||||
|
|
@ -191,37 +330,81 @@ void AutoAnnotationDialog::onExecute() {
|
|||
if (!self) return;
|
||||
if (!ok) {
|
||||
QMessageBox::warning(self, self->windowTitle(),
|
||||
msg.isEmpty() ? QStringLiteral("执行失败") : msg);
|
||||
msg.isEmpty() ? QStringLiteral("自动标注执行失败") : msg);
|
||||
return;
|
||||
}
|
||||
// 预览异常:兼容 data 直接为数组(wireObject 包成 value) 或 data.list。
|
||||
QJsonArray list = data.value(QStringLiteral("value")).toArray();
|
||||
if (list.isEmpty()) list = data.value(QStringLiteral("list")).toArray();
|
||||
self->previewExceptions_ = list;
|
||||
self->detectedLabel_->setText(
|
||||
QStringLiteral("自动标注结果(共识别到 %1 个异常)").arg(list.size()));
|
||||
self->previewTable_->setRowCount(list.size());
|
||||
for (int i = 0; i < list.size(); ++i) {
|
||||
const QJsonObject o = list[i].toObject();
|
||||
self->previewTable_->setItem(i, 0, new QTableWidgetItem(QString::number(i + 1)));
|
||||
self->previewTable_->setItem(
|
||||
i, 0, new QTableWidgetItem(o.value(QStringLiteral("exceptionName")).toString()));
|
||||
i, 1, new QTableWidgetItem(o.value(QStringLiteral("exceptionName")).toString()));
|
||||
self->previewTable_->setItem(
|
||||
i, 1,
|
||||
i, 2,
|
||||
new QTableWidgetItem(o.value(QStringLiteral("exceptionTypeName")).toString()));
|
||||
self->previewTable_->setItem(
|
||||
i, 2, new QTableWidgetItem(o.value(QStringLiteral("remark")).toString()));
|
||||
i, 3, new QTableWidgetItem(o.value(QStringLiteral("remark")).toString()));
|
||||
self->previewTable_->setItem(
|
||||
i, 3,
|
||||
i, 4,
|
||||
new QTableWidgetItem(o.value(QStringLiteral("thresholdModeName")).toString()));
|
||||
// 操作列:逐条删除(对照原版预览表 删除)。
|
||||
auto* delBtn = new QPushButton(QStringLiteral("删除"), self->previewTable_);
|
||||
QPointer<AutoAnnotationDialog> weak(self);
|
||||
QObject::connect(delBtn, &QPushButton::clicked, self, [weak, i]() {
|
||||
if (weak) weak->deletePreviewRow(i);
|
||||
});
|
||||
self->previewTable_->setCellWidget(i, 5, delBtn);
|
||||
}
|
||||
self->saveBtn_->setEnabled(!list.isEmpty());
|
||||
if (list.isEmpty())
|
||||
QMessageBox::information(self, self->windowTitle(),
|
||||
QStringLiteral("未生成异常(无满足规则的区域)"));
|
||||
QMessageBox::information(self, self->windowTitle(), QStringLiteral("暂未识别到异常"));
|
||||
});
|
||||
}
|
||||
|
||||
void AutoAnnotationDialog::deletePreviewRow(int row) {
|
||||
if (row < 0 || row >= previewExceptions_.size()) return;
|
||||
const QString name = previewExceptions_[row].toObject().value(QStringLiteral("exceptionName")).toString();
|
||||
if (QMessageBox::question(this, QStringLiteral("确认删除"),
|
||||
QStringLiteral("%1,确认删除?").arg(name)) != QMessageBox::Yes)
|
||||
return;
|
||||
previewExceptions_.removeAt(row);
|
||||
// 重建预览表(重排序号 + 重绑删除)。复用 onExecute 的填表分支会重发请求,故就地重建。
|
||||
previewTable_->setRowCount(previewExceptions_.size());
|
||||
for (int i = 0; i < previewExceptions_.size(); ++i) {
|
||||
const QJsonObject o = previewExceptions_[i].toObject();
|
||||
previewTable_->setItem(i, 0, new QTableWidgetItem(QString::number(i + 1)));
|
||||
previewTable_->setItem(
|
||||
i, 1, new QTableWidgetItem(o.value(QStringLiteral("exceptionName")).toString()));
|
||||
previewTable_->setItem(
|
||||
i, 2, new QTableWidgetItem(o.value(QStringLiteral("exceptionTypeName")).toString()));
|
||||
previewTable_->setItem(i, 3,
|
||||
new QTableWidgetItem(o.value(QStringLiteral("remark")).toString()));
|
||||
previewTable_->setItem(
|
||||
i, 4, new QTableWidgetItem(o.value(QStringLiteral("thresholdModeName")).toString()));
|
||||
auto* delBtn = new QPushButton(QStringLiteral("删除"), previewTable_);
|
||||
QPointer<AutoAnnotationDialog> weak(this);
|
||||
connect(delBtn, &QPushButton::clicked, this, [weak, i]() {
|
||||
if (weak) weak->deletePreviewRow(i);
|
||||
});
|
||||
previewTable_->setCellWidget(i, 5, delBtn);
|
||||
}
|
||||
detectedLabel_->setText(
|
||||
QStringLiteral("自动标注结果(共识别到 %1 个异常)").arg(previewExceptions_.size()));
|
||||
saveBtn_->setEnabled(!previewExceptions_.isEmpty());
|
||||
}
|
||||
|
||||
void AutoAnnotationDialog::onSave() {
|
||||
if (!repo_ || previewExceptions_.isEmpty()) return;
|
||||
// 组装 exceptionList:保留 execute 返回项的关键字段。
|
||||
if (!repo_ || previewExceptions_.isEmpty()) {
|
||||
QMessageBox::warning(this, windowTitle(), QStringLiteral("暂无可保存的异常,请先执行自动标注"));
|
||||
return;
|
||||
}
|
||||
// 组装 exceptionList:保留 execute 返回项的关键字段(对照原版 batchCreateException)。
|
||||
QJsonArray exceptionList;
|
||||
for (const QJsonValue& v : previewExceptions_) {
|
||||
const QJsonObject o = v.toObject();
|
||||
|
|
|
|||
|
|
@ -4,12 +4,16 @@
|
|||
#include <QString>
|
||||
#include <vector>
|
||||
|
||||
class QButtonGroup;
|
||||
class QComboBox;
|
||||
class QLineEdit;
|
||||
class QSpinBox;
|
||||
class QTableWidget;
|
||||
class QPushButton;
|
||||
class QToolButton;
|
||||
class QLabel;
|
||||
class QVBoxLayout;
|
||||
class QWidget;
|
||||
|
||||
namespace geopro::data {
|
||||
class IDatasetCommandRepository;
|
||||
|
|
@ -17,40 +21,56 @@ class IDatasetCommandRepository;
|
|||
|
||||
namespace geopro::app {
|
||||
|
||||
// 自动标注对话框(I13,复刻原版 AutoAnnotationDialog):
|
||||
// 左:规则列表(阈值模式 数值/百分位、min/max、最小点数、异常类型)。
|
||||
// 执行 → executeExceptionMark(预演) → 预览表;确定保存 → batchCreateException → reloadGrid。
|
||||
// 异常类型仅支持面/polygon(原版同),故 listExceptionTypes 取 remarkSourceType="3"。
|
||||
// 自动标注对话框(I13,复刻原版 AutoAnnotationDialog,1400×600):
|
||||
// 左:规则卡片列表(标题「规则N」+ 折叠 + 删除;阈值模式 radio-button 数值/百分位、min/max、
|
||||
// 最小点数、异常类型)+「添加规则」。
|
||||
// 右上:数据统计(最大/最小/均值/中位数,从网格标量算)+ 预览图(后置标注)。
|
||||
// 右下:预览表(序号/异常名称/异常类型/阈值范围/阈值模式/操作删除)。
|
||||
// 执行 → executeExceptionMark(预演) → 预览表;确认保存 → batchCreateException → reloadGrid。
|
||||
// 异常类型仅支持面/polygon(原版同),listExceptionTypes 取 remarkSourceType="3"。
|
||||
class AutoAnnotationDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
// gridValues:网格标量(用于右上数据统计 max/min/mean/median);可空(统计显示 '-')。
|
||||
AutoAnnotationDialog(geopro::data::IDatasetCommandRepository* repo, QString dsObjectId,
|
||||
QString projectId, QWidget* parent = nullptr);
|
||||
QString projectId, std::vector<double> gridValues,
|
||||
QWidget* parent = nullptr);
|
||||
|
||||
private:
|
||||
struct RuleRow {
|
||||
QComboBox* mode = nullptr; // 1 数值 / 2 百分位
|
||||
// 一条规则卡片的控件集合(卡片标题/折叠/删除 + 模式 radio + min/max + 最小点数 + 类型)。
|
||||
struct RuleCard {
|
||||
QWidget* frame = nullptr; // 整张卡片(删除时移除)
|
||||
QWidget* body = nullptr; // 折叠隐藏的主体
|
||||
QLabel* title = nullptr; // 「规则N」
|
||||
QButtonGroup* modeGroup = nullptr; // 1 数值 / 2 百分位(radio-button)
|
||||
QLineEdit* min = nullptr;
|
||||
QLineEdit* max = nullptr;
|
||||
QSpinBox* minPoints = nullptr;
|
||||
QComboBox* type = nullptr; // userData = 异常类型 id
|
||||
};
|
||||
|
||||
void loadExceptionTypes(); // 拉面异常类型(填充所有规则行下拉)
|
||||
void buildStatsBar(QVBoxLayout* into); // 右上统计条(max/min/mean/median)
|
||||
void loadExceptionTypes(); // 拉面异常类型(填充所有规则卡片下拉)
|
||||
void addRule(); // 加一条规则卡片
|
||||
void removeRule(QWidget* frame); // 删除指定卡片(至少保留一条)
|
||||
void renumberRules(); // 重排卡片标题「规则N」
|
||||
int currentMode(const RuleCard& c) const; // 取当前 radio 模式(1/2)
|
||||
QJsonArray buildRuleList() const; // 组装 exceptionMarkRuleList
|
||||
void onExecute(); // executeExceptionMark → 预览
|
||||
void onSave(); // batchCreateException
|
||||
void deletePreviewRow(int row); // 删除一条预览异常
|
||||
|
||||
geopro::data::IDatasetCommandRepository* repo_ = nullptr;
|
||||
QString dsObjectId_;
|
||||
QString projectId_;
|
||||
std::vector<double> gridValues_;
|
||||
|
||||
QVBoxLayout* ruleHost_ = nullptr;
|
||||
std::vector<RuleRow> rules_;
|
||||
QJsonArray exceptionTypeOptions_; // 缓存的类型列表({id,name}),新增规则行复用
|
||||
std::vector<RuleCard> rules_;
|
||||
QJsonArray exceptionTypeOptions_; // 缓存的类型列表({id,name}),新增规则卡片复用
|
||||
QTableWidget* previewTable_ = nullptr;
|
||||
QJsonArray previewExceptions_; // execute 返回的预览异常(confirm 时批量存)
|
||||
QLabel* detectedLabel_ = nullptr; // 「共识别到 N 个异常」
|
||||
QPushButton* saveBtn_ = nullptr;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
#include "panels/chart/ExceptionDetailDialog.hpp"
|
||||
|
||||
#include <QColorDialog>
|
||||
#include <QComboBox>
|
||||
#include <QDoubleSpinBox>
|
||||
#include <QFile>
|
||||
#include <QFileDialog>
|
||||
#include <QFormLayout>
|
||||
#include <QFrame>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QJsonObject>
|
||||
|
|
@ -13,7 +14,9 @@
|
|||
#include <QPlainTextEdit>
|
||||
#include <QPointer>
|
||||
#include <QPushButton>
|
||||
#include <QTabWidget>
|
||||
#include <QTableWidget>
|
||||
#include <QTextStream>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "FormKit.hpp"
|
||||
|
|
@ -22,82 +25,58 @@
|
|||
|
||||
namespace geopro::app {
|
||||
|
||||
namespace {
|
||||
|
||||
// 只读色块(对照原版 disabled ColorPicker):显示 hex + 背景色,不可点。
|
||||
QLabel* readonlySwatch(const QString& hex, QWidget* parent) {
|
||||
auto* lbl = new QLabel(hex, parent);
|
||||
lbl->setStyleSheet(
|
||||
QStringLiteral("background:%1;border:1px solid #ccc;padding:2px 6px;color:#fff;").arg(hex));
|
||||
return lbl;
|
||||
}
|
||||
|
||||
// 线型 code → 中文(对照原版 solid/dash)。
|
||||
QString lineTypeName(bool dashed) {
|
||||
return dashed ? QStringLiteral("虚线") : QStringLiteral("实线");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ExceptionDetailDialog::ExceptionDetailDialog(geopro::data::IDatasetCommandRepository* repo,
|
||||
const geopro::core::Anomaly& anomaly, QWidget* parent)
|
||||
: QDialog(parent), repo_(repo), anomaly_(anomaly) {
|
||||
setWindowTitle(QStringLiteral("异常详情"));
|
||||
setWindowTitle(QStringLiteral("标注详情"));
|
||||
setModal(true);
|
||||
resize(420, 460);
|
||||
|
||||
lineColor_ = QString::fromStdString(anomaly_.lineColor);
|
||||
if (lineColor_.isEmpty()) lineColor_ = QStringLiteral("#000000");
|
||||
// 右侧抽屉观感:窄而高(对照原版 ADrawer width=380)。
|
||||
resize(geopro::app::scaledPx(380), geopro::app::scaledPx(560));
|
||||
|
||||
auto* root = formkit::dialogRoot(this);
|
||||
|
||||
auto* card = formkit::formCard(this);
|
||||
auto* cardLay = formkit::cardBody(card);
|
||||
auto* form = formkit::makeEditForm();
|
||||
|
||||
// ── 头部:名称(可编辑) + 异常类型(只读) ───────────────────────────────────
|
||||
auto* head = formkit::makeEditForm();
|
||||
nameEdit_ = new QLineEdit(QString::fromStdString(anomaly_.name), this);
|
||||
formkit::capField(nameEdit_);
|
||||
form->addRow(formkit::editLabel(QStringLiteral("名称")), nameEdit_);
|
||||
|
||||
head->addRow(formkit::editLabel(QStringLiteral("名称")), nameEdit_);
|
||||
auto* typeLabel = new QLabel(QString::fromStdString(anomaly_.typeName), this);
|
||||
form->addRow(formkit::editLabel(QStringLiteral("异常类型")), typeLabel);
|
||||
head->addRow(formkit::editLabel(QStringLiteral("异常类型")), typeLabel);
|
||||
root->addLayout(head);
|
||||
|
||||
// 图例:线色 / 线宽 / 线型(对照原版 legend.polyline*)。
|
||||
colorBtn_ = new QPushButton(lineColor_, this);
|
||||
colorBtn_->setStyleSheet(QStringLiteral("background:%1;").arg(lineColor_));
|
||||
connect(colorBtn_, &QPushButton::clicked, this, [this]() {
|
||||
const QColor c = QColorDialog::getColor(QColor(lineColor_), this, QStringLiteral("线色"));
|
||||
if (c.isValid()) {
|
||||
lineColor_ = c.name();
|
||||
colorBtn_->setText(lineColor_);
|
||||
colorBtn_->setStyleSheet(QStringLiteral("background:%1;").arg(lineColor_));
|
||||
}
|
||||
});
|
||||
formkit::capField(colorBtn_);
|
||||
form->addRow(formkit::editLabel(QStringLiteral("线色")), colorBtn_);
|
||||
|
||||
widthSpin_ = new QDoubleSpinBox(this);
|
||||
widthSpin_->setRange(0.1, 20.0);
|
||||
widthSpin_->setSingleStep(0.5);
|
||||
widthSpin_->setValue(anomaly_.lineWidth > 0 ? anomaly_.lineWidth : 1.0);
|
||||
formkit::capField(widthSpin_);
|
||||
form->addRow(formkit::editLabel(QStringLiteral("线宽")), widthSpin_);
|
||||
|
||||
shapeCombo_ = new QComboBox(this);
|
||||
shapeCombo_->addItem(QStringLiteral("实线"), QStringLiteral("solid"));
|
||||
shapeCombo_->addItem(QStringLiteral("虚线"), QStringLiteral("dash"));
|
||||
shapeCombo_->setCurrentIndex(anomaly_.dashed ? 1 : 0);
|
||||
formkit::capField(shapeCombo_);
|
||||
form->addRow(formkit::editLabel(QStringLiteral("线型")), shapeCombo_);
|
||||
// ── 双 Tab:图例信息 / 坐标信息 ───────────────────────────────────────────
|
||||
auto* tabs = new QTabWidget(this);
|
||||
tabs->addTab(buildLegendTab(), QStringLiteral("图例信息"));
|
||||
tabs->addTab(buildCoordTab(), QStringLiteral("坐标信息"));
|
||||
root->addWidget(tabs, 1);
|
||||
|
||||
// ── 底部:备注(可编辑) ───────────────────────────────────────────────────
|
||||
root->addWidget(new QLabel(QStringLiteral("备注:"), this));
|
||||
remarkEdit_ = new QPlainTextEdit(QString::fromStdString(anomaly_.remark), this);
|
||||
remarkEdit_->setFixedHeight(geopro::app::scaledPx(60));
|
||||
formkit::capField(remarkEdit_);
|
||||
form->addRow(formkit::editLabel(QStringLiteral("备注")), remarkEdit_);
|
||||
cardLay->addLayout(form);
|
||||
root->addWidget(card);
|
||||
|
||||
// 坐标(只读展示,对照原版坐标信息 tab)。
|
||||
root->addWidget(new QLabel(QStringLiteral("坐标:"), this));
|
||||
auto* coordTable = new QTableWidget(static_cast<int>(anomaly_.localPts.size()), 2, this);
|
||||
coordTable->setHorizontalHeaderLabels({QStringLiteral("x"), QStringLiteral("y")});
|
||||
coordTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
|
||||
coordTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
for (int r = 0; r < static_cast<int>(anomaly_.localPts.size()); ++r) {
|
||||
coordTable->setItem(r, 0,
|
||||
new QTableWidgetItem(QString::number(anomaly_.localPts[r].x, 'f', 7)));
|
||||
coordTable->setItem(r, 1,
|
||||
new QTableWidgetItem(QString::number(anomaly_.localPts[r].y, 'f', 7)));
|
||||
}
|
||||
root->addWidget(coordTable, 1);
|
||||
remarkEdit_->setFixedHeight(geopro::app::scaledPx(70));
|
||||
root->addWidget(remarkEdit_);
|
||||
|
||||
auto* btnLay = new QHBoxLayout();
|
||||
btnLay->addStretch();
|
||||
auto* cancelBtn = new QPushButton(QStringLiteral("取消"), this);
|
||||
okBtn_ = new QPushButton(QStringLiteral("确定"), this);
|
||||
okBtn_ = new QPushButton(QStringLiteral("更新"), this); // 对照原版 ok-text="更新"
|
||||
okBtn_->setDefault(true);
|
||||
btnLay->addWidget(cancelBtn);
|
||||
btnLay->addWidget(okBtn_);
|
||||
|
|
@ -107,6 +86,132 @@ ExceptionDetailDialog::ExceptionDetailDialog(geopro::data::IDatasetCommandReposi
|
|||
connect(okBtn_, &QPushButton::clicked, this, &ExceptionDetailDialog::onConfirm);
|
||||
}
|
||||
|
||||
QWidget* ExceptionDetailDialog::buildLegendTab() {
|
||||
auto* tab = new QWidget(this);
|
||||
auto* form = formkit::makeEditForm();
|
||||
|
||||
// 类型 + 顶点数/端点数(对照原版:多边形→顶点数,多段线→端点数)。
|
||||
const int mt = static_cast<int>(anomaly_.markType);
|
||||
const QString geoTypeName = mt == 1 ? QStringLiteral("点")
|
||||
: mt == 3 ? QStringLiteral("多边形")
|
||||
: mt == 4 ? QStringLiteral("文字")
|
||||
: QStringLiteral("多段线");
|
||||
form->addRow(formkit::editLabel(QStringLiteral("类型")), new QLabel(geoTypeName, tab));
|
||||
if (mt == 2 || mt == 3) {
|
||||
const QString cntLabel = mt == 3 ? QStringLiteral("顶点数") : QStringLiteral("端点数");
|
||||
form->addRow(formkit::editLabel(cntLabel),
|
||||
new QLabel(QString::number(anomaly_.localPts.size()), tab));
|
||||
}
|
||||
|
||||
// 线样式(只读展示,对照原版 disabled 控件:线色/线宽/线型)。
|
||||
form->addRow(formkit::editLabel(QStringLiteral("线色")),
|
||||
readonlySwatch(QString::fromStdString(anomaly_.lineColor), tab));
|
||||
form->addRow(formkit::editLabel(QStringLiteral("线宽")),
|
||||
new QLabel(QString::number(anomaly_.lineWidth), tab));
|
||||
form->addRow(formkit::editLabel(QStringLiteral("线型")),
|
||||
new QLabel(lineTypeName(anomaly_.dashed), tab));
|
||||
|
||||
// 文字类型:另展示字体/字号/字色/不透明度(只读)。
|
||||
if (mt == 4) {
|
||||
form->addRow(formkit::editLabel(QStringLiteral("内容")),
|
||||
new QLabel(QString::fromStdString(anomaly_.textContent), tab));
|
||||
form->addRow(formkit::editLabel(QStringLiteral("字色")),
|
||||
readonlySwatch(QString::fromStdString(anomaly_.textColor), tab));
|
||||
form->addRow(formkit::editLabel(QStringLiteral("字号")),
|
||||
new QLabel(QString::number(anomaly_.textSize), tab));
|
||||
}
|
||||
|
||||
auto* lay = new QVBoxLayout(tab);
|
||||
lay->addLayout(form);
|
||||
lay->addStretch();
|
||||
return tab;
|
||||
}
|
||||
|
||||
QWidget* ExceptionDetailDialog::buildCoordTab() {
|
||||
auto* tab = new QWidget(this);
|
||||
auto* lay = new QVBoxLayout(tab);
|
||||
|
||||
// 坐标系切换 + 顶点数 + 导出(对照原版坐标信息 tab)。
|
||||
auto* topRow = new QHBoxLayout();
|
||||
topRow->addWidget(new QLabel(QStringLiteral("坐标系:"), tab));
|
||||
coordSysCombo_ = new QComboBox(tab);
|
||||
coordSysCombo_->addItem(QStringLiteral("图形坐标"), QStringLiteral("jb"));
|
||||
// 经纬度/投影:客户端暂无换算数据(DTO 只解析图形坐标),先占位、切换给提示,不静默。
|
||||
coordSysCombo_->addItem(QStringLiteral("经纬度坐标"), QStringLiteral("lonlat"));
|
||||
coordSysCombo_->addItem(QStringLiteral("投影坐标"), QStringLiteral("projection"));
|
||||
topRow->addWidget(coordSysCombo_);
|
||||
topRow->addStretch();
|
||||
vertexCountLabel_ =
|
||||
new QLabel(QStringLiteral("顶点数:%1").arg(anomaly_.localPts.size()), tab);
|
||||
topRow->addWidget(vertexCountLabel_);
|
||||
auto* exportBtn = new QPushButton(QStringLiteral("导出"), tab);
|
||||
topRow->addWidget(exportBtn);
|
||||
lay->addLayout(topRow);
|
||||
|
||||
coordTable_ = new QTableWidget(0, 4, tab);
|
||||
coordTable_->setHorizontalHeaderLabels(
|
||||
{QStringLiteral("序号"), QStringLiteral("X"), QStringLiteral("Y"), QStringLiteral("Z")});
|
||||
coordTable_->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
|
||||
coordTable_->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
lay->addWidget(coordTable_, 1);
|
||||
|
||||
connect(coordSysCombo_, &QComboBox::currentIndexChanged, this,
|
||||
[this](int) { onCoordSystemChanged(); });
|
||||
connect(exportBtn, &QPushButton::clicked, this, &ExceptionDetailDialog::exportCoords);
|
||||
|
||||
onCoordSystemChanged(); // 初次填图形坐标
|
||||
return tab;
|
||||
}
|
||||
|
||||
void ExceptionDetailDialog::onCoordSystemChanged() {
|
||||
if (!coordTable_) return;
|
||||
const QString sys = coordSysCombo_->currentData().toString();
|
||||
// 仅图形坐标有数据;经纬度/投影暂无换算能力 → 清表 + 提示(不静默吞)。
|
||||
if (sys != QStringLiteral("jb")) {
|
||||
coordTable_->setRowCount(0);
|
||||
QMessageBox::information(this, windowTitle(),
|
||||
QStringLiteral("经纬度/投影坐标换算暂未在客户端提供,仅支持图形坐标。"));
|
||||
return;
|
||||
}
|
||||
const int n = static_cast<int>(anomaly_.localPts.size());
|
||||
coordTable_->setRowCount(n);
|
||||
for (int r = 0; r < n; ++r) {
|
||||
coordTable_->setItem(r, 0, new QTableWidgetItem(QString::number(r + 1)));
|
||||
coordTable_->setItem(
|
||||
r, 1, new QTableWidgetItem(QString::number(anomaly_.localPts[r].x, 'f', 7)));
|
||||
coordTable_->setItem(
|
||||
r, 2, new QTableWidgetItem(QString::number(anomaly_.localPts[r].y, 'f', 7)));
|
||||
coordTable_->setItem(r, 3, new QTableWidgetItem(QString())); // Z 空(对照原版)
|
||||
}
|
||||
}
|
||||
|
||||
void ExceptionDetailDialog::exportCoords() {
|
||||
if (coordSysCombo_->currentData().toString() != QStringLiteral("jb") ||
|
||||
anomaly_.localPts.empty()) {
|
||||
QMessageBox::information(this, windowTitle(), QStringLiteral("当前坐标系无可导出的坐标。"));
|
||||
return;
|
||||
}
|
||||
const QString base = QString::fromStdString(anomaly_.name);
|
||||
const QString path = QFileDialog::getSaveFileName(
|
||||
this, QStringLiteral("导出坐标"),
|
||||
(base.isEmpty() ? QStringLiteral("coordinates") : base) + QStringLiteral(".txt"),
|
||||
QStringLiteral("Text (*.txt)"));
|
||||
if (path.isEmpty()) return;
|
||||
QFile f(path);
|
||||
if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
QMessageBox::warning(this, windowTitle(), QStringLiteral("无法写入文件"));
|
||||
return;
|
||||
}
|
||||
QTextStream ts(&f);
|
||||
// 对照原版:TSV「序号\tX\tY\tZ」,X/Y 7位小数,Z 空。
|
||||
ts << QStringLiteral("序号\tX\tY\tZ\n");
|
||||
for (int i = 0; i < static_cast<int>(anomaly_.localPts.size()); ++i) {
|
||||
ts << (i + 1) << '\t' << QString::number(anomaly_.localPts[i].x, 'f', 7) << '\t'
|
||||
<< QString::number(anomaly_.localPts[i].y, 'f', 7) << "\t\n";
|
||||
}
|
||||
f.close();
|
||||
}
|
||||
|
||||
void ExceptionDetailDialog::onConfirm() {
|
||||
if (!repo_ || anomaly_.id.empty()) { reject(); return; }
|
||||
const QString name = nameEdit_->text().trimmed();
|
||||
|
|
@ -114,9 +219,8 @@ void ExceptionDetailDialog::onConfirm() {
|
|||
QMessageBox::warning(this, windowTitle(), QStringLiteral("请输入名称"));
|
||||
return;
|
||||
}
|
||||
// 原版详情抽屉「改名称/备注」走 PUT /business/exception 的局部更新,
|
||||
// 仅发 {id, exceptionName, remark}(线样式是另一条独立 PUT,且抽屉里样式控件 disabled)。
|
||||
// 对齐原版 contourPage.vue onOk:不合并/不重发 legend。
|
||||
// 对照原版 drawerExceptionInfo onOk:图例样式控件 disabled、不发 legend,
|
||||
// 仅 PUT {id, exceptionName, remark}。
|
||||
QJsonObject body{
|
||||
{QStringLiteral("id"), QString::fromStdString(anomaly_.id)},
|
||||
{QStringLiteral("exceptionName"), name},
|
||||
|
|
|
|||
|
|
@ -6,9 +6,10 @@
|
|||
|
||||
class QLineEdit;
|
||||
class QPlainTextEdit;
|
||||
class QDoubleSpinBox;
|
||||
class QComboBox;
|
||||
class QTableWidget;
|
||||
class QPushButton;
|
||||
class QLabel;
|
||||
|
||||
namespace geopro::data {
|
||||
class IDatasetCommandRepository;
|
||||
|
|
@ -16,10 +17,13 @@ class IDatasetCommandRepository;
|
|||
|
||||
namespace geopro::app {
|
||||
|
||||
// 异常详情/编辑对话框(I11,复刻原版 drawerExceptionInfo 的可编辑部分):
|
||||
// 名称(可编辑) / 异常类型(只读) / 图例样式(线色/线宽/线型) / 备注(可编辑) / 坐标(只读展示)。
|
||||
// 确认 → updateException(PUT body {id, exceptionName, remark, legend:{polylineColor,
|
||||
// polylineWidth, polylineShape}}),成功 accept(),调用方随后 reloadGrid。
|
||||
// 异常详情/编辑对话框(I11,复刻原版 drawerExceptionInfo 右侧抽屉形态):
|
||||
// 双 Tab「图例信息 / 坐标信息」。
|
||||
// - 头部:名称(可编辑) + 异常类型(只读)。
|
||||
// - 图例信息:类型 + 顶点/端点数 + 线色/线宽/线型/不透明度(全部「只读展示」,对照原版 disabled)。
|
||||
// - 坐标信息:坐标系切换(图形/经纬度/投影) + 顶点数 + 坐标表(7位小数) + 导出 txt。
|
||||
// - 底部:备注(可编辑)。
|
||||
// 确认 → updateException(PUT body 仅 {id, exceptionName, remark},与原版一致;线样式只读不发)。
|
||||
class ExceptionDetailDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
|
|
@ -28,16 +32,19 @@ public:
|
|||
|
||||
private:
|
||||
void onConfirm();
|
||||
void onCoordSystemChanged(); // 切换坐标系 → 重填坐标表(图形有数据,经纬度/投影暂无)
|
||||
void exportCoords(); // 导出当前坐标系坐标为 txt(7位小数)
|
||||
QWidget* buildLegendTab(); // 图例信息 Tab(只读样式)
|
||||
QWidget* buildCoordTab(); // 坐标信息 Tab
|
||||
|
||||
geopro::data::IDatasetCommandRepository* repo_ = nullptr;
|
||||
geopro::core::Anomaly anomaly_; // 拷贝(不可变范式:编辑后组装新 body,不改原对象)
|
||||
|
||||
QLineEdit* nameEdit_ = nullptr;
|
||||
QPlainTextEdit* remarkEdit_ = nullptr;
|
||||
QPushButton* colorBtn_ = nullptr; // 线色选择(弹 QColorDialog)
|
||||
QString lineColor_; // 当前线色 hex
|
||||
QDoubleSpinBox* widthSpin_ = nullptr;
|
||||
QComboBox* shapeCombo_ = nullptr; // solid / dash
|
||||
QComboBox* coordSysCombo_ = nullptr; // jb 图形 / lonlat 经纬度 / projection 投影
|
||||
QTableWidget* coordTable_ = nullptr;
|
||||
QLabel* vertexCountLabel_ = nullptr;
|
||||
QPushButton* okBtn_ = nullptr;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -56,9 +56,24 @@ ExceptionDialog::ExceptionDialog(geopro::data::IDatasetCommandRepository* repo,
|
|||
formkit::capField(markTypeCombo_);
|
||||
form->addRow(formkit::editLabel(QStringLiteral("标注类型")), markTypeCombo_);
|
||||
|
||||
// 异常类型行:下拉 + 「新增异常类型」按钮(对照原版 exceptionDialog 同行布局)。
|
||||
exceptionTypeCombo_ = new QComboBox(this);
|
||||
formkit::capField(exceptionTypeCombo_);
|
||||
form->addRow(formkit::editLabel(QStringLiteral("异常类型")), exceptionTypeCombo_);
|
||||
addTypeBtn_ = new QPushButton(QStringLiteral("新增异常类型"), this);
|
||||
auto* typeRow = new QWidget(this);
|
||||
auto* typeRowLay = new QHBoxLayout(typeRow);
|
||||
typeRowLay->setContentsMargins(0, 0, 0, 0);
|
||||
typeRowLay->addWidget(exceptionTypeCombo_, 1);
|
||||
typeRowLay->addWidget(addTypeBtn_);
|
||||
form->addRow(formkit::editLabel(QStringLiteral("异常类型")), typeRow);
|
||||
connect(addTypeBtn_, &QPushButton::clicked, this, [this]() {
|
||||
// 原版点开「标注类型」新增弹窗(异常属性+名称,含完整 legend),提交 addExceptionType。
|
||||
// 该子流程含整套 legend 编辑,后端 addExceptionType 端点客户端尚未接入;此处先提示,
|
||||
// 不静默吞操作。用户可在 web 端新增类型后回到客户端选用。
|
||||
QMessageBox::information(
|
||||
this, windowTitle(),
|
||||
QStringLiteral("新增异常类型请在 Web 端完成,新增后此处下拉会同步可选。"));
|
||||
});
|
||||
|
||||
nameEdit_ = new QLineEdit(this);
|
||||
nameEdit_->setPlaceholderText(QStringLiteral("数据名称+异常类型代号+序号"));
|
||||
|
|
@ -143,10 +158,20 @@ QJsonArray ExceptionDialog::manualCoordinates() const {
|
|||
}
|
||||
|
||||
void ExceptionDialog::onTypeChanged() {
|
||||
// 主路径为图上绘形 → 坐标表默认留空(不自动补行),仅刷新异常类型列表。
|
||||
// 对照原版 handleAnnotationTypeChange:标注类型变 → 清空名称(待重选类型后回填)+
|
||||
// 重拉对应几何形态的异常类型列表 + 刷新「新增类型」按钮可用性。
|
||||
nameEdit_->clear();
|
||||
updateAddTypeEnabled();
|
||||
loadExceptionTypes();
|
||||
}
|
||||
|
||||
void ExceptionDialog::updateAddTypeEnabled() {
|
||||
if (!addTypeBtn_) return;
|
||||
// 原版:文字类型(4) 或 未选标注类型时禁用「新增异常类型」。
|
||||
const QString mt = markTypeValue();
|
||||
addTypeBtn_->setEnabled(!mt.isEmpty() && mt != QStringLiteral("4"));
|
||||
}
|
||||
|
||||
void ExceptionDialog::loadExceptionTypes() {
|
||||
if (!repo_) return;
|
||||
QPointer<ExceptionDialog> self(this);
|
||||
|
|
@ -172,12 +197,9 @@ void ExceptionDialog::suggestName() {
|
|||
const QString typeId = exceptionTypeCombo_->currentData().toString();
|
||||
if (typeId.isEmpty()) return;
|
||||
QPointer<ExceptionDialog> self(this);
|
||||
repo_->getExceptionName(typeId, remarkSourceId_, [self](bool ok, QJsonObject data, QString) {
|
||||
// 对照原版 handleExceptionTypeChange:每次选/换异常类型都回填名称(res.data 为纯字符串)。
|
||||
repo_->getExceptionName(typeId, remarkSourceId_, [self](bool ok, QString name, QString) {
|
||||
if (!self || !ok) return;
|
||||
// 仅当用户未手填时回填建议名(避免覆盖)。
|
||||
if (!self->nameEdit_->text().trimmed().isEmpty()) return;
|
||||
QString name = data.value(QStringLiteral("exceptionName")).toString();
|
||||
if (name.isEmpty()) name = data.value(QStringLiteral("name")).toString();
|
||||
self->nameEdit_->setText(name);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,10 +39,11 @@ public:
|
|||
QJsonArray manualCoordinates() const;
|
||||
|
||||
private:
|
||||
void onTypeChanged(); // 标注类型变 → 重拉异常类型列表 + 调整坐标表最少行
|
||||
void onTypeChanged(); // 标注类型变 → 清名称 + 重拉异常类型列表 + 刷新「新增类型」可用性
|
||||
void loadExceptionTypes(); // listExceptionTypes(projectId, remarkSourceType)
|
||||
void suggestName(); // getExceptionName(exceptionTypeId, remarkSourceId) → 名称建议
|
||||
void suggestName(); // getExceptionName(exceptionTypeId, remarkSourceId) → 名称回填
|
||||
void onConfirm(); // 校验 → 有手填坐标则直接 newException,否则 accept() 交给绘形
|
||||
void updateAddTypeEnabled(); // 「新增异常类型」可用性:文字类型/未选类型时禁用(对照原版)
|
||||
|
||||
geopro::data::IDatasetCommandRepository* repo_ = nullptr;
|
||||
QString projectId_;
|
||||
|
|
@ -50,6 +51,7 @@ private:
|
|||
|
||||
QComboBox* markTypeCombo_ = nullptr; // userData = "1".."4"
|
||||
QComboBox* exceptionTypeCombo_ = nullptr; // userData = 异常类型 id
|
||||
QPushButton* addTypeBtn_ = nullptr; // 新增异常类型(对照原版,文字/未选类型禁用)
|
||||
QLineEdit* nameEdit_ = nullptr;
|
||||
QPlainTextEdit* remarkEdit_ = nullptr;
|
||||
QTableWidget* coordTable_ = nullptr;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,113 @@
|
|||
#include "panels/chart/ExceptionTextDialog.hpp"
|
||||
|
||||
#include <QColorDialog>
|
||||
#include <QComboBox>
|
||||
#include <QFormLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QPlainTextEdit>
|
||||
#include <QPushButton>
|
||||
#include <QSlider>
|
||||
#include <QSpinBox>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "FormKit.hpp"
|
||||
#include "Theme.hpp"
|
||||
|
||||
namespace geopro::app {
|
||||
|
||||
ExceptionTextDialog::ExceptionTextDialog(QWidget* parent)
|
||||
: QDialog(parent), color_(QStringLiteral("#000000")) {
|
||||
setWindowTitle(QStringLiteral("文本编辑"));
|
||||
setModal(true);
|
||||
resize(geopro::app::scaledPx(560), geopro::app::scaledPx(420));
|
||||
|
||||
auto* root = formkit::dialogRoot(this);
|
||||
auto* card = formkit::formCard(this);
|
||||
auto* cardLay = formkit::cardBody(card);
|
||||
auto* form = formkit::makeEditForm();
|
||||
|
||||
// 字体(对照原版 1宋体/2微软雅黑/3黑体/4楷体,值为字符串 int)。
|
||||
fontCombo_ = new QComboBox(this);
|
||||
fontCombo_->addItem(QStringLiteral("宋体"), QStringLiteral("1"));
|
||||
fontCombo_->addItem(QStringLiteral("微软雅黑"), QStringLiteral("2"));
|
||||
fontCombo_->addItem(QStringLiteral("黑体"), QStringLiteral("3"));
|
||||
fontCombo_->addItem(QStringLiteral("楷体"), QStringLiteral("4"));
|
||||
formkit::capField(fontCombo_);
|
||||
form->addRow(formkit::editLabel(QStringLiteral("字体")), fontCombo_);
|
||||
|
||||
// 大小(px,默认 12)。
|
||||
sizeSpin_ = new QSpinBox(this);
|
||||
sizeSpin_->setRange(1, 200);
|
||||
sizeSpin_->setValue(12);
|
||||
formkit::capField(sizeSpin_);
|
||||
form->addRow(formkit::editLabel(QStringLiteral("大小")), sizeSpin_);
|
||||
|
||||
// 颜色(默认黑)。
|
||||
colorBtn_ = new QPushButton(color_, this);
|
||||
colorBtn_->setStyleSheet(QStringLiteral("background:%1;").arg(color_));
|
||||
connect(colorBtn_, &QPushButton::clicked, this, &ExceptionTextDialog::pickColor);
|
||||
formkit::capField(colorBtn_);
|
||||
form->addRow(formkit::editLabel(QStringLiteral("颜色")), colorBtn_);
|
||||
|
||||
// 不透明度(0–100%,默认 100)。
|
||||
auto* opRow = new QWidget(this);
|
||||
auto* opLay = new QHBoxLayout(opRow);
|
||||
opLay->setContentsMargins(0, 0, 0, 0);
|
||||
opacitySlider_ = new QSlider(Qt::Horizontal, opRow);
|
||||
opacitySlider_->setRange(0, 100);
|
||||
opacitySlider_->setValue(100);
|
||||
opacityLabel_ = new QLabel(QStringLiteral("100%"), opRow);
|
||||
opacityLabel_->setFixedWidth(geopro::app::scaledPx(40));
|
||||
connect(opacitySlider_, &QSlider::valueChanged, this,
|
||||
[this](int v) { opacityLabel_->setText(QStringLiteral("%1%").arg(v)); });
|
||||
opLay->addWidget(opacitySlider_, 1);
|
||||
opLay->addWidget(opacityLabel_);
|
||||
form->addRow(formkit::editLabel(QStringLiteral("不透明度")), opRow);
|
||||
cardLay->addLayout(form);
|
||||
|
||||
// 内容(必填)。
|
||||
cardLay->addWidget(new QLabel(QStringLiteral("内容:"), this));
|
||||
contentEdit_ = new QPlainTextEdit(this);
|
||||
contentEdit_->setPlaceholderText(QStringLiteral("请输入内容"));
|
||||
contentEdit_->setFixedHeight(geopro::app::scaledPx(140));
|
||||
cardLay->addWidget(contentEdit_);
|
||||
root->addWidget(card);
|
||||
|
||||
auto* btnLay = new QHBoxLayout();
|
||||
btnLay->addStretch();
|
||||
auto* cancelBtn = new QPushButton(QStringLiteral("取消"), this);
|
||||
auto* okBtn = new QPushButton(QStringLiteral("确定"), this);
|
||||
okBtn->setDefault(true);
|
||||
btnLay->addWidget(cancelBtn);
|
||||
btnLay->addWidget(okBtn);
|
||||
root->addLayout(btnLay);
|
||||
|
||||
connect(cancelBtn, &QPushButton::clicked, this, &QDialog::reject);
|
||||
connect(okBtn, &QPushButton::clicked, this, &ExceptionTextDialog::onConfirm);
|
||||
}
|
||||
|
||||
void ExceptionTextDialog::pickColor() {
|
||||
const QColor c = QColorDialog::getColor(QColor(color_), this, QStringLiteral("颜色"));
|
||||
if (!c.isValid()) return;
|
||||
color_ = c.name(QColor::HexRgb);
|
||||
colorBtn_->setText(color_);
|
||||
colorBtn_->setStyleSheet(QStringLiteral("background:%1;").arg(color_));
|
||||
}
|
||||
|
||||
QString ExceptionTextDialog::fontFamilyValue() const { return fontCombo_->currentData().toString(); }
|
||||
int ExceptionTextDialog::fontSize() const { return sizeSpin_->value(); }
|
||||
QString ExceptionTextDialog::color() const { return color_; }
|
||||
int ExceptionTextDialog::opacityPercent() const { return opacitySlider_->value(); }
|
||||
QString ExceptionTextDialog::content() const { return contentEdit_->toPlainText().trimmed(); }
|
||||
|
||||
void ExceptionTextDialog::onConfirm() {
|
||||
if (content().isEmpty()) { // 对照原版:内容必填。
|
||||
QMessageBox::warning(this, windowTitle(), QStringLiteral("请输入文本内容"));
|
||||
return;
|
||||
}
|
||||
accept();
|
||||
}
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
#pragma once
|
||||
#include <QDialog>
|
||||
#include <QString>
|
||||
|
||||
class QComboBox;
|
||||
class QSpinBox;
|
||||
class QSlider;
|
||||
class QPlainTextEdit;
|
||||
class QPushButton;
|
||||
class QLabel;
|
||||
|
||||
namespace geopro::app {
|
||||
|
||||
// 文字标注编辑对话框(I9,复刻原版 exceptionText.vue「文本编辑」):
|
||||
// 字体(1宋体/2微软雅黑/3黑体/4楷体) / 大小(px) / 颜色 / 不透明度(0–100%) / 内容(必填)。
|
||||
// 确定 → accept(),调用方读取各字段组装 newException 的 customLegend。
|
||||
// 时序对照原版:文字类型绘制落点后弹此对话框,提交带 customLegend
|
||||
// {text, content, color, size, font(CSS族), opacity(0–1)}。
|
||||
class ExceptionTextDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ExceptionTextDialog(QWidget* parent = nullptr);
|
||||
|
||||
// accept() 后供调用方读取。
|
||||
QString fontFamilyValue() const; // "1".."4"(字体族 int 字符串)
|
||||
int fontSize() const; // px
|
||||
QString color() const; // #rrggbb
|
||||
int opacityPercent() const; // 0–100
|
||||
QString content() const; // 文字内容
|
||||
|
||||
private:
|
||||
void onConfirm(); // 校验内容非空 → accept()
|
||||
void pickColor();
|
||||
|
||||
QComboBox* fontCombo_ = nullptr;
|
||||
QSpinBox* sizeSpin_ = nullptr;
|
||||
QPushButton* colorBtn_ = nullptr;
|
||||
QString color_; // 当前色 #rrggbb
|
||||
QSlider* opacitySlider_ = nullptr;
|
||||
QLabel* opacityLabel_ = nullptr;
|
||||
QPlainTextEdit* contentEdit_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace geopro::app
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
#include <QCheckBox>
|
||||
#include <QCursor>
|
||||
#include <QHash>
|
||||
#include <QHBoxLayout>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
|
|
@ -40,6 +41,7 @@
|
|||
#include "panels/chart/ContourPlotItem.hpp"
|
||||
#include "panels/chart/ExceptionDetailDialog.hpp"
|
||||
#include "panels/chart/ExceptionDialog.hpp"
|
||||
#include "panels/chart/ExceptionTextDialog.hpp"
|
||||
#include "panels/chart/FilterDialog.hpp"
|
||||
#include "panels/chart/GridWizardDialog.hpp"
|
||||
#include "panels/chart/LivePanner.hpp"
|
||||
|
|
@ -435,6 +437,28 @@ void GridDataChartView::openExceptionDialog() {
|
|||
QJsonArray coords;
|
||||
for (const QPointF& p : pts)
|
||||
coords.append(QJsonObject{{QStringLiteral("x"), p.x()}, {QStringLiteral("y"), p.y()}});
|
||||
// 文字类型(4):落点后另弹「文本编辑」对话框,提交带 customLegend(对照原版 exceptionText)。
|
||||
if (markType == QStringLiteral("4")) {
|
||||
ExceptionTextDialog tdlg(self);
|
||||
if (tdlg.exec() != QDialog::Accepted) return; // 取消 → 不提交
|
||||
// 字体族 int → CSS family(对照原版 fontFamily 映射)。
|
||||
static const QHash<QString, QString> kCssFont{
|
||||
{QStringLiteral("1"), QStringLiteral("SimSun")},
|
||||
{QStringLiteral("2"), QStringLiteral("Microsoft YaHei")},
|
||||
{QStringLiteral("3"), QStringLiteral("SimHei")},
|
||||
{QStringLiteral("4"), QStringLiteral("KaiTi")}};
|
||||
const QString content = tdlg.content();
|
||||
QJsonObject customLegend{
|
||||
{QStringLiteral("text"), content},
|
||||
{QStringLiteral("content"), content},
|
||||
{QStringLiteral("color"), tdlg.color()},
|
||||
{QStringLiteral("size"), tdlg.fontSize()},
|
||||
{QStringLiteral("font"), kCssFont.value(tdlg.fontFamilyValue())},
|
||||
{QStringLiteral("opacity"), tdlg.opacityPercent() / 100.0}, // 0–1
|
||||
};
|
||||
self->submitDrawnException(markType, typeId, name, remark, coords, customLegend);
|
||||
return;
|
||||
}
|
||||
self->submitDrawnException(markType, typeId, name, remark, coords);
|
||||
});
|
||||
drawTool_->setOnCancel([] {}); // 取消绘形:无操作(不提交)
|
||||
|
|
@ -443,7 +467,8 @@ void GridDataChartView::openExceptionDialog() {
|
|||
|
||||
void GridDataChartView::submitDrawnException(const QString& markType, const QString& typeId,
|
||||
const QString& name, const QString& remark,
|
||||
const QJsonArray& coords) {
|
||||
const QJsonArray& coords,
|
||||
const QJsonObject& customLegend) {
|
||||
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
|
||||
const QString projectId = projectIdGetter_ ? projectIdGetter_() : QString();
|
||||
if (!cmdRepo_ || dsId.isEmpty()) return;
|
||||
|
|
@ -456,6 +481,8 @@ void GridDataChartView::submitDrawnException(const QString& markType, const QStr
|
|||
{QStringLiteral("projectId"), projectId},
|
||||
{QStringLiteral("location"), QJsonObject{{QStringLiteral("coordinate"), coords}}},
|
||||
};
|
||||
// 文字类型带 customLegend(对照原版:仅文字非空,其它形态不带此字段)。
|
||||
if (!customLegend.isEmpty()) body.insert(QStringLiteral("customLegend"), customLegend);
|
||||
QPointer<GridDataChartView> self(this);
|
||||
cmdRepo_->newException(body, [self](bool ok, QString msg) {
|
||||
if (!self) return;
|
||||
|
|
@ -472,7 +499,8 @@ void GridDataChartView::openAutoAnnotation() {
|
|||
const QString dsId = dsIdGetter_ ? dsIdGetter_() : QString();
|
||||
const QString projectId = projectIdGetter_ ? projectIdGetter_() : QString();
|
||||
if (!cmdRepo_ || dsId.isEmpty()) { showNotImplemented(nullptr); return; }
|
||||
AutoAnnotationDialog dlg(cmdRepo_, dsId, projectId, this);
|
||||
// 透传网格标量用于右上数据统计(max/min/mean/median)。
|
||||
AutoAnnotationDialog dlg(cmdRepo_, dsId, projectId, grid_.values(), this);
|
||||
if (dlg.exec() == QDialog::Accepted) reloadGrid();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <QJsonObject> // submitDrawnException 默认参数 const QJsonObject& = {} 需完整类型
|
||||
#include <QString>
|
||||
#include <QWidget>
|
||||
|
||||
|
|
@ -81,10 +82,12 @@ private:
|
|||
void applySimplify(); // I8:把当前滑块容差透传给 ContourPlotItem 并重绘
|
||||
void showNotImplemented(QWidget* anchor); // 占位提示(无仓储/无 dsId)
|
||||
|
||||
void openExceptionDialog(); // I9 异常创建(弹窗选类型 → 图上绘形 → 提交)
|
||||
void openExceptionDialog(); // I9 异常创建(弹窗选类型 → 图上绘形 →[文字另弹文本编辑]→ 提交)
|
||||
// I9 图上绘形完成:组装 body 提交 newException(成功 reloadGrid)。
|
||||
// customLegend 仅文字类型非空(对照原版:文字 customLegend,其它形态留空 {})。
|
||||
void submitDrawnException(const QString& markType, const QString& typeId, const QString& name,
|
||||
const QString& remark, const QJsonArray& coords);
|
||||
const QString& remark, const QJsonArray& coords,
|
||||
const QJsonObject& customLegend = {});
|
||||
void openAutoAnnotation(); // I13 自动标注
|
||||
void deleteAnomaly(int index); // I10 异常删除
|
||||
void showAnomalyDetail(int index); // I11 异常详情/编辑
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
#include <vector>
|
||||
namespace geopro::core {
|
||||
|
||||
enum class AnomalyMarkType { Point = 1, Polyline = 2, Polygon = 3 };
|
||||
enum class AnomalyMarkType { Point = 1, Polyline = 2, Polygon = 3, Text = 4 };
|
||||
|
||||
struct Vec2 { double x, y; };
|
||||
struct Vec3 { double x, y, z; };
|
||||
|
|
@ -27,6 +27,13 @@ struct Anomaly {
|
|||
std::string lineColor = "#000000"; // legend.polylineColor
|
||||
double lineWidth = 1.0; // legend.polylineWidth
|
||||
bool dashed = true; // legend.polylineShape == "dash"
|
||||
// 文字标注(markType==Text)专属:customLegend 字段(对照原版 exceptionText.vue)。
|
||||
// 仅文字类型有意义;其它类型留默认。
|
||||
std::string textContent; // customLegend.content / text(文字内容)
|
||||
std::string textColor = "#000000"; // customLegend.color(字色)
|
||||
int textSize = 12; // customLegend.size(字号 px)
|
||||
int textFont = 1; // customLegend 字体族 int(1宋体/2微软雅黑/3黑体/4楷体)
|
||||
double textOpacity = 1.0; // customLegend.opacity(0–1)
|
||||
};
|
||||
|
||||
} // namespace geopro::core
|
||||
|
|
|
|||
|
|
@ -50,6 +50,24 @@ void wireArray(net::IApiCall* call, std::function<void(bool, QJsonArray, QString
|
|||
});
|
||||
}
|
||||
|
||||
// 返回字符串:(bool ok, QString value, QString msg)。
|
||||
// 原版 res.data 为纯字符串时,parseBody 会包成 {"value": "<string>"},故取 data.value。
|
||||
void wireString(net::IApiCall* call, std::function<void(bool, QString, QString)> cb) {
|
||||
if (call == nullptr) {
|
||||
if (cb) cb(false, {}, QStringLiteral("请求创建失败"));
|
||||
return;
|
||||
}
|
||||
QObject::connect(call, &net::IApiCall::finished, call,
|
||||
[cb = std::move(cb)](const net::ApiResponse& resp) {
|
||||
if (!cb) return;
|
||||
if (!isOk(resp)) {
|
||||
cb(false, {}, resp.msg);
|
||||
return;
|
||||
}
|
||||
cb(true, resp.data.value(QStringLiteral("value")).toString(), resp.msg);
|
||||
});
|
||||
}
|
||||
|
||||
// 返回对象:(bool ok, QJsonObject data, QString msg)。
|
||||
void wireObject(net::IApiCall* call, std::function<void(bool, QJsonObject, QString)> cb) {
|
||||
if (call == nullptr) {
|
||||
|
|
@ -368,10 +386,11 @@ void ApiDatasetCommandRepository::listExceptionTypes(
|
|||
|
||||
void ApiDatasetCommandRepository::getExceptionName(
|
||||
const QString& exceptionTypeId, const QString& remarkSourceId,
|
||||
std::function<void(bool, QJsonObject, QString)> cb) {
|
||||
std::function<void(bool, QString, QString)> cb) {
|
||||
QJsonObject body{{QStringLiteral("exceptionTypeId"), exceptionTypeId},
|
||||
{QStringLiteral("remarkSourceId"), remarkSourceId}};
|
||||
wireObject(api_.postJsonAsync(QStringLiteral("/business/exception/getExceptionName"), body),
|
||||
// 原版 res.data 直接是名称字符串,回传纯字符串。
|
||||
wireString(api_.postJsonAsync(QStringLiteral("/business/exception/getExceptionName"), body),
|
||||
std::move(cb));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ public:
|
|||
std::function<void(bool ok, QJsonArray list, QString msg)> cb) override;
|
||||
void getExceptionName(
|
||||
const QString& exceptionTypeId, const QString& remarkSourceId,
|
||||
std::function<void(bool ok, QJsonObject data, QString msg)> cb) override;
|
||||
std::function<void(bool ok, QString name, QString msg)> cb) override;
|
||||
void newException(const QJsonObject& body,
|
||||
std::function<void(bool ok, QString msg)> cb) override;
|
||||
void deleteException(const QString& id,
|
||||
|
|
|
|||
|
|
@ -71,13 +71,23 @@ std::vector<Anomaly> parseDatasetAnomalies(const QJsonArray& arr) {
|
|||
a.exceptionTypeId = o.value("exceptionTypeId").toString().toStdString();
|
||||
a.remark = o.value("remark").toString().toStdString();
|
||||
a.createTime = o.value("createTime").toString().toStdString();
|
||||
const int mt = o.value("exceptionMarkType").toInt(2); // 1=点 2=线 3=面
|
||||
a.markType = (mt >= 1 && mt <= 3) ? static_cast<AnomalyMarkType>(mt)
|
||||
const int mt = o.value("exceptionMarkType").toInt(2); // 1=点 2=线 3=面 4=文字
|
||||
a.markType = (mt >= 1 && mt <= 4) ? static_cast<AnomalyMarkType>(mt)
|
||||
: AnomalyMarkType::Polyline; // 越界值兜底为线
|
||||
const QJsonObject lg = o.value("legend").toObject();
|
||||
a.lineColor = lg.value("polylineColor").toString("#000000").toStdString();
|
||||
a.lineWidth = lg.value("polylineWidth").toDouble(1.0);
|
||||
a.dashed = lg.value("polylineShape").toString() == "dash";
|
||||
// 文字标注 customLegend(详情只读展示用):content/color/size/opacity。
|
||||
const QJsonObject cl = o.value("customLegend").toObject();
|
||||
if (!cl.isEmpty()) {
|
||||
QString content = cl.value("content").toString();
|
||||
if (content.isEmpty()) content = cl.value("text").toString();
|
||||
a.textContent = content.toStdString();
|
||||
a.textColor = cl.value("color").toString("#000000").toStdString();
|
||||
a.textSize = cl.value("size").toInt(12);
|
||||
a.textOpacity = cl.value("opacity").toDouble(1.0);
|
||||
}
|
||||
for (auto c : o.value("location").toObject().value("coordinate").toArray()) {
|
||||
const QJsonObject p = c.toObject();
|
||||
a.localPts.push_back(Vec2{p.value("x").toDouble(), p.value("y").toDouble()});
|
||||
|
|
|
|||
|
|
@ -169,10 +169,10 @@ public:
|
|||
|
||||
// 获取异常名称:POST /business/exception/getExceptionName
|
||||
// body {exceptionTypeId, remarkSourceId}(对应原版 queryExceptionNameInProfileInversion)。
|
||||
// 回调 data = 响应 data 对象(含建议名称)。
|
||||
// 原版 res.data 是「纯字符串」(建议名称本身),故回调直接回传 name 字符串。
|
||||
virtual void getExceptionName(
|
||||
const QString& exceptionTypeId, const QString& remarkSourceId,
|
||||
std::function<void(bool ok, QJsonObject data, QString msg)> cb) = 0;
|
||||
std::function<void(bool ok, QString name, QString msg)> cb) = 0;
|
||||
|
||||
// 新增异常:POST /business/exception
|
||||
// body 含 {exceptionName, exceptionTypeId, location, projectId, remarkSourceId, remarkSourceType, remark}
|
||||
|
|
|
|||
|
|
@ -146,6 +146,55 @@ TEST(QuillDelta, OrderedListBlockRoundTrip) {
|
|||
EXPECT_TRUE(found);
|
||||
}
|
||||
|
||||
// ── 行内:背景色往返(原版 ql-background)─────────────────────────────────
|
||||
TEST(QuillDelta, BackgroundColorRoundTrip) {
|
||||
const auto ops = opsFromJson(R"([
|
||||
{"insert":"hl","attributes":{"background":"#ffff00"}},
|
||||
{"insert":"\n"}
|
||||
])");
|
||||
QTextDocument doc;
|
||||
deltaToDocument(ops, doc);
|
||||
const QTextCharFormat f = doc.begin().begin().fragment().charFormat();
|
||||
EXPECT_EQ(f.background().color().name(QColor::HexRgb), QStringLiteral("#ffff00"));
|
||||
|
||||
const QJsonObject a = attrsOf(documentToDelta(doc), 0);
|
||||
EXPECT_EQ(a.value(QStringLiteral("background")).toString(), QStringLiteral("#ffff00"));
|
||||
}
|
||||
|
||||
// ── 行内:字体族往返(原版 ql-font token ↔ Qt family)──────────────────────
|
||||
TEST(QuillDelta, FontFamilyRoundTrip) {
|
||||
const auto ops = opsFromJson(R"([
|
||||
{"insert":"font","attributes":{"font":"Microsoft-YaHei"}},
|
||||
{"insert":"\n"}
|
||||
])");
|
||||
QTextDocument doc;
|
||||
deltaToDocument(ops, doc);
|
||||
const QTextCharFormat f = doc.begin().begin().fragment().charFormat();
|
||||
EXPECT_EQ(f.fontFamilies().toStringList().value(0), QStringLiteral("Microsoft YaHei"));
|
||||
|
||||
// 回写应还原为原版 token(连字符形式)。
|
||||
const QJsonObject a = attrsOf(documentToDelta(doc), 0);
|
||||
EXPECT_EQ(a.value(QStringLiteral("font")).toString(), QStringLiteral("Microsoft-YaHei"));
|
||||
}
|
||||
|
||||
// ── 块级:对齐往返(原版 ql-align)────────────────────────────────────────
|
||||
TEST(QuillDelta, AlignBlockRoundTrip) {
|
||||
const auto ops = opsFromJson(R"([
|
||||
{"insert":"centered"},
|
||||
{"insert":"\n","attributes":{"align":"center"}}
|
||||
])");
|
||||
QTextDocument doc;
|
||||
deltaToDocument(ops, doc);
|
||||
EXPECT_EQ(doc.begin().blockFormat().alignment() & Qt::AlignHorizontal_Mask, Qt::AlignHCenter);
|
||||
|
||||
bool found = false;
|
||||
for (const QJsonValue& v : documentToDelta(doc)) {
|
||||
const QJsonObject a = v.toObject().value(QStringLiteral("attributes")).toObject();
|
||||
if (a.value(QStringLiteral("align")).toString() == QStringLiteral("center")) found = true;
|
||||
}
|
||||
EXPECT_TRUE(found);
|
||||
}
|
||||
|
||||
// ── 容错:无法识别的 attributes 降级(保留文本,不崩) ──────────────────────
|
||||
TEST(QuillDelta, UnknownAttributesDegradeGracefully) {
|
||||
const auto ops = opsFromJson(R"([
|
||||
|
|
|
|||
Loading…
Reference in New Issue