geopro/src/app/login/LoginWindow.cpp

273 lines
9.9 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "login/LoginWindow.hpp"
#include <QColor>
#include <QFont>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QPainter>
#include <QPen>
#include <QPixmap>
#include <QPushButton>
#include <QRandomGenerator>
#include <QVBoxLayout>
#include <QWidget>
#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(400, 500);
// 仅外观:登录窗自带样式(沿用全局主题令牌,保证一脉相承)。
// QLineEdit 在所有状态都显式白底深字 + 边框,避免失焦时取调色板默认色与背景相近不可读。
setStyleSheet(QStringLiteral(
"QDialog { background: #F4F6FA; }"
"#headerBand {"
" background: qlineargradient(x1:0, y1:0, x2:1, y2:1,"
" stop:0 #2D6CB5, stop:1 #234F87); }"
"#brandTitle { color: #FFFFFF; font-size: 23px; font-weight: 700; }"
"#brandSubtitle { color: rgba(255,255,255,0.82); font-size: 12px; }"
"#fieldLabel { color: #5A6B85; font-size: 12px; font-weight: 600; }"
"QLineEdit {"
" background: #FFFFFF; color: #1F2A3D;"
" border: 1px solid #C7D2E0; border-radius: 8px; padding: 0 12px;"
" selection-background-color: #2D6CB5; selection-color: #FFFFFF; }"
"QLineEdit:focus { border: 1px solid #2D6CB5; }"
"QLineEdit:disabled { background: #F0F2F6; color: #8A93A3; }"
"#captchaImg { border: 1px solid #C7D2E0; border-radius: 8px; background: #EEF2FB; }"));
auto* root = new QVBoxLayout(this);
root->setContentsMargins(0, 0, 0, 0);
root->setSpacing(0);
// ── 品牌头部:强调色横幅 + 产品名 + 副标题(建立产品身份与视觉锚点)──
auto* headerBand = new QWidget(this);
headerBand->setObjectName(QStringLiteral("headerBand"));
headerBand->setFixedHeight(108);
auto* headerLayout = new QVBoxLayout(headerBand);
headerLayout->setContentsMargins(32, 0, 32, 0);
headerLayout->setSpacing(4);
headerLayout->addStretch();
auto* brandTitle = new QLabel(QStringLiteral("Geopro 3.0"), headerBand);
brandTitle->setObjectName(QStringLiteral("brandTitle"));
auto* brandSubtitle = new QLabel(QStringLiteral("地球物理数据分析平台"), headerBand);
brandSubtitle->setObjectName(QStringLiteral("brandSubtitle"));
headerLayout->addWidget(brandTitle);
headerLayout->addWidget(brandSubtitle);
headerLayout->addStretch();
root->addWidget(headerBand);
// ── 表单主体:标签在上、输入在下的纵向字段(现代、留白充分)──
auto* body = new QWidget(this);
auto* form = new QVBoxLayout(body);
form->setContentsMargins(32, 24, 32, 26);
form->setSpacing(6);
// 统一字段构造小号muted标签 + 40px 高输入框 + 字段间距。
auto addField = [&](const QString& labelText, QLineEdit* edit) {
auto* lbl = new QLabel(labelText, body);
lbl->setObjectName(QStringLiteral("fieldLabel"));
edit->setMinimumHeight(40);
form->addWidget(lbl);
form->addWidget(edit);
form->addSpacing(6);
};
userEdit_ = new QLineEdit(body);
userEdit_->setPlaceholderText(QStringLiteral("请输入用户名"));
userEdit_->setClearButtonEnabled(true);
addField(QStringLiteral("用户名"), userEdit_);
pwdEdit_ = new QLineEdit(body);
pwdEdit_->setEchoMode(QLineEdit::Password);
pwdEdit_->setPlaceholderText(QStringLiteral("请输入密码"));
addField(QStringLiteral("密码"), pwdEdit_);
// 验证码:标签 + 一行(输入框占主,验证码图固定在右)。
auto* codeLbl = new QLabel(QStringLiteral("验证码"), body);
codeLbl->setObjectName(QStringLiteral("fieldLabel"));
form->addWidget(codeLbl);
auto* captchaRow = new QHBoxLayout();
captchaRow->setSpacing(10);
codeEdit_ = new QLineEdit(body);
codeEdit_->setMinimumHeight(40);
codeEdit_->setPlaceholderText(QStringLiteral("请输入验证码"));
captchaLabel_ = new QLabel(body);
captchaLabel_->setObjectName(QStringLiteral("captchaImg"));
captchaLabel_->setFixedSize(kCaptchaWidth, kCaptchaHeight);
captchaLabel_->setAlignment(Qt::AlignCenter);
captchaRow->addWidget(codeEdit_, 1);
captchaRow->addWidget(captchaLabel_);
form->addLayout(captchaRow);
// 刷新链接(右对齐,次要操作弱化为文字链接)。
auto* refreshRow = new QHBoxLayout();
refreshRow->addStretch();
refreshBtn_ = new QPushButton(QStringLiteral("看不清?换一张"), body);
refreshBtn_->setFlat(true);
refreshBtn_->setCursor(Qt::PointingHandCursor);
refreshBtn_->setStyleSheet(QStringLiteral(
"QPushButton { color: #2D6CB5; border: none; background: transparent; padding: 2px 0; }"
"QPushButton:hover { color: #234F87; text-decoration: underline; }"));
refreshRow->addWidget(refreshBtn_);
form->addLayout(refreshRow);
// 错误提示:固定占位高度,避免出现时整体布局跳动。
errorLabel_ = new QLabel(body);
errorLabel_->setStyleSheet(QStringLiteral("color: #C0392B; font-size: 12px;"));
errorLabel_->setWordWrap(true);
errorLabel_->setMinimumHeight(18);
form->addWidget(errorLabel_);
form->addStretch();
// 主操作满宽强调主按钮von Restorff唯一高强调元素引导主流程
loginBtn_ = new QPushButton(QStringLiteral("登 录"), body);
loginBtn_->setMinimumHeight(44);
loginBtn_->setCursor(Qt::PointingHandCursor);
loginBtn_->setStyleSheet(QStringLiteral(
"QPushButton { background: #2D6CB5; color: #FFFFFF; border: none; border-radius: 8px; "
"font-size: 15px; font-weight: 600; }"
"QPushButton:hover { background: #2862A6; }"
"QPushButton:pressed { background: #234F87; }"
"QPushButton:disabled { background: #9FB4CC; }"));
loginBtn_->setDefault(true);
form->addWidget(loginBtn_);
root->addWidget(body, 1);
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(); // 打开即拉一张验证码
userEdit_->setFocus(); // 焦点落在第一个待填字段
}
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