@flun/html-template 4.4.2 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,28 +5,24 @@
5
5
  <meta charset="UTF-8">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
7
  <title>双因素验证处理页</title>
8
- <!-- 引入公共主题变量与全局样式 -->
9
- <link rel="stylesheet" href="/static/constants.css" /> <!-- 样式常量 -->
10
- <link rel="stylesheet" href="/static/public.css" /> <!-- 公共样式 -->
11
- <link rel="stylesheet" href="/static/themeImg.css" /> <!-- 主题图标 -->
12
- <link rel="stylesheet" href="/static/topImg.css" /> <!-- 返回顶部图标 -->
13
-
8
+ <link rel="stylesheet" href="/static/constants.css" />
9
+ <link rel="stylesheet" href="/static/public.css" />
10
+ <link rel="stylesheet" href="/static/themeImg.css" />
11
+ <link rel="stylesheet" href="/static/topImg.css" />
14
12
  <style>
15
- /* 覆盖 body 布局:本页需要居中卡片,而非默认的纵向弹性布局 */
16
13
  body {
17
14
  display: flex;
18
15
  align-items: center;
19
16
  justify-content: center;
20
17
  padding: 20px;
21
- /* 背景由公共变量接管,自动适配深浅色 */
22
18
  background-color: var(--bg-color);
23
19
  background-image: var(--body-bg);
24
20
  }
25
21
 
26
- /* 卡片容器 */
27
22
  .card {
28
23
  background: var(--container-bg);
29
24
  border-radius: 12px;
25
+ border: 1px solid #2e3135;
30
26
  box-shadow: 0 20px 40px var(--content-shadow);
31
27
  width: 100%;
32
28
  max-width: 400px;
@@ -34,7 +30,6 @@
34
30
  transition: background 0.5s ease, box-shadow 0.5s ease;
35
31
  }
36
32
 
37
- /* 卡片标题 */
38
33
  .card h2 {
39
34
  color: var(--h2-color);
40
35
  margin-bottom: 20px;
@@ -43,18 +38,10 @@
43
38
  font-size: 28px;
44
39
  }
45
40
 
46
- /* 提示文字 */
47
- .card p {
48
- text-align: center;
49
- margin-bottom: 20px;
50
- color: var(--p-color);
51
- }
52
-
53
41
  .form-group {
54
42
  margin-bottom: 20px;
55
43
  }
56
44
 
57
- /* 验证码输入框 */
58
45
  .form-group input {
59
46
  width: 100%;
60
47
  padding: 12px 16px;
@@ -62,20 +49,20 @@
62
49
  border-radius: 8px;
63
50
  font-size: 16px;
64
51
  text-align: center;
65
- letter-spacing: 4px;
66
52
  background: var(--li-bg);
67
53
  color: var(--text-color);
68
- transition: border-color 0.3s ease, background 0.5s ease, color 0.5s ease;
69
54
  }
70
55
 
71
56
  .form-group input:focus {
72
57
  border-color: var(--link-color);
73
58
  }
74
59
 
75
- /* 验证按钮 */
60
+ .totp-input {
61
+ letter-spacing: 4px;
62
+ }
63
+
76
64
  .btn {
77
65
  width: 100%;
78
- display: block;
79
66
  padding: 14px;
80
67
  background: var(--btn-bg);
81
68
  color: var(--text-color);
@@ -84,7 +71,6 @@
84
71
  font-size: 16px;
85
72
  font-weight: 600;
86
73
  cursor: pointer;
87
- margin-top: 0;
88
74
  box-shadow: 0 4px 12px var(--content-shadow);
89
75
  transition: background 0.3s ease, transform 0.2s ease;
90
76
  }
@@ -101,7 +87,15 @@
101
87
  transform: none;
102
88
  }
103
89
 
104
- /* 消息提示 */
90
+ .btn-secondary {
91
+ background: var(--btn-secondary-bg, #6c757d);
92
+ margin-top: 10px;
93
+ }
94
+
95
+ .btn-secondary:hover {
96
+ background: var(--btn-secondary-hover, #5a6268);
97
+ }
98
+
105
99
  .error {
106
100
  color: #e53e3e;
107
101
  font-size: 14px;
@@ -109,22 +103,35 @@
109
103
  text-align: center;
110
104
  }
111
105
 
112
- /* 备用码区域 */
113
- .backup-link {
106
+ .backup-panel {
114
107
  margin-top: 20px;
108
+ padding-top: 15px;
109
+ border-top: 1px solid var(--content-border);
110
+ }
111
+
112
+ .hardware-loader {
115
113
  text-align: center;
116
- font-size: 14px;
114
+ margin: 20px 0;
117
115
  color: var(--text-color);
118
116
  }
119
117
 
120
- .backup-link a {
121
- color: var(--link-color);
122
- text-decoration: none;
118
+ [hidden] {
119
+ display: none !important;
123
120
  }
124
121
 
125
- .backup-link a:hover {
126
- color: var(--link-hover);
127
- text-decoration: underline;
122
+ @media (max-width: 640px) {
123
+ .card {
124
+ padding: 28px 20px;
125
+ }
126
+
127
+ .card h2 {
128
+ font-size: 24px;
129
+ }
130
+
131
+ .form-group input {
132
+ font-size: 16px;
133
+ padding: 12px;
134
+ }
128
135
  }
129
136
  </style>
130
137
  </head>
@@ -132,52 +139,187 @@
132
139
  <body>
133
140
  <div class="card">
134
141
  <h2>双因素验证</h2>
135
- <form onsubmit="return false;">
136
- <p style="text-align:center; margin-bottom:20px; color:#666;">请输入身份验证器中的6位数字验证码</p>
142
+
143
+ <!-- TOTP 模式区域 -->
144
+ <div id="totpSection">
145
+ <p id="promptText">请输入身份验证器中的6位数字验证码</p>
137
146
  <div class="form-group">
138
- <input type="text" id="token" placeholder="在此输入6位验证码" maxlength="6" autofocus
139
- autocomplete="one-time-code">
147
+ <input type="text" id="tokenInput" placeholder="6位验证码" maxlength="6" autofocus
148
+ autocomplete="one-time-code" class="totp-input">
149
+ </div>
150
+ <button type="button" class="btn" id="verifyTotpBtn">验证</button>
151
+ </div>
152
+
153
+ <!-- 硬件验证专用区域 -->
154
+ <div id="webauthnSection" hidden>
155
+ <p>正在请求硬件验证,请按提示操作...</p>
156
+ <div class="hardware-loader">
157
+ <button type="button" class="btn btn-secondary" id="retryHardwareBtn">重新尝试硬件验证</button>
140
158
  </div>
141
- <button type="button" class="btn" id="verifyBtn">验证</button>
142
- </form>
143
- <div class="error" id="message"></div>
144
- <div class="backup-link">
145
- <a href="#" id="useBackup">使用备用码</a>
146
159
  </div>
160
+
161
+ <!-- 备份码面板(异常时动态显示) -->
162
+ <div id="backupPanel" class="backup-panel" hidden>
163
+ <div class="form-group">
164
+ <input type="text" id="backupCodeInput" placeholder="请输入10位备用码" maxlength="10" autocomplete="off">
165
+ </div>
166
+ <button type="button" class="btn" id="submitBackupBtn">登录</button>
167
+ </div>
168
+
169
+ <div class="error" id="message"></div>
147
170
  </div>
148
171
 
149
- <!-- 公共逻辑 -->
150
- <script src="/static/themeModule.js" defer></script><!-- 引入主题自适应模块 -->
151
- <script src="/static/mouseOrTouch.js" defer></script><!-- 引入鼠标或触摸操作 -->
152
- <script src="/static/themeImg.js" defer></script> <!-- 引入主题图标模块 -->
153
- <script src="/static/topImg.js" defer></script> <!-- 引入返回顶部图标模块 -->
172
+ <!-- 公共脚本 -->
173
+ <script src="/static/themeModule.js" defer></script>
174
+ <script src="/static/mouseOrTouch.js" defer></script>
175
+ <script src="/static/themeImg.js" defer></script>
176
+ <script src="/static/topImg.js" defer></script>
177
+ <script src="/static/utils/browser.js"></script>
154
178
  <script>
155
- const [verifyBtn, tokenEl, messageDiv, useBackup] = ['verifyBtn', 'token', 'message', 'useBackup']
156
- .map(id => document.getElementById(id)),
157
- handleError = msg => {
158
- messageDiv.textContent = msg, tokenEl.value = '', verifyBtn.disabled = false, tokenEl.focus();
179
+ const [totpSection, webauthnSection, backupPanel, messageDiv, tokenInput, verifyTotpBtn, backupCodeInput, submitBackupBtn,
180
+ retryHardwareBtn] = ['totpSection', 'webauthnSection', 'backupPanel', 'message', 'tokenInput',
181
+ 'verifyTotpBtn', 'backupCodeInput', 'submitBackupBtn', 'retryHardwareBtn']
182
+ .map(id => document.getElementById(id)),
183
+ urlParams = new URLSearchParams(window.location.search), method = urlParams.get('method'),
184
+ urlUsername = urlParams.get('username') || '', maxNum = 3; // 失败次数达到3次时显示备用码面板
185
+
186
+ let hardwareRunning = false, currentUsername = urlUsername;
187
+ const setMessage = msg => messageDiv.textContent = msg, clearMessage = () => messageDiv.textContent = '',
188
+ // 禁用/启用表单控件
189
+ setFormDisabled = disabled => {
190
+ const inputs = [tokenInput, backupCodeInput, verifyTotpBtn, submitBackupBtn, retryHardwareBtn];
191
+ inputs.forEach(el => { if (el) el.disabled = disabled; });
192
+ },
193
+ // 显示备份码面板
194
+ showBackupPanel = optionalMsg => {
195
+ if (optionalMsg) setMessage(optionalMsg);
196
+ totpSection.hidden = true, webauthnSection.hidden = true, backupPanel.hidden = false;
197
+ backupCodeInput.value = '', backupCodeInput.focus();
159
198
  },
160
- verify = async () => {
161
- verifyBtn.disabled = true; messageDiv.textContent = '';
162
- const token = tokenEl.value.trim();
163
- if (!token) return handleError('请输入验证码');
199
+ // 统一处理验证响应(TOTP 和备份码共用)
200
+ handleVerifyResponse = async input => {
201
+ if (!input) {
202
+ setMessage('请输入验证码/备用码');
203
+ return false;
204
+ }
205
+ const isToken = input.length === 6, isBackup = input.length === 10;
206
+ if (!isToken && !isBackup) {
207
+ setMessage('验证码必须为6位数字,备用码为10位');
208
+ return false;
209
+ }
210
+
211
+ // 临时禁用提交按钮防止重复请求
212
+ const activeBtn = isToken ? verifyTotpBtn : submitBackupBtn;
213
+ activeBtn.disabled = true, clearMessage();
214
+
164
215
  try {
165
216
  const response = await fetch('/api/verify-2fa', {
166
- method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token })
217
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
218
+ body: JSON.stringify({ input })
167
219
  }), data = await response.json();
168
- if (response.ok) tokenEl.value = '', window.location.href = '/';
169
- else handleError(data.message);
170
- } catch (err) { handleError('网络错误,请稍后重试') }
171
- }
172
- verifyBtn.addEventListener('click', verify);
173
- useBackup.addEventListener('click', e => {
174
- e.preventDefault(), tokenEl.placeholder = '请输入10位备用码', tokenEl.maxLength = 10, tokenEl.value = '';
175
- useBackup.style.display = 'none'; tokenEl.focus();
176
- });
177
- tokenEl.addEventListener('keypress', e => {
178
- if (e.key === 'Enter') verify();
179
- });
180
- tokenEl.addEventListener('input', () => messageDiv.textContent = '');
220
+
221
+ if (response.ok) {
222
+ window.location.href = '/';
223
+ return true;
224
+ }
225
+
226
+ // 处理锁定情况
227
+ if (data.locked) {
228
+ setMessage(data.message), setFormDisabled(true);
229
+ return false;
230
+ }
231
+
232
+ // 处理普通失败(包含 remainingAttempts)
233
+ if (data.remainingAttempts !== undefined) {
234
+ const failuresSoFar = 9 - data.remainingAttempts;
235
+ setMessage(`${data.message} (已失败 ${failuresSoFar} 次)`);
236
+ if (failuresSoFar >= maxNum && isToken) showBackupPanel(`连续失败 ${failuresSoFar} 次,请使用备用码登录`);
237
+ }
238
+ else setMessage(data.message);
239
+
240
+ // 清空输入框,准备下次尝试
241
+ tokenInput?.value = '', backupCodeInput?.value = '', activeBtn.disabled = false;
242
+
243
+ // 判断焦点归属
244
+ if (isBackup) backupCodeInput.focus();
245
+ else if (isToken) tokenInput.focus();
246
+ return false;
247
+ } catch (err) {
248
+ setMessage(err.message), activeBtn.disabled = false;
249
+ return false;
250
+ }
251
+ },
252
+ // TOTP验证和备份码提交入口
253
+ performTotpVerification = () => handleVerifyResponse(tokenInput.value.trim()),
254
+ submitBackupCode = () => handleVerifyResponse(backupCodeInput.value.trim()),
255
+ // 硬件验证核心
256
+ performHardwareVerification = async () => {
257
+ if (!window.PublicKeyCredential) {
258
+ showBackupPanel('当前浏览器不支持硬件安全验证,请使用备用码登录');
259
+ return false;
260
+ }
261
+ if (!currentUsername) {
262
+ showBackupPanel('无法获取用户身份,请使用备用码登录');
263
+ return false;
264
+ }
265
+ if (retryHardwareBtn && !retryHardwareBtn.hasListener)
266
+ retryHardwareBtn.addEventListener('click', () => performHardwareVerification()), retryHardwareBtn.hasListener = true;
267
+ if (hardwareRunning) return false;
268
+
269
+ hardwareRunning = true, clearMessage();
270
+ try {
271
+ let platformAvailable = false;
272
+ if (window.PublicKeyCredential) {
273
+ try {
274
+ platformAvailable = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
275
+ } catch (err) { console.warn(err); }
276
+ }
277
+ if (!platformAvailable) throw new Error('当前环境不支持平台硬件验证');
278
+
279
+ const beginRes = await fetch('/api/webauthn/login/begin', {
280
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
281
+ body: JSON.stringify({ username: currentUsername })
282
+ }), options = await beginRes.json();
283
+
284
+ if (!beginRes.ok) throw new Error(options.message);
285
+ const assertionResp = await flunWebAuthnBrowser.startAuthentication(options),
286
+ completeRes = await fetch('/api/webauthn/login/complete', {
287
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
288
+ body: JSON.stringify({ assertionResponse: assertionResp })
289
+ }), completeData = await completeRes.json();
290
+
291
+ if (completeRes.ok) {
292
+ window.location.href = '/';
293
+ return true;
294
+ }
295
+ else throw new Error(completeData.message);
296
+ } catch (err) {
297
+ let errorMsg = err.message;
298
+ if (err.name === 'NotAllowedError') errorMsg = '硬件验证已取消';
299
+ showBackupPanel(`${errorMsg},请使用备用码登录`);
300
+ return false;
301
+ } finally { hardwareRunning = false }
302
+ },
303
+ // 页面初始化
304
+ initPage = () => {
305
+ backupPanel.hidden = true;
306
+ if (method === 'webauthn')
307
+ totpSection.hidden = true, webauthnSection.hidden = false, performHardwareVerification().catch(e => console.warn(e));
308
+ else totpSection.hidden = false, webauthnSection.hidden = true, tokenInput.maxLength = 6, tokenInput.focus();
309
+
310
+ // 事件监听
311
+ submitBackupBtn.addEventListener('click', submitBackupCode);
312
+ verifyTotpBtn.addEventListener('click', performTotpVerification);
313
+ backupCodeInput.addEventListener('keypress', e => {
314
+ if (e.key === 'Enter') submitBackupCode();
315
+ });
316
+ tokenInput.addEventListener('keypress', e => {
317
+ if (e.key === 'Enter') performTotpVerification();
318
+ });
319
+ [backupCodeInput, tokenInput].forEach(el => el.addEventListener('input', clearMessage));
320
+ };
321
+
322
+ initPage();
181
323
  </script>
182
324
  </body>
183
325
 
@@ -18,16 +18,15 @@
18
18
  align-items: center;
19
19
  justify-content: center;
20
20
  padding: 20px;
21
- /* 背景由公共变量接管,自动适配深浅色 */
22
21
  background-color: var(--bg-color);
23
22
  background-image: var(--body-bg);
24
- /* 保留页面原有的字体偏好,覆盖 public.css 中的通用字体 */
25
23
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
26
24
  }
27
25
 
28
26
  .card {
29
27
  background: var(--container-bg);
30
28
  border-radius: 12px;
29
+ border: 1px solid #2e3135;
31
30
  box-shadow: 0 20px 40px var(--content-shadow);
32
31
  width: 100%;
33
32
  max-width: 400px;
@@ -129,8 +128,58 @@
129
128
  text-decoration: underline;
130
129
  }
131
130
 
132
- .hidden {
133
- display: none;
131
+ [hidden] {
132
+ display: none !important;
133
+ }
134
+
135
+ /* ==================== 移动端样式适配 (≤640px) ==================== */
136
+ @media (max-width: 640px) {
137
+ body {
138
+ padding: 16px;
139
+ }
140
+
141
+ .card {
142
+ max-width: 100%;
143
+ padding: 28px 20px;
144
+ border-radius: 16px;
145
+ }
146
+
147
+ .card h2 {
148
+ font-size: 24px;
149
+ margin-bottom: 8px;
150
+ }
151
+
152
+ .card p {
153
+ font-size: 13px;
154
+ margin-bottom: 24px;
155
+ }
156
+
157
+ .form-group input {
158
+ font-size: 16px;
159
+ /* 防止iOS缩放 */
160
+ padding: 12px 14px;
161
+ }
162
+
163
+ .btn {
164
+ padding: 12px;
165
+ font-size: 16px;
166
+ }
167
+
168
+ .links {
169
+ font-size: 13px;
170
+ margin-top: 20px;
171
+ }
172
+ }
173
+
174
+ /* 极小屏幕 (≤480px) 微调 */
175
+ @media (max-width: 480px) {
176
+ .card {
177
+ padding: 24px 16px;
178
+ }
179
+
180
+ .card h2 {
181
+ font-size: 22px;
182
+ }
134
183
  }
135
184
  </style>
136
185
  </head>
@@ -30,6 +30,7 @@
30
30
  .card {
31
31
  background: var(--container-bg);
32
32
  border-radius: 12px;
33
+ border: 1px solid #2e3135;
33
34
  box-shadow: 0 20px 40px var(--content-shadow);
34
35
  width: 100%;
35
36
  max-width: 400px;
@@ -127,6 +128,68 @@
127
128
  color: var(--link-hover);
128
129
  text-decoration: underline;
129
130
  }
131
+
132
+ /* ==================== 移动端样式适配 (≤640px) ==================== */
133
+ @media (max-width: 640px) {
134
+ body {
135
+ padding: 16px;
136
+ }
137
+
138
+ .card {
139
+ max-width: 100%;
140
+ padding: 28px 20px;
141
+ border-radius: 16px;
142
+ }
143
+
144
+ .card h2 {
145
+ font-size: 24px;
146
+ margin-bottom: 24px;
147
+ }
148
+
149
+ .form-group {
150
+ margin-bottom: 18px;
151
+ }
152
+
153
+ .form-group label {
154
+ font-size: 13px;
155
+ margin-bottom: 6px;
156
+ }
157
+
158
+ .form-group input {
159
+ font-size: 16px;
160
+ padding: 12px 14px;
161
+ }
162
+
163
+ .btn {
164
+ padding: 12px;
165
+ font-size: 16px;
166
+ }
167
+
168
+ .links {
169
+ margin-top: 20px;
170
+ font-size: 13px;
171
+ }
172
+
173
+ .links a {
174
+ margin: 0 8px;
175
+ }
176
+ }
177
+
178
+ @media (max-width: 480px) {
179
+ .card {
180
+ padding: 24px 16px;
181
+ }
182
+
183
+ .card h2 {
184
+ font-size: 22px;
185
+ margin-bottom: 20px;
186
+ }
187
+
188
+ .links a {
189
+ display: inline-block;
190
+ margin: 4px 6px;
191
+ }
192
+ }
130
193
  </style>
131
194
  </head>
132
195
 
@@ -153,77 +216,44 @@
153
216
  </div>
154
217
 
155
218
  <!-- 公共逻辑 -->
156
- <script src="/static/themeModule.js" defer></script><!-- 引入主题自适应模块 -->
157
- <script src="/static/mouseOrTouch.js" defer></script><!-- 引入鼠标或触摸操作 -->
158
- <script src="/static/themeImg.js" defer></script> <!-- 引入主题图标模块 -->
159
- <script src="/static/topImg.js" defer></script> <!-- 引入返回顶部图标模块 -->
160
- <script src="/static/utils/browser.js"></script> <!-- 引入@flun前端硬件验证模块 -->
219
+ <script src="/static/themeModule.js" defer></script>
220
+ <script src="/static/mouseOrTouch.js" defer></script>
221
+ <script src="/static/themeImg.js" defer></script>
222
+ <script src="/static/topImg.js" defer></script>
161
223
  <script>
162
- const [usernameEl, passwordEl, messageDiv, loginBtn] = ['username', 'password', 'message', 'loginBtn']
224
+ const [usernameEl, passwordEl, messageDiv, loginBtn,] = ['username', 'password', 'message', 'loginBtn',]
163
225
  .map(id => document.getElementById(id)),
164
226
  handleError = msg => {
165
227
  messageDiv.textContent = msg, loginBtn.disabled = false;
166
228
  };
167
229
 
230
+ // 登录主逻辑(统一跳转到2fa页面,携带对应验证方式)
168
231
  loginBtn.addEventListener('click', async () => {
169
232
  loginBtn.disabled = true, messageDiv.textContent = '';
233
+
170
234
  const [username, password] = [usernameEl, passwordEl].map(input => input.value.trim());
171
235
  if (!username || !password) return handleError('请输入用户名和密码');
172
-
173
236
  try {
174
- // 密码验证
175
237
  const response = await fetch('/api/login', {
176
- method: 'POST',
177
- headers: { 'Content-Type': 'application/json' },
238
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
178
239
  body: JSON.stringify({ username, password })
179
240
  }), data = await response.json();
180
241
 
181
242
  if (!response.ok) return handleError(data.message);
182
- // 硬件验证
183
- if (data.requireWebAuthn) {
184
- let platformAvailable = false;
185
- if (window.PublicKeyCredential) {
186
- try {
187
- platformAvailable = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
188
- } catch (err) { console.warn(err) };
189
- }
190
- if (!platformAvailable)
191
- return handleError('当前环境不支持硬件验证,请使用HTTPS或localhost访问,并确保浏览器支持指纹/人脸功能;');
192
- try {
193
- // 获取 WebAuthn 登录选项
194
- const beginRes = await fetch('/api/webauthn/login/begin', {
195
- method: 'POST',
196
- headers: { 'Content-Type': 'application/json' },
197
- body: JSON.stringify({ username })
198
- }), options = await beginRes.json();
199
- if (!beginRes.ok) return handleError(options.message);
200
-
201
- // 调用浏览器硬件验证
202
- const assertionResp = await flunWebAuthnBrowser.startAuthentication(options),
203
- completeRes = await fetch('/api/webauthn/login/complete', {
204
- method: 'POST',
205
- headers: { 'Content-Type': 'application/json' },
206
- body: JSON.stringify({ assertionResponse: assertionResp })
207
- }), completeData = await completeRes.json();
208
-
209
- if (completeRes.ok) window.location.href = '/';
210
- else return handleError(completeData.message);
211
- } catch (webauthnErr) {
212
- let msg = webauthnErr.message;
213
- if (webauthnErr.name === 'NotAllowedError') msg = '认证操作已取消';
214
- handleError(msg);
215
- }
216
- }
217
- else if (data.require2FA) window.location.href = '/2fa';
218
- else if (data.success) window.location.href = '/';
243
+ if (data.requireWebAuthn)
244
+ window.location.href = `/2fa?method=webauthn&username=${encodeURIComponent(username)}`;
245
+ else if (data.require2FA)
246
+ window.location.href = `/2fa?method=totp&username=${encodeURIComponent(username)}`;
247
+ else if (data.success) window.location.href = '/'; // 无二次验证,直接进入首页
219
248
  else handleError(data.message);
220
- } catch (err) { handleError('网络错误,请稍后重试') }
249
+ } catch (err) { handleError(err.message) }
221
250
  });
222
251
 
252
+ // 输入时清除错误消息
253
+ [usernameEl, passwordEl].forEach(el => el.addEventListener('input', () => messageDiv.textContent = ''));
223
254
  passwordEl.addEventListener('keypress', e => {
224
255
  if (e.key === 'Enter') loginBtn.click();
225
256
  });
226
- [usernameEl, passwordEl].forEach(el => el.addEventListener('input', () => messageDiv.textContent = ''));
227
257
  </script>
228
258
  </body>
229
259