feat(app): LoginWindow(验证码+RSA真实登录) + 启动登录流程

This commit is contained in:
gaozheng 2026-06-07 21:32:18 +08:00
parent 3d59387ab1
commit 711103e0a1
4 changed files with 324 additions and 22 deletions

View File

@ -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 1ColorScale
geopro_data # Phase 2 / /
geopro_net # Phase 3 + RSA + login2
)
vtk_module_autoinit(TARGETS geopro_desktop MODULES ${VTK_LIBRARIES})

View File

@ -0,0 +1,212 @@
#include "login/LoginWindow.hpp"
#include <QColor>
#include <QFont>
#include <QFormLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QPainter>
#include <QPen>
#include <QPixmap>
#include <QPushButton>
#include <QRandomGenerator>
#include <QVBoxLayout>
#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

View File

@ -0,0 +1,46 @@
#pragma once
// 登录窗Phase 3验证码图本地绘制服务端明文答案+ RSA 真实登录。
// 成功后 accept() 并经 token() 暴露 accessToken 给主流程注入 ApiClient。
#include <QDialog>
#include <QString>
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 <hash>");未登录为空。
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

View File

@ -3,9 +3,12 @@
// render(VTK banded contour) + view(Qt/ADS 三栏停靠)。
// 数据docs/剖面网格数据的色阶数据2等文件/真实样本UTF-8 中文路径,经 QFile 读取)。
#include <fstream>
#include <sstream>
#include <string>
#include <QApplication>
#include <QDialog>
#include <QFormLayout>
#include <QLabel>
#include <QMainWindow>
@ -21,6 +24,10 @@
#include "model/Field.hpp"
#include "repo/LocalSampleRepository.hpp"
#include "ApiClient.hpp"
#include "AuthService.hpp"
#include "login/LoginWindow.hpp"
#include <QVTKOpenGLStereoWidget.h>
#include <vtkActor.h>
#include <vtkBandedPolyDataContourFilter.h>
@ -140,21 +147,20 @@ void populateTree(QTreeWidget* tree, const std::vector<geopro::data::GsNode>& 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<vtkGenericOpenGLRenderWindow> 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 + 登录编排 AuthServiceRSA 公钥从 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();
}