Compare commits

..

2 Commits

Author SHA1 Message Date
gaozheng 890a3b95d9 Merge pull request 'feat(m1): dock 布局持久化 + 记住登录(QtKeychain) + 纵向夸张统一' (#1) from feat/m1-finishing into main
Reviewed-on: https://gitea.geomative.cn/gaozheng/geopro/pulls/1
2026-06-09 18:48:02 +08:00
gaozheng 462cfaac9d feat(m1): dock 布局持久化 + 记住登录(QtKeychain) + 纵向夸张统一
- 3 dock 布局/窗口几何 QSettings 持久化(退出保存、启动恢复;ADS 按标题作键)
- 4 记住登录:FetchContent QtKeychain v0.14.0(Qt6,静态) + Credential 同步存取;登录窗加「记住登录(30天)」复选框;启动有有效 token 则免登录
- 5 Z 基准统一:kCurtainZScale(3)/kDetailYScale(1.5) 合并为单一 kVerticalExaggeration(2.0),帘面/体素/切片/剖面/地形一致
2026-06-09 09:42:27 +08:00
7 changed files with 174 additions and 17 deletions

View File

@ -52,6 +52,17 @@ FetchContent_Declare(ads
GIT_TAG 4.3.1)
FetchContent_MakeAvailable(ads)
# QtKeychain §7.4""FetchContent Qt
# Qt6 Qt5 DLL
set(BUILD_WITH_QT6 ON CACHE BOOL "" FORCE)
set(BUILD_TEST_APPLICATION OFF CACHE BOOL "" FORCE)
set(BUILD_TRANSLATIONS OFF CACHE BOOL "" FORCE)
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
FetchContent_Declare(qtkeychain
GIT_REPOSITORY https://github.com/frankosterfeld/qtkeychain.git
GIT_TAG v0.14.0)
FetchContent_MakeAvailable(qtkeychain)
add_subdirectory(src)
enable_testing()

View File

@ -19,16 +19,20 @@ add_executable(geopro_desktop WIN32
TopBar.cpp
Glyphs.cpp
PanelHeader.cpp
Credential.cpp
login/LoginWindow.cpp
panels/AnomalyListPanel.cpp
panels/DatasetListPanel.cpp)
target_include_directories(geopro_desktop PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
# QtKeychain FetchContent target / export
target_include_directories(geopro_desktop PRIVATE ${qtkeychain_SOURCE_DIR} ${qtkeychain_BINARY_DIR})
target_link_libraries(geopro_desktop PRIVATE
Qt6::Core Qt6::Gui Qt6::Widgets Qt6::Svg
${VTK_LIBRARIES}
ads::qt6advanceddocking
qt6keychain
nlohmann_json::nlohmann_json
geopro_core # Phase 1ColorScale
geopro_data # Phase 2 / /

74
src/app/Credential.cpp Normal file
View File

@ -0,0 +1,74 @@
#include "Credential.hpp"
#include <QDateTime>
#include <QEventLoop>
#include <keychain.h>
namespace geopro::app {
namespace {
const QString kService = QStringLiteral("Geopro3");
const QString kKey = QStringLiteral("session");
// QtKeychain 的 job 是异步的;用局部事件循环跑成同步(启动/登录时一次性调用,可接受)。
QString readRaw()
{
QKeychain::ReadPasswordJob job(kService);
job.setAutoDelete(false);
job.setKey(kKey);
QEventLoop loop;
QObject::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
job.start();
loop.exec();
if (job.error() != QKeychain::NoError) return {};
return job.textData();
}
} // namespace
void rememberSession(const QString& token)
{
if (token.isEmpty()) return;
const QString payload =
QString::number(QDateTime::currentSecsSinceEpoch()) + QStringLiteral("|") + token;
QKeychain::WritePasswordJob job(kService);
job.setAutoDelete(false);
job.setKey(kKey);
job.setTextData(payload);
QEventLoop loop;
QObject::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
job.start();
loop.exec();
}
QString recallValidToken(int maxAgeDays)
{
const QString raw = readRaw();
if (raw.isEmpty()) return {};
const int sep = raw.indexOf(QLatin1Char('|'));
if (sep <= 0) return {};
bool ok = false;
const qint64 ts = raw.left(sep).toLongLong(&ok);
if (!ok) return {};
const qint64 ageSec = QDateTime::currentSecsSinceEpoch() - ts;
if (ageSec < 0 || ageSec > static_cast<qint64>(maxAgeDays) * 24 * 3600) return {};
return raw.mid(sep + 1);
}
void forgetSession()
{
QKeychain::DeletePasswordJob job(kService);
job.setAutoDelete(false);
job.setKey(kKey);
QEventLoop loop;
QObject::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
job.start();
loop.exec();
}
} // namespace geopro::app

20
src/app/Credential.hpp Normal file
View File

@ -0,0 +1,20 @@
#pragma once
// 登录凭证安全存取(规约 §7.4:严禁明文)。基于 QtKeychain → 系统凭证库
// Windows 凭据管理器 / macOS Keychain / Linux Secret Service
// M1 用法:登录成功且勾选「记住」时存 token+时间戳;启动时若未超期则免登录。
#include <QString>
namespace geopro::app {
// 记住会话:把 token + 当前时间戳安全存入系统凭证库。token 为空则忽略。
void rememberSession(const QString& token);
// 取回有效 token已存且未超过 maxAgeDays 天 → 返回 token否则返回空串。
QString recallValidToken(int maxAgeDays = 30);
// 清除已记住的会话(未勾选记住 / 注销时调用)。
void forgetSession();
} // namespace geopro::app

View File

@ -1,5 +1,6 @@
#include "login/LoginWindow.hpp"
#include <QCheckBox>
#include <QColor>
#include <QFont>
#include <QHBoxLayout>
@ -77,7 +78,7 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent)
: QDialog(parent), auth_(auth)
{
setWindowTitle(QStringLiteral("Geopro 3.0 登录"));
setFixedSize(400, 500);
setFixedSize(400, 528);
// 仅外观:登录窗自带样式(沿用全局主题令牌,保证一脉相承)。
// QLineEdit 在所有状态都显式白底深字 + 边框,避免失焦时取调色板默认色与背景相近不可读。
@ -174,6 +175,12 @@ LoginWindow::LoginWindow(geopro::net::AuthService& auth, QWidget* parent)
refreshRow->addWidget(refreshBtn_);
form->addLayout(refreshRow);
// 记住登录:勾选后成功登录将安全存储 token30 天内免登录。默认不勾(更安全)。
rememberChk_ = new QCheckBox(QStringLiteral("记住登录30 天内免登录)"), body);
rememberChk_->setCursor(Qt::PointingHandCursor);
rememberChk_->setStyleSheet(QStringLiteral("color:#5A6B85; font-size:13px;"));
form->addWidget(rememberChk_);
// 错误提示:固定占位高度,避免出现时整体布局跳动。
errorLabel_ = new QLabel(body);
errorLabel_->setStyleSheet(QStringLiteral("color: #C0392B; font-size: 12px;"));
@ -264,6 +271,11 @@ void LoginWindow::attemptLogin()
refreshCaptcha(); // 失败刷新验证码
}
bool LoginWindow::remember() const
{
return rememberChk_ && rememberChk_->isChecked();
}
void LoginWindow::showError(const QString& msg)
{
errorLabel_->setText(msg);

View File

@ -6,6 +6,7 @@
#include <QDialog>
#include <QString>
class QCheckBox;
class QLabel;
class QLineEdit;
class QPushButton;
@ -25,6 +26,9 @@ public:
// 登录成功后的 accessToken形如 "Geomative <hash>");未登录为空。
QString token() const { return token_; }
// 用户是否勾选「记住登录」(成功后据此决定是否安全存储 token
bool remember() const;
private:
void refreshCaptcha(); // 拉新验证码并重绘图片
void attemptLogin(); // 校验输入并发起阻塞登录
@ -40,6 +44,7 @@ private:
QLabel* captchaLabel_ = nullptr;
QPushButton* refreshBtn_ = nullptr;
QPushButton* loginBtn_ = nullptr;
QCheckBox* rememberChk_ = nullptr;
QLabel* errorLabel_ = nullptr;
};

View File

@ -33,6 +33,7 @@
#include <QLabel>
#include <QListWidget>
#include <QListWidgetItem>
#include <QSettings>
#include <QSignalBlocker>
#include <QStringList>
#include <QTabWidget>
@ -56,6 +57,7 @@
#include "ApiClient.hpp"
#include "AuthService.hpp"
#include "Credential.hpp"
#include "Glyphs.hpp"
#include "PanelHeader.hpp"
#include "Theme.hpp"
@ -158,9 +160,10 @@ enum class DetailMode { Section18, Scatter17 };
// #17 散点屏幕像素方块边长。
constexpr float kScatterPointSize = 4.0F;
// 纵向夸张倍数:三维断面墙沿 z 拉伸成墙;数据详情 #18 沿 y 拉伸填面板。
constexpr double kCurtainZScale = 3.0;
constexpr double kDetailYScale = 1.5;
// 纵向夸张倍数Z 基准统一M-3全项目共用同一倍数使 帘面(z) / 体素 / 切片 /
// 数据详情剖面(y) / 地形(relief) 的纵向比例一致——避免「剖面×1.5、帘面×3」不一致。
// 单一可调常量:要整体调纵向观感改这一处即可。
constexpr double kVerticalExaggeration = 2.0;
// 项目 CRS(实证确定,STATUS §4):散点 projX/Y→经纬→GeoLocalFrame 配准体素到世界系。
constexpr const char* kProjectCrs = "EPSG:4547"; // CGCS2000 / 3-degree GK CM 114E
@ -441,7 +444,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
// ── 中央视图重建(核心)─────────────────────────────────────────────
// 按勾选的测线(TM)整体重建scene.clear() → 对每个勾选 TM 的 dd_section 加对应 actor。
// 二维地图 = buildSurveyLine红线俯视浅底背景+ applyTop2D。
// 三维视图 = buildCurtain断面墙SetScale(1,1,kCurtainZScale) + applyFree3D白底
// 三维视图 = buildCurtain断面墙SetScale(1,1,kVerticalExaggeration) + applyFree3D白底
// frame/structure 全局共享;切视图/勾选变化都调用此函数重建当前视图。
auto rebuildCentral = [scene, rendererPtr, renderWindowPtr, viewMode, &repo, frame, tree,
structure, showCurtain, showVoxel, showTerrain, showSlice, slicePlane,
@ -463,7 +466,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
const auto cs = repo.loadColorScale(id);
auto curtain = geopro::render::buildCurtain(g, cs, *frame);
if (curtain) {
curtain->SetScale(1.0, 1.0, kCurtainZScale); // 纵向夸张成墙
curtain->SetScale(1.0, 1.0, kVerticalExaggeration); // 纵向夸张成墙
scene->addActor(curtain);
}
}
@ -492,9 +495,9 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
if (!is2D && (*showVoxel || *showSlice) && crs) {
const auto profs = repo.loadVoxelScatters();
const auto vcs = repo.loadScatterColorScale("grid1");
// 纵向夸张烤进 image(zDisplayScale=kCurtainZScale),使体绘制/切片/帘面纵向一致。
// 纵向夸张烤进 image(zDisplayScale=kVerticalExaggeration),使体绘制/切片/帘面纵向一致。
auto vr = geopro::render::buildVoxelFromScatters(profs, vcs, *crs, *frame, 1.0, 0.5, 2.0,
4.0, kCurtainZScale);
4.0, kVerticalExaggeration);
if (vr.valid()) {
if (*showVoxel) {
rendererPtr->AddVolume(vr.volume); // 夸张已烤进 image无需 actor SetScale
@ -585,18 +588,18 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
const auto cs = repo.loadColorScale(id);
const auto actors = geopro::render::buildGridContour(g, cs);
if (actors.bands) {
actors.bands->SetScale(1.0, kDetailYScale, 1.0);
actors.bands->SetScale(1.0, kVerticalExaggeration, 1.0);
detailRendererPtr->AddViewProp(actors.bands);
}
if (actors.edges && *showContour) {
actors.edges->SetScale(1.0, kDetailYScale, 1.0);
actors.edges->SetScale(1.0, kVerticalExaggeration, 1.0);
detailRendererPtr->AddViewProp(actors.edges);
}
// 顶部电极标记 ▼(仅网格数据;同纵向夸张对齐)。
if (*showElectrodes) {
auto elec = geopro::render::buildElectrodes(g);
if (elec) {
elec->SetScale(1.0, kDetailYScale, 1.0);
elec->SetScale(1.0, kVerticalExaggeration, 1.0);
detailRendererPtr->AddViewProp(elec);
}
}
@ -606,7 +609,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
const auto scs = repo.loadScatterColorScale(id);
auto a = geopro::render::buildScatter(s, scs, kScatterPointSize);
if (a) {
a->SetScale(1.0, kDetailYScale, 1.0);
a->SetScale(1.0, kVerticalExaggeration, 1.0);
detailRendererPtr->AddViewProp(a);
}
}
@ -616,7 +619,7 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
for (int i = 0; i < static_cast<int>(anomalies.size()); ++i) {
if (hiddenAnoms->count(i)) continue; // 列表中取消勾选→隐藏
for (auto& act : geopro::render::buildAnomalies({anomalies[i]})) {
act->SetScale(1.0, kDetailYScale, 1.0);
act->SetScale(1.0, kVerticalExaggeration, 1.0);
detailRendererPtr->AddViewProp(act);
}
}
@ -800,6 +803,22 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
.arg(QString::fromUtf8(kProjectCrs))
.arg(lat0, 0, 'f', 5)
.arg(lon0, 0, 'f', 5));
// ── dock 布局/窗口几何持久化 ──────────────────────────────────────────
// 恢复上次的停靠布局与窗口几何(须在全部 dock 创建后ADS 按 dock 标题作键匹配)。
{
const QSettings settings;
const QByteArray geo = settings.value(QStringLiteral("ui/geometry")).toByteArray();
if (!geo.isEmpty()) window.restoreGeometry(geo);
const QByteArray dockState = settings.value(QStringLiteral("ui/dockState")).toByteArray();
if (!dockState.isEmpty()) dockManager->restoreState(dockState);
}
// 退出时保存当前布局与几何aboutToQuit 早于 window 析构dockManager/window 仍存活)。
QObject::connect(qApp, &QCoreApplication::aboutToQuit, dockManager, [dockManager, &window]() {
QSettings settings;
settings.setValue(QStringLiteral("ui/geometry"), window.saveGeometry());
settings.setValue(QStringLiteral("ui/dockState"), dockManager->saveState());
});
}
} // namespace
@ -815,6 +834,10 @@ int main(int argc, char* argv[])
QSurfaceFormat::setDefaultFormat(QVTKOpenGLStereoWidget::defaultFormat());
QApplication app(argc, argv);
// 组织/应用名QSettings 持久化dock 布局、登录记忆等)按此定位存储位置。
QCoreApplication::setOrganizationName(QStringLiteral("Geomative"));
QCoreApplication::setApplicationName(QStringLiteral("Geopro3"));
// 浅色专业主题Fusion + 调色板 + 全局样式表):仅外观,登录窗与工作台共用。
geopro::app::applyTheme(app);
@ -841,11 +864,19 @@ int main(int argc, char* argv[])
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;
// 记住登录:若上次勾选「记住」且未超 30 天,凭证库里有有效 token → 免登录直接进。
QString token = geopro::app::recallValidToken(30);
if (token.isEmpty()) {
geopro::app::LoginWindow login(auth);
if (login.exec() != QDialog::Accepted) return 0;
token = login.token();
if (login.remember())
geopro::app::rememberSession(token); // 安全存 token + 时间戳
else
geopro::app::forgetSession(); // 未勾选:清除旧记忆
}
api.setToken(login.token()); // 注入 token 供后续 API 使用
api.setToken(token); // 注入 token 供后续 API 使用
// 登录成功 → 构建并显示工作台。
geopro::data::LocalSampleRepository repo(