feat/vtk-3d-view #7

Merged
gaozheng merged 301 commits from feat/vtk-3d-view into main 2026-06-27 18:43:52 +08:00
13 changed files with 91 additions and 262 deletions
Showing only changes of commit 75cf8d40ba - Show all commits

View File

@ -41,7 +41,6 @@ add_executable(geopro_desktop WIN32
panels/QuillDelta.cpp panels/QuillDelta.cpp
panels/chart/RawDataChartView.cpp panels/chart/RawDataChartView.cpp
panels/chart/InversionFormDialog.cpp panels/chart/InversionFormDialog.cpp
panels/chart/InversionFormParse.cpp
panels/chart/ScatterDataOps.cpp panels/chart/ScatterDataOps.cpp
panels/chart/SaveAsDialog.cpp panels/chart/SaveAsDialog.cpp
panels/chart/ScatterFilterDialog.cpp panels/chart/ScatterFilterDialog.cpp

View File

@ -10,10 +10,13 @@
#include <QPainter> #include <QPainter>
#include <QPointer> #include <QPointer>
#include <QPushButton> #include <QPushButton>
#include <QRadioButton>
#include <QTableView> #include <QTableView>
#include <QToolTip> #include <QToolTip>
#include <QVBoxLayout> #include <QVBoxLayout>
#include "Theme.hpp"
#include "panels/chart/InversionFormDialog.hpp" #include "panels/chart/InversionFormDialog.hpp"
#include "panels/chart/ScatterDataOps.hpp" // toggledDisplayStatus #include "panels/chart/ScatterDataOps.hpp" // toggledDisplayStatus
#include "panels/chart/TablePager.hpp" #include "panels/chart/TablePager.hpp"
@ -155,11 +158,16 @@ DataTableView::DataTableView(QWidget* parent) : QWidget(parent) {
lay->setContentsMargins(0, 0, 0, 0); lay->setContentsMargins(0, 0, 0, 0);
lay->setSpacing(0); lay->setSpacing(0);
// 顶部功能按钮行(默认隐藏;仅 dd_grid 载荷带 functionButtons 时显示)。右对齐贴近原版布局。 // 顶部功能按钮行(默认隐藏;仅 dd_grid 载荷带 functionButtons 时显示)。
// 布局对照原版 DdGrid/index.vue .swicth左侧 radio-group(「电法列表」单选项) + 右侧主按钮组,
// space-between。radio 仅视觉占位(原版亦仅一项、无实际切换作用)。
toolbar_ = new QWidget(this); toolbar_ = new QWidget(this);
toolbarLay_ = new QHBoxLayout(toolbar_); toolbarLay_ = new QHBoxLayout(toolbar_);
toolbarLay_->setContentsMargins(0, 0, 0, 8); toolbarLay_->setContentsMargins(0, 0, 0, 8);
toolbarLay_->addStretch(1); auto* listRadio = new QRadioButton(QStringLiteral("电法列表"), toolbar_);
listRadio->setChecked(true);
toolbarLay_->addWidget(listRadio); // index 0左侧单选项
toolbarLay_->addStretch(1); // index 1把功能按钮推到右侧rebuildToolbar 在末尾追加)
toolbar_->hide(); toolbar_->hide();
lay->addWidget(toolbar_); lay->addWidget(toolbar_);
@ -230,24 +238,36 @@ void DataTableView::setCommandRepo(geopro::data::IDatasetCommandRepository* repo
} }
void DataTableView::rebuildToolbar(const std::vector<geopro::core::TableFunctionButton>& buttons) { void DataTableView::rebuildToolbar(const std::vector<geopro::core::TableFunctionButton>& buttons) {
// 清空旧按钮(保留末尾 addStretch逐项删 QPushButton)。 // 清空旧功能按钮(仅删 QPushButton保留左侧 radio 与中间 stretch)。
for (int i = toolbarLay_->count() - 1; i >= 0; --i) { for (int i = toolbarLay_->count() - 1; i >= 0; --i) {
if (auto* w = toolbarLay_->itemAt(i)->widget()) { if (auto* btn = qobject_cast<QPushButton*>(toolbarLay_->itemAt(i)->widget())) {
toolbarLay_->removeWidget(w); toolbarLay_->removeWidget(btn);
w->deleteLater(); btn->deleteLater();
} }
} }
// 仅渲染 enable 的按钮(原版 v-show="enable");全空/全禁用 → 隐藏整行。 // 仅渲染 enable 的按钮(原版 v-show="enable");全空/全禁用 → 隐藏整行。
// 主按钮蓝色实心(对照原版 type="primary"),复用 primaryBtn QSS。
int shown = 0; int shown = 0;
for (const auto& b : buttons) { for (const auto& b : buttons) {
if (!b.enable) continue; if (!b.enable) continue;
auto* btn = new QPushButton(b.nameChn, toolbar_); auto* btn = new QPushButton(b.nameChn, toolbar_);
btn->setObjectName(QStringLiteral("primaryBtn"));
const QString code = b.code; const QString code = b.code;
connect(btn, &QPushButton::clicked, this, [this, code] { onFunctionButton(code); }); connect(btn, &QPushButton::clicked, this, [this, code] { onFunctionButton(code); });
toolbarLay_->addWidget(btn); toolbarLay_->addWidget(btn); // 末尾追加 → 落在 stretch 之后(右侧)
++shown; ++shown;
} }
if (shown > 0) {
applyTokenizedStyleSheet(
toolbar_,
QStringLiteral(
"QPushButton#primaryBtn { background: {{accent/primary}}; color: {{text/on-primary}};"
" border: 1px solid {{accent/primary}}; border-radius: 6px; padding: 6px 14px; }"
"QPushButton#primaryBtn:hover { background: {{accent/primary-hover}};"
" border-color: {{accent/primary-hover}}; }"
"QPushButton#primaryBtn:pressed { background: {{accent/primary-pressed}}; }"));
}
toolbar_->setVisible(shown > 0); toolbar_->setVisible(shown > 0);
} }

View File

@ -3,22 +3,21 @@
#include <utility> #include <utility>
#include <QComboBox> #include <QComboBox>
#include <QFrame>
#include <QGridLayout>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QMessageBox> #include <QMessageBox>
#include <QPointer> #include <QPointer>
#include <QPushButton> #include <QPushButton>
#include <QScrollArea>
#include <QSignalBlocker> #include <QSignalBlocker>
#include <QVBoxLayout> #include <QVBoxLayout>
#include "FormKit.hpp" #include "FormKit.hpp"
#include "dto/NavDto.hpp" // parseEditableForm与对象/结构编辑共用的动态表单解析)
#include "panels/DynamicFormEditor.hpp"
#include "repo/IDatasetCommandRepository.hpp" #include "repo/IDatasetCommandRepository.hpp"
namespace geopro::app { namespace geopro::app {
// 纯解析/组装函数定义在 InversionFormParse.cppQt-Core-only便于单测
namespace { namespace {
constexpr char kVisualResistivityCode[] = "script_visual_resistivity_data"; constexpr char kVisualResistivityCode[] = "script_visual_resistivity_data";
} // namespace } // namespace
@ -45,11 +44,14 @@ InversionFormDialog::InversionFormDialog(Mode mode, geopro::data::IDatasetComman
modelLay->addWidget(modelCombo_); modelLay->addWidget(modelCombo_);
root->addLayout(modelLay); root->addLayout(modelLay);
// 动态字段容器(按 groups_ 重建)。 // 动态字段容器:复用 DynamicFormEditor按 displayComponentType 渲染 11 种控件 + 必填校验)。
formHost_ = new QWidget(this); // 放滚动区内,避免字段多时撑爆对话框。
formHostLay_ = new QVBoxLayout(formHost_); auto* scroll = new QScrollArea(this);
formHostLay_->setContentsMargins(0, 0, 0, 0); scroll->setWidgetResizable(true);
root->addWidget(formHost_, 1); scroll->setFrameShape(QFrame::NoFrame);
editor_ = new DynamicFormEditor();
scroll->setWidget(editor_);
root->addWidget(scroll, 1);
// 底部按钮(取消 / 确定)。 // 底部按钮(取消 / 确定)。
auto* btnLay = new QHBoxLayout(); auto* btnLay = new QHBoxLayout();
@ -103,9 +105,8 @@ void InversionFormDialog::loadScripts() {
void InversionFormDialog::onModelChanged() { void InversionFormDialog::onModelChanged() {
const QString typeId = modelCombo_->currentData().toString(); const QString typeId = modelCombo_->currentData().toString();
groups_.clear();
if (typeId.isEmpty()) { if (typeId.isEmpty()) {
rebuildFormArea(); // 清空表单(复刻 changeModel: 清空 dynamicForms editor_->clear(); // 清空表单(复刻 changeModel: 清空 dynamicForms
return; return;
} }
loadDynamicForm(typeId); loadDynamicForm(typeId);
@ -117,50 +118,12 @@ void InversionFormDialog::loadDynamicForm(const QString& typeId) {
repo_->getDynamicForm(projectId_, typeId, [self](bool ok, QJsonObject data, QString) { repo_->getDynamicForm(projectId_, typeId, [self](bool ok, QJsonObject data, QString) {
if (!self) return; if (!self) return;
if (!ok) return; if (!ok) return;
self->groups_ = parseDynamicForm(data); // 复用 parseEditableFormformList → values → displayComponentType/requiredType/optionsObject
self->rebuildFormArea(); // + DynamicFormEditor11 种控件渲染。confType 对反演渲染无影响,取 0 占位。
self->editor_->setForm(geopro::data::dto::parseEditableForm(data, 0));
}); });
} }
void InversionFormDialog::rebuildFormArea() {
// 清空旧字段控件(含布局子项),重置取值索引。
fieldCombos_.clear();
fieldCodes_.clear();
QLayoutItem* item = nullptr;
while ((item = formHostLay_->takeAt(0)) != nullptr) {
if (item->widget()) item->widget()->deleteLater();
delete item;
}
const bool fillDefaults = (mode_ == Mode::ApparentResistivity);
for (const auto& g : groups_) {
// 分组卡片:标题 + 字段两列网格(复刻原版 a-card 分组 + 半宽字段)。
auto* card = new QFrame(formHost_);
card->setObjectName(QStringLiteral("inversionGroupCard"));
auto* cardLay = new QVBoxLayout(card);
if (!g.groupName.isEmpty())
formkit::addSection(cardLay, g.groupName, card, /*topGap=*/false);
auto* grid = new QGridLayout();
cardLay->addLayout(grid);
int col = 0, gridRow = 0;
for (const auto& f : g.fields) {
auto* fieldBox = new QVBoxLayout();
fieldBox->addWidget(formkit::editLabel(f.fieldName, card));
auto* combo = new QComboBox(card);
for (const auto& o : f.options) combo->addItem(o.label, o.value);
// 生成视电阻率:默认选首项(复刻 initDynamicFieldsDefaultValues
if (fillDefaults && combo->count() > 0) combo->setCurrentIndex(0);
fieldBox->addWidget(combo);
grid->addLayout(fieldBox, gridRow, col);
fieldCombos_.push_back(combo);
fieldCodes_.push_back(f.fieldCode);
if (++col >= 2) { col = 0; ++gridRow; }
}
formHostLay_->addWidget(card);
}
formHostLay_->addStretch();
}
void InversionFormDialog::onConfirm() { void InversionFormDialog::onConfirm() {
if (!repo_) { reject(); return; } if (!repo_) { reject(); return; }
const QString scriptId = modelCombo_->currentData().toString(); const QString scriptId = modelCombo_->currentData().toString();
@ -169,14 +132,24 @@ void InversionFormDialog::onConfirm() {
return; return;
} }
// 由当前各字段下拉选值装配 {fieldCode: value}。 // 必填校验requiredType===1拦截提交并聚焦首个缺失字段对照原版 a-form rules
QJsonObject selected; QString missing;
for (size_t i = 0; i < fieldCombos_.size(); ++i) { if (!editor_->validateRequired(&missing)) {
selected.insert(fieldCodes_[static_cast<int>(i)], editor_->focusFirstInvalid();
fieldCombos_[i]->currentData().toString()); QMessageBox::warning(this, windowTitle(),
QStringLiteral("请填写必填项:%1").arg(missing));
return;
}
// 由各动态控件收集 {fieldCode: value}。生成视电阻率:空值不进体(对照原版 if(selectedValue)
// 反演运算:保留全部字段(对照原版 InversionForm 提交 form.properties 整体)。
const auto values = editor_->collectValues();
QJsonObject fields;
const bool omitEmpty = (mode_ == Mode::ApparentResistivity);
for (auto it = values.constBegin(); it != values.constEnd(); ++it) {
if (omitEmpty && it.value().trimmed().isEmpty()) continue;
fields.insert(it.key(), it.value());
} }
const bool fillDefaults = (mode_ == Mode::ApparentResistivity);
const QJsonObject fields = assembleFieldMap(groups_, selected, fillDefaults);
okBtn_->setEnabled(false); okBtn_->setEnabled(false);
QPointer<InversionFormDialog> self(this); QPointer<InversionFormDialog> self(this);

View File

@ -1,14 +1,9 @@
#pragma once #pragma once
#include <vector>
#include <QDialog> #include <QDialog>
#include <QJsonObject> #include <QJsonObject>
#include <QString> #include <QString>
#include "panels/chart/InversionFormParse.hpp" // InversionGroup + 纯解析/组装函数
class QComboBox; class QComboBox;
class QVBoxLayout;
class QPushButton; class QPushButton;
class QWidget; class QWidget;
@ -18,14 +13,18 @@ class IDatasetCommandRepository;
namespace geopro::app { namespace geopro::app {
class DynamicFormEditor;
// 反演动态表单对话框1:1 复刻原版 web。一套对话框服务两个入口用 Mode 区分: // 反演动态表单对话框1:1 复刻原版 web。一套对话框服务两个入口用 Mode 区分:
// - Inversion → measurement「反演运算」原版 InversionForm.vue + postInversionTask // - Inversion → measurement「反演运算」原版 InversionForm.vue + postInversionTask
// - ApparentResistivity → measurement「生成视电阻率」原版 InversionDialog.vue + createVisualResistivityData // - ApparentResistivity → measurement「生成视电阻率」原版 InversionDialog.vue + createVisualResistivityData
// 共同流程:① 拉模型列表 → ② 选模型 → ③ 按 typeId 拉动态表单 → ④ 分组卡片渲染字段 → ⑤ 提交。 // 共同流程:① 拉模型列表 → ② 选模型 → ③ 按 typeId 拉动态表单 → ④ 分组卡片渲染字段 → ⑤ 提交。
// 动态字段渲染复用项目内 DynamicFormEditor与对象/结构编辑同一套控件),按 displayComponentType
// 渲染 11 种控件并做 requiredType===1 必填校验、requiredType===2 只读禁用(对照原版 FormItem.vue
// 差异(严格对照原版): // 差异(严格对照原版):
// Inversion模型下拉可选(allow-clear)、无默认选中、无字段默认值;提交体 {dsId,scriptId,properties}。 // Inversion模型下拉可选(allow-clear)、无默认选中;提交体 {dsId,scriptId,properties}。
// ApparentResistivity模型下拉禁用、默认选中 code=='script_visual_resistivity_data' // ApparentResistivity模型下拉禁用、默认选中 code=='script_visual_resistivity_data'
// 字段默认取首个选项;提交体 {dsObjectId,scriptId,scriptParamListJsonStr}。 // 提交体 {dsObjectId,scriptId,scriptParamListJsonStr}。
// 回调用 QPointer 守卫(对话框 modal exec但异步回调仍可能在关闭后到达 // 回调用 QPointer 守卫(对话框 modal exec但异步回调仍可能在关闭后到达
class InversionFormDialog : public QDialog { class InversionFormDialog : public QDialog {
Q_OBJECT Q_OBJECT
@ -42,7 +41,6 @@ private:
void loadScripts(); // 拉模型列表填下拉 void loadScripts(); // 拉模型列表填下拉
void onModelChanged(); // 模型变更 → 拉动态表单 void onModelChanged(); // 模型变更 → 拉动态表单
void loadDynamicForm(const QString& typeId); void loadDynamicForm(const QString& typeId);
void rebuildFormArea(); // 按 groups_ 重建分组卡片
void onConfirm(); // 提交(按 mode 走不同端点) void onConfirm(); // 提交(按 mode 走不同端点)
Mode mode_; Mode mode_;
@ -51,13 +49,8 @@ private:
QString projectId_; QString projectId_;
QComboBox* modelCombo_ = nullptr; QComboBox* modelCombo_ = nullptr;
QWidget* formHost_ = nullptr; // 动态字段容器(重建时清空重填) DynamicFormEditor* editor_ = nullptr; // 动态字段渲染/收集/必填校验(项目内复用)
QVBoxLayout* formHostLay_ = nullptr;
QPushButton* okBtn_ = nullptr; QPushButton* okBtn_ = nullptr;
std::vector<InversionGroup> groups_; // 当前模型的动态表单(已解析)
std::vector<QComboBox*> fieldCombos_; // 与 groups_ 展平后的字段同序(取值用)
std::vector<QString> fieldCodes_; // 与 fieldCombos_ 同序的 fieldCode
}; };
} // namespace geopro::app } // namespace geopro::app

View File

@ -1,52 +0,0 @@
#include "panels/chart/InversionFormParse.hpp"
#include <utility>
#include <QJsonArray>
#include <QJsonValue>
namespace geopro::app {
std::vector<InversionGroup> parseDynamicForm(const QJsonObject& data) {
std::vector<InversionGroup> groups;
const QJsonArray formList = data.value(QStringLiteral("formList")).toArray();
for (const QJsonValue& gv : formList) {
const QJsonObject g = gv.toObject();
InversionGroup group;
group.groupName = g.value(QStringLiteral("groupName")).toString();
const QJsonArray values = g.value(QStringLiteral("values")).toArray();
for (const QJsonValue& fv : values) {
const QJsonObject f = fv.toObject();
InversionField field;
field.fieldCode = f.value(QStringLiteral("fieldCode")).toString();
field.fieldName = f.value(QStringLiteral("fieldName")).toString();
const QJsonArray opts = f.value(QStringLiteral("optionsObject")).toArray();
for (const QJsonValue& ov : opts) {
const QJsonObject o = ov.toObject();
field.options.push_back({o.value(QStringLiteral("label")).toString(),
o.value(QStringLiteral("value")).toString()});
}
group.fields.push_back(std::move(field));
}
groups.push_back(std::move(group));
}
return groups;
}
QJsonObject assembleFieldMap(const std::vector<InversionGroup>& groups, const QJsonObject& selected,
bool fillDefaults) {
QJsonObject out;
for (const auto& g : groups) {
for (const auto& f : g.fields) {
QString value;
if (selected.contains(f.fieldCode)) value = selected.value(f.fieldCode).toString();
if (value.isEmpty() && fillDefaults && !f.options.empty()) {
value = f.options.front().value; // 复刻 initDynamicFieldsDefaultValues
}
if (!value.isEmpty()) out.insert(f.fieldCode, value); // 复刻 handleConfirm空值不进体
}
}
return out;
}
} // namespace geopro::app

View File

@ -1,35 +0,0 @@
#pragma once
#include <vector>
#include <QJsonObject>
#include <QString>
namespace geopro::app {
// 反演动态表单的数据模型 + 纯解析/组装函数(仅依赖 QtCore JSON无 Widgets/MOC
// 拆出独立 TU 以便单测tests 链 geopro_data/Qt6::Core 即可,不必拖入对话框)。
// 一个动态表单字段(仅取渲染/提交所需,复刻原版 optionsObject + fieldCode/fieldName
struct InversionFieldOption {
QString label;
QString value;
};
struct InversionField {
QString fieldCode;
QString fieldName;
std::vector<InversionFieldOption> options; // optionsObjectSelect 选项)
};
struct InversionGroup {
QString groupName;
std::vector<InversionField> fields;
};
// 解析动态表单响应 data.formList → 分组/字段模型。
std::vector<InversionGroup> parseDynamicForm(const QJsonObject& data);
// 组装提交字段表 {fieldCode: value}。fillDefaults=true 时空选值回退首个选项(生成视电阻率用)。
// 复刻原版 handleConfirm仅写入有值的字段空值不进体
QJsonObject assembleFieldMap(const std::vector<InversionGroup>& groups, const QJsonObject& selected,
bool fillDefaults);
} // namespace geopro::app

View File

@ -405,6 +405,7 @@ void RawDataChartView::openInversionColorScale(QWidget* anchor) {
if (!cmdRepo_ || dsId.isEmpty()) return; if (!cmdRepo_ || dsId.isEmpty()) return;
QJsonObject body{ QJsonObject body{
{QStringLiteral("dsObjectId"), dsId}, {QStringLiteral("dsObjectId"), dsId},
{QStringLiteral("templateId"), data_.templateId}, // 读取到的色阶模板 id对照原版可空
{QStringLiteral("businessCode"), QString()}, {QStringLiteral("businessCode"), QString()},
{QStringLiteral("projectId"), projectId}, {QStringLiteral("projectId"), projectId},
{QStringLiteral("properties"), buildColorScaleProperties(data_.scale, dlg.lineConfig())}, {QStringLiteral("properties"), buildColorScaleProperties(data_.scale, dlg.lineConfig())},
@ -584,6 +585,7 @@ void RawDataChartView::openScatterColorScale(QWidget* anchor) {
if (!cmdRepo_ || dsId.isEmpty()) return; // 无仓储 → 仅本地生效(不阻塞) if (!cmdRepo_ || dsId.isEmpty()) return; // 无仓储 → 仅本地生效(不阻塞)
QJsonObject body{ QJsonObject body{
{QStringLiteral("dsObjectId"), dsId}, {QStringLiteral("dsObjectId"), dsId},
{QStringLiteral("templateId"), data_.templateId}, // 读取到的色阶模板 id对照原版可空
{QStringLiteral("businessCode"), currentVFieldCode()}, {QStringLiteral("businessCode"), currentVFieldCode()},
{QStringLiteral("projectId"), projectId}, {QStringLiteral("projectId"), projectId},
{QStringLiteral("properties"), buildColorScaleProperties(data_.scale, dlg.lineConfig())}, {QStringLiteral("properties"), buildColorScaleProperties(data_.scale, dlg.lineConfig())},

View File

@ -38,6 +38,9 @@ struct ScatterPayload {
ScatterToolbarConf toolbar; ScatterToolbarConf toolbar;
std::vector<double> altXHorizontal, altXSlope; // x 下拉:平距 / 斜距 std::vector<double> altXHorizontal, altXSlope; // x 下拉:平距 / 斜距
std::vector<double> altYPseudo, altYElevationPseudo; // y 下拉:伪深度 / 伪深度+高程 std::vector<double> altYPseudo, altYElevationPseudo; // y 下拉:伪深度 / 伪深度+高程
// 色阶模板 id来自 lvl/colorGradation/getDetail 的 templateId保存色阶时回带
// (对照原版 newLvlColorLevel 带读取到的 templateId可空
QString templateId;
}; };
// 等值面载荷grid(rows) + 色阶 + 异常(≈ data::GridParts // 等值面载荷grid(rows) + 色阶 + 异常(≈ data::GridParts

View File

@ -27,6 +27,7 @@ QString enc(const std::string& s) {
struct ChartParts { struct ChartParts {
geopro::core::ScatterField scatter; geopro::core::ScatterField scatter;
geopro::core::ColorScale scatterScale; geopro::core::ColorScale scatterScale;
QString templateId; // 散点色阶模板 id保存色阶回带对照原版 lvlTemplateId
}; };
// 网格数据加载结果grid(rows) + 网格色阶(type2) + 异常。 // 网格数据加载结果grid(rows) + 网格色阶(type2) + 异常。
struct GridParts { struct GridParts {
@ -58,6 +59,7 @@ ChartParts parseScatterParts(const QList<net::ApiResponse>& r) {
ChartParts p; ChartParts p;
p.scatter = dto::parseScatterGraph(r[0].data); p.scatter = dto::parseScatterGraph(r[0].data);
p.scatterScale = dto::parseColorBar(r[1].data); p.scatterScale = dto::parseColorBar(r[1].data);
p.templateId = r[1].data.value(QStringLiteral("templateId")).toVariant().toString();
return p; return p;
} }
@ -168,7 +170,9 @@ DetailLoad* ApiDatasetRepository::makeInversionScatter(const std::string& dsId)
// 复用同一批次 + 解析器,再映射为 ScatterPayload不复制 JSON 解析逻辑)。 // 复用同一批次 + 解析器,再映射为 ScatterPayload不复制 JSON 解析逻辑)。
return new ApiDetailLoad(inversionScatterBatch(api_, dsId), [](const QList<net::ApiResponse>& r) { return new ApiDetailLoad(inversionScatterBatch(api_, dsId), [](const QList<net::ApiResponse>& r) {
ChartParts p = parseScatterParts(r); ChartParts p = parseScatterParts(r);
return QVariant::fromValue(core::ScatterPayload{p.scatter, p.scatterScale}); core::ScatterPayload payload{p.scatter, p.scatterScale};
payload.templateId = p.templateId; // 色阶保存回带(对照原版 lvlTemplateId
return QVariant::fromValue(payload);
}); });
} }

View File

@ -130,6 +130,8 @@ ScatterPayload parseMeasurementScatter(const QJsonObject& scatterData, const QJs
} }
p.scale = parseColorBar(colorBarData); // 复用既有混合格式解析器AlphaScale::Unit p.scale = parseColorBar(colorBarData); // 复用既有混合格式解析器AlphaScale::Unit
// 色阶模板 id保存色阶时回带对照原版 lvlTemplateId = lvlConfig?.templateId
p.templateId = objStr(colorBarData, QStringLiteral("templateId"));
return p; return p;
} }

View File

@ -154,11 +154,6 @@ target_sources(geopro_tests PRIVATE
app/test_dataset_dimension.cpp app/test_dataset_dimension.cpp
${CMAKE_SOURCE_DIR}/src/app/DatasetDimension.cpp ${CMAKE_SOURCE_DIR}/src/app/DatasetDimension.cpp
) )
# /parseDynamicForm/assembleFieldMap Qt6::Core JSON
target_sources(geopro_tests PRIVATE
app/test_inversion_form_parse.cpp
${CMAKE_SOURCE_DIR}/src/app/panels/chart/InversionFormParse.cpp
)
# measurement / id / / Qt6::Core JSON + core model # measurement / id / / Qt6::Core JSON + core model
target_sources(geopro_tests PRIVATE target_sources(geopro_tests PRIVATE
app/test_scatter_data_ops.cpp app/test_scatter_data_ops.cpp

View File

@ -1,87 +0,0 @@
#include <gtest/gtest.h>
#include <QJsonDocument>
#include <QJsonObject>
#include "panels/chart/InversionFormParse.hpp"
using namespace geopro::app;
namespace {
// 取自原版动态表单响应 data 结构POST /business/project/getDynamicForm
// formList → [{groupName, values:[{fieldCode, fieldName, optionsObject:[{label,value}]}]}]。
const char* kFormData = R"({
"formList": [
{
"groupName": "基础参数",
"values": [
{ "fieldCode": "elevation", "fieldName": "是否含高程",
"optionsObject": [ {"label": "", "value": "1"}, {"label": "", "value": "0"} ] },
{ "fieldCode": "method", "fieldName": "反演方法",
"optionsObject": [ {"label": "最小二乘", "value": "ls"} ] }
]
},
{
"groupName": "高级参数",
"values": [
{ "fieldCode": "noOptions", "fieldName": "无选项字段", "optionsObject": [] }
]
}
]
})";
QJsonObject formData() { return QJsonDocument::fromJson(kFormData).object(); }
} // namespace
TEST(InversionFormParse, ParsesGroupsFieldsAndOptions) {
const auto groups = parseDynamicForm(formData());
ASSERT_EQ(groups.size(), 2u);
EXPECT_EQ(groups[0].groupName.toStdString(), std::string("基础参数"));
ASSERT_EQ(groups[0].fields.size(), 2u);
const auto& f0 = groups[0].fields[0];
EXPECT_EQ(f0.fieldCode.toStdString(), std::string("elevation"));
EXPECT_EQ(f0.fieldName.toStdString(), std::string("是否含高程"));
ASSERT_EQ(f0.options.size(), 2u);
EXPECT_EQ(f0.options[0].label.toStdString(), std::string(""));
EXPECT_EQ(f0.options[0].value.toStdString(), std::string("1"));
EXPECT_TRUE(groups[1].fields[0].options.empty());
}
TEST(InversionFormParse, EmptyDataYieldsNoGroups) {
EXPECT_TRUE(parseDynamicForm(QJsonObject{}).empty());
}
TEST(InversionFormParse, AssembleUsesSelectedValues) {
const auto groups = parseDynamicForm(formData());
QJsonObject selected{{"elevation", "0"}, {"method", "ls"}};
// fillDefaults=false反演运算仅含已选且非空的字段无选值字段不进体。
const QJsonObject out = assembleFieldMap(groups, selected, /*fillDefaults*/ false);
EXPECT_EQ(out.value("elevation").toString().toStdString(), std::string("0"));
EXPECT_EQ(out.value("method").toString().toStdString(), std::string("ls"));
EXPECT_FALSE(out.contains("noOptions")); // 无选项 + 未选 → 不进体
}
TEST(InversionFormParse, AssembleFillsDefaultsWhenRequested) {
const auto groups = parseDynamicForm(formData());
// fillDefaults=true生成视电阻率空选值回退首个选项。
const QJsonObject out = assembleFieldMap(groups, QJsonObject{}, /*fillDefaults*/ true);
EXPECT_EQ(out.value("elevation").toString().toStdString(), std::string("1")); // 首项 value
EXPECT_EQ(out.value("method").toString().toStdString(), std::string("ls"));
EXPECT_FALSE(out.contains("noOptions")); // 无选项 → 即便 fillDefaults 也无值可填
}
TEST(InversionFormParse, AssembleOmitsEmptySelectedValues) {
const auto groups = parseDynamicForm(formData());
QJsonObject selected{{"elevation", ""}}; // 显式空字符串
// fillDefaults=false空值不进体复刻原版 handleConfirm 的 if(selectedValue))。
const QJsonObject out = assembleFieldMap(groups, selected, /*fillDefaults*/ false);
EXPECT_FALSE(out.contains("elevation"));
}

View File

@ -218,6 +218,18 @@ TEST(MeasurementDto, ParsesScatterColorBarOpaque) {
EXPECT_EQ(stops.back().second.a, 255); // 回归alpha=1 须映射为 255 不透明 EXPECT_EQ(stops.back().second.a, 255); // 回归alpha=1 须映射为 255 不透明
} }
TEST(MeasurementDto, CapturesColorBarTemplateId) {
// 色阶 getDetail 顶层 templateId → ScatterPayload.templateId保存色阶回带对照原版
auto withTpl = parseMeasurementScatter(
obj(kScatterData),
obj(R"json({"templateId":"tpl-123","properties":{"colorBar":[["0.00","#00008B"]]}})json"));
EXPECT_EQ(withTpl.templateId.toStdString(), "tpl-123");
// 缺省 templateId → 空串(原版亦可空)。
auto noTpl = parseMeasurementScatter(obj(kScatterData), obj(kColorBarData));
EXPECT_TRUE(noTpl.templateId.isEmpty());
}
TEST(MeasurementDto, ParsesTableColumnsAndVmapFlattened) { TEST(MeasurementDto, ParsesTableColumnsAndVmapFlattened) {
auto t = parseMeasurementTable(obj(kRowsData)); auto t = parseMeasurementTable(obj(kRowsData));