From d435fca32d05828d709af4b0105ebf130b86dd0c Mon Sep 17 00:00:00 2001 From: gaozheng Date: Wed, 10 Jun 2026 21:18:12 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E5=B1=9E=E6=80=A7=E9=9D=A2?= =?UTF-8?q?=E6=9D=BF=E6=94=B9=E4=B8=A4=E5=88=97=E5=8D=A1=E7=89=87=E5=BC=8F?= =?UTF-8?q?=E8=A1=A8=E5=8D=95=EF=BC=88=E8=BE=B9=E6=A1=86/=E5=BA=95?= =?UTF-8?q?=E8=89=B2/=E5=88=86=E9=9A=94=E7=BA=BF=EF=BC=8C=E4=B8=BB?= =?UTF-8?q?=E9=A2=98=E5=8D=8F=E8=B0=83=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/panels/DynamicFormView.cpp | 191 +++++++++++++++++++++++------ src/app/panels/DynamicFormView.hpp | 12 +- 2 files changed, 162 insertions(+), 41 deletions(-) diff --git a/src/app/panels/DynamicFormView.cpp b/src/app/panels/DynamicFormView.cpp index c5af31d..b2dd154 100644 --- a/src/app/panels/DynamicFormView.cpp +++ b/src/app/panels/DynamicFormView.cpp @@ -1,6 +1,7 @@ #include "panels/DynamicFormView.hpp" -#include +#include +#include #include #include #include @@ -9,7 +10,67 @@ namespace geopro::app { -DynamicFormView::DynamicFormView(QWidget* parent) : QWidget(parent) { +namespace { + +// 两列字段网格的逻辑列:label/value 各两份,value 列吸收伸展、label 列贴合内容。 +constexpr int kColLabelA = 0; +constexpr int kColValueA = 1; +constexpr int kColLabelB = 2; +constexpr int kColValueB = 3; +constexpr int kColSpanAll = 4; // 分组标题带横跨全部 4 列 + +// 字段标签(次要色,右侧留点呼吸,顶对齐以配合值换行)。 +QLabel* makeLabel(const QString& text) +{ + auto* k = new QLabel(text); + k->setAlignment(Qt::AlignLeft | Qt::AlignTop); + geopro::app::applyTokenizedStyleSheet( + k, QStringLiteral("color:{{text/secondary}}; background:transparent; padding:2px 0;")); + return k; +} + +// 字段值(主色、可换行、可选中复制)。 +QLabel* makeValue(const QString& text) +{ + auto* v = new QLabel(text); + v->setWordWrap(true); + v->setAlignment(Qt::AlignLeft | Qt::AlignTop); + v->setTextInteractionFlags(Qt::TextSelectableByMouse); + geopro::app::applyTokenizedStyleSheet( + v, QStringLiteral("color:{{text/primary}}; background:transparent; padding:2px 0;")); + return v; +} + +// 行间横向分隔线(1px,divider 令牌,随主题重着色)。 +QFrame* makeRowDivider() +{ + auto* line = new QFrame(); + line->setFrameShape(QFrame::HLine); + line->setFrameShadow(QFrame::Plain); + line->setFixedHeight(1); + geopro::app::applyTokenizedStyleSheet( + line, QStringLiteral("background:{{divider}}; border:none;")); + return line; +} + +// 分组标题带:横跨整行的淡底强调条,半粗次要色,给表单清晰的层级。 +QLabel* makeGroupHeader(const QString& name) +{ + auto* title = new QLabel(name); + geopro::app::applyTokenizedStyleSheet( + title, QStringLiteral("color:{{text/secondary}}; background:{{bg/hover}};" + "font-weight:%1; font-size:%2px;" + "border-radius:%3px; padding:5px 10px;") + .arg(geopro::app::type::kWeightSemibold) + .arg(geopro::app::scaledPx(geopro::app::type::kBody)) + .arg(geopro::app::radius::kSm)); + return title; +} + +} // namespace + +DynamicFormView::DynamicFormView(QWidget* parent) : QWidget(parent) +{ auto* outer = new QVBoxLayout(this); outer->setContentsMargins(0, 0, 0, 0); outer->setSpacing(0); @@ -17,65 +78,119 @@ DynamicFormView::DynamicFormView(QWidget* parent) : QWidget(parent) { auto* scroll = new QScrollArea(this); scroll->setWidgetResizable(true); scroll->setFrameShape(QFrame::NoFrame); + + // 滚动内容宿主:仅承载表单卡片,四周留出与面板一致的内边距,让卡片浮于面板底上。 auto* host = new QWidget(); - content_ = new QVBoxLayout(host); - content_->setContentsMargins(geopro::app::space::kLg, geopro::app::space::kMd, - geopro::app::space::kLg, geopro::app::space::kMd); - content_->setSpacing(geopro::app::space::kSm); - content_->addStretch(); + auto* hostLayout = new QVBoxLayout(host); + hostLayout->setContentsMargins(geopro::app::space::kLg, geopro::app::space::kLg, + geopro::app::space::kLg, geopro::app::space::kLg); + hostLayout->setSpacing(0); + + // 表单卡片:浅一档底色 + 1px 边框 + 中圆角,从面板底上读出独立「表单」面。 + card_ = new QFrame(); + card_->setObjectName(QStringLiteral("attrForm")); + geopro::app::applyTokenizedStyleSheet( + card_, QStringLiteral("#attrForm { background:{{bg/panel-subtle}};" + "border:1px solid {{border/default}}; border-radius:%1px; }") + .arg(geopro::app::radius::kMd)); + cardLayout_ = new QVBoxLayout(card_); + cardLayout_->setContentsMargins(geopro::app::space::kLg, geopro::app::space::kLg, + geopro::app::space::kLg, geopro::app::space::kLg); + cardLayout_->setSpacing(geopro::app::space::kMd); + + hostLayout->addWidget(card_); + hostLayout->addStretch(); scroll->setWidget(host); outer->addWidget(scroll); showMessage(QStringLiteral("(选中后显示属性详情)")); } -void DynamicFormView::clear() { - while (content_->count() > 0) { - QLayoutItem* it = content_->takeAt(0); +void DynamicFormView::clear() +{ + while (cardLayout_->count() > 0) { + QLayoutItem* it = cardLayout_->takeAt(0); if (it->widget()) it->widget()->deleteLater(); + if (it->layout()) { + // 嵌套网格:先回收其子控件,再删布局,避免残留控件泄漏/重叠。 + QLayout* sub = it->layout(); + while (sub->count() > 0) { + QLayoutItem* sit = sub->takeAt(0); + if (sit->widget()) sit->widget()->deleteLater(); + delete sit; + } + delete sub; + } delete it; } } -void DynamicFormView::showMessage(const QString& message) { - clear(); +void DynamicFormView::showCardMessage(const QString& message) +{ auto* hint = new QLabel(message); hint->setAlignment(Qt::AlignCenter); - geopro::app::applyTokenizedStyleSheet(hint, - QStringLiteral("color:{{text/disabled}}; padding:16px;")); - content_->addWidget(hint); - content_->addStretch(); + geopro::app::applyTokenizedStyleSheet( + hint, QStringLiteral("color:{{text/disabled}}; background:transparent; padding:%1px;") + .arg(geopro::app::space::kXl)); + cardLayout_->addWidget(hint); } -void DynamicFormView::setForm(const geopro::data::DynamicForm& form) { +void DynamicFormView::showMessage(const QString& message) +{ + clear(); + showCardMessage(message); + cardLayout_->addStretch(); +} + +void DynamicFormView::setForm(const geopro::data::DynamicForm& form) +{ clear(); if (form.groups.empty()) { - showMessage(QStringLiteral("(暂无属性)")); + showCardMessage(QStringLiteral("(暂无属性)")); + cardLayout_->addStretch(); return; } - for (const auto& group : form.groups) { - auto* title = new QLabel(QString::fromStdString(group.name)); - geopro::app::applyTokenizedStyleSheet( - title, QStringLiteral("color:{{text/secondary}}; font-weight:%1; padding-top:6px;") - .arg(geopro::app::type::kWeightSemibold)); - content_->addWidget(title); - auto* form_w = new QWidget(); - auto* fl = new QFormLayout(form_w); - fl->setContentsMargins(0, 0, 0, 0); - fl->setLabelAlignment(Qt::AlignLeft | Qt::AlignTop); - fl->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow); - for (const auto& f : group.fields) { - auto* k = new QLabel(QString::fromStdString(f.name)); - geopro::app::applyTokenizedStyleSheet(k, QStringLiteral("color:{{text/secondary}};")); - auto* v = new QLabel(QString::fromStdString(f.value)); - v->setWordWrap(true); - geopro::app::applyTokenizedStyleSheet(v, QStringLiteral("color:{{text/primary}};")); - fl->addRow(k, v); + for (const auto& group : form.groups) { + // 每组一个独立网格:分组标题带横跨 4 列,字段两列自上而下、自左而右铺排。 + auto* grid = new QGridLayout(); + grid->setContentsMargins(0, 0, 0, 0); + grid->setHorizontalSpacing(geopro::app::space::kLg); + grid->setVerticalSpacing(geopro::app::space::kXs); + grid->setColumnStretch(kColValueA, 1); + grid->setColumnStretch(kColValueB, 1); + + // gridRow 线性递增:标题占一行,之后「分隔线行 + 字段对行」交替。 + int gridRow = 0; + grid->addWidget(makeGroupHeader(QString::fromStdString(group.name)), gridRow, 0, 1, + kColSpanAll); + ++gridRow; + + const int n = static_cast(group.fields.size()); + if (n == 0) { + grid->addWidget(makeLabel(QStringLiteral("(本组暂无字段)")), gridRow, 0, 1, + kColSpanAll); } - content_->addWidget(form_w); + // 字段两两成对,每对一行:偶数下标落左对(labelA|valueA),奇数落右对(labelB|valueB)。 + for (int i = 0; i < n; ++i) { + const auto& f = group.fields[static_cast(i)]; + const bool left = (i % 2 == 0); + if (left) { + // 每新起一行前先放一条横向分隔线(铺满 4 列),再让字段对落到下一行。 + grid->addWidget(makeRowDivider(), gridRow, 0, 1, kColSpanAll); + ++gridRow; + } + const int labelCol = left ? kColLabelA : kColLabelB; + const int valueCol = left ? kColValueA : kColValueB; + grid->addWidget(makeLabel(QString::fromStdString(f.name)), gridRow, labelCol); + grid->addWidget(makeValue(QString::fromStdString(f.value)), gridRow, valueCol); + if (!left) ++gridRow; // 右对填满,行完结;左对则等右对或循环结束 + } + + cardLayout_->addLayout(grid); } - content_->addStretch(); + + cardLayout_->addStretch(); } } // namespace geopro::app diff --git a/src/app/panels/DynamicFormView.hpp b/src/app/panels/DynamicFormView.hpp index c3c6cd3..31ea617 100644 --- a/src/app/panels/DynamicFormView.hpp +++ b/src/app/panels/DynamicFormView.hpp @@ -3,10 +3,13 @@ #include "repo/RepoTypes.hpp" class QVBoxLayout; +class QFrame; namespace geopro::app { -// 被动:渲染 DynamicForm(分组键值)。对象属性 / 数据集属性两面板共用。 +// 被动:渲染 DynamicForm(分组键值)为「两列卡片式属性表单」。对象属性 / 数据集属性两面板共用。 +// 视觉:外层滚动区内嵌一张带边框/底色/圆角的表单卡片;每组一个分组标题带(横跨整行), +// 组内字段两列排布(labelA|valueA labelB|valueB),行间细分隔线。颜色全走主题令牌。 class DynamicFormView : public QWidget { public: explicit DynamicFormView(QWidget* parent = nullptr); @@ -14,8 +17,11 @@ public: void showMessage(const QString& message); // 空/错占位 private: - void clear(); - QVBoxLayout* content_ = nullptr; // 滚动区内容布局 + void clear(); // 拆掉卡片内全部内容(含分隔线/标题/字段) + void showCardMessage(const QString& message); // 卡片内居中淡色提示 + + QFrame* card_ = nullptr; // 表单卡片(objectName=attrForm) + QVBoxLayout* cardLayout_ = nullptr; // 卡片内纵向布局(容纳各分组网格 / 占位提示) }; } // namespace geopro::app