功能:实现终端登录页与状态机骨架

变更详情:feat(kiosk-shell): 首版登录页与状态机骨架

按《VDI 云桌面软件开发方案》3.2 节实现:
- 状态机 BOOT/CONFIG/CONNECTING/SESSION/RECONNECT/ERROR
- 首配页(SN 绑定、服务器地址、账号、密码、显示档位)
- 全局热键 Ctrl+Alt+Shift+M 呼出管理菜单
- FreeRDP 3 以 libfreerdp3 API 内嵌,非拉起外部进程
This commit is contained in:
songyiming committed 2026-10-23 17:22:55 +08:00
commit c25bc2908a
14 files changed
+1374

No files matched your search

+324
View File
@@ -0,0 +1,324 @@
#include "main_window.h"
#include <QAction>
#include <QApplication>
#include <QCloseEvent>
#include <QKeySequence>
#include <QLabel>
#include <QMenu>
#include <QMessageBox>
#include <QScreen>
#include <QShortcut>
#include <QStackedWidget>
#include <QStatusBar>
#include <QTimer>
#include <QVBoxLayout>
#include "login_page.h"
namespace vdi {
namespace {
// FreeRDP 像素格式 -> QImage::Format
QImage::Format toQImageFormat(uint32_t freerdpFormat) {
switch (freerdpFormat) {
case 0x00000020: // PIXEL_FORMAT_BGRX8888
return QImage::Format_RGB888;
case 0x00000030: // PIXEL_FORMAT_BGRA8888
return QImage::Format_RGBA8888;
case 0x00000000: // PIXEL_FORMAT_RGBX8888
return QImage::Format_RGBX8888;
default:
return QImage::Format_RGBA8888;
}
}
constexpr int kReconnectDelayMs = 3000;
constexpr int kMaxReconnectAttempts = 5;
} // namespace
MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) {
fsm_ = StateMachine([this](State from, State to, const std::string& detail) {
onStateChanged(from, to, QString::fromStdString(detail));
});
setupUi();
setupHotkey();
// BOOT -> CONFIG
config_ = loadConfig();
fsm_.transitionTo(State::kConfig, "启动完成");
showLogin();
}
MainWindow::~MainWindow() {
stopSession();
}
void MainWindow::setupUi() {
setWindowTitle(QStringLiteral("VDI 云终端"));
setWindowState(Qt::WindowFullScreen);
setStyleSheet(QStringLiteral("QMainWindow { background: #0f1420; }"));
stack_ = new QStackedWidget(this);
stack_->setStyleSheet(QStringLiteral("background: #0f1420;"));
setCentralWidget(stack_);
// 登录页
config_ = loadConfig();
auto* login = new LoginPage(config_, this);
loginPage_ = login;
login->setStyleSheet(
QStringLiteral("QWidget { background: #0f1420; color: #e6e9ef; }"
"QLineEdit { background: #1a2233; border: 1px solid #2b3654; "
"border-radius: 4px; padding: 8px; color: #e6e9ef; }"
"QLineEdit:focus { border: 1px solid #2f6feb; }"
"QComboBox { background: #1a2233; border: 1px solid #2b3654; "
"border-radius: 4px; padding: 8px; color: #e6e9ef; }"
"QPushButton { background: #232d44; border: 1px solid #2b3654; "
"border-radius: 6px; color: #e6e9ef; }"
"QPushButton:hover { background: #2b3654; }"
"QCheckBox { color: #8a94a6; }"));
stack_->addWidget(login);
connect(login, &LoginPage::connectRequested, this, &MainWindow::onConnectRequested);
connect(login, &LoginPage::quitRequested, this, &MainWindow::onQuitRequested);
// 会话视图
auto* sessionWidget = new QWidget(this);
auto* sessionLayout = new QVBoxLayout(sessionWidget);
sessionLayout->setContentsMargins(0, 0, 0, 0);
sessionView_ = new QLabel(sessionWidget);
sessionView_->setAlignment(Qt::AlignCenter);
sessionView_->setStyleSheet(QStringLiteral("background: #000000; color: #e6e9ef;"));
sessionView_->setText(QStringLiteral("正在建立远程会话…"));
sessionOverlay_ = new QLabel(sessionWidget);
sessionOverlay_->setAlignment(Qt::AlignCenter);
sessionOverlay_->setStyleSheet(
QStringLiteral("background: rgba(15,20,32,0.82); color: #e6e9ef; "
"font-size: 20px; padding: 40px;"));
sessionOverlay_->setText(QStringLiteral("正在连接…"));
sessionOverlay_->hide();
sessionLayout->addWidget(sessionView_);
sessionLayout->addWidget(sessionOverlay_);
stack_->addWidget(sessionWidget);
// 状态条
statusBar_ = new QStatusBar(this);
statusBar_->setStyleSheet(QStringLiteral("QStatusBar { background: #161d2e; color: #8a94a6; }"));
setStatusBar(statusBar_);
// 重连定时器
reconnectTimer_ = new QTimer(this);
reconnectTimer_->setSingleShot(true);
connect(reconnectTimer_, &QTimer::timeout, this, [this]() {
if (fsm_.current() == State::kReconnect && reconnectAttempts_ < kMaxReconnectAttempts) {
++reconnectAttempts_;
startSession(config_);
} else if (fsm_.current() == State::kReconnect) {
fsm_.transitionTo(State::kError, "重连失败,请检查网络");
}
});
}
void MainWindow::setupHotkey() {
// 全局热键 Ctrl+Alt+Shift+M 呼出管理菜单(方案 3.2 节)
auto* shortcut = new QShortcut(QKeySequence(QStringLiteral("Ctrl+Alt+Shift+M")), this);
shortcut->setContext(Qt::ApplicationShortcut);
connect(shortcut, &QShortcut::activated, this, [this]() { onMenuAction(0); });
}
void MainWindow::onStateChanged(State from, State to, const QString& detail) {
Q_UNUSED(from);
const QString stateName = QString::fromLatin1(stateToString(to));
updateStatusBar(detail.isEmpty() ? stateName : QStringLiteral("%1 %2").arg(stateName, detail));
switch (to) {
case State::kConfig:
showLogin();
break;
case State::kConnecting:
if (stack_->currentWidget() != sessionView_->parentWidget()) {
stack_->setCurrentWidget(sessionView_->parentWidget());
}
sessionOverlay_->setText(QStringLiteral("正在连接 %1 …")
.arg(QString::fromStdString(config_.server)));
sessionOverlay_->show();
break;
case State::kSession:
sessionOverlay_->hide();
break;
case State::kReconnect:
sessionOverlay_->setText(QStringLiteral("连接已断开,正在重连…"));
sessionOverlay_->show();
reconnectTimer_->start(kReconnectDelayMs);
break;
case State::kError:
sessionOverlay_->setText(QStringLiteral("连接失败:%1").arg(detail));
sessionOverlay_->show();
if (loginPage_) {
loginPage_->setBusy(false);
loginPage_->setStatus(QStringLiteral("连接失败:%1").arg(detail), true);
}
break;
case State::kBoot:
break;
}
}
void MainWindow::updateStatusBar(const QString& text) {
statusBar_->showMessage(text);
}
void MainWindow::showLogin() {
stack_->setCurrentWidget(loginPage_);
if (loginPage_) {
loginPage_->setBusy(false);
loginPage_->setStatus(QStringLiteral("就绪"));
}
}
void MainWindow::onConnectRequested(const vdi::TerminalConfig& cfg) {
config_ = cfg;
reconnectAttempts_ = 0;
startSession(cfg);
}
void MainWindow::startSession(const TerminalConfig& cfg) {
if (!fsm_.transitionTo(State::kConnecting, QString::fromStdString(cfg.server))) {
// 已经在 CONNECTING 或 SESSION,忽略重复触发
if (fsm_.current() != State::kConnecting) {
return;
}
}
RdpOptions opts;
opts.server = cfg.server;
opts.username = cfg.username;
opts.password = cfg.password;
// 解析分辨率
int w = 1920;
int h = 1440;
if (sscanf(cfg.resolution.c_str(), "%dx%d", &w, &h) != 2) {
w = 1920;
h = 1440;
}
opts.width = w;
opts.height = h;
opts.bpp = 32;
opts.fullscreen = true;
// 与 xfreerdp3 相同的 GFX 能力组合语义:
// FreeRDP 3.32.1 不接受单个管道名,必须成组。
opts.gfx = "progressive|RFX|AVC420|AVC444";
opts.ignoreCertificate = true; // POC 阶段;正式产品必须做证书校验
RdpCallbacks cb;
cb.onFrame = [this](const uint8_t* data, uint32_t width, uint32_t height, uint32_t stride,
uint32_t format) {
// 帧数据来自 FreeRDP 线程,切换到 GUI 线程更新
QMetaObject::invokeMethod(
this, [this, data, width, height, stride, format]() {
onFrame(data, width, height, stride, format);
},
Qt::QueuedConnection);
};
cb.onState = [this](bool connected, const std::string& message) {
QMetaObject::invokeMethod(
this,
[this, connected, message]() {
onSessionState(connected, QString::fromStdString(message));
},
Qt::QueuedConnection);
};
stopSession();
session_ = std::make_unique<RdpSession>();
session_->start(opts, cb);
}
void MainWindow::stopSession() {
if (session_) {
session_->stop();
session_.reset();
}
}
void MainWindow::onSessionState(bool connected, const QString& message) {
if (connected) {
reconnectAttempts_ = 0;
fsm_.transitionTo(State::kSession, message);
} else {
if (fsm_.current() == State::kConnecting) {
// 首次连接失败
fsm_.transitionTo(State::kError, message);
} else if (fsm_.current() == State::kSession) {
fsm_.transitionTo(State::kReconnect, message);
}
}
}
void MainWindow::onFrame(const uint8_t* data, uint32_t width, uint32_t height, uint32_t stride,
uint32_t format) {
if (!data || width == 0 || height == 0) {
return;
}
const QImage::Format fmt = toQImageFormat(format);
// FreeRDP 的 primary_buffer 是 BGRA/BGRX 排列,QImage 需要拷贝
QImage image(reinterpret_cast<const uchar*>(data), static_cast<int>(width),
static_cast<int>(height), static_cast<int>(stride), fmt);
lastFrame_ = image.copy(); // 拷贝,脱离 FreeRDP 缓冲生命周期
frameDirty_ = true;
if (sessionView_) {
sessionView_->setPixmap(QPixmap::fromImage(lastFrame_).scaled(
sessionView_->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
}
}
void MainWindow::onQuitRequested() {
stopSession();
QApplication::quit();
}
void MainWindow::onMenuAction(int action) {
switch (action) {
case 1: // 重新连接
reconnectAttempts_ = 0;
stopSession();
startSession(config_);
break;
case 2: // 断开回登录页
stopSession();
fsm_.transitionTo(State::kConfig, "已断开");
break;
case 3: // 退出终端
onQuitRequested();
break;
default: {
QMenu menu(this);
QAction* reconnect = menu.addAction(QStringLiteral("重新连接"));
QAction* disconnect = menu.addAction(QStringLiteral("断开并返回登录页"));
menu.addSeparator();
QAction* quit = menu.addAction(QStringLiteral("退出终端"));
QAction* chosen = menu.exec(QCursor::pos());
if (chosen == reconnect) {
onMenuAction(1);
} else if (chosen == disconnect) {
onMenuAction(2);
} else if (chosen == quit) {
onMenuAction(3);
}
break;
}
}
}
bool MainWindow::eventFilter(QObject* watched, QEvent* event) {
return QMainWindow::eventFilter(watched, event);
}
} // namespace vdi