@chenpu17/tcpmapping 2.1.1 → 2.4.1

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.
package/public/login.html CHANGED
@@ -4,7 +4,51 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>TCP转发服务器 - 登录</title>
7
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Crect width='24' height='24' rx='6' fill='%233b82f6'/%3E%3Cpath d='M6 12h3l2-5 3 10 2-5h2' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E">
8
+ <script src="/common.js"></script>
7
9
  <style>
10
+ :root {
11
+ color-scheme: light;
12
+ --bg-1: #eef2ff;
13
+ --bg-2: #e0f2fe;
14
+ --surface: #ffffff;
15
+ --surface-muted: #f8fafc;
16
+ --text: #0f172a;
17
+ --text-muted: #64748b;
18
+ --text-faint: #94a3b8;
19
+ --border: #e2e8f0;
20
+ --border-strong: #cbd5e1;
21
+ --primary: #6366f1;
22
+ --primary-soft: rgba(99, 102, 241, 0.14);
23
+ --green: #10b981;
24
+ --green-soft: rgba(16, 185, 129, 0.12);
25
+ --red: #ef4444;
26
+ --red-soft: rgba(239, 68, 68, 0.12);
27
+ --gradient: linear-gradient(135deg, #6366f1, #8b5cf6);
28
+ --shadow-lg: 0 30px 80px -24px rgba(15, 23, 42, 0.35);
29
+ --mono: 'SFMono-Regular', ui-monospace, 'Cascadia Code', Consolas, monospace;
30
+ }
31
+
32
+ [data-theme="dark"] {
33
+ color-scheme: dark;
34
+ --bg-1: #0b1120;
35
+ --bg-2: #111827;
36
+ --surface: #111827;
37
+ --surface-muted: #0f172a;
38
+ --text: #e2e8f0;
39
+ --text-muted: #94a3b8;
40
+ --text-faint: #64748b;
41
+ --border: #1e293b;
42
+ --border-strong: #334155;
43
+ --primary: #818cf8;
44
+ --primary-soft: rgba(129, 140, 248, 0.2);
45
+ --green: #34d399;
46
+ --green-soft: rgba(52, 211, 153, 0.15);
47
+ --red: #f87171;
48
+ --red-soft: rgba(248, 113, 113, 0.15);
49
+ --shadow-lg: 0 40px 90px -24px rgba(0, 0, 0, 0.75);
50
+ }
51
+
8
52
  * {
9
53
  margin: 0;
10
54
  padding: 0;
@@ -12,29 +56,48 @@
12
56
  }
13
57
 
14
58
  body {
15
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
16
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
59
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'PingFang SC', 'Microsoft YaHei', sans-serif;
60
+ background:
61
+ radial-gradient(700px 500px at 12% 12%, rgba(99, 102, 241, 0.28), transparent 62%),
62
+ radial-gradient(700px 500px at 88% 82%, rgba(14, 165, 233, 0.24), transparent 60%),
63
+ linear-gradient(135deg, var(--bg-1), var(--bg-2));
17
64
  min-height: 100vh;
18
65
  display: flex;
66
+ flex-direction: column;
19
67
  align-items: center;
20
68
  justify-content: center;
21
- padding: 1rem;
69
+ padding: 2rem 1rem;
70
+ color: var(--text);
71
+ transition: background-color 0.25s ease, color 0.25s ease;
72
+ }
73
+
74
+ svg {
75
+ display: block;
76
+ }
77
+
78
+ .page {
79
+ width: 100%;
80
+ max-width: 430px;
81
+ display: flex;
82
+ flex-direction: column;
83
+ align-items: center;
22
84
  }
23
85
 
24
86
  .login-container {
25
- background: white;
26
- padding: 2.5rem;
27
- border-radius: 20px;
28
- box-shadow: 0 25px 50px rgba(0, 0, 0, 0.25);
87
+ position: relative;
29
88
  width: 100%;
30
- max-width: 420px;
31
- animation: slideUp 0.5s ease;
89
+ background: var(--surface);
90
+ padding: 2.25rem 2rem 2rem;
91
+ border: 1px solid var(--border);
92
+ border-radius: 22px;
93
+ box-shadow: var(--shadow-lg);
94
+ animation: slideUp 0.45s ease;
32
95
  }
33
96
 
34
97
  @keyframes slideUp {
35
98
  from {
36
99
  opacity: 0;
37
- transform: translateY(30px);
100
+ transform: translateY(24px);
38
101
  }
39
102
  to {
40
103
  opacity: 1;
@@ -42,202 +105,462 @@
42
105
  }
43
106
  }
44
107
 
108
+ .top-controls {
109
+ position: absolute;
110
+ top: 1rem;
111
+ right: 1rem;
112
+ display: flex;
113
+ align-items: center;
114
+ gap: 0.4rem;
115
+ }
116
+
117
+ .icon-btn {
118
+ width: 34px;
119
+ height: 34px;
120
+ display: grid;
121
+ place-items: center;
122
+ border: 1px solid var(--border);
123
+ border-radius: 9px;
124
+ background: var(--surface-muted);
125
+ color: var(--text-muted);
126
+ cursor: pointer;
127
+ transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
128
+ }
129
+
130
+ .icon-btn:hover {
131
+ background: var(--surface);
132
+ color: var(--text);
133
+ border-color: var(--border-strong);
134
+ }
135
+
136
+ .icon-btn:focus-visible {
137
+ outline: 2px solid var(--primary);
138
+ outline-offset: 2px;
139
+ }
140
+
141
+ .icon-btn svg {
142
+ width: 16px;
143
+ height: 16px;
144
+ }
145
+
146
+ .icon-btn .icon-sun {
147
+ display: none;
148
+ }
149
+
150
+ [data-theme="dark"] .icon-btn .icon-moon {
151
+ display: none;
152
+ }
153
+
154
+ [data-theme="dark"] .icon-btn .icon-sun {
155
+ display: block;
156
+ }
157
+
158
+ .select-wrap {
159
+ position: relative;
160
+ display: flex;
161
+ align-items: center;
162
+ }
163
+
164
+ .select-wrap select {
165
+ appearance: none;
166
+ -webkit-appearance: none;
167
+ padding: 0.45rem 1.6rem 0.45rem 0.65rem;
168
+ border: 1px solid var(--border);
169
+ border-radius: 9px;
170
+ background: var(--surface-muted);
171
+ color: var(--text);
172
+ font-size: 0.75rem;
173
+ font-weight: 500;
174
+ font-family: inherit;
175
+ cursor: pointer;
176
+ }
177
+
178
+ .select-wrap select:hover {
179
+ border-color: var(--border-strong);
180
+ }
181
+
182
+ .select-wrap select:focus-visible {
183
+ outline: 2px solid var(--primary);
184
+ outline-offset: 2px;
185
+ }
186
+
187
+ .select-wrap::after {
188
+ content: '';
189
+ position: absolute;
190
+ right: 0.55rem;
191
+ width: 5px;
192
+ height: 5px;
193
+ border-right: 1.5px solid var(--text-muted);
194
+ border-bottom: 1.5px solid var(--text-muted);
195
+ transform: rotate(45deg) translateY(-1px);
196
+ pointer-events: none;
197
+ }
198
+
45
199
  .login-header {
46
200
  text-align: center;
47
- margin-bottom: 2rem;
201
+ margin: 1.5rem 0 1.75rem;
48
202
  }
49
203
 
50
204
  .login-header .logo {
51
- width: 70px;
52
- height: 70px;
53
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
54
- border-radius: 20px;
205
+ width: 64px;
206
+ height: 64px;
207
+ background: var(--gradient);
208
+ border-radius: 18px;
55
209
  display: flex;
56
210
  align-items: center;
57
211
  justify-content: center;
58
- margin: 0 auto 1.5rem;
59
- box-shadow: 0 10px 30px rgba(102, 126, 234, 0.4);
212
+ margin: 0 auto 1.1rem;
213
+ box-shadow: 0 16px 36px -12px rgba(99, 102, 241, 0.8);
60
214
  }
61
215
 
62
216
  .login-header .logo svg {
63
- width: 36px;
64
- height: 36px;
65
- color: white;
217
+ width: 32px;
218
+ height: 32px;
219
+ color: #ffffff;
66
220
  }
67
221
 
68
222
  .login-header h1 {
69
- color: #2d3436;
70
- font-size: 24px;
223
+ color: var(--text);
224
+ font-size: 1.4rem;
71
225
  font-weight: 700;
72
- margin-bottom: 0.5rem;
226
+ letter-spacing: -0.02em;
227
+ margin-bottom: 0.35rem;
73
228
  }
74
229
 
75
230
  .login-header p {
76
- color: #636e72;
77
- font-size: 14px;
231
+ color: var(--text-muted);
232
+ font-size: 0.85rem;
78
233
  }
79
234
 
80
235
  .form-group {
81
- margin-bottom: 1.5rem;
236
+ margin-bottom: 1.15rem;
82
237
  }
83
238
 
84
239
  .form-group label {
85
240
  display: block;
86
- margin-bottom: 0.5rem;
87
- color: #2d3436;
241
+ margin-bottom: 0.4rem;
242
+ color: var(--text);
88
243
  font-weight: 600;
89
- font-size: 14px;
244
+ font-size: 0.8rem;
245
+ }
246
+
247
+ .input-wrap {
248
+ position: relative;
249
+ display: flex;
250
+ align-items: center;
251
+ }
252
+
253
+ .input-wrap > svg {
254
+ position: absolute;
255
+ left: 0.9rem;
256
+ width: 17px;
257
+ height: 17px;
258
+ color: var(--text-faint);
259
+ pointer-events: none;
90
260
  }
91
261
 
92
262
  .form-group input {
93
263
  width: 100%;
94
- padding: 0.9rem 1rem;
95
- border: 2px solid #e9ecef;
264
+ padding: 0.8rem 2.6rem 0.8rem 2.6rem;
265
+ border: 1.5px solid var(--border);
96
266
  border-radius: 12px;
97
- font-size: 15px;
98
- transition: all 0.3s ease;
267
+ font-size: 0.9rem;
268
+ font-family: inherit;
269
+ background: var(--surface-muted);
270
+ color: var(--text);
271
+ transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
99
272
  }
100
273
 
101
274
  .form-group input:focus {
102
275
  outline: none;
103
- border-color: #667eea;
104
- box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.15);
276
+ border-color: var(--primary);
277
+ background: var(--surface);
278
+ box-shadow: 0 0 0 4px var(--primary-soft);
105
279
  }
106
280
 
107
281
  .form-group input::placeholder {
108
- color: #b2bec3;
282
+ color: var(--text-muted);
283
+ }
284
+
285
+ .toggle-password {
286
+ position: absolute;
287
+ right: 0.55rem;
288
+ width: 30px;
289
+ height: 30px;
290
+ display: grid;
291
+ place-items: center;
292
+ border: none;
293
+ border-radius: 7px;
294
+ background: transparent;
295
+ color: var(--text-faint);
296
+ cursor: pointer;
297
+ }
298
+
299
+ .toggle-password:hover {
300
+ background: var(--surface-muted);
301
+ color: var(--text);
302
+ }
303
+
304
+ .toggle-password:focus-visible {
305
+ outline: 2px solid var(--primary);
306
+ outline-offset: 1px;
307
+ }
308
+
309
+ .toggle-password svg {
310
+ width: 16px;
311
+ height: 16px;
312
+ }
313
+
314
+ .toggle-password .icon-eye-off {
315
+ display: none;
316
+ }
317
+
318
+ .toggle-password.is-visible .icon-eye {
319
+ display: none;
320
+ }
321
+
322
+ .toggle-password.is-visible .icon-eye-off {
323
+ display: block;
109
324
  }
110
325
 
111
326
  .login-btn {
112
327
  width: 100%;
113
- padding: 0.95rem;
114
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
115
- color: white;
328
+ padding: 0.85rem;
329
+ margin-top: 0.35rem;
330
+ background: var(--gradient);
331
+ color: #ffffff;
116
332
  border: none;
117
333
  border-radius: 12px;
118
- font-size: 16px;
119
- font-weight: 600;
334
+ font-size: 0.92rem;
335
+ font-weight: 650;
336
+ font-family: inherit;
120
337
  cursor: pointer;
121
- transition: all 0.3s ease;
122
- box-shadow: 0 10px 30px rgba(102, 126, 234, 0.4);
338
+ transition: transform 0.15s ease, box-shadow 0.2s ease, filter 0.2s ease;
339
+ box-shadow: 0 14px 30px -12px rgba(99, 102, 241, 0.85);
123
340
  }
124
341
 
125
342
  .login-btn:hover {
126
- transform: translateY(-3px);
127
- box-shadow: 0 15px 35px rgba(102, 126, 234, 0.5);
343
+ filter: brightness(1.07);
128
344
  }
129
345
 
130
346
  .login-btn:active {
131
- transform: translateY(-1px);
347
+ transform: translateY(1px);
348
+ }
349
+
350
+ .login-btn:focus-visible {
351
+ outline: 2px solid var(--primary);
352
+ outline-offset: 2px;
132
353
  }
133
354
 
134
355
  .login-btn:disabled {
135
- opacity: 0.7;
356
+ opacity: 0.65;
136
357
  cursor: not-allowed;
137
- transform: none;
358
+ filter: none;
138
359
  box-shadow: none;
139
360
  }
140
361
 
141
- .error-message {
142
- color: #ee5a5a;
143
- font-size: 14px;
362
+ .error-message,
363
+ .success-message {
364
+ font-size: 0.82rem;
144
365
  margin-top: 1rem;
145
- padding: 0.85rem 1rem;
146
- background: #fff5f5;
147
- border-radius: 10px;
366
+ padding: 0.7rem 0.9rem;
367
+ border-radius: 9px;
148
368
  display: none;
149
369
  text-align: center;
150
370
  }
151
371
 
372
+ .error-message {
373
+ color: var(--red);
374
+ background: var(--red-soft);
375
+ }
376
+
152
377
  .success-message {
153
- color: #2ed573;
154
- font-size: 14px;
155
- margin-top: 1rem;
156
- padding: 0.85rem 1rem;
157
- background: #f0fff4;
158
- border-radius: 10px;
159
- display: none;
160
- text-align: center;
378
+ color: var(--green);
379
+ background: var(--green-soft);
161
380
  }
162
381
 
163
382
  .help-info {
164
- background: linear-gradient(135deg, #f8f9fa 0%, #f1f3f4 100%);
383
+ background: var(--surface-muted);
384
+ border: 1px solid var(--border);
165
385
  border-radius: 12px;
166
- padding: 1rem 1.25rem;
167
- margin-top: 1.5rem;
168
- font-size: 13px;
169
- color: #636e72;
170
- line-height: 1.6;
386
+ padding: 0.9rem 1rem;
387
+ margin-top: 1.4rem;
388
+ font-size: 0.76rem;
389
+ color: var(--text-muted);
390
+ line-height: 1.7;
171
391
  }
172
392
 
173
393
  .help-info code {
174
- background: #e9ecef;
175
- padding: 0.2rem 0.5rem;
394
+ background: var(--surface);
395
+ border: 1px solid var(--border);
396
+ padding: 0.1rem 0.4rem;
176
397
  border-radius: 5px;
177
- font-family: 'SFMono-Regular', Consolas, monospace;
178
- font-size: 12px;
179
- color: #764ba2;
398
+ font-family: var(--mono);
399
+ font-size: 0.72rem;
400
+ color: var(--primary);
401
+ overflow-wrap: anywhere;
180
402
  }
181
403
 
182
404
  .footer {
183
405
  text-align: center;
184
- margin-top: 2rem;
185
- color: rgba(255,255,255,0.7);
186
- font-size: 13px;
406
+ margin-top: 1.5rem;
407
+ color: var(--text-muted);
408
+ font-size: 0.78rem;
187
409
  }
188
410
 
189
411
  .footer a {
190
- color: rgba(255,255,255,0.9);
412
+ color: var(--primary);
191
413
  text-decoration: none;
414
+ font-weight: 600;
192
415
  }
193
416
 
194
417
  .footer a:hover {
195
418
  text-decoration: underline;
196
419
  }
420
+
421
+ @media (prefers-reduced-motion: reduce) {
422
+ *,
423
+ *::before,
424
+ *::after {
425
+ animation-duration: 0.01ms !important;
426
+ transition-duration: 0.01ms !important;
427
+ }
428
+ }
429
+
430
+ @media (max-width: 480px) {
431
+ .login-container {
432
+ padding: 1.75rem 1.25rem 1.5rem;
433
+ }
434
+
435
+ .login-header {
436
+ margin-top: 2rem;
437
+ }
438
+ }
197
439
  </style>
198
440
  </head>
199
441
  <body>
200
- <div>
442
+ <div class="page">
201
443
  <div class="login-container">
444
+ <div class="top-controls">
445
+ <div class="select-wrap">
446
+ <select id="languageSelect" data-i18n-aria-label="common.language">
447
+ <option value="zh-CN">中文</option>
448
+ <option value="en-US">English</option>
449
+ </select>
450
+ </div>
451
+ <button type="button" class="icon-btn" data-theme-toggle data-i18n-title="common.theme" data-i18n-aria-label="common.theme">
452
+ <svg class="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
453
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
454
+ </svg>
455
+ <svg class="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
456
+ <circle cx="12" cy="12" r="4"/>
457
+ <path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
458
+ </svg>
459
+ </button>
460
+ </div>
461
+
202
462
  <div class="login-header">
203
463
  <div class="logo">
204
464
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
205
465
  <path d="M22 12h-4l-3 9L9 3l-3 9H2"/>
206
466
  </svg>
207
467
  </div>
208
- <h1>TCP转发服务器</h1>
209
- <p>管理界面登录</p>
468
+ <h1 data-i18n="brand.name">TCP 转发服务器</h1>
469
+ <p data-i18n="login.subtitle">管理界面登录</p>
210
470
  </div>
211
471
 
212
472
  <form id="loginForm">
213
473
  <div class="form-group">
214
- <label for="username">用户名</label>
215
- <input type="text" id="username" name="username" placeholder="请输入用户名" required autocomplete="username">
474
+ <label for="username" data-i18n="login.username">用户名</label>
475
+ <div class="input-wrap">
476
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
477
+ <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
478
+ <circle cx="12" cy="7" r="4"/>
479
+ </svg>
480
+ <input type="text" id="username" name="username" data-i18n-placeholder="login.usernamePlaceholder" placeholder="请输入用户名" required autocomplete="username" autofocus>
481
+ </div>
216
482
  </div>
217
483
 
218
484
  <div class="form-group">
219
- <label for="password">密码</label>
220
- <input type="password" id="password" name="password" placeholder="请输入密码" required autocomplete="current-password">
485
+ <label for="password" data-i18n="login.password">密码</label>
486
+ <div class="input-wrap">
487
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
488
+ <rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
489
+ <path d="M7 11V7a5 5 0 0 1 10 0v4"/>
490
+ </svg>
491
+ <input type="password" id="password" name="password" data-i18n-placeholder="login.passwordPlaceholder" placeholder="请输入密码" required autocomplete="current-password">
492
+ <button type="button" class="toggle-password" id="togglePassword" data-i18n-title="login.showPassword" data-i18n-aria-label="login.showPassword">
493
+ <svg class="icon-eye" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
494
+ <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
495
+ <circle cx="12" cy="12" r="3"/>
496
+ </svg>
497
+ <svg class="icon-eye-off" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
498
+ <path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/>
499
+ <line x1="1" y1="1" x2="23" y2="23"/>
500
+ </svg>
501
+ </button>
502
+ </div>
221
503
  </div>
222
504
 
223
- <button type="submit" class="login-btn" id="loginBtn">登录</button>
505
+ <button type="submit" class="login-btn" id="loginBtn" data-i18n="login.submit">登录</button>
224
506
 
225
507
  <div class="error-message" id="errorMessage"></div>
226
508
  <div class="success-message" id="successMessage"></div>
227
509
  </form>
228
510
 
229
- <div class="help-info">
511
+ <div class="help-info" data-i18n-html="login.forgot">
230
512
  忘记密码?运行 <code>tcpmapping setup-password</code> 或 <code>npm run reset-password</code> 重置
231
513
  </div>
232
514
  </div>
233
515
 
234
516
  <div class="footer">
235
- <a href="https://github.com/chenpu17/TcpMapping" target="_blank">TcpMapping</a> - TCP端口转发服务器
517
+ <a href="https://github.com/chenpu17/TcpMapping" target="_blank" rel="noopener noreferrer">TcpMapping</a> ·
518
+ <span data-i18n="login.footer">TCP 端口转发服务器</span>
236
519
  </div>
237
520
  </div>
238
521
 
239
522
  <script>
240
- document.getElementById('loginForm').addEventListener('submit', async (e) => {
523
+ function t(key, params) {
524
+ return window.I18n ? window.I18n.t(key, params) : key;
525
+ }
526
+
527
+ let loginPending = false;
528
+
529
+ document.addEventListener('DOMContentLoaded', function() {
530
+ document.title = t('page.login');
531
+
532
+ const toggleButton = document.getElementById('togglePassword');
533
+ const passwordInput = document.getElementById('password');
534
+
535
+ toggleButton.addEventListener('click', function() {
536
+ const visible = passwordInput.type === 'text';
537
+ passwordInput.type = visible ? 'password' : 'text';
538
+ toggleButton.classList.toggle('is-visible', !visible);
539
+
540
+ const label = t(visible ? 'login.showPassword' : 'login.hidePassword');
541
+ toggleButton.setAttribute('title', label);
542
+ toggleButton.setAttribute('aria-label', label);
543
+ passwordInput.focus();
544
+ });
545
+ });
546
+
547
+ document.addEventListener('languagechange', function() {
548
+ document.title = t('page.login');
549
+
550
+ if (loginPending) {
551
+ document.getElementById('loginBtn').textContent = t('login.submitting');
552
+ }
553
+
554
+ const toggleButton = document.getElementById('togglePassword');
555
+ const passwordInput = document.getElementById('password');
556
+ if (toggleButton && passwordInput) {
557
+ const label = t(passwordInput.type === 'text' ? 'login.hidePassword' : 'login.showPassword');
558
+ toggleButton.setAttribute('title', label);
559
+ toggleButton.setAttribute('aria-label', label);
560
+ }
561
+ });
562
+
563
+ document.getElementById('loginForm').addEventListener('submit', async function(e) {
241
564
  e.preventDefault();
242
565
 
243
566
  const loginBtn = document.getElementById('loginBtn');
@@ -246,50 +569,50 @@
246
569
  const username = document.getElementById('username').value;
247
570
  const password = document.getElementById('password').value;
248
571
 
249
- // 重置消息
250
572
  errorMessage.style.display = 'none';
251
573
  successMessage.style.display = 'none';
252
574
 
253
- // 禁用按钮
575
+ loginPending = true;
254
576
  loginBtn.disabled = true;
255
- loginBtn.textContent = '登录中...';
577
+ loginBtn.textContent = t('login.submitting');
256
578
 
257
579
  try {
258
580
  const response = await fetch('/api/login', {
259
581
  method: 'POST',
260
582
  headers: {
261
583
  'Content-Type': 'application/json',
584
+ 'X-Language': window.I18n ? window.I18n.getLanguage() : 'zh-CN'
262
585
  },
586
+ credentials: 'same-origin',
263
587
  body: JSON.stringify({ username, password })
264
588
  });
265
589
 
266
- const data = await response.json();
590
+ let data;
591
+ try {
592
+ data = await response.json();
593
+ } catch (error) {
594
+ data = { success: false, message: t('common.invalidResponse') };
595
+ }
267
596
 
268
597
  if (data.success) {
269
- successMessage.textContent = '登录成功,正在跳转...';
598
+ successMessage.textContent = t('login.success');
270
599
  successMessage.style.display = 'block';
271
600
 
272
601
  setTimeout(() => {
273
602
  window.location.href = '/dashboard';
274
- }, 800);
275
- } else {
276
- errorMessage.textContent = data.message;
277
- errorMessage.style.display = 'block';
603
+ }, 700);
604
+ return;
278
605
  }
606
+
607
+ errorMessage.textContent = data.message || t('login.failedNetwork');
608
+ errorMessage.style.display = 'block';
279
609
  } catch (error) {
280
- errorMessage.textContent = '登录失败,请检查网络连接';
610
+ errorMessage.textContent = t('login.failedNetwork');
281
611
  errorMessage.style.display = 'block';
282
612
  } finally {
283
- // 恢复按钮
613
+ loginPending = false;
284
614
  loginBtn.disabled = false;
285
- loginBtn.textContent = '登录';
286
- }
287
- });
288
-
289
- // 回车提交
290
- document.addEventListener('keypress', (e) => {
291
- if (e.key === 'Enter') {
292
- document.getElementById('loginForm').dispatchEvent(new Event('submit'));
615
+ loginBtn.textContent = t('login.submit');
293
616
  }
294
617
  });
295
618
  </script>