@chenpu17/tcpmapping 2.0.0 → 2.0.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.
@@ -13,22 +13,24 @@
13
13
 
14
14
  body {
15
15
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
16
- background: #f5f5f5;
16
+ background: linear-gradient(135deg, #f5f7fa 0%, #e4e8ec 100%);
17
17
  min-height: 100vh;
18
18
  }
19
19
 
20
20
  .header {
21
- background: white;
21
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
22
22
  padding: 1rem 2rem;
23
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
23
+ box-shadow: 0 4px 20px rgba(102, 126, 234, 0.3);
24
24
  display: flex;
25
25
  justify-content: space-between;
26
26
  align-items: center;
27
27
  }
28
28
 
29
29
  .header h1 {
30
- color: #333;
31
- font-size: 24px;
30
+ color: white;
31
+ font-size: 22px;
32
+ font-weight: 600;
33
+ text-shadow: 0 2px 4px rgba(0,0,0,0.1);
32
34
  }
33
35
 
34
36
  .header-actions {
@@ -37,95 +39,152 @@
37
39
  align-items: center;
38
40
  }
39
41
 
42
+ .header-actions span {
43
+ color: rgba(255,255,255,0.9);
44
+ font-size: 14px;
45
+ }
46
+
40
47
  .btn {
41
- padding: 0.5rem 1rem;
48
+ padding: 0.6rem 1.2rem;
42
49
  border: none;
43
- border-radius: 5px;
50
+ border-radius: 6px;
44
51
  cursor: pointer;
45
52
  font-size: 14px;
46
- transition: all 0.2s;
53
+ font-weight: 500;
54
+ transition: all 0.3s ease;
55
+ display: inline-flex;
56
+ align-items: center;
57
+ gap: 6px;
47
58
  }
48
59
 
49
60
  .btn-primary {
50
- background: #007bff;
61
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
51
62
  color: white;
63
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
52
64
  }
53
65
 
54
66
  .btn-primary:hover {
55
- background: #0056b3;
67
+ transform: translateY(-2px);
68
+ box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5);
56
69
  }
57
70
 
58
71
  .btn-danger {
59
- background: #dc3545;
72
+ background: linear-gradient(135deg, #ff6b6b 0%, #ee5a5a 100%);
60
73
  color: white;
74
+ box-shadow: 0 4px 15px rgba(255, 107, 107, 0.3);
61
75
  }
62
76
 
63
77
  .btn-danger:hover {
64
- background: #c82333;
78
+ transform: translateY(-2px);
79
+ box-shadow: 0 6px 20px rgba(255, 107, 107, 0.4);
65
80
  }
66
81
 
67
82
  .btn-success {
68
- background: #28a745;
83
+ background: linear-gradient(135deg, #2ed573 0%, #26de81 100%);
69
84
  color: white;
85
+ box-shadow: 0 4px 15px rgba(46, 213, 115, 0.3);
70
86
  }
71
87
 
72
88
  .btn-success:hover {
73
- background: #218838;
89
+ transform: translateY(-2px);
90
+ box-shadow: 0 6px 20px rgba(46, 213, 115, 0.4);
91
+ }
92
+
93
+ .btn-warning {
94
+ background: linear-gradient(135deg, #ffa502 0%, #ff9f43 100%);
95
+ color: white;
96
+ box-shadow: 0 4px 15px rgba(255, 165, 2, 0.3);
97
+ }
98
+
99
+ .btn-warning:hover {
100
+ transform: translateY(-2px);
101
+ box-shadow: 0 6px 20px rgba(255, 165, 2, 0.4);
102
+ }
103
+
104
+ .btn-sm {
105
+ padding: 0.4rem 0.8rem;
106
+ font-size: 12px;
74
107
  }
75
108
 
76
109
  .container {
77
- max-width: 1200px;
110
+ max-width: 1300px;
78
111
  margin: 0 auto;
79
112
  padding: 2rem;
80
113
  }
81
114
 
82
115
  .card {
83
116
  background: white;
84
- border-radius: 8px;
85
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
117
+ border-radius: 16px;
118
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.08);
86
119
  margin-bottom: 2rem;
120
+ overflow: hidden;
87
121
  }
88
122
 
89
123
  .card-header {
90
- padding: 1.5rem;
91
- border-bottom: 1px solid #eee;
124
+ padding: 1.5rem 2rem;
125
+ border-bottom: 1px solid #f0f0f0;
92
126
  display: flex;
93
127
  justify-content: space-between;
94
128
  align-items: center;
129
+ background: linear-gradient(135deg, #fafbfc 0%, #f8f9fa 100%);
95
130
  }
96
131
 
97
132
  .card-header h2 {
98
- color: #333;
99
- font-size: 20px;
133
+ color: #2d3436;
134
+ font-size: 18px;
135
+ font-weight: 600;
100
136
  }
101
137
 
102
138
  .card-body {
103
- padding: 1.5rem;
139
+ padding: 1.5rem 2rem;
104
140
  }
105
141
 
106
142
  .stats-grid {
107
143
  display: grid;
108
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
109
- gap: 1rem;
110
- margin-bottom: 2rem;
144
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
145
+ gap: 1.5rem;
146
+ margin-bottom: 0;
111
147
  }
112
148
 
113
149
  .stat-card {
114
150
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
115
151
  color: white;
116
- padding: 1.5rem;
117
- border-radius: 8px;
152
+ padding: 1.8rem;
153
+ border-radius: 16px;
118
154
  text-align: center;
155
+ box-shadow: 0 10px 30px rgba(102, 126, 234, 0.3);
156
+ transition: transform 0.3s ease;
157
+ }
158
+
159
+ .stat-card:hover {
160
+ transform: translateY(-5px);
161
+ }
162
+
163
+ .stat-card:nth-child(2) {
164
+ background: linear-gradient(135deg, #2ed573 0%, #26de81 100%);
165
+ box-shadow: 0 10px 30px rgba(46, 213, 115, 0.3);
166
+ }
167
+
168
+ .stat-card:nth-child(3) {
169
+ background: linear-gradient(135deg, #ffa502 0%, #ff9f43 100%);
170
+ box-shadow: 0 10px 30px rgba(255, 165, 2, 0.3);
171
+ }
172
+
173
+ .stat-card:nth-child(4) {
174
+ background: linear-gradient(135deg, #45aaf2 0%, #2d98da 100%);
175
+ box-shadow: 0 10px 30px rgba(69, 170, 242, 0.3);
119
176
  }
120
177
 
121
178
  .stat-card h3 {
122
- font-size: 32px;
179
+ font-size: 36px;
123
180
  margin-bottom: 0.5rem;
181
+ font-weight: 700;
124
182
  }
125
183
 
126
184
  .stat-card p {
127
185
  font-size: 14px;
128
186
  opacity: 0.9;
187
+ font-weight: 500;
129
188
  }
130
189
 
131
190
  .table {
@@ -135,39 +194,67 @@
135
194
 
136
195
  .table th,
137
196
  .table td {
138
- padding: 0.75rem;
197
+ padding: 1rem;
139
198
  text-align: left;
140
- border-bottom: 1px solid #eee;
199
+ border-bottom: 1px solid #f0f0f0;
141
200
  }
142
201
 
143
202
  .table th {
144
- background: #f8f9fa;
203
+ background: linear-gradient(135deg, #f8f9fa 0%, #f1f3f4 100%);
145
204
  font-weight: 600;
146
- color: #333;
205
+ color: #2d3436;
206
+ font-size: 13px;
207
+ text-transform: uppercase;
208
+ letter-spacing: 0.5px;
209
+ }
210
+
211
+ .table tr:hover {
212
+ background-color: #fafbfc;
213
+ }
214
+
215
+ .table td {
216
+ color: #636e72;
217
+ font-size: 14px;
147
218
  }
148
219
 
149
220
  .status-badge {
150
- padding: 0.25rem 0.5rem;
151
- border-radius: 12px;
221
+ padding: 0.35rem 0.75rem;
222
+ border-radius: 20px;
152
223
  font-size: 12px;
153
- font-weight: 500;
224
+ font-weight: 600;
225
+ display: inline-block;
154
226
  }
155
227
 
156
228
  .status-running {
157
- background: #d4edda;
229
+ background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%);
158
230
  color: #155724;
159
231
  }
160
232
 
161
233
  .status-stopped {
162
- background: #f8d7da;
234
+ background: linear-gradient(135deg, #f8d7da 0%, #f5c6cb 100%);
163
235
  color: #721c24;
164
236
  }
165
237
 
166
238
  .status-connected {
167
- background: #d1ecf1;
239
+ background: linear-gradient(135deg, #d1ecf1 0%, #bee5eb 100%);
168
240
  color: #0c5460;
169
241
  }
170
242
 
243
+ .traffic-info {
244
+ font-size: 12px;
245
+ color: #636e72;
246
+ line-height: 1.6;
247
+ }
248
+
249
+ .traffic-info .up {
250
+ color: #2ed573;
251
+ }
252
+
253
+ .traffic-info .down {
254
+ color: #45aaf2;
255
+ }
256
+
257
+ /* Modal Styles */
171
258
  .modal {
172
259
  display: none;
173
260
  position: fixed;
@@ -175,87 +262,232 @@
175
262
  left: 0;
176
263
  width: 100%;
177
264
  height: 100%;
178
- background: rgba(0, 0, 0, 0.5);
265
+ background: rgba(0, 0, 0, 0.6);
266
+ backdrop-filter: blur(4px);
179
267
  z-index: 1000;
268
+ animation: fadeIn 0.3s ease;
269
+ }
270
+
271
+ @keyframes fadeIn {
272
+ from { opacity: 0; }
273
+ to { opacity: 1; }
274
+ }
275
+
276
+ @keyframes slideIn {
277
+ from {
278
+ opacity: 0;
279
+ transform: translateY(-30px) scale(0.95);
280
+ }
281
+ to {
282
+ opacity: 1;
283
+ transform: translateY(0) scale(1);
284
+ }
180
285
  }
181
286
 
182
287
  .modal-content {
183
288
  background: white;
184
- margin: 10% auto;
185
- padding: 2rem;
186
- border-radius: 8px;
187
- max-width: 500px;
289
+ margin: 8% auto;
290
+ padding: 0;
291
+ border-radius: 20px;
292
+ max-width: 480px;
188
293
  width: 90%;
294
+ box-shadow: 0 25px 50px rgba(0, 0, 0, 0.25);
295
+ animation: slideIn 0.3s ease;
296
+ overflow: hidden;
189
297
  }
190
298
 
191
299
  .modal-header {
192
300
  display: flex;
193
301
  justify-content: space-between;
194
302
  align-items: center;
195
- margin-bottom: 1rem;
303
+ padding: 1.5rem 2rem;
304
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
305
+ color: white;
196
306
  }
197
307
 
198
308
  .modal-header h3 {
199
- color: #333;
309
+ font-size: 18px;
310
+ font-weight: 600;
200
311
  }
201
312
 
202
313
  .close {
203
- font-size: 24px;
314
+ font-size: 28px;
204
315
  cursor: pointer;
205
- color: #666;
316
+ color: rgba(255,255,255,0.8);
317
+ transition: color 0.2s;
318
+ line-height: 1;
206
319
  }
207
320
 
208
321
  .close:hover {
209
- color: #333;
322
+ color: white;
323
+ }
324
+
325
+ .modal-body {
326
+ padding: 2rem;
210
327
  }
211
328
 
212
329
  .form-group {
213
- margin-bottom: 1rem;
330
+ margin-bottom: 1.5rem;
214
331
  }
215
332
 
216
333
  .form-group label {
217
334
  display: block;
218
335
  margin-bottom: 0.5rem;
219
- color: #333;
220
- font-weight: 500;
336
+ color: #2d3436;
337
+ font-weight: 600;
338
+ font-size: 14px;
221
339
  }
222
340
 
223
341
  .form-group input {
224
342
  width: 100%;
225
- padding: 0.5rem;
226
- border: 1px solid #ddd;
227
- border-radius: 4px;
343
+ padding: 0.85rem 1rem;
344
+ border: 2px solid #e9ecef;
345
+ border-radius: 10px;
228
346
  font-size: 14px;
347
+ transition: all 0.3s ease;
229
348
  }
230
349
 
231
350
  .form-group input:focus {
232
351
  outline: none;
233
- border-color: #007bff;
352
+ border-color: #667eea;
353
+ box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);
354
+ }
355
+
356
+ .form-group input::placeholder {
357
+ color: #b2bec3;
358
+ }
359
+
360
+ .modal-footer {
361
+ padding: 0 2rem 2rem 2rem;
362
+ display: flex;
363
+ gap: 1rem;
364
+ }
365
+
366
+ .modal-footer .btn {
367
+ flex: 1;
368
+ justify-content: center;
369
+ padding: 0.85rem 1.5rem;
370
+ }
371
+
372
+ .btn-cancel {
373
+ background: #f1f3f4;
374
+ color: #636e72;
375
+ }
376
+
377
+ .btn-cancel:hover {
378
+ background: #e9ecef;
234
379
  }
235
380
 
236
381
  .error-message {
237
- color: #dc3545;
382
+ color: #ee5a5a;
238
383
  font-size: 14px;
239
- margin-top: 0.5rem;
384
+ margin-top: 1rem;
385
+ padding: 0.75rem 1rem;
386
+ background: #fff5f5;
387
+ border-radius: 8px;
240
388
  display: none;
241
389
  }
242
390
 
243
391
  .success-message {
244
- color: #28a745;
392
+ color: #2ed573;
245
393
  font-size: 14px;
246
- margin-top: 0.5rem;
394
+ margin-top: 1rem;
395
+ padding: 0.75rem 1rem;
396
+ background: #f0fff4;
397
+ border-radius: 8px;
247
398
  display: none;
248
399
  }
249
400
 
250
- .connection-details {
251
- font-size: 12px;
252
- color: #666;
253
- margin-top: 0.5rem;
401
+ .connection-card {
402
+ margin-bottom: 1rem;
403
+ padding: 1.25rem;
404
+ border: 1px solid #e9ecef;
405
+ border-radius: 12px;
406
+ background: linear-gradient(135deg, #fafbfc 0%, #f8f9fa 100%);
407
+ transition: all 0.3s ease;
254
408
  }
255
409
 
256
- .refresh-btn {
410
+ .connection-card:hover {
411
+ border-color: #667eea;
412
+ box-shadow: 0 5px 20px rgba(102, 126, 234, 0.1);
413
+ }
414
+
415
+ .connection-card h4 {
416
+ color: #2d3436;
417
+ font-size: 15px;
418
+ margin-bottom: 0.75rem;
419
+ display: flex;
420
+ align-items: center;
421
+ gap: 8px;
422
+ }
423
+
424
+ .connection-card h4 .port-badge {
425
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
426
+ color: white;
427
+ padding: 0.25rem 0.6rem;
428
+ border-radius: 6px;
257
429
  font-size: 12px;
258
- padding: 0.25rem 0.5rem;
430
+ }
431
+
432
+ .connection-details {
433
+ font-size: 13px;
434
+ color: #636e72;
435
+ }
436
+
437
+ .connection-item {
438
+ padding: 0.5rem 0;
439
+ border-bottom: 1px dashed #e9ecef;
440
+ display: flex;
441
+ align-items: center;
442
+ gap: 8px;
443
+ }
444
+
445
+ .connection-item:last-child {
446
+ border-bottom: none;
447
+ }
448
+
449
+ .no-connections {
450
+ text-align: center;
451
+ padding: 3rem;
452
+ color: #b2bec3;
453
+ }
454
+
455
+ .no-connections svg {
456
+ width: 60px;
457
+ height: 60px;
458
+ margin-bottom: 1rem;
459
+ opacity: 0.5;
460
+ }
461
+
462
+ .action-buttons {
463
+ display: flex;
464
+ gap: 8px;
465
+ }
466
+
467
+ @media (max-width: 768px) {
468
+ .header {
469
+ flex-direction: column;
470
+ gap: 1rem;
471
+ text-align: center;
472
+ }
473
+
474
+ .container {
475
+ padding: 1rem;
476
+ }
477
+
478
+ .card-header {
479
+ flex-direction: column;
480
+ gap: 1rem;
481
+ }
482
+
483
+ .table {
484
+ display: block;
485
+ overflow-x: auto;
486
+ }
487
+
488
+ .stats-grid {
489
+ grid-template-columns: repeat(2, 1fr);
490
+ }
259
491
  }
260
492
  </style>
261
493
  </head>
@@ -264,7 +496,7 @@
264
496
  <h1>TCP转发服务器管理面板</h1>
265
497
  <div class="header-actions">
266
498
  <span>欢迎,管理员</span>
267
- <button class="btn btn-danger" onclick="logout()">退出登录</button>
499
+ <button class="btn btn-danger btn-sm" onclick="logout()">退出登录</button>
268
500
  </div>
269
501
  </div>
270
502
 
@@ -273,7 +505,7 @@
273
505
  <div class="card">
274
506
  <div class="card-header">
275
507
  <h2>系统状态</h2>
276
- <button class="btn btn-primary refresh-btn" onclick="refreshStatus()">刷新</button>
508
+ <button class="btn btn-primary btn-sm" onclick="refreshStatus()">刷新</button>
277
509
  </div>
278
510
  <div class="card-body">
279
511
  <div class="stats-grid" id="statsGrid">
@@ -286,7 +518,7 @@
286
518
  <div class="card">
287
519
  <div class="card-header">
288
520
  <h2>端口映射管理</h2>
289
- <button class="btn btn-success" onclick="showAddMappingModal()">添加映射</button>
521
+ <button class="btn btn-success" onclick="showAddMappingModal()">+ 添加映射</button>
290
522
  </div>
291
523
  <div class="card-body">
292
524
  <table class="table">
@@ -312,7 +544,7 @@
312
544
  <div class="card">
313
545
  <div class="card-header">
314
546
  <h2>连接状态监控</h2>
315
- <button class="btn btn-primary refresh-btn" onclick="refreshConnections()">刷新</button>
547
+ <button class="btn btn-primary btn-sm" onclick="refreshConnections()">刷新</button>
316
548
  </div>
317
549
  <div class="card-body">
318
550
  <div id="connectionsDisplay">
@@ -330,25 +562,30 @@
330
562
  <span class="close" onclick="closeModal()">&times;</span>
331
563
  </div>
332
564
  <form id="addMappingForm">
333
- <div class="form-group">
334
- <label for="mappingName">映射名称</label>
335
- <input type="text" id="mappingName" name="name" required>
336
- </div>
337
- <div class="form-group">
338
- <label for="listenPort">监听端口</label>
339
- <input type="number" id="listenPort" name="listenPort" min="1" max="65535" required>
340
- </div>
341
- <div class="form-group">
342
- <label for="targetHost">目标主机</label>
343
- <input type="text" id="targetHost" name="targetHost" required>
565
+ <div class="modal-body">
566
+ <div class="form-group">
567
+ <label for="mappingName">映射名称</label>
568
+ <input type="text" id="mappingName" name="name" placeholder="例如:Web服务" required>
569
+ </div>
570
+ <div class="form-group">
571
+ <label for="listenPort">监听端口</label>
572
+ <input type="number" id="listenPort" name="listenPort" min="1" max="65535" placeholder="1-65535" required>
573
+ </div>
574
+ <div class="form-group">
575
+ <label for="targetHost">目标主机</label>
576
+ <input type="text" id="targetHost" name="targetHost" placeholder="IP地址或域名" required>
577
+ </div>
578
+ <div class="form-group">
579
+ <label for="targetPort">目标端口</label>
580
+ <input type="number" id="targetPort" name="targetPort" min="1" max="65535" placeholder="1-65535" required>
581
+ </div>
582
+ <div class="error-message" id="modalErrorMessage"></div>
583
+ <div class="success-message" id="modalSuccessMessage"></div>
344
584
  </div>
345
- <div class="form-group">
346
- <label for="targetPort">目标端口</label>
347
- <input type="number" id="targetPort" name="targetPort" min="1" max="65535" required>
585
+ <div class="modal-footer">
586
+ <button type="button" class="btn btn-cancel" onclick="closeModal()">取消</button>
587
+ <button type="submit" class="btn btn-success">确认添加</button>
348
588
  </div>
349
- <button type="submit" class="btn btn-success">添加映射</button>
350
- <div class="error-message" id="modalErrorMessage"></div>
351
- <div class="success-message" id="modalSuccessMessage"></div>
352
589
  </form>
353
590
  </div>
354
591
  </div>
@@ -363,7 +600,7 @@
363
600
  loadMappings();
364
601
  loadConnections();
365
602
  loadStatus();
366
-
603
+
367
604
  // 每5秒自动刷新连接状态
368
605
  setInterval(() => {
369
606
  loadConnections();
@@ -376,7 +613,7 @@
376
613
  try {
377
614
  const response = await fetch('/api/mappings');
378
615
  const data = await response.json();
379
-
616
+
380
617
  if (data.success) {
381
618
  mappings = data.data;
382
619
  renderMappings();
@@ -391,7 +628,7 @@
391
628
  try {
392
629
  const response = await fetch('/api/connections');
393
630
  const data = await response.json();
394
-
631
+
395
632
  if (data.success) {
396
633
  connections = data.data;
397
634
  renderConnections();
@@ -406,7 +643,7 @@
406
643
  try {
407
644
  const response = await fetch('/api/status');
408
645
  const data = await response.json();
409
-
646
+
410
647
  if (data.success) {
411
648
  status = data.data;
412
649
  renderStatus();
@@ -430,17 +667,24 @@
430
667
  const tbody = document.getElementById('mappingsTable');
431
668
  tbody.innerHTML = '';
432
669
 
670
+ if (mappings.length === 0) {
671
+ tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;color:#b2bec3;padding:3rem;">暂无端口映射,点击上方按钮添加</td></tr>';
672
+ return;
673
+ }
674
+
433
675
  mappings.forEach((mapping, index) => {
434
676
  const row = document.createElement('tr');
435
677
  const identifier = mapping.id || index;
436
678
  row.innerHTML = `
437
- <td>${mapping.name}</td>
438
- <td>${mapping.listen.host}:${mapping.listen.port}</td>
439
- <td>${mapping.target.host}:${mapping.target.port}</td>
679
+ <td><strong>${mapping.name}</strong></td>
680
+ <td><code>${mapping.listen.host}:${mapping.listen.port}</code></td>
681
+ <td><code>${mapping.target.host}:${mapping.target.port}</code></td>
440
682
  <td><span class="status-badge status-connected">${mapping.connectionCount || 0}</span></td>
441
683
  <td>
442
- <small>↑ ${formatBytes(mapping.bytesReceived || 0)}</small><br>
443
- <small>↓ ${formatBytes(mapping.bytesSent || 0)}</small>
684
+ <div class="traffic-info">
685
+ <span class="up">↑ ${formatBytes(mapping.bytesReceived || 0)}</span><br>
686
+ <span class="down">↓ ${formatBytes(mapping.bytesSent || 0)}</span>
687
+ </div>
444
688
  </td>
445
689
  <td>
446
690
  <span class="status-badge ${mapping.status === 'running' ? 'status-running' : 'status-stopped'}">
@@ -448,11 +692,13 @@
448
692
  </span>
449
693
  </td>
450
694
  <td>
451
- ${mapping.status === 'running'
452
- ? `<button class="btn btn-primary" style="margin-right:5px" onclick="stopMapping('${identifier}')">停止</button>`
453
- : `<button class="btn btn-success" style="margin-right:5px" onclick="startMapping('${identifier}')">启动</button>`
454
- }
455
- <button class="btn btn-danger" onclick="deleteMapping('${identifier}')">删除</button>
695
+ <div class="action-buttons">
696
+ ${mapping.status === 'running'
697
+ ? `<button class="btn btn-warning btn-sm" onclick="stopMapping('${identifier}')">停止</button>`
698
+ : `<button class="btn btn-success btn-sm" onclick="startMapping('${identifier}')">启动</button>`
699
+ }
700
+ <button class="btn btn-danger btn-sm" onclick="deleteMapping('${identifier}')">删除</button>
701
+ </div>
456
702
  </td>
457
703
  `;
458
704
  tbody.appendChild(row);
@@ -463,39 +709,53 @@
463
709
  function renderConnections() {
464
710
  const container = document.getElementById('connectionsDisplay');
465
711
  container.innerHTML = '';
466
-
712
+
467
713
  if (connections.byPort && connections.byPort.length > 0) {
468
714
  connections.byPort.forEach(portStat => {
469
715
  const portDiv = document.createElement('div');
470
- portDiv.style.marginBottom = '1rem';
471
- portDiv.style.padding = '1rem';
472
- portDiv.style.border = '1px solid #eee';
473
- portDiv.style.borderRadius = '5px';
474
-
716
+ portDiv.className = 'connection-card';
717
+
475
718
  portDiv.innerHTML = `
476
- <h4>端口 ${portStat.port} (${portStat.mappingName})</h4>
477
- <p>总连接数: ${portStat.totalConnections}</p>
719
+ <h4><span class="port-badge">端口 ${portStat.port}</span> ${portStat.mappingName}</h4>
720
+ <p style="margin-bottom:0.75rem;color:#636e72;">活跃连接: <strong>${portStat.totalConnections}</strong></p>
478
721
  <div class="connection-details">
479
722
  ${portStat.connections.map(conn => `
480
- <div>
723
+ <div class="connection-item">
481
724
  <span class="status-badge status-connected">${conn.status}</span>
482
- ${conn.clientAddress} → ${conn.targetAddress}
483
- (上行: ${(conn.bytesReceived / 1024).toFixed(2)} KB, 下行: ${(conn.bytesSent / 1024).toFixed(2)} KB)
725
+ <span>${conn.clientAddress} → ${conn.targetAddress}</span>
726
+ <span style="margin-left:auto;color:#b2bec3;">↑${formatBytes(conn.bytesReceived)} ↓${formatBytes(conn.bytesSent)}</span>
484
727
  </div>
485
728
  `).join('')}
486
729
  </div>
487
730
  `;
488
-
731
+
489
732
  container.appendChild(portDiv);
490
733
  });
491
734
  } else {
492
- container.innerHTML = '<p>当前没有活跃连接</p>';
735
+ container.innerHTML = `
736
+ <div class="no-connections">
737
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
738
+ <path d="M8.5 14.5A2.5 2.5 0 0011 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 11-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 002.5 2.5z"/>
739
+ </svg>
740
+ <p>当前没有活跃连接</p>
741
+ </div>
742
+ `;
493
743
  }
494
744
  }
495
745
 
496
746
  // 渲染系统状态
497
747
  function renderStatus() {
498
748
  const container = document.getElementById('statsGrid');
749
+ const uptime = status.uptime || 0;
750
+ let uptimeStr;
751
+ if (uptime < 60) {
752
+ uptimeStr = Math.floor(uptime) + 's';
753
+ } else if (uptime < 3600) {
754
+ uptimeStr = Math.floor(uptime / 60) + 'm';
755
+ } else {
756
+ uptimeStr = Math.floor(uptime / 3600) + 'h ' + Math.floor((uptime % 3600) / 60) + 'm';
757
+ }
758
+
499
759
  container.innerHTML = `
500
760
  <div class="stat-card">
501
761
  <h3>${status.totalMappings || 0}</h3>
@@ -510,7 +770,7 @@
510
770
  <p>总连接数</p>
511
771
  </div>
512
772
  <div class="stat-card">
513
- <h3>${Math.floor((status.uptime || 0) / 60)}m</h3>
773
+ <h3>${uptimeStr}</h3>
514
774
  <p>运行时间</p>
515
775
  </div>
516
776
  `;
@@ -519,6 +779,7 @@
519
779
  // 显示添加映射模态框
520
780
  function showAddMappingModal() {
521
781
  document.getElementById('addMappingModal').style.display = 'block';
782
+ document.getElementById('mappingName').focus();
522
783
  }
523
784
 
524
785
  // 关闭模态框
@@ -532,7 +793,7 @@
532
793
  // 添加端口映射
533
794
  document.getElementById('addMappingForm').addEventListener('submit', async function(e) {
534
795
  e.preventDefault();
535
-
796
+
536
797
  const formData = new FormData(e.target);
537
798
  const mappingData = {
538
799
  name: formData.get('name'),
@@ -545,7 +806,7 @@
545
806
  port: parseInt(formData.get('targetPort'))
546
807
  }
547
808
  };
548
-
809
+
549
810
  try {
550
811
  const response = await fetch('/api/mappings', {
551
812
  method: 'POST',
@@ -554,13 +815,14 @@
554
815
  },
555
816
  body: JSON.stringify(mappingData)
556
817
  });
557
-
818
+
558
819
  const data = await response.json();
559
-
820
+
560
821
  if (data.success) {
561
822
  document.getElementById('modalSuccessMessage').textContent = data.message;
562
823
  document.getElementById('modalSuccessMessage').style.display = 'block';
563
-
824
+ document.getElementById('modalErrorMessage').style.display = 'none';
825
+
564
826
  setTimeout(() => {
565
827
  closeModal();
566
828
  loadMappings();
@@ -568,6 +830,7 @@
568
830
  } else {
569
831
  document.getElementById('modalErrorMessage').textContent = data.message;
570
832
  document.getElementById('modalErrorMessage').style.display = 'block';
833
+ document.getElementById('modalSuccessMessage').style.display = 'none';
571
834
  }
572
835
  } catch (error) {
573
836
  document.getElementById('modalErrorMessage').textContent = '添加失败,请检查网络连接';
@@ -639,6 +902,7 @@
639
902
  // 刷新状态
640
903
  function refreshStatus() {
641
904
  loadStatus();
905
+ loadMappings();
642
906
  }
643
907
 
644
908
  // 刷新连接
@@ -656,13 +920,12 @@
656
920
  }
657
921
  }
658
922
 
659
- // 点击模态框外部关闭
660
- window.onclick = function(event) {
661
- const modal = document.getElementById('addMappingModal');
662
- if (event.target === modal) {
923
+ // ESC键关闭模态框
924
+ document.addEventListener('keydown', function(e) {
925
+ if (e.key === 'Escape') {
663
926
  closeModal();
664
927
  }
665
- }
928
+ });
666
929
  </script>
667
930
  </body>
668
- </html>
931
+ </html>