@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/CHANGELOG.md +108 -0
- package/README.md +113 -10
- package/bin/tcpmapping.js +476 -176
- package/docs/API.md +532 -0
- package/docs/screenshots/dashboard-en-dark.webp +0 -0
- package/docs/screenshots/dashboard-mobile-zh.webp +0 -0
- package/docs/screenshots/dashboard-zh-light.webp +0 -0
- package/docs/screenshots/login-zh-light.webp +0 -0
- package/docs//345/212/237/350/203/275/346/200/273/347/273/223.md +200 -0
- package/docs//345/256/211/345/205/250/351/205/215/347/275/256/350/257/264/346/230/216.md +154 -0
- package/package.json +6 -4
- package/public/common.js +411 -0
- package/public/dashboard.html +1624 -437
- package/public/login.html +430 -107
- package/server.js +1219 -464
- package/setup-password.js +137 -57
package/public/dashboard.html
CHANGED
|
@@ -4,7 +4,77 @@
|
|
|
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: #f3f5fb;
|
|
13
|
+
--bg-accent-1: rgba(99, 102, 241, 0.16);
|
|
14
|
+
--bg-accent-2: rgba(14, 165, 233, 0.12);
|
|
15
|
+
--surface: #ffffff;
|
|
16
|
+
--surface-muted: #f8fafc;
|
|
17
|
+
--surface-hover: #f1f5f9;
|
|
18
|
+
--text: #0f172a;
|
|
19
|
+
--text-muted: #64748b;
|
|
20
|
+
--text-faint: #94a3b8;
|
|
21
|
+
--border: #e2e8f0;
|
|
22
|
+
--border-strong: #cbd5e1;
|
|
23
|
+
--primary: #6366f1;
|
|
24
|
+
--primary-strong: #4f46e5;
|
|
25
|
+
--primary-soft: rgba(99, 102, 241, 0.12);
|
|
26
|
+
--green: #10b981;
|
|
27
|
+
--green-soft: rgba(16, 185, 129, 0.12);
|
|
28
|
+
--amber: #f59e0b;
|
|
29
|
+
--amber-soft: rgba(245, 158, 11, 0.14);
|
|
30
|
+
--red: #ef4444;
|
|
31
|
+
--red-soft: rgba(239, 68, 68, 0.12);
|
|
32
|
+
--sky: #0ea5e9;
|
|
33
|
+
--sky-soft: rgba(14, 165, 233, 0.12);
|
|
34
|
+
--violet: #8b5cf6;
|
|
35
|
+
--violet-soft: rgba(139, 92, 246, 0.12);
|
|
36
|
+
--header-bg: rgba(255, 255, 255, 0.82);
|
|
37
|
+
--shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.06);
|
|
38
|
+
--shadow-md: 0 12px 30px -12px rgba(15, 23, 42, 0.18);
|
|
39
|
+
--shadow-lg: 0 24px 60px -20px rgba(15, 23, 42, 0.25);
|
|
40
|
+
--radius: 18px;
|
|
41
|
+
--radius-sm: 12px;
|
|
42
|
+
--gradient: linear-gradient(135deg, #6366f1, #8b5cf6);
|
|
43
|
+
--mono: 'SFMono-Regular', ui-monospace, 'Cascadia Code', Consolas, monospace;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
[data-theme="dark"] {
|
|
47
|
+
color-scheme: dark;
|
|
48
|
+
--bg: #0b1120;
|
|
49
|
+
--bg-accent-1: rgba(99, 102, 241, 0.22);
|
|
50
|
+
--bg-accent-2: rgba(14, 165, 233, 0.14);
|
|
51
|
+
--surface: #111827;
|
|
52
|
+
--surface-muted: #0f172a;
|
|
53
|
+
--surface-hover: #1e293b;
|
|
54
|
+
--text: #e2e8f0;
|
|
55
|
+
--text-muted: #94a3b8;
|
|
56
|
+
--text-faint: #64748b;
|
|
57
|
+
--border: #1e293b;
|
|
58
|
+
--border-strong: #334155;
|
|
59
|
+
--primary: #818cf8;
|
|
60
|
+
--primary-strong: #6366f1;
|
|
61
|
+
--primary-soft: rgba(129, 140, 248, 0.18);
|
|
62
|
+
--green: #34d399;
|
|
63
|
+
--green-soft: rgba(52, 211, 153, 0.15);
|
|
64
|
+
--amber: #fbbf24;
|
|
65
|
+
--amber-soft: rgba(251, 191, 36, 0.15);
|
|
66
|
+
--red: #f87171;
|
|
67
|
+
--red-soft: rgba(248, 113, 113, 0.15);
|
|
68
|
+
--sky: #38bdf8;
|
|
69
|
+
--sky-soft: rgba(56, 189, 248, 0.15);
|
|
70
|
+
--violet: #a78bfa;
|
|
71
|
+
--violet-soft: rgba(167, 139, 250, 0.15);
|
|
72
|
+
--header-bg: rgba(11, 17, 32, 0.8);
|
|
73
|
+
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
|
|
74
|
+
--shadow-md: 0 16px 40px -16px rgba(0, 0, 0, 0.6);
|
|
75
|
+
--shadow-lg: 0 30px 70px -24px rgba(0, 0, 0, 0.7);
|
|
76
|
+
}
|
|
77
|
+
|
|
8
78
|
* {
|
|
9
79
|
margin: 0;
|
|
10
80
|
padding: 0;
|
|
@@ -12,260 +82,757 @@
|
|
|
12
82
|
}
|
|
13
83
|
|
|
14
84
|
body {
|
|
15
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
16
|
-
background:
|
|
85
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
|
86
|
+
background:
|
|
87
|
+
radial-gradient(900px 520px at 8% -12%, var(--bg-accent-1), transparent 60%),
|
|
88
|
+
radial-gradient(900px 520px at 100% 0%, var(--bg-accent-2), transparent 55%),
|
|
89
|
+
var(--bg);
|
|
90
|
+
background-attachment: fixed;
|
|
91
|
+
color: var(--text);
|
|
17
92
|
min-height: 100vh;
|
|
93
|
+
transition: background-color 0.25s ease, color 0.25s ease;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
svg {
|
|
97
|
+
display: block;
|
|
18
98
|
}
|
|
19
99
|
|
|
20
|
-
.header {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
100
|
+
.app-header {
|
|
101
|
+
position: sticky;
|
|
102
|
+
top: 0;
|
|
103
|
+
z-index: 50;
|
|
24
104
|
display: flex;
|
|
105
|
+
align-items: center;
|
|
25
106
|
justify-content: space-between;
|
|
107
|
+
gap: 1rem;
|
|
108
|
+
padding: 0.85rem 1.5rem;
|
|
109
|
+
background: var(--header-bg);
|
|
110
|
+
backdrop-filter: blur(16px);
|
|
111
|
+
-webkit-backdrop-filter: blur(16px);
|
|
112
|
+
border-bottom: 1px solid var(--border);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.brand {
|
|
116
|
+
display: flex;
|
|
26
117
|
align-items: center;
|
|
118
|
+
gap: 0.8rem;
|
|
119
|
+
min-width: 0;
|
|
27
120
|
}
|
|
28
121
|
|
|
29
|
-
.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
122
|
+
.brand-logo {
|
|
123
|
+
width: 42px;
|
|
124
|
+
height: 42px;
|
|
125
|
+
border-radius: 13px;
|
|
126
|
+
background: var(--gradient);
|
|
127
|
+
display: grid;
|
|
128
|
+
place-items: center;
|
|
129
|
+
color: #ffffff;
|
|
130
|
+
box-shadow: 0 8px 20px -6px rgba(99, 102, 241, 0.6);
|
|
131
|
+
flex-shrink: 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.brand-logo svg {
|
|
135
|
+
width: 22px;
|
|
136
|
+
height: 22px;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.brand-text h1 {
|
|
140
|
+
font-size: 1rem;
|
|
141
|
+
font-weight: 700;
|
|
142
|
+
letter-spacing: -0.01em;
|
|
143
|
+
white-space: nowrap;
|
|
144
|
+
overflow: hidden;
|
|
145
|
+
text-overflow: ellipsis;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.brand-text p {
|
|
149
|
+
font-size: 0.75rem;
|
|
150
|
+
color: var(--text-muted);
|
|
151
|
+
margin-top: 0.1rem;
|
|
34
152
|
}
|
|
35
153
|
|
|
36
154
|
.header-actions {
|
|
37
155
|
display: flex;
|
|
38
|
-
|
|
156
|
+
align-items: center;
|
|
157
|
+
gap: 0.6rem;
|
|
158
|
+
flex-wrap: wrap;
|
|
159
|
+
justify-content: flex-end;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.icon-btn {
|
|
163
|
+
width: 36px;
|
|
164
|
+
height: 36px;
|
|
165
|
+
display: grid;
|
|
166
|
+
place-items: center;
|
|
167
|
+
border: 1px solid var(--border);
|
|
168
|
+
border-radius: 10px;
|
|
169
|
+
background: var(--surface);
|
|
170
|
+
color: var(--text-muted);
|
|
171
|
+
cursor: pointer;
|
|
172
|
+
transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.icon-btn:hover {
|
|
176
|
+
background: var(--surface-hover);
|
|
177
|
+
color: var(--text);
|
|
178
|
+
border-color: var(--border-strong);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.icon-btn:focus-visible {
|
|
182
|
+
outline: 2px solid var(--primary);
|
|
183
|
+
outline-offset: 2px;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.icon-btn svg {
|
|
187
|
+
width: 18px;
|
|
188
|
+
height: 18px;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.icon-btn .icon-sun {
|
|
192
|
+
display: none;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
[data-theme="dark"] .icon-btn .icon-moon {
|
|
196
|
+
display: none;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
[data-theme="dark"] .icon-btn .icon-sun {
|
|
200
|
+
display: block;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.select-wrap {
|
|
204
|
+
position: relative;
|
|
205
|
+
display: flex;
|
|
39
206
|
align-items: center;
|
|
40
207
|
}
|
|
41
208
|
|
|
42
|
-
.
|
|
43
|
-
|
|
44
|
-
|
|
209
|
+
.select-wrap svg {
|
|
210
|
+
position: absolute;
|
|
211
|
+
left: 0.6rem;
|
|
212
|
+
width: 15px;
|
|
213
|
+
height: 15px;
|
|
214
|
+
color: var(--text-muted);
|
|
215
|
+
pointer-events: none;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.select-wrap select {
|
|
219
|
+
appearance: none;
|
|
220
|
+
-webkit-appearance: none;
|
|
221
|
+
padding: 0.5rem 1.9rem 0.5rem 1.9rem;
|
|
222
|
+
border: 1px solid var(--border);
|
|
223
|
+
border-radius: 10px;
|
|
224
|
+
background: var(--surface);
|
|
225
|
+
color: var(--text);
|
|
226
|
+
font-size: 0.8rem;
|
|
227
|
+
font-weight: 500;
|
|
228
|
+
cursor: pointer;
|
|
229
|
+
transition: border-color 0.2s ease;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.select-wrap select:hover {
|
|
233
|
+
border-color: var(--border-strong);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.select-wrap select:focus-visible {
|
|
237
|
+
outline: 2px solid var(--primary);
|
|
238
|
+
outline-offset: 2px;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.select-wrap::after {
|
|
242
|
+
content: '';
|
|
243
|
+
position: absolute;
|
|
244
|
+
right: 0.7rem;
|
|
245
|
+
width: 6px;
|
|
246
|
+
height: 6px;
|
|
247
|
+
border-right: 1.5px solid var(--text-muted);
|
|
248
|
+
border-bottom: 1.5px solid var(--text-muted);
|
|
249
|
+
transform: rotate(45deg) translateY(-1px);
|
|
250
|
+
pointer-events: none;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.user-chip {
|
|
254
|
+
display: flex;
|
|
255
|
+
align-items: center;
|
|
256
|
+
gap: 0.5rem;
|
|
257
|
+
padding: 0.25rem 0.7rem 0.25rem 0.3rem;
|
|
258
|
+
border: 1px solid var(--border);
|
|
259
|
+
border-radius: 999px;
|
|
260
|
+
background: var(--surface);
|
|
261
|
+
font-size: 0.8rem;
|
|
262
|
+
color: var(--text-muted);
|
|
263
|
+
max-width: 220px;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.user-chip .avatar {
|
|
267
|
+
width: 28px;
|
|
268
|
+
height: 28px;
|
|
269
|
+
border-radius: 50%;
|
|
270
|
+
background: var(--gradient);
|
|
271
|
+
color: #ffffff;
|
|
272
|
+
display: grid;
|
|
273
|
+
place-items: center;
|
|
274
|
+
font-size: 0.75rem;
|
|
275
|
+
font-weight: 700;
|
|
276
|
+
flex-shrink: 0;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.user-chip #welcomeText {
|
|
280
|
+
overflow: hidden;
|
|
281
|
+
text-overflow: ellipsis;
|
|
282
|
+
white-space: nowrap;
|
|
45
283
|
}
|
|
46
284
|
|
|
47
285
|
.btn {
|
|
48
|
-
padding: 0.
|
|
286
|
+
padding: 0.55rem 1rem;
|
|
49
287
|
border: none;
|
|
50
|
-
border-radius:
|
|
288
|
+
border-radius: 10px;
|
|
51
289
|
cursor: pointer;
|
|
52
|
-
font-size:
|
|
53
|
-
font-weight:
|
|
54
|
-
|
|
290
|
+
font-size: 0.8rem;
|
|
291
|
+
font-weight: 600;
|
|
292
|
+
font-family: inherit;
|
|
293
|
+
transition: transform 0.15s ease, box-shadow 0.2s ease, filter 0.2s ease, background 0.2s ease;
|
|
55
294
|
display: inline-flex;
|
|
56
295
|
align-items: center;
|
|
57
|
-
|
|
296
|
+
justify-content: center;
|
|
297
|
+
gap: 0.4rem;
|
|
298
|
+
white-space: nowrap;
|
|
58
299
|
}
|
|
59
300
|
|
|
60
|
-
.btn-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
|
301
|
+
.btn:focus-visible {
|
|
302
|
+
outline: 2px solid var(--primary);
|
|
303
|
+
outline-offset: 2px;
|
|
64
304
|
}
|
|
65
305
|
|
|
66
|
-
.btn
|
|
67
|
-
transform: translateY(
|
|
68
|
-
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5);
|
|
306
|
+
.btn:active {
|
|
307
|
+
transform: translateY(1px);
|
|
69
308
|
}
|
|
70
309
|
|
|
71
|
-
.btn
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
box-shadow: 0 4px 15px rgba(255, 107, 107, 0.3);
|
|
310
|
+
.btn svg {
|
|
311
|
+
width: 15px;
|
|
312
|
+
height: 15px;
|
|
75
313
|
}
|
|
76
314
|
|
|
77
|
-
.btn-
|
|
78
|
-
|
|
79
|
-
|
|
315
|
+
.btn-primary {
|
|
316
|
+
background: var(--gradient);
|
|
317
|
+
color: #ffffff;
|
|
318
|
+
box-shadow: 0 8px 18px -8px rgba(99, 102, 241, 0.7);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.btn-primary:hover {
|
|
322
|
+
filter: brightness(1.07);
|
|
80
323
|
}
|
|
81
324
|
|
|
82
325
|
.btn-success {
|
|
83
|
-
background:
|
|
84
|
-
color:
|
|
85
|
-
box-shadow: 0
|
|
326
|
+
background: var(--green);
|
|
327
|
+
color: #ffffff;
|
|
328
|
+
box-shadow: 0 8px 18px -10px var(--green);
|
|
86
329
|
}
|
|
87
330
|
|
|
88
331
|
.btn-success:hover {
|
|
89
|
-
|
|
90
|
-
box-shadow: 0 6px 20px rgba(46, 213, 115, 0.4);
|
|
332
|
+
filter: brightness(1.07);
|
|
91
333
|
}
|
|
92
334
|
|
|
93
335
|
.btn-warning {
|
|
94
|
-
background:
|
|
95
|
-
color:
|
|
96
|
-
box-shadow: 0
|
|
336
|
+
background: var(--amber);
|
|
337
|
+
color: #1f2937;
|
|
338
|
+
box-shadow: 0 8px 18px -10px var(--amber);
|
|
97
339
|
}
|
|
98
340
|
|
|
99
341
|
.btn-warning:hover {
|
|
100
|
-
|
|
101
|
-
|
|
342
|
+
filter: brightness(1.05);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.btn-danger {
|
|
346
|
+
background: var(--red);
|
|
347
|
+
color: #ffffff;
|
|
348
|
+
box-shadow: 0 8px 18px -10px var(--red);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.btn-danger:hover {
|
|
352
|
+
filter: brightness(1.07);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.btn-cancel {
|
|
356
|
+
background: var(--surface-muted);
|
|
357
|
+
color: var(--text-muted);
|
|
358
|
+
border: 1px solid var(--border);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.btn-cancel:hover {
|
|
362
|
+
background: var(--surface-hover);
|
|
363
|
+
color: var(--text);
|
|
102
364
|
}
|
|
103
365
|
|
|
104
366
|
.btn-sm {
|
|
105
|
-
padding: 0.
|
|
106
|
-
font-size:
|
|
367
|
+
padding: 0.35rem 0.65rem;
|
|
368
|
+
font-size: 0.72rem;
|
|
369
|
+
border-radius: 8px;
|
|
107
370
|
}
|
|
108
371
|
|
|
109
372
|
.container {
|
|
110
|
-
max-width:
|
|
373
|
+
max-width: 1320px;
|
|
111
374
|
margin: 0 auto;
|
|
112
|
-
padding:
|
|
375
|
+
padding: 1.75rem 1.5rem 3rem;
|
|
113
376
|
}
|
|
114
377
|
|
|
115
378
|
.card {
|
|
116
|
-
background:
|
|
117
|
-
border
|
|
118
|
-
|
|
119
|
-
|
|
379
|
+
background: var(--surface);
|
|
380
|
+
border: 1px solid var(--border);
|
|
381
|
+
border-radius: var(--radius);
|
|
382
|
+
box-shadow: var(--shadow-sm);
|
|
383
|
+
margin-bottom: 1.5rem;
|
|
120
384
|
overflow: hidden;
|
|
385
|
+
transition: box-shadow 0.25s ease, border-color 0.25s ease;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.card:hover {
|
|
389
|
+
box-shadow: var(--shadow-md);
|
|
121
390
|
}
|
|
122
391
|
|
|
123
392
|
.card-header {
|
|
124
|
-
padding: 1.5rem
|
|
125
|
-
border-bottom: 1px solid
|
|
393
|
+
padding: 1.1rem 1.5rem;
|
|
394
|
+
border-bottom: 1px solid var(--border);
|
|
126
395
|
display: flex;
|
|
127
396
|
justify-content: space-between;
|
|
128
397
|
align-items: center;
|
|
129
|
-
|
|
398
|
+
gap: 1rem;
|
|
399
|
+
background: var(--surface-muted);
|
|
130
400
|
}
|
|
131
401
|
|
|
132
402
|
.card-header h2 {
|
|
133
|
-
color:
|
|
134
|
-
font-size:
|
|
135
|
-
font-weight:
|
|
403
|
+
color: var(--text);
|
|
404
|
+
font-size: 0.95rem;
|
|
405
|
+
font-weight: 650;
|
|
406
|
+
letter-spacing: 0.01em;
|
|
136
407
|
}
|
|
137
408
|
|
|
138
409
|
.card-body {
|
|
139
|
-
padding: 1.5rem
|
|
410
|
+
padding: 1.5rem;
|
|
140
411
|
}
|
|
141
412
|
|
|
142
413
|
.stats-grid {
|
|
143
414
|
display: grid;
|
|
144
|
-
grid-template-columns: repeat(auto-fit, minmax(
|
|
145
|
-
gap:
|
|
146
|
-
margin-bottom: 0;
|
|
415
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
416
|
+
gap: 1rem;
|
|
147
417
|
}
|
|
148
418
|
|
|
149
419
|
.stat-card {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
border-radius:
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
420
|
+
position: relative;
|
|
421
|
+
background: var(--surface-muted);
|
|
422
|
+
border: 1px solid var(--border);
|
|
423
|
+
border-radius: var(--radius-sm);
|
|
424
|
+
padding: 1.15rem 1.2rem 1.15rem 1.4rem;
|
|
425
|
+
display: flex;
|
|
426
|
+
align-items: center;
|
|
427
|
+
gap: 0.9rem;
|
|
428
|
+
overflow: hidden;
|
|
429
|
+
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
|
157
430
|
}
|
|
158
431
|
|
|
159
|
-
.stat-card
|
|
160
|
-
|
|
432
|
+
.stat-card::before {
|
|
433
|
+
content: '';
|
|
434
|
+
position: absolute;
|
|
435
|
+
left: 0;
|
|
436
|
+
top: 0;
|
|
437
|
+
bottom: 0;
|
|
438
|
+
width: 4px;
|
|
439
|
+
background: var(--accent);
|
|
161
440
|
}
|
|
162
441
|
|
|
163
|
-
.stat-card:
|
|
164
|
-
|
|
165
|
-
box-shadow:
|
|
442
|
+
.stat-card:hover {
|
|
443
|
+
transform: translateY(-2px);
|
|
444
|
+
box-shadow: var(--shadow-md);
|
|
445
|
+
border-color: var(--border-strong);
|
|
166
446
|
}
|
|
167
447
|
|
|
168
|
-
.stat-
|
|
169
|
-
|
|
170
|
-
|
|
448
|
+
.stat-icon {
|
|
449
|
+
width: 44px;
|
|
450
|
+
height: 44px;
|
|
451
|
+
border-radius: 12px;
|
|
452
|
+
display: grid;
|
|
453
|
+
place-items: center;
|
|
454
|
+
background: var(--accent-soft);
|
|
455
|
+
color: var(--accent);
|
|
456
|
+
flex-shrink: 0;
|
|
171
457
|
}
|
|
172
458
|
|
|
173
|
-
.stat-
|
|
174
|
-
|
|
175
|
-
|
|
459
|
+
.stat-icon svg {
|
|
460
|
+
width: 22px;
|
|
461
|
+
height: 22px;
|
|
176
462
|
}
|
|
177
463
|
|
|
178
464
|
.stat-card h3 {
|
|
179
|
-
font-size:
|
|
180
|
-
margin-bottom: 0.5rem;
|
|
465
|
+
font-size: 1.45rem;
|
|
181
466
|
font-weight: 700;
|
|
467
|
+
line-height: 1.15;
|
|
468
|
+
color: var(--text);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.stat-card h3.small {
|
|
472
|
+
font-size: 1.02rem;
|
|
473
|
+
display: flex;
|
|
474
|
+
flex-wrap: wrap;
|
|
475
|
+
align-items: baseline;
|
|
476
|
+
column-gap: 0.5rem;
|
|
477
|
+
row-gap: 0.1rem;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
.stat-card h3.small span {
|
|
481
|
+
white-space: nowrap;
|
|
182
482
|
}
|
|
183
483
|
|
|
184
484
|
.stat-card p {
|
|
185
|
-
font-size:
|
|
186
|
-
|
|
485
|
+
font-size: 0.76rem;
|
|
486
|
+
color: var(--text-muted);
|
|
487
|
+
margin-top: 0.2rem;
|
|
187
488
|
font-weight: 500;
|
|
188
489
|
}
|
|
189
490
|
|
|
491
|
+
.stat-card--indigo { --accent: var(--primary); --accent-soft: var(--primary-soft); }
|
|
492
|
+
.stat-card--green { --accent: var(--green); --accent-soft: var(--green-soft); }
|
|
493
|
+
.stat-card--amber { --accent: var(--amber); --accent-soft: var(--amber-soft); }
|
|
494
|
+
.stat-card--sky { --accent: var(--sky); --accent-soft: var(--sky-soft); }
|
|
495
|
+
.stat-card--violet { --accent: var(--violet); --accent-soft: var(--violet-soft); }
|
|
496
|
+
|
|
497
|
+
/* 服务资源 */
|
|
498
|
+
.resource-sampled {
|
|
499
|
+
display: inline-flex;
|
|
500
|
+
align-items: center;
|
|
501
|
+
gap: 0.45rem;
|
|
502
|
+
font-size: 0.72rem;
|
|
503
|
+
color: var(--text-muted);
|
|
504
|
+
font-variant-numeric: tabular-nums;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
.live-dot {
|
|
508
|
+
width: 7px;
|
|
509
|
+
height: 7px;
|
|
510
|
+
border-radius: 50%;
|
|
511
|
+
background: var(--green);
|
|
512
|
+
box-shadow: 0 0 0 4px var(--green-soft);
|
|
513
|
+
animation: livePulse 2s ease-in-out infinite;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
@keyframes livePulse {
|
|
517
|
+
0%, 100% { opacity: 1; }
|
|
518
|
+
50% { opacity: 0.45; }
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.resource-grid {
|
|
522
|
+
display: grid;
|
|
523
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
524
|
+
gap: 1rem;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
.resource-card {
|
|
528
|
+
background: var(--surface-muted);
|
|
529
|
+
border: 1px solid var(--border);
|
|
530
|
+
border-radius: var(--radius-sm);
|
|
531
|
+
padding: 1rem 1.1rem;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
.resource-card-head {
|
|
535
|
+
display: flex;
|
|
536
|
+
justify-content: space-between;
|
|
537
|
+
align-items: baseline;
|
|
538
|
+
gap: 0.75rem;
|
|
539
|
+
margin-bottom: 0.7rem;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
.resource-label {
|
|
543
|
+
font-size: 0.78rem;
|
|
544
|
+
font-weight: 600;
|
|
545
|
+
color: var(--text-muted);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
.resource-value {
|
|
549
|
+
font-size: 1.3rem;
|
|
550
|
+
font-weight: 700;
|
|
551
|
+
color: var(--text);
|
|
552
|
+
font-variant-numeric: tabular-nums;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
.resource-track {
|
|
556
|
+
height: 8px;
|
|
557
|
+
border-radius: 999px;
|
|
558
|
+
background: var(--border);
|
|
559
|
+
overflow: hidden;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
.resource-fill {
|
|
563
|
+
display: block;
|
|
564
|
+
width: 0;
|
|
565
|
+
height: 100%;
|
|
566
|
+
border-radius: inherit;
|
|
567
|
+
background: var(--primary);
|
|
568
|
+
transition: width 0.4s ease;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
.resource-fill--memory {
|
|
572
|
+
background: var(--green);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
.resource-hint {
|
|
576
|
+
margin-top: 0.6rem;
|
|
577
|
+
font-size: 0.72rem;
|
|
578
|
+
color: var(--text-muted);
|
|
579
|
+
font-variant-numeric: tabular-nums;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
.resource-traffic {
|
|
583
|
+
display: flex;
|
|
584
|
+
flex-wrap: wrap;
|
|
585
|
+
align-items: baseline;
|
|
586
|
+
gap: 0.4rem 1.4rem;
|
|
587
|
+
font-family: var(--mono);
|
|
588
|
+
font-size: 1.02rem;
|
|
589
|
+
font-weight: 700;
|
|
590
|
+
font-variant-numeric: tabular-nums;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
.resource-traffic .up { color: var(--green); }
|
|
594
|
+
.resource-traffic .down { color: var(--sky); }
|
|
595
|
+
|
|
596
|
+
.table-wrap {
|
|
597
|
+
overflow-x: auto;
|
|
598
|
+
border-radius: var(--radius-sm);
|
|
599
|
+
border: 1px solid var(--border);
|
|
600
|
+
}
|
|
601
|
+
|
|
190
602
|
.table {
|
|
191
603
|
width: 100%;
|
|
192
604
|
border-collapse: collapse;
|
|
605
|
+
min-width: 760px;
|
|
193
606
|
}
|
|
194
607
|
|
|
195
608
|
.table th,
|
|
196
609
|
.table td {
|
|
197
|
-
padding: 1rem;
|
|
610
|
+
padding: 0.85rem 1rem;
|
|
198
611
|
text-align: left;
|
|
199
|
-
border-bottom: 1px solid #f0f0f0;
|
|
200
612
|
}
|
|
201
613
|
|
|
202
614
|
.table th {
|
|
203
|
-
background:
|
|
615
|
+
background: var(--surface-muted);
|
|
204
616
|
font-weight: 600;
|
|
205
|
-
color:
|
|
206
|
-
font-size:
|
|
617
|
+
color: var(--text-muted);
|
|
618
|
+
font-size: 0.7rem;
|
|
207
619
|
text-transform: uppercase;
|
|
208
|
-
letter-spacing: 0.
|
|
620
|
+
letter-spacing: 0.07em;
|
|
621
|
+
white-space: nowrap;
|
|
622
|
+
border-bottom: 1px solid var(--border);
|
|
209
623
|
}
|
|
210
624
|
|
|
211
|
-
.table
|
|
212
|
-
|
|
625
|
+
.table td {
|
|
626
|
+
color: var(--text-muted);
|
|
627
|
+
font-size: 0.85rem;
|
|
628
|
+
border-bottom: 1px solid var(--border);
|
|
629
|
+
vertical-align: middle;
|
|
213
630
|
}
|
|
214
631
|
|
|
215
|
-
.table td {
|
|
216
|
-
|
|
217
|
-
|
|
632
|
+
.table tbody tr:last-child td {
|
|
633
|
+
border-bottom: none;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
.table tbody tr {
|
|
637
|
+
transition: background 0.15s ease;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
.table tbody tr:hover td {
|
|
641
|
+
background: var(--surface-hover);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
.table strong {
|
|
645
|
+
color: var(--text);
|
|
646
|
+
font-weight: 600;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
.table code {
|
|
650
|
+
font-family: var(--mono);
|
|
651
|
+
font-size: 0.78rem;
|
|
652
|
+
color: var(--text);
|
|
653
|
+
background: var(--surface-muted);
|
|
654
|
+
border: 1px solid var(--border);
|
|
655
|
+
padding: 0.15rem 0.4rem;
|
|
656
|
+
border-radius: 6px;
|
|
657
|
+
white-space: nowrap;
|
|
218
658
|
}
|
|
219
659
|
|
|
220
660
|
.status-badge {
|
|
221
|
-
padding: 0.
|
|
222
|
-
border-radius:
|
|
223
|
-
font-size:
|
|
661
|
+
padding: 0.25rem 0.6rem;
|
|
662
|
+
border-radius: 999px;
|
|
663
|
+
font-size: 0.7rem;
|
|
224
664
|
font-weight: 600;
|
|
225
|
-
display: inline-
|
|
665
|
+
display: inline-flex;
|
|
666
|
+
align-items: center;
|
|
667
|
+
gap: 0.3rem;
|
|
668
|
+
white-space: nowrap;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
.status-badge::before {
|
|
672
|
+
content: '';
|
|
673
|
+
width: 6px;
|
|
674
|
+
height: 6px;
|
|
675
|
+
border-radius: 50%;
|
|
676
|
+
background: currentColor;
|
|
226
677
|
}
|
|
227
678
|
|
|
228
679
|
.status-running {
|
|
229
|
-
background:
|
|
230
|
-
color:
|
|
680
|
+
background: var(--green-soft);
|
|
681
|
+
color: var(--green);
|
|
231
682
|
}
|
|
232
683
|
|
|
233
684
|
.status-stopped {
|
|
234
|
-
background:
|
|
235
|
-
color:
|
|
685
|
+
background: var(--red-soft);
|
|
686
|
+
color: var(--red);
|
|
236
687
|
}
|
|
237
688
|
|
|
238
689
|
.status-connected {
|
|
239
|
-
background:
|
|
240
|
-
color:
|
|
690
|
+
background: var(--sky-soft);
|
|
691
|
+
color: var(--sky);
|
|
241
692
|
}
|
|
242
693
|
|
|
243
694
|
.traffic-info {
|
|
244
|
-
font-size:
|
|
245
|
-
color:
|
|
246
|
-
line-height: 1.
|
|
695
|
+
font-size: 0.75rem;
|
|
696
|
+
color: var(--text-muted);
|
|
697
|
+
line-height: 1.7;
|
|
698
|
+
white-space: nowrap;
|
|
247
699
|
}
|
|
248
700
|
|
|
249
701
|
.traffic-info .up {
|
|
250
|
-
color:
|
|
702
|
+
color: var(--green);
|
|
251
703
|
}
|
|
252
704
|
|
|
253
705
|
.traffic-info .down {
|
|
254
|
-
color:
|
|
706
|
+
color: var(--sky);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
.traffic-info .active-traffic {
|
|
710
|
+
color: var(--text-muted);
|
|
711
|
+
font-size: 0.7rem;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
.action-buttons {
|
|
715
|
+
display: flex;
|
|
716
|
+
gap: 0.35rem;
|
|
717
|
+
flex-wrap: wrap;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
.connection-card {
|
|
721
|
+
margin-bottom: 0.9rem;
|
|
722
|
+
padding: 1.1rem 1.2rem;
|
|
723
|
+
border: 1px solid var(--border);
|
|
724
|
+
border-radius: 14px;
|
|
725
|
+
background: var(--surface-muted);
|
|
726
|
+
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
.connection-card:last-child {
|
|
730
|
+
margin-bottom: 0;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
.connection-card:hover {
|
|
734
|
+
border-color: var(--border-strong);
|
|
735
|
+
box-shadow: var(--shadow-sm);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
.connection-card h4 {
|
|
739
|
+
color: var(--text);
|
|
740
|
+
font-size: 0.9rem;
|
|
741
|
+
font-weight: 650;
|
|
742
|
+
margin-bottom: 0.55rem;
|
|
743
|
+
display: flex;
|
|
744
|
+
align-items: center;
|
|
745
|
+
gap: 0.5rem;
|
|
746
|
+
flex-wrap: wrap;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
.port-badge {
|
|
750
|
+
background: var(--gradient);
|
|
751
|
+
color: #ffffff;
|
|
752
|
+
padding: 0.2rem 0.55rem;
|
|
753
|
+
border-radius: 7px;
|
|
754
|
+
font-size: 0.72rem;
|
|
755
|
+
font-family: var(--mono);
|
|
756
|
+
font-weight: 500;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
.connection-meta {
|
|
760
|
+
display: flex;
|
|
761
|
+
align-items: center;
|
|
762
|
+
gap: 0.5rem;
|
|
763
|
+
flex-wrap: wrap;
|
|
764
|
+
color: var(--text-muted);
|
|
765
|
+
font-size: 0.78rem;
|
|
766
|
+
margin-bottom: 0.6rem;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
.connection-meta code {
|
|
770
|
+
font-family: var(--mono);
|
|
771
|
+
font-size: 0.72rem;
|
|
772
|
+
color: var(--text);
|
|
773
|
+
background: var(--surface);
|
|
774
|
+
border: 1px solid var(--border);
|
|
775
|
+
padding: 0.1rem 0.35rem;
|
|
776
|
+
border-radius: 5px;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
.connection-item {
|
|
780
|
+
display: grid;
|
|
781
|
+
grid-template-columns: auto 1fr auto;
|
|
782
|
+
align-items: center;
|
|
783
|
+
gap: 0.75rem;
|
|
784
|
+
padding: 0.55rem 0;
|
|
785
|
+
border-bottom: 1px dashed var(--border);
|
|
786
|
+
font-size: 0.78rem;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
.connection-item:last-child {
|
|
790
|
+
border-bottom: none;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
.conn-endpoints {
|
|
794
|
+
color: var(--text);
|
|
795
|
+
font-family: var(--mono);
|
|
796
|
+
font-size: 0.75rem;
|
|
797
|
+
overflow-wrap: anywhere;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
.conn-metrics {
|
|
801
|
+
color: var(--text-muted);
|
|
802
|
+
text-align: right;
|
|
803
|
+
white-space: nowrap;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
.empty-state {
|
|
807
|
+
text-align: center;
|
|
808
|
+
padding: 2.5rem 1rem;
|
|
809
|
+
color: var(--text-muted);
|
|
810
|
+
font-size: 0.85rem;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
.empty-state svg {
|
|
814
|
+
width: 52px;
|
|
815
|
+
height: 52px;
|
|
816
|
+
margin: 0 auto 0.9rem;
|
|
817
|
+
opacity: 0.45;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
.history-hint {
|
|
821
|
+
color: var(--text-muted);
|
|
822
|
+
font-size: 0.78rem;
|
|
823
|
+
margin-bottom: 0.9rem;
|
|
255
824
|
}
|
|
256
825
|
|
|
257
|
-
/* Modal Styles */
|
|
258
826
|
.modal {
|
|
259
827
|
display: none;
|
|
260
828
|
position: fixed;
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
background: rgba(0, 0, 0, 0.6);
|
|
266
|
-
backdrop-filter: blur(4px);
|
|
829
|
+
inset: 0;
|
|
830
|
+
background: rgba(2, 6, 23, 0.55);
|
|
831
|
+
backdrop-filter: blur(6px);
|
|
832
|
+
-webkit-backdrop-filter: blur(6px);
|
|
267
833
|
z-index: 1000;
|
|
268
|
-
animation: fadeIn 0.
|
|
834
|
+
animation: fadeIn 0.2s ease;
|
|
835
|
+
overflow-y: auto;
|
|
269
836
|
}
|
|
270
837
|
|
|
271
838
|
@keyframes fadeIn {
|
|
@@ -276,7 +843,7 @@
|
|
|
276
843
|
@keyframes slideIn {
|
|
277
844
|
from {
|
|
278
845
|
opacity: 0;
|
|
279
|
-
transform: translateY(-
|
|
846
|
+
transform: translateY(-18px) scale(0.97);
|
|
280
847
|
}
|
|
281
848
|
to {
|
|
282
849
|
opacity: 1;
|
|
@@ -285,14 +852,15 @@
|
|
|
285
852
|
}
|
|
286
853
|
|
|
287
854
|
.modal-content {
|
|
288
|
-
background:
|
|
289
|
-
|
|
290
|
-
|
|
855
|
+
background: var(--surface);
|
|
856
|
+
color: var(--text);
|
|
857
|
+
margin: 7% auto;
|
|
858
|
+
border: 1px solid var(--border);
|
|
291
859
|
border-radius: 20px;
|
|
292
|
-
max-width:
|
|
293
|
-
width:
|
|
294
|
-
box-shadow:
|
|
295
|
-
animation: slideIn 0.
|
|
860
|
+
max-width: 520px;
|
|
861
|
+
width: calc(100% - 2rem);
|
|
862
|
+
box-shadow: var(--shadow-lg);
|
|
863
|
+
animation: slideIn 0.24s ease;
|
|
296
864
|
overflow: hidden;
|
|
297
865
|
}
|
|
298
866
|
|
|
@@ -300,320 +868,623 @@
|
|
|
300
868
|
display: flex;
|
|
301
869
|
justify-content: space-between;
|
|
302
870
|
align-items: center;
|
|
303
|
-
padding: 1.5rem
|
|
304
|
-
|
|
305
|
-
|
|
871
|
+
padding: 1.2rem 1.5rem;
|
|
872
|
+
border-bottom: 1px solid var(--border);
|
|
873
|
+
background: var(--surface-muted);
|
|
306
874
|
}
|
|
307
875
|
|
|
308
876
|
.modal-header h3 {
|
|
309
|
-
font-size:
|
|
310
|
-
font-weight:
|
|
877
|
+
font-size: 1rem;
|
|
878
|
+
font-weight: 650;
|
|
879
|
+
color: var(--text);
|
|
311
880
|
}
|
|
312
881
|
|
|
313
882
|
.close {
|
|
314
|
-
|
|
883
|
+
width: 32px;
|
|
884
|
+
height: 32px;
|
|
885
|
+
display: grid;
|
|
886
|
+
place-items: center;
|
|
887
|
+
font-size: 1.4rem;
|
|
315
888
|
cursor: pointer;
|
|
316
|
-
color:
|
|
317
|
-
|
|
889
|
+
color: var(--text-muted);
|
|
890
|
+
border: none;
|
|
891
|
+
background: transparent;
|
|
892
|
+
font-family: inherit;
|
|
893
|
+
border-radius: 8px;
|
|
894
|
+
transition: background 0.2s ease, color 0.2s ease;
|
|
318
895
|
line-height: 1;
|
|
319
896
|
}
|
|
320
897
|
|
|
321
898
|
.close:hover {
|
|
322
|
-
|
|
899
|
+
background: var(--surface-hover);
|
|
900
|
+
color: var(--text);
|
|
323
901
|
}
|
|
324
902
|
|
|
325
903
|
.modal-body {
|
|
326
|
-
padding:
|
|
904
|
+
padding: 1.5rem;
|
|
327
905
|
}
|
|
328
906
|
|
|
329
907
|
.form-group {
|
|
330
|
-
margin-bottom: 1.
|
|
908
|
+
margin-bottom: 1.1rem;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
.form-group:last-of-type {
|
|
912
|
+
margin-bottom: 0;
|
|
331
913
|
}
|
|
332
914
|
|
|
333
915
|
.form-group label {
|
|
334
916
|
display: block;
|
|
335
|
-
margin-bottom: 0.
|
|
336
|
-
color:
|
|
917
|
+
margin-bottom: 0.4rem;
|
|
918
|
+
color: var(--text);
|
|
337
919
|
font-weight: 600;
|
|
338
|
-
font-size:
|
|
920
|
+
font-size: 0.8rem;
|
|
339
921
|
}
|
|
340
922
|
|
|
341
923
|
.form-group input {
|
|
342
924
|
width: 100%;
|
|
343
|
-
padding: 0.85rem
|
|
344
|
-
border:
|
|
925
|
+
padding: 0.7rem 0.85rem;
|
|
926
|
+
border: 1.5px solid var(--border);
|
|
345
927
|
border-radius: 10px;
|
|
346
|
-
font-size:
|
|
347
|
-
|
|
928
|
+
font-size: 0.85rem;
|
|
929
|
+
font-family: inherit;
|
|
930
|
+
background: var(--surface-muted);
|
|
931
|
+
color: var(--text);
|
|
932
|
+
transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
|
|
348
933
|
}
|
|
349
934
|
|
|
350
935
|
.form-group input:focus {
|
|
351
936
|
outline: none;
|
|
352
|
-
border-color:
|
|
353
|
-
|
|
937
|
+
border-color: var(--primary);
|
|
938
|
+
background: var(--surface);
|
|
939
|
+
box-shadow: 0 0 0 4px var(--primary-soft);
|
|
354
940
|
}
|
|
355
941
|
|
|
356
942
|
.form-group input::placeholder {
|
|
357
|
-
color:
|
|
943
|
+
color: var(--text-muted);
|
|
358
944
|
}
|
|
359
945
|
|
|
360
|
-
.
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
gap:
|
|
946
|
+
.form-row {
|
|
947
|
+
display: grid;
|
|
948
|
+
grid-template-columns: 2fr 1fr;
|
|
949
|
+
gap: 0.75rem;
|
|
364
950
|
}
|
|
365
951
|
|
|
366
|
-
.
|
|
367
|
-
|
|
368
|
-
justify-content: center;
|
|
369
|
-
padding: 0.85rem 1.5rem;
|
|
952
|
+
.form-row .form-group {
|
|
953
|
+
margin-bottom: 1.1rem;
|
|
370
954
|
}
|
|
371
955
|
|
|
372
|
-
.
|
|
373
|
-
|
|
374
|
-
|
|
956
|
+
.modal-footer {
|
|
957
|
+
padding: 0 1.5rem 1.5rem;
|
|
958
|
+
display: flex;
|
|
959
|
+
gap: 0.75rem;
|
|
375
960
|
}
|
|
376
961
|
|
|
377
|
-
.
|
|
378
|
-
|
|
962
|
+
.modal-footer .btn {
|
|
963
|
+
flex: 1;
|
|
964
|
+
padding: 0.75rem 1.25rem;
|
|
379
965
|
}
|
|
380
966
|
|
|
381
967
|
.error-message {
|
|
382
|
-
color:
|
|
383
|
-
font-size:
|
|
968
|
+
color: var(--red);
|
|
969
|
+
font-size: 0.8rem;
|
|
384
970
|
margin-top: 1rem;
|
|
385
|
-
padding: 0.
|
|
386
|
-
background:
|
|
971
|
+
padding: 0.7rem 0.9rem;
|
|
972
|
+
background: var(--red-soft);
|
|
387
973
|
border-radius: 8px;
|
|
388
974
|
display: none;
|
|
389
975
|
}
|
|
390
976
|
|
|
391
977
|
.success-message {
|
|
392
|
-
color:
|
|
393
|
-
font-size:
|
|
978
|
+
color: var(--green);
|
|
979
|
+
font-size: 0.8rem;
|
|
394
980
|
margin-top: 1rem;
|
|
395
|
-
padding: 0.
|
|
396
|
-
background:
|
|
981
|
+
padding: 0.7rem 0.9rem;
|
|
982
|
+
background: var(--green-soft);
|
|
397
983
|
border-radius: 8px;
|
|
398
984
|
display: none;
|
|
399
985
|
}
|
|
400
986
|
|
|
401
|
-
.
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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;
|
|
429
|
-
font-size: 12px;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
.connection-details {
|
|
433
|
-
font-size: 13px;
|
|
434
|
-
color: #636e72;
|
|
987
|
+
.toast-container {
|
|
988
|
+
position: fixed;
|
|
989
|
+
right: 1.25rem;
|
|
990
|
+
bottom: 1.25rem;
|
|
991
|
+
display: flex;
|
|
992
|
+
flex-direction: column;
|
|
993
|
+
gap: 0.5rem;
|
|
994
|
+
z-index: 2000;
|
|
995
|
+
max-width: min(360px, calc(100vw - 2.5rem));
|
|
435
996
|
}
|
|
436
997
|
|
|
437
|
-
.
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
998
|
+
.toast {
|
|
999
|
+
background: var(--surface);
|
|
1000
|
+
color: var(--text);
|
|
1001
|
+
border: 1px solid var(--border);
|
|
1002
|
+
border-left: 4px solid var(--primary);
|
|
1003
|
+
padding: 0.75rem 1rem;
|
|
1004
|
+
border-radius: 10px;
|
|
1005
|
+
box-shadow: var(--shadow-lg);
|
|
1006
|
+
font-size: 0.82rem;
|
|
1007
|
+
opacity: 0;
|
|
1008
|
+
transform: translateY(8px);
|
|
1009
|
+
transition: opacity 0.2s ease, transform 0.2s ease;
|
|
1010
|
+
overflow-wrap: anywhere;
|
|
443
1011
|
}
|
|
444
1012
|
|
|
445
|
-
.
|
|
446
|
-
|
|
1013
|
+
.toast.show {
|
|
1014
|
+
opacity: 1;
|
|
1015
|
+
transform: translateY(0);
|
|
447
1016
|
}
|
|
448
1017
|
|
|
449
|
-
.
|
|
450
|
-
|
|
451
|
-
padding: 3rem;
|
|
452
|
-
color: #b2bec3;
|
|
1018
|
+
.toast-error {
|
|
1019
|
+
border-left-color: var(--red);
|
|
453
1020
|
}
|
|
454
1021
|
|
|
455
|
-
.
|
|
456
|
-
|
|
457
|
-
height: 60px;
|
|
458
|
-
margin-bottom: 1rem;
|
|
459
|
-
opacity: 0.5;
|
|
1022
|
+
.toast-success {
|
|
1023
|
+
border-left-color: var(--green);
|
|
460
1024
|
}
|
|
461
1025
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
1026
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1027
|
+
*,
|
|
1028
|
+
*::before,
|
|
1029
|
+
*::after {
|
|
1030
|
+
animation-duration: 0.01ms !important;
|
|
1031
|
+
transition-duration: 0.01ms !important;
|
|
1032
|
+
}
|
|
465
1033
|
}
|
|
466
1034
|
|
|
467
|
-
@media (max-width:
|
|
468
|
-
.header {
|
|
469
|
-
flex-
|
|
470
|
-
|
|
471
|
-
text-align: center;
|
|
1035
|
+
@media (max-width: 900px) {
|
|
1036
|
+
.app-header {
|
|
1037
|
+
flex-wrap: wrap;
|
|
1038
|
+
padding: 0.8rem 1rem;
|
|
472
1039
|
}
|
|
473
1040
|
|
|
474
1041
|
.container {
|
|
475
|
-
padding: 1rem;
|
|
1042
|
+
padding: 1.25rem 1rem 2.5rem;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
.stats-grid {
|
|
1046
|
+
grid-template-columns: repeat(2, 1fr);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
.resource-grid {
|
|
1050
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
.resource-card:last-child {
|
|
1054
|
+
grid-column: 1 / -1;
|
|
476
1055
|
}
|
|
477
1056
|
|
|
478
1057
|
.card-header {
|
|
479
|
-
flex-
|
|
480
|
-
|
|
1058
|
+
flex-wrap: wrap;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
@media (max-width: 640px) {
|
|
1063
|
+
.brand-text p {
|
|
1064
|
+
display: none;
|
|
481
1065
|
}
|
|
482
1066
|
|
|
483
|
-
.
|
|
484
|
-
display:
|
|
485
|
-
overflow-x: auto;
|
|
1067
|
+
.user-chip {
|
|
1068
|
+
display: none;
|
|
486
1069
|
}
|
|
487
1070
|
|
|
488
1071
|
.stats-grid {
|
|
489
|
-
grid-template-columns:
|
|
1072
|
+
grid-template-columns: 1fr;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
.resource-grid {
|
|
1076
|
+
grid-template-columns: 1fr;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
.resource-card:last-child {
|
|
1080
|
+
grid-column: auto;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
.card-body {
|
|
1084
|
+
padding: 1.1rem;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
.form-row {
|
|
1088
|
+
grid-template-columns: 1fr;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
.connection-item {
|
|
1092
|
+
grid-template-columns: 1fr;
|
|
1093
|
+
gap: 0.3rem;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
.conn-metrics {
|
|
1097
|
+
text-align: left;
|
|
490
1098
|
}
|
|
491
1099
|
}
|
|
492
1100
|
</style>
|
|
493
1101
|
</head>
|
|
494
1102
|
<body>
|
|
495
|
-
<
|
|
496
|
-
<
|
|
1103
|
+
<header class="app-header">
|
|
1104
|
+
<div class="brand">
|
|
1105
|
+
<div class="brand-logo">
|
|
1106
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
1107
|
+
<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>
|
|
1108
|
+
</svg>
|
|
1109
|
+
</div>
|
|
1110
|
+
<div class="brand-text">
|
|
1111
|
+
<h1 data-i18n="brand.name">TCP 转发服务器</h1>
|
|
1112
|
+
<p data-i18n="brand.subtitle">管理面板</p>
|
|
1113
|
+
</div>
|
|
1114
|
+
</div>
|
|
1115
|
+
|
|
497
1116
|
<div class="header-actions">
|
|
498
|
-
<
|
|
499
|
-
|
|
1117
|
+
<div class="select-wrap">
|
|
1118
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
1119
|
+
<circle cx="12" cy="12" r="10"/>
|
|
1120
|
+
<line x1="2" y1="12" x2="22" y2="12"/>
|
|
1121
|
+
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
|
|
1122
|
+
</svg>
|
|
1123
|
+
<select id="languageSelect" data-i18n-aria-label="common.language">
|
|
1124
|
+
<option value="zh-CN">中文</option>
|
|
1125
|
+
<option value="en-US">English</option>
|
|
1126
|
+
</select>
|
|
1127
|
+
</div>
|
|
1128
|
+
|
|
1129
|
+
<button type="button" class="icon-btn" data-theme-toggle data-i18n-title="common.theme" data-i18n-aria-label="common.theme">
|
|
1130
|
+
<svg class="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
1131
|
+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
|
1132
|
+
</svg>
|
|
1133
|
+
<svg class="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
1134
|
+
<circle cx="12" cy="12" r="4"/>
|
|
1135
|
+
<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"/>
|
|
1136
|
+
</svg>
|
|
1137
|
+
</button>
|
|
1138
|
+
|
|
1139
|
+
<div class="user-chip">
|
|
1140
|
+
<span class="avatar" id="userAvatar">A</span>
|
|
1141
|
+
<span id="welcomeText">管理员</span>
|
|
1142
|
+
</div>
|
|
1143
|
+
|
|
1144
|
+
<button class="btn btn-danger btn-sm" onclick="logout()" data-i18n="header.logout">退出登录</button>
|
|
500
1145
|
</div>
|
|
501
|
-
</
|
|
1146
|
+
</header>
|
|
502
1147
|
|
|
503
|
-
<
|
|
1148
|
+
<main class="container">
|
|
504
1149
|
<!-- 系统状态 -->
|
|
505
|
-
<
|
|
1150
|
+
<section class="card">
|
|
506
1151
|
<div class="card-header">
|
|
507
|
-
<h2>系统状态</h2>
|
|
508
|
-
<button class="btn btn-primary btn-sm" onclick="
|
|
1152
|
+
<h2 data-i18n="stats.title">系统状态</h2>
|
|
1153
|
+
<button class="btn btn-primary btn-sm" onclick="refreshAll()">
|
|
1154
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
1155
|
+
<path d="M23 4v6h-6M1 20v-6h6"/>
|
|
1156
|
+
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
|
|
1157
|
+
</svg>
|
|
1158
|
+
<span data-i18n="common.refresh">刷新</span>
|
|
1159
|
+
</button>
|
|
509
1160
|
</div>
|
|
510
1161
|
<div class="card-body">
|
|
511
1162
|
<div class="stats-grid" id="statsGrid">
|
|
512
1163
|
<!-- 状态卡片将通过JavaScript动态生成 -->
|
|
513
1164
|
</div>
|
|
514
1165
|
</div>
|
|
515
|
-
</
|
|
1166
|
+
</section>
|
|
1167
|
+
|
|
1168
|
+
<!-- 服务资源 -->
|
|
1169
|
+
<section class="card">
|
|
1170
|
+
<div class="card-header">
|
|
1171
|
+
<h2 data-i18n="resources.title">服务资源</h2>
|
|
1172
|
+
<span class="resource-sampled">
|
|
1173
|
+
<span class="live-dot" aria-hidden="true"></span>
|
|
1174
|
+
<span id="resourceSampledAt" data-i18n="resources.waiting">等待采样…</span>
|
|
1175
|
+
</span>
|
|
1176
|
+
</div>
|
|
1177
|
+
<div class="card-body">
|
|
1178
|
+
<div class="resource-grid">
|
|
1179
|
+
<div class="resource-card">
|
|
1180
|
+
<div class="resource-card-head">
|
|
1181
|
+
<span class="resource-label" data-i18n="resources.cpu">CPU 使用率</span>
|
|
1182
|
+
<strong class="resource-value" id="cpuValue">0.0%</strong>
|
|
1183
|
+
</div>
|
|
1184
|
+
<div class="resource-track" id="cpuBar" role="progressbar"
|
|
1185
|
+
data-i18n-aria-label="resources.cpu" aria-label="CPU usage"
|
|
1186
|
+
aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">
|
|
1187
|
+
<span class="resource-fill" id="cpuFill"></span>
|
|
1188
|
+
</div>
|
|
1189
|
+
<p class="resource-hint" id="cpuHint">—</p>
|
|
1190
|
+
</div>
|
|
1191
|
+
|
|
1192
|
+
<div class="resource-card">
|
|
1193
|
+
<div class="resource-card-head">
|
|
1194
|
+
<span class="resource-label" data-i18n="resources.memory">内存占用</span>
|
|
1195
|
+
<strong class="resource-value" id="memoryValue">0 B</strong>
|
|
1196
|
+
</div>
|
|
1197
|
+
<div class="resource-track" id="memoryBar" role="progressbar"
|
|
1198
|
+
data-i18n-aria-label="resources.memory" aria-label="Memory usage"
|
|
1199
|
+
aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">
|
|
1200
|
+
<span class="resource-fill resource-fill--memory" id="memoryFill"></span>
|
|
1201
|
+
</div>
|
|
1202
|
+
<p class="resource-hint" id="memoryHint">—</p>
|
|
1203
|
+
</div>
|
|
1204
|
+
|
|
1205
|
+
<div class="resource-card">
|
|
1206
|
+
<div class="resource-card-head">
|
|
1207
|
+
<span class="resource-label" data-i18n="resources.traffic">实时流量</span>
|
|
1208
|
+
</div>
|
|
1209
|
+
<div class="resource-traffic">
|
|
1210
|
+
<span class="up">↑ <strong id="trafficUp">0 B/s</strong></span>
|
|
1211
|
+
<span class="down">↓ <strong id="trafficDown">0 B/s</strong></span>
|
|
1212
|
+
</div>
|
|
1213
|
+
<p class="resource-hint" id="trafficHint">—</p>
|
|
1214
|
+
</div>
|
|
1215
|
+
</div>
|
|
1216
|
+
</div>
|
|
1217
|
+
</section>
|
|
516
1218
|
|
|
517
1219
|
<!-- 端口映射管理 -->
|
|
518
|
-
<
|
|
1220
|
+
<section class="card">
|
|
519
1221
|
<div class="card-header">
|
|
520
|
-
<h2>端口映射管理</h2>
|
|
521
|
-
<button class="btn btn-success" onclick="showAddMappingModal()">+ 添加映射</button>
|
|
1222
|
+
<h2 data-i18n="mappings.title">端口映射管理</h2>
|
|
1223
|
+
<button class="btn btn-success btn-sm" onclick="showAddMappingModal()" data-i18n="mappings.add">+ 添加映射</button>
|
|
522
1224
|
</div>
|
|
523
1225
|
<div class="card-body">
|
|
524
|
-
<
|
|
525
|
-
<
|
|
526
|
-
<
|
|
527
|
-
<
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
1226
|
+
<div class="table-wrap">
|
|
1227
|
+
<table class="table">
|
|
1228
|
+
<thead>
|
|
1229
|
+
<tr>
|
|
1230
|
+
<th data-i18n="mappings.thName">名称</th>
|
|
1231
|
+
<th data-i18n="mappings.thListen">监听端口</th>
|
|
1232
|
+
<th data-i18n="mappings.thTarget">目标地址</th>
|
|
1233
|
+
<th data-i18n="mappings.thConnections">连接数</th>
|
|
1234
|
+
<th data-i18n="mappings.thTraffic">流量统计</th>
|
|
1235
|
+
<th data-i18n="mappings.thStatus">状态</th>
|
|
1236
|
+
<th data-i18n="mappings.thActions">操作</th>
|
|
1237
|
+
</tr>
|
|
1238
|
+
</thead>
|
|
1239
|
+
<tbody id="mappingsTable">
|
|
1240
|
+
<!-- 端口映射列表将通过JavaScript动态生成 -->
|
|
1241
|
+
</tbody>
|
|
1242
|
+
</table>
|
|
1243
|
+
</div>
|
|
540
1244
|
</div>
|
|
541
|
-
</
|
|
1245
|
+
</section>
|
|
542
1246
|
|
|
543
1247
|
<!-- 连接状态监控 -->
|
|
544
|
-
<
|
|
1248
|
+
<section class="card">
|
|
545
1249
|
<div class="card-header">
|
|
546
|
-
<h2>连接状态监控</h2>
|
|
547
|
-
<button class="btn btn-primary btn-sm" onclick="
|
|
1250
|
+
<h2 data-i18n="connections.title">连接状态监控</h2>
|
|
1251
|
+
<button class="btn btn-primary btn-sm" onclick="refreshAll()">
|
|
1252
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
1253
|
+
<path d="M23 4v6h-6M1 20v-6h6"/>
|
|
1254
|
+
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
|
|
1255
|
+
</svg>
|
|
1256
|
+
<span data-i18n="common.refresh">刷新</span>
|
|
1257
|
+
</button>
|
|
548
1258
|
</div>
|
|
549
1259
|
<div class="card-body">
|
|
550
1260
|
<div id="connectionsDisplay">
|
|
551
1261
|
<!-- 连接状态将通过JavaScript动态生成 -->
|
|
552
1262
|
</div>
|
|
553
1263
|
</div>
|
|
554
|
-
</
|
|
555
|
-
|
|
1264
|
+
</section>
|
|
1265
|
+
|
|
1266
|
+
<!-- 最近连接记录 -->
|
|
1267
|
+
<section class="card">
|
|
1268
|
+
<div class="card-header">
|
|
1269
|
+
<h2 data-i18n="history.title">最近连接记录</h2>
|
|
1270
|
+
<div style="display:flex;gap:0.5rem;align-items:center;flex-wrap:wrap;">
|
|
1271
|
+
<div class="select-wrap">
|
|
1272
|
+
<select id="historyFilter" onchange="loadHistory()" data-i18n-aria-label="history.thMapping">
|
|
1273
|
+
<option value="" data-i18n="history.allMappings">全部映射</option>
|
|
1274
|
+
</select>
|
|
1275
|
+
</div>
|
|
1276
|
+
<button class="btn btn-primary btn-sm" onclick="loadHistory()">
|
|
1277
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
1278
|
+
<path d="M23 4v6h-6M1 20v-6h6"/>
|
|
1279
|
+
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
|
|
1280
|
+
</svg>
|
|
1281
|
+
<span data-i18n="common.refresh">刷新</span>
|
|
1282
|
+
</button>
|
|
1283
|
+
</div>
|
|
1284
|
+
</div>
|
|
1285
|
+
<div class="card-body">
|
|
1286
|
+
<p class="history-hint" data-i18n="history.hint">记录已关闭连接的源地址、目的地址与持续时长,最多保留最近 500 条(服务重启后清空)。</p>
|
|
1287
|
+
<div class="table-wrap">
|
|
1288
|
+
<table class="table" style="min-width: 880px;">
|
|
1289
|
+
<thead>
|
|
1290
|
+
<tr>
|
|
1291
|
+
<th data-i18n="history.thClosedAt">关闭时间</th>
|
|
1292
|
+
<th data-i18n="history.thMapping">映射</th>
|
|
1293
|
+
<th data-i18n="history.thSource">源地址</th>
|
|
1294
|
+
<th data-i18n="history.thDestination">目的地址</th>
|
|
1295
|
+
<th data-i18n="history.thDuration">持续时长</th>
|
|
1296
|
+
<th data-i18n="history.thTraffic">流量</th>
|
|
1297
|
+
<th data-i18n="history.thReason">结束原因</th>
|
|
1298
|
+
</tr>
|
|
1299
|
+
</thead>
|
|
1300
|
+
<tbody id="historyTable">
|
|
1301
|
+
<!-- 历史记录将通过JavaScript动态生成 -->
|
|
1302
|
+
</tbody>
|
|
1303
|
+
</table>
|
|
1304
|
+
</div>
|
|
1305
|
+
</div>
|
|
1306
|
+
</section>
|
|
1307
|
+
</main>
|
|
556
1308
|
|
|
557
|
-
<!--
|
|
1309
|
+
<!-- 添加 / 编辑映射模态框 -->
|
|
558
1310
|
<div id="addMappingModal" class="modal">
|
|
559
1311
|
<div class="modal-content">
|
|
560
1312
|
<div class="modal-header">
|
|
561
|
-
<h3>添加端口映射</h3>
|
|
562
|
-
<
|
|
1313
|
+
<h3 id="modalTitle" data-i18n="modal.addTitle">添加端口映射</h3>
|
|
1314
|
+
<button type="button" class="close" onclick="closeModal()" aria-label="Close">×</button>
|
|
563
1315
|
</div>
|
|
564
1316
|
<form id="addMappingForm">
|
|
565
1317
|
<div class="modal-body">
|
|
566
1318
|
<div class="form-group">
|
|
567
|
-
<label for="mappingName">映射名称</label>
|
|
568
|
-
<input type="text" id="mappingName" name="name" placeholder="例如:Web服务" required>
|
|
1319
|
+
<label for="mappingName" data-i18n="modal.name">映射名称</label>
|
|
1320
|
+
<input type="text" id="mappingName" name="name" maxlength="100" data-i18n-placeholder="modal.namePlaceholder" placeholder="例如:Web服务" required>
|
|
569
1321
|
</div>
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
<
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
<
|
|
1322
|
+
|
|
1323
|
+
<div class="form-row">
|
|
1324
|
+
<div class="form-group">
|
|
1325
|
+
<label for="listenHost" data-i18n="modal.listenHost">监听地址</label>
|
|
1326
|
+
<input type="text" id="listenHost" name="listenHost" value="0.0.0.0" data-i18n-placeholder="modal.listenHostPlaceholder" placeholder="0.0.0.0 表示所有网卡" required>
|
|
1327
|
+
</div>
|
|
1328
|
+
<div class="form-group">
|
|
1329
|
+
<label for="listenPort" data-i18n="modal.listenPort">监听端口</label>
|
|
1330
|
+
<input type="number" id="listenPort" name="listenPort" min="1" max="65535" data-i18n-placeholder="modal.portPlaceholder" placeholder="1-65535" required>
|
|
1331
|
+
</div>
|
|
577
1332
|
</div>
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
<
|
|
1333
|
+
|
|
1334
|
+
<div class="form-row">
|
|
1335
|
+
<div class="form-group">
|
|
1336
|
+
<label for="targetHost" data-i18n="modal.targetHost">目标主机</label>
|
|
1337
|
+
<input type="text" id="targetHost" name="targetHost" data-i18n-placeholder="modal.targetHostPlaceholder" placeholder="IP地址或域名" required>
|
|
1338
|
+
</div>
|
|
1339
|
+
<div class="form-group">
|
|
1340
|
+
<label for="targetPort" data-i18n="modal.targetPort">目标端口</label>
|
|
1341
|
+
<input type="number" id="targetPort" name="targetPort" min="1" max="65535" data-i18n-placeholder="modal.portPlaceholder" placeholder="1-65535" required>
|
|
1342
|
+
</div>
|
|
581
1343
|
</div>
|
|
1344
|
+
|
|
582
1345
|
<div class="error-message" id="modalErrorMessage"></div>
|
|
583
1346
|
<div class="success-message" id="modalSuccessMessage"></div>
|
|
584
1347
|
</div>
|
|
585
1348
|
<div class="modal-footer">
|
|
586
|
-
<button type="button" class="btn btn-cancel" onclick="closeModal()">取消</button>
|
|
587
|
-
<button type="submit" class="btn btn-success">确认添加</button>
|
|
1349
|
+
<button type="button" class="btn btn-cancel" onclick="closeModal()" data-i18n="common.cancel">取消</button>
|
|
1350
|
+
<button type="submit" class="btn btn-success" id="modalSubmitBtn" data-i18n="modal.confirmAdd">确认添加</button>
|
|
588
1351
|
</div>
|
|
589
1352
|
</form>
|
|
590
1353
|
</div>
|
|
591
1354
|
</div>
|
|
592
1355
|
|
|
1356
|
+
<div id="toastContainer" class="toast-container" role="status" aria-live="polite"></div>
|
|
1357
|
+
|
|
593
1358
|
<script>
|
|
594
1359
|
let mappings = [];
|
|
595
|
-
let connections = {};
|
|
1360
|
+
let connections = { byPort: [] };
|
|
1361
|
+
let connectionHistory = [];
|
|
596
1362
|
let status = {};
|
|
1363
|
+
let editingId = null;
|
|
1364
|
+
let refreshTick = 0;
|
|
1365
|
+
let historyFilterSignature = '';
|
|
1366
|
+
|
|
1367
|
+
// 统一翻译入口
|
|
1368
|
+
function t(key, params) {
|
|
1369
|
+
return window.I18n ? window.I18n.t(key, params) : key;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
// 转义 HTML,避免映射名称等内容造成 XSS
|
|
1373
|
+
function escapeHtml(value) {
|
|
1374
|
+
return String(value === undefined || value === null ? '' : value)
|
|
1375
|
+
.replace(/&/g, '&')
|
|
1376
|
+
.replace(/</g, '<')
|
|
1377
|
+
.replace(/>/g, '>')
|
|
1378
|
+
.replace(/"/g, '"')
|
|
1379
|
+
.replace(/'/g, ''');
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
// IPv6 地址加方括号
|
|
1383
|
+
function formatEndpoint(host, port) {
|
|
1384
|
+
const value = String(host === undefined || host === null ? '' : host);
|
|
1385
|
+
return value.includes(':') ? `[${value}]:${port}` : `${value}:${port}`;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
// 统一 API 请求:携带语言并处理登录失效
|
|
1389
|
+
async function api(url, options = {}) {
|
|
1390
|
+
const headers = Object.assign(
|
|
1391
|
+
{ 'X-Language': t ? (window.I18n ? window.I18n.getLanguage() : 'zh-CN') : 'zh-CN' },
|
|
1392
|
+
options.headers || {}
|
|
1393
|
+
);
|
|
1394
|
+
|
|
1395
|
+
const response = await fetch(url, {
|
|
1396
|
+
credentials: 'same-origin',
|
|
1397
|
+
...options,
|
|
1398
|
+
headers
|
|
1399
|
+
});
|
|
1400
|
+
|
|
1401
|
+
if (response.status === 401) {
|
|
1402
|
+
window.location.href = '/login';
|
|
1403
|
+
throw new Error(t('common.sessionExpired'));
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
let data;
|
|
1407
|
+
try {
|
|
1408
|
+
data = await response.json();
|
|
1409
|
+
} catch (error) {
|
|
1410
|
+
data = { success: false, message: t('common.invalidResponse') };
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
if (!response.ok && data.message === undefined) {
|
|
1414
|
+
data.message = t('common.requestFailed', { status: response.status });
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
return data;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
// 轻量提示条
|
|
1421
|
+
function showToast(message, type) {
|
|
1422
|
+
const container = document.getElementById('toastContainer');
|
|
1423
|
+
if (!container) return;
|
|
1424
|
+
|
|
1425
|
+
const toast = document.createElement('div');
|
|
1426
|
+
toast.className = 'toast' + (type ? ` toast-${type}` : '');
|
|
1427
|
+
toast.textContent = message;
|
|
1428
|
+
container.appendChild(toast);
|
|
1429
|
+
|
|
1430
|
+
requestAnimationFrame(() => toast.classList.add('show'));
|
|
1431
|
+
|
|
1432
|
+
setTimeout(() => {
|
|
1433
|
+
toast.classList.remove('show');
|
|
1434
|
+
setTimeout(() => toast.remove(), 250);
|
|
1435
|
+
}, 3600);
|
|
1436
|
+
}
|
|
597
1437
|
|
|
598
|
-
// 页面加载时初始化数据
|
|
599
1438
|
document.addEventListener('DOMContentLoaded', function() {
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
loadStatus();
|
|
1439
|
+
document.title = t('page.dashboard');
|
|
1440
|
+
refreshAll();
|
|
603
1441
|
|
|
604
|
-
// 每5
|
|
1442
|
+
// 每 5 秒自动刷新;页面隐藏时暂停
|
|
605
1443
|
setInterval(() => {
|
|
606
|
-
|
|
607
|
-
|
|
1444
|
+
if (!document.hidden) {
|
|
1445
|
+
refreshAll();
|
|
1446
|
+
}
|
|
608
1447
|
}, 5000);
|
|
1448
|
+
|
|
1449
|
+
document.addEventListener('visibilitychange', () => {
|
|
1450
|
+
if (!document.hidden) {
|
|
1451
|
+
refreshAll();
|
|
1452
|
+
}
|
|
1453
|
+
});
|
|
609
1454
|
});
|
|
610
1455
|
|
|
611
|
-
//
|
|
1456
|
+
// 切换语言后重新渲染动态内容
|
|
1457
|
+
document.addEventListener('languagechange', function() {
|
|
1458
|
+
document.title = t('page.dashboard');
|
|
1459
|
+
historyFilterSignature = '';
|
|
1460
|
+
renderMappings();
|
|
1461
|
+
renderConnections();
|
|
1462
|
+
// 关闭原因由服务端按请求语言返回,切换语言后需重新拉取
|
|
1463
|
+
loadHistory();
|
|
1464
|
+
renderStatus();
|
|
1465
|
+
|
|
1466
|
+
// 模态框打开时同步标题与按钮
|
|
1467
|
+
const modal = document.getElementById('addMappingModal');
|
|
1468
|
+
if (modal && modal.style.display === 'block') {
|
|
1469
|
+
document.getElementById('modalTitle').textContent = editingId ? t('modal.editTitle') : t('modal.addTitle');
|
|
1470
|
+
document.getElementById('modalSubmitBtn').textContent = editingId ? t('modal.saveChanges') : t('modal.confirmAdd');
|
|
1471
|
+
}
|
|
1472
|
+
});
|
|
1473
|
+
|
|
1474
|
+
function refreshAll() {
|
|
1475
|
+
loadMappings();
|
|
1476
|
+
loadConnections();
|
|
1477
|
+
loadStatus();
|
|
1478
|
+
|
|
1479
|
+
// 历史记录变化较慢,每 15 秒刷新一次即可
|
|
1480
|
+
if (refreshTick++ % 3 === 0) {
|
|
1481
|
+
loadHistory();
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
|
|
612
1485
|
async function loadMappings() {
|
|
613
1486
|
try {
|
|
614
|
-
const
|
|
615
|
-
const data = await response.json();
|
|
616
|
-
|
|
1487
|
+
const data = await api('/api/mappings');
|
|
617
1488
|
if (data.success) {
|
|
618
1489
|
mappings = data.data;
|
|
619
1490
|
renderMappings();
|
|
@@ -623,12 +1494,9 @@
|
|
|
623
1494
|
}
|
|
624
1495
|
}
|
|
625
1496
|
|
|
626
|
-
// 加载连接状态
|
|
627
1497
|
async function loadConnections() {
|
|
628
1498
|
try {
|
|
629
|
-
const
|
|
630
|
-
const data = await response.json();
|
|
631
|
-
|
|
1499
|
+
const data = await api('/api/connections');
|
|
632
1500
|
if (data.success) {
|
|
633
1501
|
connections = data.data;
|
|
634
1502
|
renderConnections();
|
|
@@ -638,12 +1506,28 @@
|
|
|
638
1506
|
}
|
|
639
1507
|
}
|
|
640
1508
|
|
|
641
|
-
|
|
642
|
-
async function loadStatus() {
|
|
1509
|
+
async function loadHistory() {
|
|
643
1510
|
try {
|
|
644
|
-
const
|
|
645
|
-
const
|
|
1511
|
+
const filter = document.getElementById('historyFilter');
|
|
1512
|
+
const mappingId = filter ? filter.value : '';
|
|
1513
|
+
const params = new URLSearchParams({ limit: '100' });
|
|
1514
|
+
if (mappingId) {
|
|
1515
|
+
params.set('mappingId', mappingId);
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
const data = await api(`/api/connections/history?${params.toString()}`);
|
|
1519
|
+
if (data.success) {
|
|
1520
|
+
connectionHistory = data.data.items;
|
|
1521
|
+
renderHistory();
|
|
1522
|
+
}
|
|
1523
|
+
} catch (error) {
|
|
1524
|
+
console.error('加载连接记录失败:', error);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
646
1527
|
|
|
1528
|
+
async function loadStatus() {
|
|
1529
|
+
try {
|
|
1530
|
+
const data = await api('/api/status');
|
|
647
1531
|
if (data.success) {
|
|
648
1532
|
status = data.data;
|
|
649
1533
|
renderStatus();
|
|
@@ -653,51 +1537,167 @@
|
|
|
653
1537
|
}
|
|
654
1538
|
}
|
|
655
1539
|
|
|
656
|
-
//
|
|
1540
|
+
// 更新历史记录筛选下拉框(仅在映射列表变化时重建)
|
|
1541
|
+
function updateHistoryFilter() {
|
|
1542
|
+
const select = document.getElementById('historyFilter');
|
|
1543
|
+
if (!select) return;
|
|
1544
|
+
|
|
1545
|
+
const signature = mappings
|
|
1546
|
+
.map(mapping => `${mapping.id}:${mapping.name}:${mapping.listen.host}:${mapping.listen.port}`)
|
|
1547
|
+
.join('|');
|
|
1548
|
+
|
|
1549
|
+
if (signature === historyFilterSignature) return;
|
|
1550
|
+
historyFilterSignature = signature;
|
|
1551
|
+
|
|
1552
|
+
const current = select.value;
|
|
1553
|
+
const options = mappings.map(mapping =>
|
|
1554
|
+
`<option value="${escapeHtml(mapping.id)}">${escapeHtml(mapping.name)} (${escapeHtml(formatEndpoint(mapping.listen.host, mapping.listen.port))})</option>`
|
|
1555
|
+
).join('');
|
|
1556
|
+
|
|
1557
|
+
select.innerHTML = `<option value="">${escapeHtml(t('history.allMappings'))}</option>${options}`;
|
|
1558
|
+
|
|
1559
|
+
if (mappings.some(mapping => mapping.id === current)) {
|
|
1560
|
+
select.value = current;
|
|
1561
|
+
} else {
|
|
1562
|
+
select.value = '';
|
|
1563
|
+
if (current !== '') {
|
|
1564
|
+
loadHistory();
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
|
|
657
1569
|
function formatBytes(bytes) {
|
|
658
|
-
|
|
1570
|
+
const value = Number(bytes) || 0;
|
|
1571
|
+
if (value <= 0) return '0 B';
|
|
1572
|
+
|
|
659
1573
|
const k = 1024;
|
|
660
|
-
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
661
|
-
const i = Math.floor(Math.log(
|
|
662
|
-
return parseFloat((
|
|
1574
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
1575
|
+
const i = Math.min(sizes.length - 1, Math.floor(Math.log(value) / Math.log(k)));
|
|
1576
|
+
return parseFloat((value / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
function formatDuration(seconds) {
|
|
1580
|
+
const value = Math.max(0, Math.floor(Number(seconds) || 0));
|
|
1581
|
+
if (value < 60) return `${value}s`;
|
|
1582
|
+
if (value < 3600) return `${Math.floor(value / 60)}m ${value % 60}s`;
|
|
1583
|
+
if (value < 86400) return `${Math.floor(value / 3600)}h ${Math.floor((value % 3600) / 60)}m`;
|
|
1584
|
+
return `${Math.floor(value / 86400)}d ${Math.floor((value % 86400) / 3600)}h`;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
function formatUptime(seconds) {
|
|
1588
|
+
const value = Number(seconds) || 0;
|
|
1589
|
+
if (value < 60) return Math.floor(value) + 's';
|
|
1590
|
+
if (value < 3600) return Math.floor(value / 60) + 'm';
|
|
1591
|
+
if (value < 86400) return Math.floor(value / 3600) + 'h ' + Math.floor((value % 3600) / 60) + 'm';
|
|
1592
|
+
return Math.floor(value / 86400) + 'd ' + Math.floor((value % 86400) / 3600) + 'h';
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
// 渲染服务进程自身的资源占用(CPU / 内存 / 实时流量)
|
|
1596
|
+
function renderResources() {
|
|
1597
|
+
const resources = status.resources;
|
|
1598
|
+
if (!resources) return;
|
|
1599
|
+
|
|
1600
|
+
const cpuPercent = Math.max(0, Math.min(100, Number(resources.cpuPercent) || 0));
|
|
1601
|
+
const cpuValue = document.getElementById('cpuValue');
|
|
1602
|
+
const cpuFill = document.getElementById('cpuFill');
|
|
1603
|
+
const cpuBar = document.getElementById('cpuBar');
|
|
1604
|
+
const cpuHint = document.getElementById('cpuHint');
|
|
1605
|
+
if (cpuValue) cpuValue.textContent = cpuPercent.toFixed(1) + '%';
|
|
1606
|
+
if (cpuFill) cpuFill.style.width = cpuPercent + '%';
|
|
1607
|
+
if (cpuBar) cpuBar.setAttribute('aria-valuenow', cpuPercent.toFixed(1));
|
|
1608
|
+
if (cpuHint) cpuHint.textContent = t('resources.cpuHint', { count: resources.cpuCount || 1 });
|
|
1609
|
+
|
|
1610
|
+
const memory = resources.memory || {};
|
|
1611
|
+
const rss = Number(memory.rss) || 0;
|
|
1612
|
+
const systemTotal = Number(memory.systemTotal) || 0;
|
|
1613
|
+
const memoryPercent = systemTotal > 0 ? Math.min(100, (rss / systemTotal) * 100) : 0;
|
|
1614
|
+
const memoryValue = document.getElementById('memoryValue');
|
|
1615
|
+
const memoryFill = document.getElementById('memoryFill');
|
|
1616
|
+
const memoryBar = document.getElementById('memoryBar');
|
|
1617
|
+
const memoryHint = document.getElementById('memoryHint');
|
|
1618
|
+
if (memoryValue) memoryValue.textContent = formatBytes(rss);
|
|
1619
|
+
if (memoryFill) memoryFill.style.width = memoryPercent + '%';
|
|
1620
|
+
if (memoryBar) memoryBar.setAttribute('aria-valuenow', memoryPercent.toFixed(1));
|
|
1621
|
+
if (memoryHint) {
|
|
1622
|
+
memoryHint.textContent = t('resources.memoryHint', {
|
|
1623
|
+
percent: memoryPercent.toFixed(1),
|
|
1624
|
+
heap: formatBytes(memory.heapUsed)
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
const traffic = resources.traffic || {};
|
|
1629
|
+
const up = Number(traffic.receivedPerSecond) || 0;
|
|
1630
|
+
const down = Number(traffic.sentPerSecond) || 0;
|
|
1631
|
+
const trafficUp = document.getElementById('trafficUp');
|
|
1632
|
+
const trafficDown = document.getElementById('trafficDown');
|
|
1633
|
+
const trafficHint = document.getElementById('trafficHint');
|
|
1634
|
+
if (trafficUp) trafficUp.textContent = formatBytes(up) + '/s';
|
|
1635
|
+
if (trafficDown) trafficDown.textContent = formatBytes(down) + '/s';
|
|
1636
|
+
if (trafficHint) {
|
|
1637
|
+
trafficHint.textContent = t('resources.trafficHint', {
|
|
1638
|
+
up: formatBytes(traffic.totalReceived),
|
|
1639
|
+
down: formatBytes(traffic.totalSent)
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
const sampledAt = document.getElementById('resourceSampledAt');
|
|
1644
|
+
if (sampledAt) {
|
|
1645
|
+
if (resources.sampledAt) {
|
|
1646
|
+
const locale = I18n.getLanguage() === 'en-US' ? 'en-US' : 'zh-CN';
|
|
1647
|
+
const time = new Date(resources.sampledAt).toLocaleTimeString(locale, { hour12: false });
|
|
1648
|
+
sampledAt.textContent = t('resources.sampledAt', { time });
|
|
1649
|
+
} else {
|
|
1650
|
+
sampledAt.textContent = t('resources.waiting');
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
663
1653
|
}
|
|
664
1654
|
|
|
665
1655
|
// 渲染端口映射列表
|
|
666
1656
|
function renderMappings() {
|
|
667
1657
|
const tbody = document.getElementById('mappingsTable');
|
|
668
1658
|
tbody.innerHTML = '';
|
|
1659
|
+
updateHistoryFilter();
|
|
669
1660
|
|
|
670
1661
|
if (mappings.length === 0) {
|
|
671
|
-
tbody.innerHTML =
|
|
1662
|
+
tbody.innerHTML = `<tr><td colspan="7"><div class="empty-state">${escapeHtml(t('mappings.empty'))}</div></td></tr>`;
|
|
672
1663
|
return;
|
|
673
1664
|
}
|
|
674
1665
|
|
|
675
1666
|
mappings.forEach((mapping, index) => {
|
|
676
1667
|
const row = document.createElement('tr');
|
|
677
|
-
const
|
|
1668
|
+
const isRunning = mapping.status === 'running';
|
|
1669
|
+
const activeTraffic = mapping.connectionCount > 0
|
|
1670
|
+
? `<div class="active-traffic">${escapeHtml(t('mappings.currentTraffic', {
|
|
1671
|
+
up: formatBytes(mapping.bytesReceived),
|
|
1672
|
+
down: formatBytes(mapping.bytesSent)
|
|
1673
|
+
}))}</div>`
|
|
1674
|
+
: '';
|
|
1675
|
+
|
|
678
1676
|
row.innerHTML = `
|
|
679
|
-
<td><strong>${mapping.name}</strong></td>
|
|
680
|
-
<td><code>${mapping.listen.host
|
|
681
|
-
<td><code>${mapping.target.host
|
|
1677
|
+
<td><strong>${escapeHtml(mapping.name)}</strong></td>
|
|
1678
|
+
<td><code>${escapeHtml(formatEndpoint(mapping.listen.host, mapping.listen.port))}</code></td>
|
|
1679
|
+
<td><code>${escapeHtml(formatEndpoint(mapping.target.host, mapping.target.port))}</code></td>
|
|
682
1680
|
<td><span class="status-badge status-connected">${mapping.connectionCount || 0}</span></td>
|
|
683
1681
|
<td>
|
|
684
1682
|
<div class="traffic-info">
|
|
685
|
-
<span class="up">↑ ${formatBytes(mapping.
|
|
686
|
-
<span class="down">↓ ${formatBytes(mapping.
|
|
1683
|
+
<span class="up" title="${escapeHtml(t('mappings.totalUpload'))}">↑ ${formatBytes(mapping.totalBytesReceived)}</span><br>
|
|
1684
|
+
<span class="down" title="${escapeHtml(t('mappings.totalDownload'))}">↓ ${formatBytes(mapping.totalBytesSent)}</span>
|
|
1685
|
+
${activeTraffic}
|
|
687
1686
|
</div>
|
|
688
1687
|
</td>
|
|
689
1688
|
<td>
|
|
690
|
-
<span class="status-badge ${
|
|
691
|
-
${
|
|
1689
|
+
<span class="status-badge ${isRunning ? 'status-running' : 'status-stopped'}">
|
|
1690
|
+
${escapeHtml(isRunning ? t('common.running') : t('common.stopped'))}
|
|
692
1691
|
</span>
|
|
693
1692
|
</td>
|
|
694
1693
|
<td>
|
|
695
1694
|
<div class="action-buttons">
|
|
696
|
-
${
|
|
697
|
-
|
|
698
|
-
|
|
1695
|
+
<button class="btn btn-primary btn-sm" onclick="showEditMappingModal(${index})">${escapeHtml(t('common.edit'))}</button>
|
|
1696
|
+
${isRunning
|
|
1697
|
+
? `<button class="btn btn-warning btn-sm" onclick="stopMapping(${index})">${escapeHtml(t('common.stop'))}</button>`
|
|
1698
|
+
: `<button class="btn btn-success btn-sm" onclick="startMapping(${index})">${escapeHtml(t('common.start'))}</button>`
|
|
699
1699
|
}
|
|
700
|
-
<button class="btn btn-danger btn-sm" onclick="deleteMapping(
|
|
1700
|
+
<button class="btn btn-danger btn-sm" onclick="deleteMapping(${index})">${escapeHtml(t('common.delete'))}</button>
|
|
701
1701
|
</div>
|
|
702
1702
|
</td>
|
|
703
1703
|
`;
|
|
@@ -705,222 +1705,409 @@
|
|
|
705
1705
|
});
|
|
706
1706
|
}
|
|
707
1707
|
|
|
708
|
-
//
|
|
1708
|
+
// 渲染活跃连接
|
|
709
1709
|
function renderConnections() {
|
|
710
1710
|
const container = document.getElementById('connectionsDisplay');
|
|
711
1711
|
container.innerHTML = '';
|
|
712
1712
|
|
|
713
|
-
if (connections.byPort
|
|
714
|
-
connections.byPort.forEach(portStat => {
|
|
715
|
-
const portDiv = document.createElement('div');
|
|
716
|
-
portDiv.className = 'connection-card';
|
|
717
|
-
|
|
718
|
-
portDiv.innerHTML = `
|
|
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>
|
|
721
|
-
<div class="connection-details">
|
|
722
|
-
${portStat.connections.map(conn => `
|
|
723
|
-
<div class="connection-item">
|
|
724
|
-
<span class="status-badge status-connected">${conn.status}</span>
|
|
725
|
-
<span>${conn.clientAddress} → ${conn.targetAddress}</span>
|
|
726
|
-
<span style="margin-left:auto;color:#b2bec3;">↑${formatBytes(conn.bytesReceived)} ↓${formatBytes(conn.bytesSent)}</span>
|
|
727
|
-
</div>
|
|
728
|
-
`).join('')}
|
|
729
|
-
</div>
|
|
730
|
-
`;
|
|
731
|
-
|
|
732
|
-
container.appendChild(portDiv);
|
|
733
|
-
});
|
|
734
|
-
} else {
|
|
1713
|
+
if (!connections.byPort || connections.byPort.length === 0) {
|
|
735
1714
|
container.innerHTML = `
|
|
736
|
-
<div class="
|
|
1715
|
+
<div class="empty-state">
|
|
737
1716
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
738
1717
|
<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
1718
|
</svg>
|
|
740
|
-
<p
|
|
1719
|
+
<p>${escapeHtml(t('connections.empty'))}</p>
|
|
741
1720
|
</div>
|
|
742
1721
|
`;
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
connections.byPort.forEach(portStat => {
|
|
1726
|
+
const portDiv = document.createElement('div');
|
|
1727
|
+
portDiv.className = 'connection-card';
|
|
1728
|
+
|
|
1729
|
+
const items = portStat.connections.map(conn => {
|
|
1730
|
+
const startTime = Date.parse(conn.startTime);
|
|
1731
|
+
const durationSeconds = Number.isFinite(Number(conn.durationMs))
|
|
1732
|
+
? Number(conn.durationMs) / 1000
|
|
1733
|
+
: (Number.isFinite(startTime) ? Math.max(0, (Date.now() - startTime) / 1000) : 0);
|
|
1734
|
+
const statusText = conn.status === 'connected' ? t('common.connected') : t('common.connecting');
|
|
1735
|
+
|
|
1736
|
+
return `
|
|
1737
|
+
<div class="connection-item">
|
|
1738
|
+
<span class="status-badge status-connected">${escapeHtml(statusText)}</span>
|
|
1739
|
+
<span class="conn-endpoints">${escapeHtml(conn.clientAddress)} → ${escapeHtml(conn.targetAddress)}</span>
|
|
1740
|
+
<span class="conn-metrics">${formatDuration(durationSeconds)} · ↑${formatBytes(conn.bytesReceived)} ↓${formatBytes(conn.bytesSent)}</span>
|
|
1741
|
+
</div>
|
|
1742
|
+
`;
|
|
1743
|
+
}).join('');
|
|
1744
|
+
|
|
1745
|
+
const firstConn = portStat.connections[0] || {};
|
|
1746
|
+
const configuredListen = formatEndpoint(portStat.host, portStat.port);
|
|
1747
|
+
const actualListen = firstConn.listenAddress && firstConn.listenAddress !== configuredListen
|
|
1748
|
+
? `<span>${escapeHtml(t('connections.actualListenLabel'))} <code>${escapeHtml(firstConn.listenAddress)}</code></span>`
|
|
1749
|
+
: '';
|
|
1750
|
+
|
|
1751
|
+
portDiv.innerHTML = `
|
|
1752
|
+
<h4><span class="port-badge">${escapeHtml(configuredListen)}</span> ${escapeHtml(portStat.mappingName)}</h4>
|
|
1753
|
+
<div class="connection-meta">
|
|
1754
|
+
<span>${escapeHtml(t('connections.activeCount', { count: portStat.totalConnections }))}</span>
|
|
1755
|
+
${actualListen}
|
|
1756
|
+
</div>
|
|
1757
|
+
<div class="connection-details">${items}</div>
|
|
1758
|
+
`;
|
|
1759
|
+
|
|
1760
|
+
container.appendChild(portDiv);
|
|
1761
|
+
});
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
// 将服务端记录的关闭原因转换为当前界面语言
|
|
1765
|
+
function translateCloseReason(reason) {
|
|
1766
|
+
if (!window.I18n || window.I18n.getLanguage() !== 'en-US') {
|
|
1767
|
+
return reason || '-';
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
const map = {
|
|
1771
|
+
'客户端关闭': 'Client closed',
|
|
1772
|
+
'目标服务器关闭': 'Target closed',
|
|
1773
|
+
'目标连接超时': 'Target connect timeout',
|
|
1774
|
+
'目标连接空闲超时': 'Target idle timeout',
|
|
1775
|
+
'客户端空闲超时': 'Client idle timeout',
|
|
1776
|
+
'目标连接错误': 'Target connection error',
|
|
1777
|
+
'客户端连接错误': 'Client connection error',
|
|
1778
|
+
'端口映射已停止': 'Mapping stopped',
|
|
1779
|
+
'服务正在关闭': 'Server shutting down',
|
|
1780
|
+
'连接被主动断开': 'Closed by administrator'
|
|
1781
|
+
};
|
|
1782
|
+
|
|
1783
|
+
return map[reason] || reason || '-';
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
// 渲染最近连接记录
|
|
1787
|
+
function renderHistory() {
|
|
1788
|
+
const tbody = document.getElementById('historyTable');
|
|
1789
|
+
if (!tbody) return;
|
|
1790
|
+
tbody.innerHTML = '';
|
|
1791
|
+
|
|
1792
|
+
if (!connectionHistory || connectionHistory.length === 0) {
|
|
1793
|
+
tbody.innerHTML = `<tr><td colspan="7"><div class="empty-state">${escapeHtml(t('history.empty'))}</div></td></tr>`;
|
|
1794
|
+
return;
|
|
743
1795
|
}
|
|
1796
|
+
|
|
1797
|
+
const locale = window.I18n ? window.I18n.getLanguage() : 'zh-CN';
|
|
1798
|
+
|
|
1799
|
+
connectionHistory.forEach(item => {
|
|
1800
|
+
const endTime = new Date(item.endTime);
|
|
1801
|
+
const endText = Number.isNaN(endTime.getTime()) ? '-' : endTime.toLocaleString(locale);
|
|
1802
|
+
const durationSeconds = Number(item.durationMs || 0) / 1000;
|
|
1803
|
+
const statusText = item.status === 'connected' ? '' : t('history.notEstablished');
|
|
1804
|
+
|
|
1805
|
+
const row = document.createElement('tr');
|
|
1806
|
+
row.innerHTML = `
|
|
1807
|
+
<td>${escapeHtml(endText)}</td>
|
|
1808
|
+
<td>
|
|
1809
|
+
<strong>${escapeHtml(item.mappingName)}</strong>
|
|
1810
|
+
<div style="color:var(--text-muted);font-size:0.72rem;">${escapeHtml(item.listenAddress || '')}</div>
|
|
1811
|
+
</td>
|
|
1812
|
+
<td><code>${escapeHtml(item.clientAddress)}</code></td>
|
|
1813
|
+
<td><code>${escapeHtml(item.targetAddress)}</code></td>
|
|
1814
|
+
<td>${escapeHtml(formatDuration(durationSeconds))}${escapeHtml(statusText)}</td>
|
|
1815
|
+
<td><span class="up">↑ ${formatBytes(item.bytesReceived)}</span><br><span class="down">↓ ${formatBytes(item.bytesSent)}</span></td>
|
|
1816
|
+
<td>${escapeHtml(translateCloseReason(item.closeReason))}</td>
|
|
1817
|
+
`;
|
|
1818
|
+
tbody.appendChild(row);
|
|
1819
|
+
});
|
|
744
1820
|
}
|
|
745
1821
|
|
|
746
1822
|
// 渲染系统状态
|
|
747
1823
|
function renderStatus() {
|
|
748
1824
|
const container = document.getElementById('statsGrid');
|
|
749
|
-
const
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
uptimeStr = Math.floor(uptime / 60) + 'm';
|
|
755
|
-
} else {
|
|
756
|
-
uptimeStr = Math.floor(uptime / 3600) + 'h ' + Math.floor((uptime % 3600) / 60) + 'm';
|
|
1825
|
+
const username = status.username || t('header.welcomeFallback');
|
|
1826
|
+
|
|
1827
|
+
const welcome = document.getElementById('welcomeText');
|
|
1828
|
+
if (welcome) {
|
|
1829
|
+
welcome.textContent = t('header.welcome', { name: username });
|
|
757
1830
|
}
|
|
758
1831
|
|
|
1832
|
+
const avatar = document.getElementById('userAvatar');
|
|
1833
|
+
if (avatar) {
|
|
1834
|
+
avatar.textContent = String(username).trim().charAt(0).toUpperCase() || 'A';
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
const totalUp = mappings.reduce((sum, mapping) => sum + (Number(mapping.totalBytesReceived) || 0), 0);
|
|
1838
|
+
const totalDown = mappings.reduce((sum, mapping) => sum + (Number(mapping.totalBytesSent) || 0), 0);
|
|
1839
|
+
|
|
759
1840
|
container.innerHTML = `
|
|
760
|
-
<div class="stat-card">
|
|
761
|
-
<
|
|
762
|
-
|
|
1841
|
+
<div class="stat-card stat-card--indigo">
|
|
1842
|
+
<div class="stat-icon">
|
|
1843
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
1844
|
+
<polygon points="12 2 2 7 12 12 22 7 12 2"/>
|
|
1845
|
+
<polyline points="2 17 12 22 22 17"/>
|
|
1846
|
+
<polyline points="2 12 12 17 22 12"/>
|
|
1847
|
+
</svg>
|
|
1848
|
+
</div>
|
|
1849
|
+
<div>
|
|
1850
|
+
<h3>${status.totalMappings || 0}</h3>
|
|
1851
|
+
<p>${escapeHtml(t('stats.totalMappings'))}</p>
|
|
1852
|
+
</div>
|
|
1853
|
+
</div>
|
|
1854
|
+
<div class="stat-card stat-card--green">
|
|
1855
|
+
<div class="stat-icon">
|
|
1856
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
1857
|
+
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
|
|
1858
|
+
</svg>
|
|
1859
|
+
</div>
|
|
1860
|
+
<div>
|
|
1861
|
+
<h3>${status.activeMappings || 0}</h3>
|
|
1862
|
+
<p>${escapeHtml(t('stats.activeMappings'))}</p>
|
|
1863
|
+
</div>
|
|
763
1864
|
</div>
|
|
764
|
-
<div class="stat-card">
|
|
765
|
-
<
|
|
766
|
-
|
|
1865
|
+
<div class="stat-card stat-card--amber">
|
|
1866
|
+
<div class="stat-icon">
|
|
1867
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
1868
|
+
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
|
|
1869
|
+
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
|
|
1870
|
+
</svg>
|
|
1871
|
+
</div>
|
|
1872
|
+
<div>
|
|
1873
|
+
<h3>${status.totalConnections || 0}</h3>
|
|
1874
|
+
<p>${escapeHtml(t('stats.activeConnections'))}</p>
|
|
1875
|
+
</div>
|
|
767
1876
|
</div>
|
|
768
|
-
<div class="stat-card">
|
|
769
|
-
<
|
|
770
|
-
|
|
1877
|
+
<div class="stat-card stat-card--sky">
|
|
1878
|
+
<div class="stat-icon">
|
|
1879
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
1880
|
+
<circle cx="12" cy="12" r="10"/>
|
|
1881
|
+
<polyline points="12 6 12 12 16 14"/>
|
|
1882
|
+
</svg>
|
|
1883
|
+
</div>
|
|
1884
|
+
<div>
|
|
1885
|
+
<h3>${formatUptime(status.uptime)}</h3>
|
|
1886
|
+
<p>${escapeHtml(t('stats.uptime'))}</p>
|
|
1887
|
+
</div>
|
|
771
1888
|
</div>
|
|
772
|
-
<div class="stat-card">
|
|
773
|
-
<
|
|
774
|
-
|
|
1889
|
+
<div class="stat-card stat-card--violet">
|
|
1890
|
+
<div class="stat-icon">
|
|
1891
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
1892
|
+
<polyline points="17 1 21 5 17 9"/>
|
|
1893
|
+
<path d="M3 11V9a4 4 0 0 1 4-4h14"/>
|
|
1894
|
+
<polyline points="7 23 3 19 7 15"/>
|
|
1895
|
+
<path d="M21 13v2a4 4 0 0 1-4 4H3"/>
|
|
1896
|
+
</svg>
|
|
1897
|
+
</div>
|
|
1898
|
+
<div>
|
|
1899
|
+
<h3 class="small"><span>↑${formatBytes(totalUp)}</span><span>↓${formatBytes(totalDown)}</span></h3>
|
|
1900
|
+
<p>${escapeHtml(t('stats.totalTraffic'))}</p>
|
|
1901
|
+
</div>
|
|
775
1902
|
</div>
|
|
776
1903
|
`;
|
|
1904
|
+
|
|
1905
|
+
renderResources();
|
|
777
1906
|
}
|
|
778
1907
|
|
|
779
|
-
// 显示添加映射模态框
|
|
780
1908
|
function showAddMappingModal() {
|
|
781
|
-
|
|
1909
|
+
editingId = null;
|
|
1910
|
+
|
|
1911
|
+
const form = document.getElementById('addMappingForm');
|
|
1912
|
+
form.reset();
|
|
1913
|
+
document.getElementById('listenHost').value = '0.0.0.0';
|
|
1914
|
+
document.getElementById('modalTitle').textContent = t('modal.addTitle');
|
|
1915
|
+
document.getElementById('modalSubmitBtn').textContent = t('modal.confirmAdd');
|
|
1916
|
+
|
|
1917
|
+
showModal();
|
|
1918
|
+
document.getElementById('mappingName').focus();
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
function showEditMappingModal(index) {
|
|
1922
|
+
const mapping = mappings[index];
|
|
1923
|
+
if (!mapping) return;
|
|
1924
|
+
|
|
1925
|
+
editingId = mapping.id;
|
|
1926
|
+
|
|
1927
|
+
document.getElementById('mappingName').value = mapping.name || '';
|
|
1928
|
+
document.getElementById('listenHost').value = mapping.listen.host || '0.0.0.0';
|
|
1929
|
+
document.getElementById('listenPort').value = mapping.listen.port;
|
|
1930
|
+
document.getElementById('targetHost').value = mapping.target.host || '';
|
|
1931
|
+
document.getElementById('targetPort').value = mapping.target.port;
|
|
1932
|
+
document.getElementById('modalTitle').textContent = t('modal.editTitle');
|
|
1933
|
+
document.getElementById('modalSubmitBtn').textContent = t('modal.saveChanges');
|
|
1934
|
+
|
|
1935
|
+
showModal();
|
|
782
1936
|
document.getElementById('mappingName').focus();
|
|
783
1937
|
}
|
|
784
1938
|
|
|
785
|
-
|
|
1939
|
+
function showModal() {
|
|
1940
|
+
document.getElementById('addMappingModal').style.display = 'block';
|
|
1941
|
+
hideModalMessages();
|
|
1942
|
+
}
|
|
1943
|
+
|
|
786
1944
|
function closeModal() {
|
|
787
1945
|
document.getElementById('addMappingModal').style.display = 'none';
|
|
788
1946
|
document.getElementById('addMappingForm').reset();
|
|
1947
|
+
editingId = null;
|
|
1948
|
+
hideModalMessages();
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
function hideModalMessages() {
|
|
789
1952
|
document.getElementById('modalErrorMessage').style.display = 'none';
|
|
790
1953
|
document.getElementById('modalSuccessMessage').style.display = 'none';
|
|
791
1954
|
}
|
|
792
1955
|
|
|
793
|
-
|
|
1956
|
+
function showModalError(message) {
|
|
1957
|
+
const errorElement = document.getElementById('modalErrorMessage');
|
|
1958
|
+
errorElement.textContent = message;
|
|
1959
|
+
errorElement.style.display = 'block';
|
|
1960
|
+
document.getElementById('modalSuccessMessage').style.display = 'none';
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
// 添加 / 更新端口映射
|
|
794
1964
|
document.getElementById('addMappingForm').addEventListener('submit', async function(e) {
|
|
795
1965
|
e.preventDefault();
|
|
796
1966
|
|
|
797
1967
|
const formData = new FormData(e.target);
|
|
798
|
-
const
|
|
799
|
-
name: formData.get('name'),
|
|
1968
|
+
const payload = {
|
|
1969
|
+
name: String(formData.get('name') || '').trim(),
|
|
800
1970
|
listen: {
|
|
801
|
-
host: '
|
|
802
|
-
port: parseInt(formData.get('listenPort'))
|
|
1971
|
+
host: String(formData.get('listenHost') || '').trim(),
|
|
1972
|
+
port: parseInt(formData.get('listenPort'), 10)
|
|
803
1973
|
},
|
|
804
1974
|
target: {
|
|
805
|
-
host: formData.get('targetHost'),
|
|
806
|
-
port: parseInt(formData.get('targetPort'))
|
|
1975
|
+
host: String(formData.get('targetHost') || '').trim(),
|
|
1976
|
+
port: parseInt(formData.get('targetPort'), 10)
|
|
807
1977
|
}
|
|
808
1978
|
};
|
|
809
1979
|
|
|
1980
|
+
const isEdit = editingId !== null;
|
|
1981
|
+
const mapping = isEdit ? mappings.find(item => item.id === editingId) : null;
|
|
1982
|
+
|
|
1983
|
+
if (isEdit && !mapping) {
|
|
1984
|
+
showModalError(t('modal.mappingMissing'));
|
|
1985
|
+
return;
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
const submitBtn = document.getElementById('modalSubmitBtn');
|
|
1989
|
+
const originalText = submitBtn.textContent;
|
|
1990
|
+
submitBtn.disabled = true;
|
|
1991
|
+
submitBtn.textContent = t('modal.saving');
|
|
1992
|
+
|
|
810
1993
|
try {
|
|
811
|
-
const
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
1994
|
+
const url = isEdit
|
|
1995
|
+
? `/api/mappings/${encodeURIComponent(editingId)}`
|
|
1996
|
+
: '/api/mappings';
|
|
1997
|
+
|
|
1998
|
+
const data = await api(url, {
|
|
1999
|
+
method: isEdit ? 'PUT' : 'POST',
|
|
2000
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2001
|
+
body: JSON.stringify(payload)
|
|
817
2002
|
});
|
|
818
2003
|
|
|
819
|
-
const data = await response.json();
|
|
820
|
-
|
|
821
2004
|
if (data.success) {
|
|
822
|
-
document.getElementById('modalSuccessMessage')
|
|
823
|
-
|
|
2005
|
+
const successElement = document.getElementById('modalSuccessMessage');
|
|
2006
|
+
successElement.textContent = isEdit ? t('modal.saveSuccess') : t('modal.addSuccess');
|
|
2007
|
+
successElement.style.display = 'block';
|
|
824
2008
|
document.getElementById('modalErrorMessage').style.display = 'none';
|
|
825
2009
|
|
|
826
2010
|
setTimeout(() => {
|
|
827
2011
|
closeModal();
|
|
828
|
-
|
|
829
|
-
},
|
|
2012
|
+
refreshAll();
|
|
2013
|
+
}, 600);
|
|
830
2014
|
} else {
|
|
831
|
-
|
|
832
|
-
document.getElementById('modalErrorMessage').style.display = 'block';
|
|
833
|
-
document.getElementById('modalSuccessMessage').style.display = 'none';
|
|
2015
|
+
showModalError(data.message || t('modal.operationFailed'));
|
|
834
2016
|
}
|
|
835
2017
|
} catch (error) {
|
|
836
|
-
|
|
837
|
-
|
|
2018
|
+
if (error.message !== t('common.sessionExpired')) {
|
|
2019
|
+
showModalError(t('common.networkError'));
|
|
2020
|
+
}
|
|
2021
|
+
} finally {
|
|
2022
|
+
submitBtn.disabled = false;
|
|
2023
|
+
submitBtn.textContent = originalText;
|
|
838
2024
|
}
|
|
839
2025
|
});
|
|
840
2026
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
if (!
|
|
2027
|
+
async function deleteMapping(index) {
|
|
2028
|
+
const mapping = mappings[index];
|
|
2029
|
+
if (!mapping) return;
|
|
2030
|
+
|
|
2031
|
+
if (!confirm(t('actions.deleteConfirm', { name: mapping.name }))) {
|
|
844
2032
|
return;
|
|
845
2033
|
}
|
|
846
2034
|
|
|
847
2035
|
try {
|
|
848
|
-
const
|
|
2036
|
+
const data = await api(`/api/mappings/${encodeURIComponent(mapping.id)}`, {
|
|
849
2037
|
method: 'DELETE'
|
|
850
2038
|
});
|
|
851
2039
|
|
|
852
|
-
const data = await response.json();
|
|
853
|
-
|
|
854
2040
|
if (data.success) {
|
|
855
|
-
|
|
2041
|
+
showToast(t('actions.deleted'), 'success');
|
|
2042
|
+
refreshAll();
|
|
856
2043
|
} else {
|
|
857
|
-
|
|
2044
|
+
showToast(t('actions.deleteFailed', { message: data.message }), 'error');
|
|
858
2045
|
}
|
|
859
2046
|
} catch (error) {
|
|
860
|
-
|
|
2047
|
+
if (error.message !== t('common.sessionExpired')) {
|
|
2048
|
+
showToast(t('common.networkError'), 'error');
|
|
2049
|
+
}
|
|
861
2050
|
}
|
|
862
2051
|
}
|
|
863
2052
|
|
|
864
|
-
|
|
865
|
-
|
|
2053
|
+
async function startMapping(index) {
|
|
2054
|
+
const mapping = mappings[index];
|
|
2055
|
+
if (!mapping) return;
|
|
2056
|
+
|
|
866
2057
|
try {
|
|
867
|
-
const
|
|
2058
|
+
const data = await api(`/api/mappings/${encodeURIComponent(mapping.id)}/start`, {
|
|
868
2059
|
method: 'POST'
|
|
869
2060
|
});
|
|
870
2061
|
|
|
871
|
-
const data = await response.json();
|
|
872
|
-
|
|
873
2062
|
if (data.success) {
|
|
874
|
-
|
|
2063
|
+
showToast(t('actions.started'), 'success');
|
|
2064
|
+
refreshAll();
|
|
875
2065
|
} else {
|
|
876
|
-
|
|
2066
|
+
showToast(t('actions.startFailed', { message: data.message }), 'error');
|
|
877
2067
|
}
|
|
878
2068
|
} catch (error) {
|
|
879
|
-
|
|
2069
|
+
if (error.message !== t('common.sessionExpired')) {
|
|
2070
|
+
showToast(t('common.networkError'), 'error');
|
|
2071
|
+
}
|
|
880
2072
|
}
|
|
881
2073
|
}
|
|
882
2074
|
|
|
883
|
-
|
|
884
|
-
|
|
2075
|
+
async function stopMapping(index) {
|
|
2076
|
+
const mapping = mappings[index];
|
|
2077
|
+
if (!mapping) return;
|
|
2078
|
+
|
|
885
2079
|
try {
|
|
886
|
-
const
|
|
2080
|
+
const data = await api(`/api/mappings/${encodeURIComponent(mapping.id)}/stop`, {
|
|
887
2081
|
method: 'POST'
|
|
888
2082
|
});
|
|
889
2083
|
|
|
890
|
-
const data = await response.json();
|
|
891
|
-
|
|
892
2084
|
if (data.success) {
|
|
893
|
-
|
|
2085
|
+
showToast(t('actions.stopped'), 'success');
|
|
2086
|
+
refreshAll();
|
|
894
2087
|
} else {
|
|
895
|
-
|
|
2088
|
+
showToast(t('actions.stopFailed', { message: data.message }), 'error');
|
|
896
2089
|
}
|
|
897
2090
|
} catch (error) {
|
|
898
|
-
|
|
2091
|
+
if (error.message !== t('common.sessionExpired')) {
|
|
2092
|
+
showToast(t('common.networkError'), 'error');
|
|
2093
|
+
}
|
|
899
2094
|
}
|
|
900
2095
|
}
|
|
901
2096
|
|
|
902
|
-
// 刷新状态
|
|
903
|
-
function refreshStatus() {
|
|
904
|
-
loadStatus();
|
|
905
|
-
loadMappings();
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
// 刷新连接
|
|
909
|
-
function refreshConnections() {
|
|
910
|
-
loadConnections();
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
// 退出登录
|
|
914
2097
|
async function logout() {
|
|
915
2098
|
try {
|
|
916
|
-
await fetch('/api/logout', {
|
|
917
|
-
|
|
2099
|
+
await fetch('/api/logout', {
|
|
2100
|
+
method: 'POST',
|
|
2101
|
+
credentials: 'same-origin',
|
|
2102
|
+
headers: { 'X-Language': window.I18n ? window.I18n.getLanguage() : 'zh-CN' }
|
|
2103
|
+
});
|
|
918
2104
|
} catch (error) {
|
|
919
2105
|
console.error('退出登录失败:', error);
|
|
920
2106
|
}
|
|
2107
|
+
window.location.href = '/login';
|
|
921
2108
|
}
|
|
922
2109
|
|
|
923
|
-
// ESC
|
|
2110
|
+
// ESC 关闭模态框
|
|
924
2111
|
document.addEventListener('keydown', function(e) {
|
|
925
2112
|
if (e.key === 'Escape') {
|
|
926
2113
|
closeModal();
|