#include "login/LoginWindow.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #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