From 711103e0a1f15f54042cec74fa02fd5325ddf243 Mon Sep 17 00:00:00 2001 From: gaozheng Date: Sun, 7 Jun 2026 21:32:18 +0800 Subject: [PATCH] =?UTF-8?q?feat(app):=20LoginWindow(=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E7=A0=81+RSA=E7=9C=9F=E5=AE=9E=E7=99=BB=E5=BD=95)=20+=20?= =?UTF-8?q?=E5=90=AF=E5=8A=A8=E7=99=BB=E5=BD=95=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/CMakeLists.txt | 7 +- src/app/login/LoginWindow.cpp | 212 ++++++++++++++++++++++++++++++++++ src/app/login/LoginWindow.hpp | 46 ++++++++ src/app/main.cpp | 81 +++++++++---- 4 files changed, 324 insertions(+), 22 deletions(-) create mode 100644 src/app/login/LoginWindow.cpp create mode 100644 src/app/login/LoginWindow.hpp diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index ad58f05..61a39a7 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -10,7 +10,11 @@ find_package(VTK REQUIRED COMPONENTS ) find_package(nlohmann_json CONFIG REQUIRED) -add_executable(geopro_desktop WIN32 main.cpp) +add_executable(geopro_desktop WIN32 + main.cpp + login/LoginWindow.cpp) + +target_include_directories(geopro_desktop PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(geopro_desktop PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets @@ -19,6 +23,7 @@ target_link_libraries(geopro_desktop PRIVATE nlohmann_json::nlohmann_json geopro_core # Phase 1:ColorScale 上色 geopro_data # Phase 2:本地样本仓储(对象树 / 网格 / 色阶) + geopro_net # Phase 3:登录(验证码 + RSA + login2) ) vtk_module_autoinit(TARGETS geopro_desktop MODULES ${VTK_LIBRARIES}) diff --git a/src/app/login/LoginWindow.cpp b/src/app/login/LoginWindow.cpp new file mode 100644 index 0000000..3002e92 --- /dev/null +++ b/src/app/login/LoginWindow.cpp @@ -0,0 +1,212 @@ +#include "login/LoginWindow.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "AuthService.hpp" + +namespace geopro::app { + +namespace { + +// 验证码图尺寸(约 120x40)。 +constexpr int kCaptchaWidth = 120; +constexpr int kCaptchaHeight = 40; +constexpr int kNoiseLines = 6; + +QColor randomDark(QRandomGenerator* rng) +{ + return QColor(rng->bounded(30, 160), rng->bounded(30, 160), rng->bounded(30, 160)); +} + +// 把验证码字符串画成一张模拟验证码图:随机颜色字符 + 干扰线。 +QPixmap renderCaptchaPixmap(const QString& code) +{ + QPixmap pix(kCaptchaWidth, kCaptchaHeight); + pix.fill(QColor("#EEF2FB")); + + QPainter p(&pix); + p.setRenderHint(QPainter::Antialiasing, true); + + auto* rng = QRandomGenerator::global(); + + // 干扰线 + for (int i = 0; i < kNoiseLines; ++i) { + QColor c = randomDark(rng); + c.setAlpha(120); + p.setPen(QPen(c, 1)); + p.drawLine(rng->bounded(kCaptchaWidth), rng->bounded(kCaptchaHeight), + rng->bounded(kCaptchaWidth), rng->bounded(kCaptchaHeight)); + } + + // 逐字符绘制,轻微旋转 + 随机颜色 + const int n = code.isEmpty() ? 0 : code.size(); + const int slot = n > 0 ? kCaptchaWidth / (n + 1) : kCaptchaWidth; + QFont font; + font.setBold(true); + font.setPixelSize(26); + for (int i = 0; i < n; ++i) { + p.save(); + const int x = slot * (i + 1) - 6; + const int y = kCaptchaHeight / 2 + 4; + p.translate(x, y); + p.rotate(rng->bounded(-25, 25)); + font.setPixelSize(rng->bounded(22, 30)); + p.setFont(font); + p.setPen(randomDark(rng)); + p.drawText(0, 0, QString(code.at(i))); + p.restore(); + } + p.end(); + return pix; +} + +} // namespace + +LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent) + : QDialog(parent), auth_(auth) +{ + setWindowTitle(QStringLiteral("Geopro 3.0 登录")); + setFixedSize(360, 300); + setStyleSheet(QStringLiteral("QDialog { background: #F5F7FD; }")); + + auto* root = new QVBoxLayout(this); + root->setContentsMargins(28, 22, 28, 22); + root->setSpacing(14); + + auto* title = new QLabel(QStringLiteral("Geopro 3.0 登录"), this); + QFont titleFont = title->font(); + titleFont.setPointSize(15); + titleFont.setBold(true); + title->setFont(titleFont); + title->setAlignment(Qt::AlignCenter); + title->setStyleSheet(QStringLiteral("color: #2B3A55;")); + root->addWidget(title); + + auto* form = new QFormLayout(); + form->setSpacing(10); + form->setLabelAlignment(Qt::AlignRight | Qt::AlignVCenter); + + userEdit_ = new QLineEdit(QStringLiteral("sydk"), this); + pwdEdit_ = new QLineEdit(QStringLiteral("123456"), this); + pwdEdit_->setEchoMode(QLineEdit::Password); + form->addRow(QStringLiteral("用户名"), userEdit_); + form->addRow(QStringLiteral("密码"), pwdEdit_); + + // 验证码行:图 + 输入框 + 刷新 + auto* captchaRow = new QHBoxLayout(); + captchaLabel_ = new QLabel(this); + captchaLabel_->setFixedSize(kCaptchaWidth, kCaptchaHeight); + captchaLabel_->setFrameShape(QFrame::StyledPanel); + codeEdit_ = new QLineEdit(this); + codeEdit_->setPlaceholderText(QStringLiteral("验证码")); + captchaRow->addWidget(captchaLabel_); + captchaRow->addWidget(codeEdit_, 1); + form->addRow(QStringLiteral("验证码"), captchaRow); + + refreshBtn_ = new QPushButton(QStringLiteral("看不清?刷新"), this); + refreshBtn_->setFlat(true); + refreshBtn_->setCursor(Qt::PointingHandCursor); + refreshBtn_->setStyleSheet(QStringLiteral("color: #3A6EA5; border: none; text-align: right;")); + form->addRow(QString(), refreshBtn_); + + root->addLayout(form); + + errorLabel_ = new QLabel(this); + errorLabel_->setStyleSheet(QStringLiteral("color: #C0392B;")); + errorLabel_->setWordWrap(true); + errorLabel_->setMinimumHeight(16); + root->addWidget(errorLabel_); + + loginBtn_ = new QPushButton(QStringLiteral("立即登录"), this); + loginBtn_->setMinimumHeight(34); + loginBtn_->setCursor(Qt::PointingHandCursor); + loginBtn_->setStyleSheet(QStringLiteral( + "QPushButton { background: #3A6EA5; color: white; border: none; border-radius: 4px; " + "font-weight: bold; }" + "QPushButton:hover { background: #325E8C; }" + "QPushButton:disabled { background: #9FB4CC; }")); + loginBtn_->setDefault(true); + root->addWidget(loginBtn_); + + connect(refreshBtn_, &QPushButton::clicked, this, &LoginWindow::refreshCaptcha); + connect(loginBtn_, &QPushButton::clicked, this, &LoginWindow::attemptLogin); + connect(codeEdit_, &QLineEdit::returnPressed, this, &LoginWindow::attemptLogin); + connect(pwdEdit_, &QLineEdit::returnPressed, this, &LoginWindow::attemptLogin); + + refreshCaptcha(); // 打开即拉一张验证码 +} + +void LoginWindow::refreshCaptcha() +{ + codeEdit_->clear(); + try { + const auto cap = auth_.fetchCaptcha(); + codeId_ = cap.codeId; + captchaLabel_->setPixmap(renderCaptchaPixmap(cap.code)); + } catch (const std::exception& e) { + showError(QStringLiteral("获取验证码失败:%1").arg(QString::fromUtf8(e.what()))); + captchaLabel_->setText(QStringLiteral("加载失败")); + } catch (...) { + showError(QStringLiteral("获取验证码失败")); + captchaLabel_->setText(QStringLiteral("加载失败")); + } +} + +void LoginWindow::attemptLogin() +{ + const QString user = userEdit_->text().trimmed(); + const QString pwd = pwdEdit_->text(); + const QString code = codeEdit_->text().trimmed(); + + if (user.isEmpty() || pwd.isEmpty() || code.isEmpty()) { + showError(QStringLiteral("请填写用户名、密码和验证码")); + return; + } + + errorLabel_->clear(); + loginBtn_->setEnabled(false); + const QString origText = loginBtn_->text(); + loginBtn_->setText(QStringLiteral("登录中...")); + loginBtn_->repaint(); // 同步阻塞前刷新按钮文案 + + geopro::net::AuthService::LoginResult result; + try { + result = auth_.login(user, pwd, code, codeId_); + } catch (const std::exception& e) { + result.ok = false; + result.error = QStringLiteral("登录异常:%1").arg(QString::fromUtf8(e.what())); + } catch (...) { + result.ok = false; + result.error = QStringLiteral("登录发生未知错误"); + } + + loginBtn_->setText(origText); + loginBtn_->setEnabled(true); + + if (result.ok) { + token_ = result.token; + accept(); + return; + } + + showError(result.error.isEmpty() ? QStringLiteral("登录失败") : result.error); + refreshCaptcha(); // 失败刷新验证码 +} + +void LoginWindow::showError(const QString& msg) +{ + errorLabel_->setText(msg); +} + +} // namespace geopro::app diff --git a/src/app/login/LoginWindow.hpp b/src/app/login/LoginWindow.hpp new file mode 100644 index 0000000..a0315ee --- /dev/null +++ b/src/app/login/LoginWindow.hpp @@ -0,0 +1,46 @@ +#pragma once + +// 登录窗(Phase 3):验证码图(本地绘制服务端明文答案)+ RSA 真实登录。 +// 成功后 accept() 并经 token() 暴露 accessToken 给主流程注入 ApiClient。 + +#include +#include + +class QLabel; +class QLineEdit; +class QPushButton; + +namespace geopro::net { +class AuthService; +} + +namespace geopro::app { + +class LoginWindow : public QDialog { + Q_OBJECT + +public: + explicit LoginWindow(geopro::net::AuthService& auth, QWidget* parent = nullptr); + + // 登录成功后的 accessToken(形如 "Geomative ");未登录为空。 + QString token() const { return token_; } + +private: + void refreshCaptcha(); // 拉新验证码并重绘图片 + void attemptLogin(); // 校验输入并发起阻塞登录 + void showError(const QString& msg); + + geopro::net::AuthService& auth_; + QString token_; + QString codeId_; + + QLineEdit* userEdit_ = nullptr; + QLineEdit* pwdEdit_ = nullptr; + QLineEdit* codeEdit_ = nullptr; + QLabel* captchaLabel_ = nullptr; + QPushButton* refreshBtn_ = nullptr; + QPushButton* loginBtn_ = nullptr; + QLabel* errorLabel_ = nullptr; +}; + +} // namespace geopro::app diff --git a/src/app/main.cpp b/src/app/main.cpp index 21b283c..ef6905a 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -3,9 +3,12 @@ // render(VTK banded contour) + view(Qt/ADS 三栏停靠)。 // 数据:docs/剖面网格数据的色阶数据2等文件/(真实样本,UTF-8 中文路径,经 QFile 读取)。 +#include +#include #include #include +#include #include #include #include @@ -21,6 +24,10 @@ #include "model/Field.hpp" #include "repo/LocalSampleRepository.hpp" +#include "ApiClient.hpp" +#include "AuthService.hpp" +#include "login/LoginWindow.hpp" + #include #include #include @@ -140,21 +147,20 @@ void populateTree(QTreeWidget* tree, const std::vector& gs tree->expandAll(); } -} // namespace - -int main(int argc, char* argv[]) +// 读取 RSA 公钥 PEM 全文(登录时密码加密用)。读不到返回空串,登录将报错。 +std::string readPem(const std::string& path) { - QSurfaceFormat::setDefaultFormat(QVTKOpenGLStereoWidget::defaultFormat()); - QApplication app(argc, argv); - - // 本地样本仓储(中文路径,末尾带 '/',readFile 直接拼文件名)。生命周期覆盖事件循环。 - geopro::data::LocalSampleRepository repo( - "D:/Git/lanbingtech/geopro/docs/剖面网格数据的色阶数据2等文件/"); - - QMainWindow window; - window.setWindowTitle(QStringLiteral("Geopro 3.0 — 项目分析视图 (M1)")); - window.resize(1280, 800); + std::ifstream in(path, std::ios::binary); + if (!in) return {}; + std::ostringstream ss; + ss << in.rdbuf(); + return ss.str(); +} +// 在给定 QMainWindow 上构建 M1 工作台:ADS 三栏 + 对象树 → 渲染联动 + 属性面板。 +// repo 生命周期须覆盖到事件循环结束(由调用方保证)。 +void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& repo) +{ // 中央 QVTK 视图(指针供联动回调使用)。 auto* vtkWidget = new QVTKOpenGLStereoWidget(); vtkNew renderWindow; @@ -187,16 +193,19 @@ int main(int argc, char* argv[]) dockManager->addDockWidget(ads::RightDockWidgetArea, rightDock); // 联动:点击 DS 项 → 加载 grid/colorScale → 渲染 + 更新属性。 - // [&] 捕获:repo / renderer / renderWindow / propLabel 均在 main 作用域, - // 生命周期覆盖到 app.exec() 返回,事件回调期间安全。 - auto renderDataset = [&](QTreeWidgetItem* item) { + // VTK 对象(renderer/renderWindow)按【裸指针值】捕获:底层对象被 widget/renderWindow + // 引用计数持有(widget 父链挂到 window),生命周期覆盖事件循环;按值捕获避免 + // buildWorkbench 返回后 vtkNew 局部变量析构导致悬空引用。repo 由调用方保活。 + vtkRenderer* rendererPtr = renderer.Get(); + vtkGenericOpenGLRenderWindow* renderWindowPtr = renderWindow.Get(); + auto renderDataset = [&repo, rendererPtr, renderWindowPtr, propLabel](QTreeWidgetItem* item) { const QString id = item->data(0, Qt::UserRole).toString(); if (id.isEmpty()) return; // GS/TM 节点无 dsId,忽略 const std::string dsId = id.toStdString(); const auto g = repo.loadGrid(dsId); const auto cs = repo.loadColorScale(dsId); - renderGrid(renderer, g, cs); - renderWindow->Render(); + renderGrid(rendererPtr, g, cs); + renderWindowPtr->Render(); propLabel->setText(QStringLiteral("数据集: %1\n网格: %2 x %3\nvmin / vmax: %4 / %5") .arg(item->text(0)) .arg(g.nx()) @@ -206,9 +215,7 @@ int main(int argc, char* argv[]) }; QObject::connect(tree, &QTreeWidget::itemClicked, tree, - [&](QTreeWidgetItem* it, int) { renderDataset(it); }); - - window.show(); + [renderDataset](QTreeWidgetItem* it, int) { renderDataset(it); }); // 默认渲染第一个 DS,让窗口一打开就有图。 if (auto* first = tree->topLevelItemCount() > 0 ? tree->topLevelItem(0) : nullptr) { @@ -228,6 +235,38 @@ int main(int argc, char* argv[]) renderDataset(dsItem); } } +} + +} // namespace + +int main(int argc, char* argv[]) +{ + // QVTK 默认 surface format 必须在 QApplication 之前设置。 + QSurfaceFormat::setDefaultFormat(QVTKOpenGLStereoWidget::defaultFormat()); + QApplication app(argc, argv); + + // 网络层:共享会话 ApiClient + 登录编排 AuthService(RSA 公钥从 resources 读取)。 + geopro::net::ApiClient api(QStringLiteral("http://tenant.geomative.cn/pop-api")); + const std::string pem = readPem("D:/Git/lanbingtech/geopro/resources/rsa_public_key.pem"); + geopro::net::AuthService auth(api, pem); + + // 先弹登录窗;用户取消/未登录则退出。 + geopro::app::LoginWindow login(auth); + if (login.exec() != QDialog::Accepted) return 0; + + api.setToken(login.token()); // 注入 token 供后续 API 使用 + + // 登录成功 → 构建并显示工作台。 + // 本地样本仓储(中文路径,末尾带 '/')。生命周期覆盖事件循环。 + geopro::data::LocalSampleRepository repo( + "D:/Git/lanbingtech/geopro/docs/剖面网格数据的色阶数据2等文件/"); + + QMainWindow window; + window.setWindowTitle(QStringLiteral("Geopro 3.0 — 项目分析视图 (M1)")); + window.resize(1280, 800); + + buildWorkbench(window, repo); + window.show(); return app.exec(); }