feat(qml): task #13 登录页接 AuthBridge 真 db 认证
LoginScreen.qml wiring 改造:
- appState.login → auth.login (走 DatabaseManager.authenticate / pymysql)
- 删 || "demo" 兜底,密码空时 UI 阻断
- submitting 状态:登录中 username/password/checkbox/button 全 disabled,按钮文字 "登录中…"
- 监听 auth.loginFailed → errorLabel 红字展示后端返回的具体原因
("用户名或密码错误" / "无法连接到服务器" 等,DatabaseManager 已统一中文化)
- usernameField 不再硬编码 "chaijin",改 placeholder
冒烟测试(直接调 Python 测真 db):
- chaijin/wrongpwd → False,loginFailed 信号"用户名或密码错误"
- chaijin/a160827 → True,loggedIn=True,currentUser='chaijin'
QML 启动登录页渲染正常(task13_login.png 已验证)。
注:
- "记住用户名/密码" checkbox UI 保留但暂未持久化,task #18 系统集成时接 config_util 落盘
- pymysql 同步阻塞主线程最多 5s(connect_timeout),暂不影响 UX;
若 db 慢成痛点 task #18 时再改 worker 异步
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Showing
1 changed file
with
44 additions
and
13 deletions
| ... | @@ -9,15 +9,40 @@ Rectangle { | ... | @@ -9,15 +9,40 @@ Rectangle { |
| 9 | color: App.Theme.bgCanvas | 9 | color: App.Theme.bgCanvas |
| 10 | focus: true | 10 | focus: true |
| 11 | 11 | ||
| 12 | Keys.onReturnPressed: appState.login(usernameField.text, passwordField.text || "demo") | 12 | // 提交期间禁止重复点击 / 回车 |
| 13 | Keys.onEnterPressed: appState.login(usernameField.text, passwordField.text || "demo") | 13 | property bool submitting: false |
| 14 | |||
| 15 | function doLogin() { | ||
| 16 | if (submitting) return | ||
| 17 | if (usernameField.text.length === 0 || passwordField.text.length === 0) { | ||
| 18 | errorLabel.text = "用户名和密码不能为空" | ||
| 19 | return | ||
| 20 | } | ||
| 21 | errorLabel.text = "" | ||
| 22 | submitting = true | ||
| 23 | // AuthBridge.login 是同步(pymysql 5s timeout),主线程会卡一下; | ||
| 24 | // task #18 时若 db 慢需求改异步再说 | ||
| 25 | var ok = auth.login(usernameField.text, passwordField.text) | ||
| 26 | submitting = false | ||
| 27 | // 失败时 auth 会发 loginFailed 信号,由下面 Connections 接住 | ||
| 28 | // 成功时 auth.loggedInChanged → AppState 转发 → Main.qml StackLayout 切换 | ||
| 29 | } | ||
| 30 | |||
| 31 | Keys.onReturnPressed: doLogin() | ||
| 32 | Keys.onEnterPressed: doLogin() | ||
| 33 | |||
| 34 | Connections { | ||
| 35 | target: auth | ||
| 36 | function onLoginFailed(message) { | ||
| 37 | errorLabel.text = message | ||
| 38 | } | ||
| 39 | } | ||
| 14 | 40 | ||
| 15 | ColumnLayout { | 41 | ColumnLayout { |
| 16 | anchors.centerIn: parent | 42 | anchors.centerIn: parent |
| 17 | width: 360 | 43 | width: 360 |
| 18 | spacing: 0 | 44 | spacing: 0 |
| 19 | 45 | ||
| 20 | // 标题 | ||
| 21 | Label { | 46 | Label { |
| 22 | text: "登录" | 47 | text: "登录" |
| 23 | font.family: App.Theme.fontFamily | 48 | font.family: App.Theme.fontFamily |
| ... | @@ -37,19 +62,19 @@ Rectangle { | ... | @@ -37,19 +62,19 @@ Rectangle { |
| 37 | Layout.bottomMargin: App.Theme.space5 | 62 | Layout.bottomMargin: App.Theme.space5 |
| 38 | } | 63 | } |
| 39 | 64 | ||
| 40 | // 用户名 | ||
| 41 | CaptionLabel { | 65 | CaptionLabel { |
| 42 | text: "用户名" | 66 | text: "用户名" |
| 43 | Layout.bottomMargin: App.Theme.space2 | 67 | Layout.bottomMargin: App.Theme.space2 |
| 44 | } | 68 | } |
| 45 | ThemedTextField { | 69 | ThemedTextField { |
| 46 | id: usernameField | 70 | id: usernameField |
| 47 | text: "chaijin" | 71 | text: "" |
| 72 | placeholderText: "请输入用户名" | ||
| 73 | enabled: !root.submitting | ||
| 48 | Layout.fillWidth: true | 74 | Layout.fillWidth: true |
| 49 | Layout.bottomMargin: App.Theme.space4 | 75 | Layout.bottomMargin: App.Theme.space4 |
| 50 | } | 76 | } |
| 51 | 77 | ||
| 52 | // 密码 | ||
| 53 | CaptionLabel { | 78 | CaptionLabel { |
| 54 | text: "密码" | 79 | text: "密码" |
| 55 | Layout.bottomMargin: App.Theme.space2 | 80 | Layout.bottomMargin: App.Theme.space2 |
| ... | @@ -58,19 +83,21 @@ Rectangle { | ... | @@ -58,19 +83,21 @@ Rectangle { |
| 58 | id: passwordField | 83 | id: passwordField |
| 59 | echoMode: TextInput.Password | 84 | echoMode: TextInput.Password |
| 60 | placeholderText: "••••••••" | 85 | placeholderText: "••••••••" |
| 86 | enabled: !root.submitting | ||
| 61 | Layout.fillWidth: true | 87 | Layout.fillWidth: true |
| 62 | Layout.bottomMargin: App.Theme.space2 | 88 | Layout.bottomMargin: App.Theme.space2 |
| 63 | } | 89 | } |
| 64 | 90 | ||
| 65 | // 复选行 | ||
| 66 | RowLayout { | 91 | RowLayout { |
| 67 | spacing: App.Theme.space4 | 92 | spacing: App.Theme.space4 |
| 68 | Layout.fillWidth: true | 93 | Layout.fillWidth: true |
| 69 | Layout.bottomMargin: App.Theme.space5 | 94 | Layout.bottomMargin: App.Theme.space5 |
| 70 | 95 | ||
| 71 | CheckBox { | 96 | CheckBox { |
| 97 | id: rememberUser | ||
| 72 | text: "记住用户名" | 98 | text: "记住用户名" |
| 73 | checked: true | 99 | checked: true |
| 100 | enabled: !root.submitting | ||
| 74 | font.family: App.Theme.fontFamily | 101 | font.family: App.Theme.fontFamily |
| 75 | font.pointSize: App.Theme.fontSm | 102 | font.pointSize: App.Theme.fontSm |
| 76 | contentItem: Text { | 103 | contentItem: Text { |
| ... | @@ -100,8 +127,10 @@ Rectangle { | ... | @@ -100,8 +127,10 @@ Rectangle { |
| 100 | } | 127 | } |
| 101 | 128 | ||
| 102 | CheckBox { | 129 | CheckBox { |
| 130 | id: rememberPassword | ||
| 103 | text: "记住密码" | 131 | text: "记住密码" |
| 104 | checked: true | 132 | checked: true |
| 133 | enabled: !root.submitting | ||
| 105 | font.family: App.Theme.fontFamily | 134 | font.family: App.Theme.fontFamily |
| 106 | font.pointSize: App.Theme.fontSm | 135 | font.pointSize: App.Theme.fontSm |
| 107 | contentItem: Text { | 136 | contentItem: Text { |
| ... | @@ -133,20 +162,22 @@ Rectangle { | ... | @@ -133,20 +162,22 @@ Rectangle { |
| 133 | Item { Layout.fillWidth: true } | 162 | Item { Layout.fillWidth: true } |
| 134 | } | 163 | } |
| 135 | 164 | ||
| 136 | // 登录按钮 | ||
| 137 | PrimaryButton { | 165 | PrimaryButton { |
| 138 | text: "登录" | 166 | text: root.submitting ? "登录中…" : "登录" |
| 167 | enabled: !root.submitting | ||
| 139 | Layout.fillWidth: true | 168 | Layout.fillWidth: true |
| 140 | onClicked: appState.login(usernameField.text, passwordField.text || "demo") | 169 | onClicked: doLogin() |
| 141 | } | 170 | } |
| 142 | 171 | ||
| 143 | Label { | 172 | Label { |
| 144 | id: hint | 173 | id: errorLabel |
| 145 | text: "" | 174 | text: "" |
| 146 | font.family: App.Theme.fontFamily | 175 | font.family: App.Theme.fontFamily |
| 147 | font.pointSize: App.Theme.fontXs | 176 | font.pointSize: App.Theme.fontXs |
| 148 | color: App.Theme.textTertiary | 177 | color: App.Theme.danger |
| 149 | Layout.alignment: Qt.AlignHCenter | 178 | wrapMode: Text.Wrap |
| 179 | horizontalAlignment: Text.AlignHCenter | ||
| 180 | Layout.fillWidth: true | ||
| 150 | Layout.topMargin: App.Theme.space3 | 181 | Layout.topMargin: App.Theme.space3 |
| 151 | } | 182 | } |
| 152 | } | 183 | } | ... | ... |
-
Please register or sign in to post a comment