273 lines
9.9 KiB
C++
273 lines
9.9 KiB
C++
#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
|