#include "panels/DatasetListPanel.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Theme.hpp" namespace geopro::app { namespace { QString humanSize(long long b) { if (b < 1024) return QStringLiteral("%1 B").arg(b); const double kb = b / 1024.0; if (kb < 1024.0) return QStringLiteral("%1 KB").arg(kb, 0, 'f', 1); return QStringLiteral("%1 MB").arg(kb / 1024.0, 0, 'f', 1); } // 数据/文件列表卡片委托:标题+元信息双行、悬停/选中圆角高亮 + 选中左 2px 强调竖条(规范§6.2)。 // 特殊行(加载更多 / 占位提示)退回为居中纯文本,不画卡片。 class DatasetCardDelegate : public QStyledItemDelegate { public: using QStyledItemDelegate::QStyledItemDelegate; QSize sizeHint(const QStyleOptionViewItem&, const QModelIndex& idx) const override { const bool special = idx.data(kDsLoadMoreRole).toBool() || !(idx.flags() & Qt::ItemIsSelectable); return QSize(0, special ? 34 : 52); } void paint(QPainter* p, const QStyleOptionViewItem& opt, const QModelIndex& idx) const override { p->save(); p->setRenderHint(QPainter::Antialiasing, true); const QString disp = idx.data(Qt::DisplayRole).toString(); // 「加载更多」:居中强调色文本(hover 时加底)。 if (idx.data(kDsLoadMoreRole).toBool()) { if (opt.state & QStyle::State_MouseOver) { QPainterPath bgp; bgp.addRoundedRect(opt.rect.adjusted(4, 2, -4, -2), 6, 6); p->fillPath(bgp, geopro::app::tokenColor("bg/hover")); } p->setPen(geopro::app::tokenColor("accent/primary")); p->drawText(opt.rect, Qt::AlignCenter, disp); p->restore(); return; } // 占位提示行(不可选):居中淡色文本。 if (!(idx.flags() & Qt::ItemIsSelectable)) { p->setPen(geopro::app::tokenColor("text/disabled")); p->drawText(opt.rect, Qt::AlignCenter, disp); p->restore(); return; } // 卡片 const QRect r = opt.rect.adjusted(4, 2, -4, -2); const bool selected = opt.state & QStyle::State_Selected; const bool hover = opt.state & QStyle::State_MouseOver; if (selected || hover) { QPainterPath path; path.addRoundedRect(r, 6, 6); p->fillPath(path, geopro::app::tokenColor(selected ? "bg/selected" : "bg/hover")); } if (selected) { // 左 2px 强调竖条(规范§6.2) p->fillRect(QRect(r.left(), r.top() + 4, 2, r.height() - 8), geopro::app::tokenColor("accent/primary")); } // 可勾选项:左侧画复选框(用当前 style 的指示器),文本整体右移。 int textLeftPad = 14; const bool checkable = (idx.flags() & Qt::ItemIsUserCheckable); if (checkable) { const int box = 16; QRect checkRect(r.left() + 12, r.top() + (r.height() - box) / 2, box, box); const auto cs = static_cast(idx.data(Qt::CheckStateRole).toInt()); QStyleOptionViewItem o(opt); o.rect = checkRect; o.state &= ~QStyle::State_HasFocus; o.state |= (cs == Qt::Checked ? QStyle::State_On : QStyle::State_Off); const QWidget* w = opt.widget; QStyle* st = w ? w->style() : QApplication::style(); st->drawPrimitive(QStyle::PE_IndicatorItemViewItemCheck, &o, p, w); textLeftPad = 12 + box + 8; // 复选框右侧留白后再放文本 } QString title = disp, meta; const int nl = disp.indexOf(QLatin1Char('\n')); if (nl >= 0) { title = disp.left(nl); meta = disp.mid(nl + 1); } const QRect textR = r.adjusted(textLeftPad, 6, -12, -6); // 标题 QFont tf = opt.font; tf.setPixelSize(geopro::app::scaledPx(13)); p->setFont(tf); p->setPen(geopro::app::tokenColor("text/primary")); const QRect titleR(textR.left(), textR.top(), textR.width(), textR.height() / 2); p->drawText(titleR, Qt::AlignLeft | Qt::AlignVCenter, p->fontMetrics().elidedText(title, Qt::ElideRight, titleR.width())); // 元信息 if (!meta.isEmpty()) { QFont mf = opt.font; mf.setPixelSize(geopro::app::scaledPx(11)); p->setFont(mf); p->setPen(geopro::app::tokenColor("text/tertiary")); const QRect metaR(textR.left(), textR.center().y() + 1, textR.width(), textR.height() / 2); p->drawText(metaR, Qt::AlignLeft | Qt::AlignVCenter, p->fontMetrics().elidedText(meta, Qt::ElideRight, metaR.width())); } p->restore(); } bool editorEvent(QEvent* ev, QAbstractItemModel* model, const QStyleOptionViewItem& opt, const QModelIndex& idx) override { if (!(idx.flags() & Qt::ItemIsUserCheckable)) return QStyledItemDelegate::editorEvent(ev, model, opt, idx); const QRect r = opt.rect.adjusted(4, 2, -4, -2); const int box = 16; // 命中区放宽到复选框左侧整段(含一点文本起始),便于点击。 const QRect hit(r.left(), r.top(), 12 + box + 8, r.height()); auto toggle = [&]() { const auto cur = static_cast(idx.data(Qt::CheckStateRole).toInt()); model->setData(idx, cur == Qt::Checked ? Qt::Unchecked : Qt::Checked, Qt::CheckStateRole); }; if (ev->type() == QEvent::MouseButtonRelease) { auto* me = static_cast(ev); if (me->button() == Qt::LeftButton && hit.contains(me->pos())) { toggle(); return true; } } else if (ev->type() == QEvent::KeyPress) { auto* ke = static_cast(ev); if (ke->key() == Qt::Key_Space || ke->key() == Qt::Key_Select) { toggle(); return true; } } return QStyledItemDelegate::editorEvent(ev, model, opt, idx); } }; } // namespace namespace { // 建一条数据集树项(不挂载):列0 文本 = dsName +「创建时间 · 类型名」,data 存各角色。 QTreeWidgetItem* makeDatasetItem(const geopro::data::DsRow& d, const QString& tmObjectId) { QString text = QString::fromStdString(d.dsName); QString sub = QString::fromStdString(d.createTime); // 名称下先创建时间 if (!d.typeName.empty()) sub += QStringLiteral(" · %1").arg(QString::fromStdString(d.typeName)); // 再跟类型 if (!sub.isEmpty()) text += QStringLiteral("\n%1").arg(sub); auto* item = new QTreeWidgetItem(); item->setText(0, text); item->setData(0, kDsIdRole, QString::fromStdString(d.id)); item->setData(0, kDsDdTypeRole, QString::fromStdString(d.ddCode)); item->setData(0, kDsDdCodeRole, QString::fromStdString(d.ddCode)); item->setData(0, kDsNameRole, QString::fromStdString(d.dsName)); item->setData(0, kDsTypeNameRole, QString::fromStdString(d.typeName)); item->setData(0, kDsCreateTimeRole, QString::fromStdString(d.createTime)); item->setData(0, kDsTmObjectIdRole, tmObjectId); // 所属 TM 对象 id(白化 structParentId) // 单击 tip:显示数据集主要属性(名称 / 类型 / 创建时间),对齐菜单文档「tip显示ds的主要属性」。 QString tip = QStringLiteral("名称:%1").arg(QString::fromStdString(d.dsName)); if (!d.typeName.empty()) tip += QStringLiteral("\n类型:%1").arg(QString::fromStdString(d.typeName)); if (!d.createTime.empty()) tip += QStringLiteral("\n创建时间:%1").arg(QString::fromStdString(d.createTime)); item->setToolTip(0, tip); return item; } } // namespace void populateDatasetList(QTreeWidget* tree, const std::vector& rows, bool append, const QString& tmObjectId) { if (!tree) return; if (!append) tree->clear(); // id→已在树中的项:!append 时为空;append(分页)时含已加载行,使新行能挂到既有父下。 QHash byId; for (QTreeWidgetItemIterator it(tree); *it; ++it) { const QString id = (*it)->data(0, kDsIdRole).toString(); if (!id.isEmpty()) byId.insert(id, *it); } // 第一遍:本批全部建项(不挂载)并登记 id,使同批内的父子也能互相找到。 std::vector batch; batch.reserve(rows.size()); for (const auto& d : rows) { auto* item = makeDatasetItem(d, tmObjectId); byId.insert(QString::fromStdString(d.id), item); batch.push_back(item); } // 第二遍:按 parentId 挂载。父在集合内→作其子;否则(父是源文件节点/不在本批)→作树根。 for (std::size_t i = 0; i < rows.size(); ++i) { const QString pid = QString::fromStdString(rows[i].parentId); QTreeWidgetItem* parent = pid.isEmpty() ? nullptr : byId.value(pid, nullptr); if (parent && parent != batch[i]) parent->addChild(batch[i]); else tree->addTopLevelItem(batch[i]); } // 默认折叠(对齐原版:仅显源数据根行,派生数据收在展开箭头内)。 } void populateFileList(QListWidget* list, const std::vector& rows, bool append) { if (!list) return; if (!append) list->clear(); if (!append && rows.empty()) { auto* hint = new QListWidgetItem(QStringLiteral("(暂无文件)"), list); hint->setFlags(Qt::NoItemFlags); hint->setTextAlignment(Qt::AlignCenter); return; } for (const auto& d : rows) { const QString fname = d.fileName.empty() ? QString::fromStdString(d.dsName) : QString::fromStdString(d.fileName); QString sub = QString::fromStdString(d.createTime); // 名称下先创建时间 sub += QStringLiteral(" · %1").arg(humanSize(d.fileSize)); // 再跟大小 const QString text = fname + QStringLiteral("\n%1").arg(sub); auto* item = new QListWidgetItem(text, list); item->setData(kDsIdRole, QString::fromStdString(d.id)); item->setData(kDsFileUrlRole, QString::fromStdString(d.fileUrl)); } } void applyDatasetCardDelegate(QAbstractItemView* view) { if (!view) return; view->setItemDelegate(new DatasetCardDelegate(view)); view->setMouseTracking(true); // 让委托收到 hover 状态 if (auto* list = qobject_cast(view)) list->setSpacing(0); // 卡间距由委托内边距控制 QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, view, [view]() { view->viewport()->update(); }); } QStringList collectDatasetTypeNames(QTreeWidget* tree) { QStringList types; if (!tree) return types; QSet seen; for (QTreeWidgetItemIterator it(tree); *it; ++it) { if ((*it)->data(0, kDsLoadMoreRole).toBool()) continue; const QString t = (*it)->data(0, kDsTypeNameRole).toString(); if (t.isEmpty() || seen.contains(t)) continue; seen.insert(t); types << t; } return types; } namespace { // 解析创建时间字符串为日期(容忍 "yyyy-MM-dd HH:mm:ss" / "yyyy-MM-dd" / "yyyy/MM/dd")。 QDate parseRowDate(const QString& s) { if (s.isEmpty()) return {}; const QString d = s.left(10); for (const char* fmt : {"yyyy-MM-dd", "yyyy/MM/dd"}) { QDate v = QDate::fromString(d, QString::fromLatin1(fmt)); if (v.isValid()) return v; } return {}; } // 递归判定项是否匹配(类型在集合内 且 创建日期 >= minDate)。 bool rowMatches(QTreeWidgetItem* item, const QSet& visibleTypes, const QDate& minDate) { const QString t = item->data(0, kDsTypeNameRole).toString(); if (!t.isEmpty() && !visibleTypes.contains(t)) return false; if (minDate.isValid()) { const QDate d = parseRowDate(item->data(0, kDsCreateTimeRole).toString()); if (d.isValid() && d < minDate) return false; } return true; } // 返回该项(或其任一后代)是否可见;据此 setHidden。父项只要有可见后代即保留。 bool applyFilterRec(QTreeWidgetItem* item, const QSet& visibleTypes, const QDate& minDate) { if (item->data(0, kDsLoadMoreRole).toBool()) return true; // 「加载更多」行恒显 bool anyChildVisible = false; for (int i = 0; i < item->childCount(); ++i) anyChildVisible |= applyFilterRec(item->child(i), visibleTypes, minDate); const bool selfMatch = rowMatches(item, visibleTypes, minDate); const bool visible = selfMatch || anyChildVisible; item->setHidden(!visible); return visible; } } // namespace void applyDatasetFilter(QTreeWidget* tree, const QSet& visibleTypes, const QDate& minDate) { if (!tree) return; for (int i = 0; i < tree->topLevelItemCount(); ++i) applyFilterRec(tree->topLevelItem(i), visibleTypes, minDate); } } // namespace geopro::app