feat(m1): dock 布局持久化 + 记住登录(QtKeychain) + 纵向夸张统一 #1
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 1:ColorScale 上色
|
||||
geopro_data # Phase 2:本地样本仓储(对象树 / 网格 / 色阶)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
|
||||
// 记住登录:勾选后成功登录将安全存储 token,30 天内免登录。默认不勾(更安全)。
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
// 先弹登录窗;用户取消/未登录则退出。
|
||||
// 记住登录:若上次勾选「记住」且未超 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(
|
||||
|
|
|
|||
Loading…
Reference in New Issue