feat(app): LoginWindow(验证码+RSA真实登录) + 启动登录流程
This commit is contained in:
parent
3d59387ab1
commit
711103e0a1
|
|
@ -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})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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 + 登录编排 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();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue