node-red-contrib-fox-admin 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,563 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Fox Control | 网络管理</title>
7
+ <link rel="icon" href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2MCA2MCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJGYWVsaW5rIEljb24iPg0KICA8c3R5bGU+DQogICAgOnJvb3Qgew0KICAgICAgLS1jLTMwMDogIzkzQzVGRDsNCiAgICAgIC0tYy00MDA6ICM2MEE1RkE7DQogICAgICAtLWMtNTAwOiAjM0I4MkY2Ow0KICAgICAgLS1jLTYwMDogIzI1NjNFQjsNCiAgICAgIC0tYy03MDA6ICMxRDRFRDg7DQogICAgICAtLWMtODAwOiAjMUU0MEFGOw0KICAgIH0NCiAgPC9zdHlsZT4NCiAgDQogIDxnIGlkPSJmb3gtbG9nby12MyI+DQogICAgICA8IS0tID09PSBDRU5URVIgU1RSVUNUVVJFID09PSAtLT4NCiAgICAgIDwhLS0gTm9zZSBCcmlkZ2UgKERpYW1vbmQpIC0tPg0KICAgICAgPHBhdGggZD0iTSAzMCAxNSBMIDM2IDI1IEwgMzAgMzggTCAyNCAyNSBaIiBmaWxsPSJ2YXIoLS1jLTMwMCkiIC8+DQogICAgICANCiAgICAgIDwhLS0gU25vdXQgVGlwIChTaGFycCBWKSAtLT4NCiAgICAgIDxwYXRoIGQ9Ik0gMjQgMjUgTCAzMCAzOCBMIDMwIDU1IFoiIGZpbGw9InZhcigtLWMtNjAwKSIgLz4gPCEtLSBMZWZ0IFNoYWRvdyAtLT4NCiAgICAgIDxwYXRoIGQ9Ik0gMzYgMjUgTCAzMCAzOCBMIDMwIDU1IFoiIGZpbGw9InZhcigtLWMtNTAwKSIgLz4gPCEtLSBSaWdodCBCYXNlIC0tPg0KICAgICAgDQogICAgICA8IS0tID09PSBFQVJTIChMYXJnZSAmIEFsZXJ0KSA9PT0gLS0+DQogICAgICA8IS0tIExlZnQgRWFyIC0tPg0KICAgICAgPHBhdGggZD0iTSA4IDIgTCAyMCAyMCBMIDI2IDE1IFoiIGZpbGw9InZhcigtLWMtNTAwKSIgLz4NCiAgICAgIDxwYXRoIGQ9Ik0gMjYgMTUgTCAyMCAyMCBMIDI0IDI1IEwgMzAgMTUgWiIgZmlsbD0idmFyKC0tYy00MDApIiAvPiA8IS0tIElubmVyIGNvbm5lY3Rpb24gLS0+DQogICAgICANCiAgICAgIDwhLS0gUmlnaHQgRWFyIC0tPg0KICAgICAgPHBhdGggZD0iTSA1MiAyIEwgNDAgMjAgTCAzNCAxNSBaIiBmaWxsPSJ2YXIoLS1jLTQwMCkiIC8+DQogICAgICA8cGF0aCBkPSJNIDM0IDE1IEwgNDAgMjAgTCAzNiAyNSBMIDMwIDE1IFoiIGZpbGw9InZhcigtLWMtMzAwKSIgLz4gPCEtLSBJbm5lciBjb25uZWN0aW9uIC0tPg0KDQogICAgICA8IS0tID09PSBDSEVFS1MgKFRoZSAiRm94IiBTaGFwZSkgPT09IC0tPg0KICAgICAgPCEtLSBMZWZ0IENoZWVrIChXaWRlc3QgUG9pbnQpIC0tPg0KICAgICAgPHBhdGggZD0iTSA4IDIgTCAyIDIwIEwgMjQgMjUgTCAyMCAyMCBaIiBmaWxsPSJ2YXIoLS1jLTQwMCkiIC8+DQogICAgICA8cGF0aCBkPSJNIDIgMjAgTCAzMCA1NSBMIDI0IDI1IFoiIGZpbGw9InZhcigtLWMtNzAwKSIgLz4gPCEtLSBTaGFycCBKYXdsaW5lIFNoYWRvdyAtLT4NCiAgICAgIA0KICAgICAgPCEtLSBSaWdodCBDaGVlayAoV2lkZXN0IFBvaW50KSAtLT4NCiAgICAgIDxwYXRoIGQ9Ik0gNTIgMiBMIDU4IDIwIEwgMzYgMjUgTCA0MCAyMCBaIiBmaWxsPSJ2YXIoLS1jLTMwMCkiIC8+DQogICAgICA8cGF0aCBkPSJNIDU4IDIwIEwgMzAgNTUgTCAzNiAyNSBaIiBmaWxsPSJ2YXIoLS1jLTUwMCkiIC8+IDwhLS0gU2hhcnAgSmF3bGluZSBCYXNlIC0tPg0KICAgICAgDQogICAgICA8IS0tID09PSBFWUVTIChJbnRlZ3JhdGVkIERlcHRoKSA9PT0gLS0+DQogICAgICA8cGF0aCBkPSJNIDIwIDIwIEwgMjYgMTUgTCAyNCAyNSBaIiBmaWxsPSJ2YXIoLS1jLTgwMCkiIC8+DQogICAgICA8cGF0aCBkPSJNIDQwIDIwIEwgMzQgMTUgTCAzNiAyNSBaIiBmaWxsPSJ2YXIoLS1jLTcwMCkiIC8+DQogIDwvZz4NCjwvc3ZnPg==" type="image/x-icon">
8
+ <style>
9
+ :root {
10
+ --primary: #ed1c24;
11
+ --primary-rgb: 237, 28, 36;
12
+ --success: #52c41a;
13
+ --warning: #faad14;
14
+ --error: #ff4d4f;
15
+ --text-main: #ffffff;
16
+ --text-muted: rgba(255, 255, 255, 0.7);
17
+ --bg-gradient: linear-gradient(135deg, #1e2a38 0%, #2c3e50 100%);
18
+ --card-bg: rgba(255, 255, 255, 0.08);
19
+ --card-border: rgba(255, 255, 255, 0.15);
20
+ }
21
+
22
+ * { box-sizing: border-box; outline: none; margin: 0; padding: 0; }
23
+
24
+ body {
25
+ font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
26
+ background: var(--bg-gradient);
27
+ color: var(--text-main);
28
+ min-height: 100vh;
29
+ }
30
+
31
+ .sidebar {
32
+ position: fixed; left: 0; top: 0;
33
+ width: 250px; height: 100vh;
34
+ background: rgba(0, 0, 0, 0.3);
35
+ backdrop-filter: blur(20px);
36
+ border-right: 1px solid var(--card-border);
37
+ padding: 30px 20px;
38
+ z-index: 100;
39
+ }
40
+
41
+ .logo {
42
+ margin-bottom: 40px; display: flex; align-items: center; justify-content: center;
43
+ }
44
+
45
+ .logo-img {
46
+ width: 40px;
47
+ height: 40px;
48
+ }
49
+
50
+ .nav-item {
51
+ display: flex; align-items: center; gap: 12px;
52
+ padding: 14px 16px;
53
+ margin-bottom: 8px;
54
+ border-radius: 12px;
55
+ cursor: pointer;
56
+ transition: all 0.3s ease;
57
+ color: var(--text-muted);
58
+ text-decoration: none;
59
+ }
60
+
61
+ .nav-item:hover, .nav-item.active {
62
+ background: var(--primary);
63
+ color: white;
64
+ box-shadow: 0 4px 15px rgba(var(--primary-rgb), 0.3);
65
+ }
66
+
67
+ .nav-icon { font-size: 20px; }
68
+
69
+ .main-content {
70
+ margin-left: 250px;
71
+ padding: 30px;
72
+ }
73
+
74
+ .header {
75
+ display: flex; justify-content: space-between; align-items: center;
76
+ margin-bottom: 30px;
77
+ }
78
+
79
+ .header h1 { font-size: 28px; font-weight: 700; }
80
+
81
+ .logout-btn {
82
+ background: rgba(255, 77, 79, 0.2);
83
+ color: var(--error);
84
+ border: 1px solid var(--error);
85
+ padding: 10px 20px;
86
+ border-radius: 8px;
87
+ cursor: pointer;
88
+ transition: all 0.3s;
89
+ }
90
+
91
+ .logout-btn:hover {
92
+ background: var(--error);
93
+ color: white;
94
+ }
95
+
96
+ .card {
97
+ background: var(--card-bg);
98
+ backdrop-filter: blur(20px);
99
+ border: 1px solid var(--card-border);
100
+ border-radius: 16px;
101
+ padding: 24px;
102
+ margin-bottom: 20px;
103
+ }
104
+
105
+ .card h3 { margin-bottom: 20px; font-size: 18px; }
106
+
107
+ .interface-list {
108
+ display: grid;
109
+ gap: 15px;
110
+ }
111
+
112
+ .interface-item {
113
+ background: rgba(255, 255, 255, 0.03);
114
+ padding: 20px;
115
+ border-radius: 12px;
116
+ border: 1px solid rgba(255, 255, 255, 0.05);
117
+ transition: all 0.3s;
118
+ }
119
+
120
+ .interface-item:hover {
121
+ border-color: var(--primary);
122
+ transform: translateX(5px);
123
+ }
124
+
125
+ .interface-header {
126
+ display: flex;
127
+ justify-content: space-between;
128
+ align-items: center;
129
+ margin-bottom: 15px;
130
+ }
131
+
132
+ .interface-name {
133
+ font-size: 18px;
134
+ font-weight: 700;
135
+ display: flex;
136
+ align-items: center;
137
+ gap: 10px;
138
+ }
139
+
140
+ .interface-status {
141
+ padding: 5px 12px;
142
+ border-radius: 20px;
143
+ font-size: 12px;
144
+ font-weight: 600;
145
+ }
146
+
147
+ .status-online {
148
+ background: rgba(82, 196, 38, 0.2);
149
+ color: var(--success);
150
+ border: 1px solid var(--success);
151
+ }
152
+
153
+ .status-offline {
154
+ background: rgba(255, 77, 79, 0.2);
155
+ color: var(--error);
156
+ border: 1px solid var(--error);
157
+ }
158
+
159
+ .interface-details {
160
+ display: grid;
161
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
162
+ gap: 10px;
163
+ font-size: 14px;
164
+ }
165
+
166
+ .detail-item {
167
+ display: flex;
168
+ justify-content: space-between;
169
+ padding: 8px 12px;
170
+ background: rgba(0, 0, 0, 0.2);
171
+ border-radius: 8px;
172
+ }
173
+
174
+ .detail-label { color: var(--text-muted); }
175
+ .detail-value { font-weight: 600; }
176
+
177
+ .config-form {
178
+ display: grid;
179
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
180
+ gap: 15px;
181
+ margin-bottom: 20px;
182
+ }
183
+
184
+ .form-group { margin-bottom: 0; }
185
+
186
+ label {
187
+ display: block; margin-bottom: 8px;
188
+ font-size: 14px; font-weight: 600;
189
+ color: var(--text-muted);
190
+ }
191
+
192
+ input[type="text"], select {
193
+ width: 100%; padding: 12px 16px;
194
+ background: rgba(255, 255, 255, 0.05);
195
+ border: 2px solid rgba(255, 255, 255, 0.1);
196
+ border-radius: 10px;
197
+ color: white; font-size: 15px;
198
+ transition: all 0.3s ease;
199
+ }
200
+
201
+ input[type="text"]:focus, select:focus {
202
+ border-color: var(--primary);
203
+ background: rgba(255, 255, 255, 0.08);
204
+ }
205
+
206
+ select option {
207
+ background: #2c3e50;
208
+ color: white;
209
+ }
210
+
211
+ .btn {
212
+ padding: 12px 24px;
213
+ border: none;
214
+ border-radius: 10px;
215
+ font-size: 15px;
216
+ font-weight: 600;
217
+ cursor: pointer;
218
+ transition: all 0.3s ease;
219
+ }
220
+
221
+ .btn-primary {
222
+ background: var(--primary);
223
+ color: white;
224
+ box-shadow: 0 4px 15px rgba(var(--primary-rgb), 0.3);
225
+ }
226
+
227
+ .btn-primary:hover {
228
+ transform: translateY(-2px);
229
+ box-shadow: 0 6px 20px rgba(var(--primary-rgb), 0.4);
230
+ }
231
+
232
+ .btn-primary:disabled {
233
+ opacity: 0.5;
234
+ cursor: not-allowed;
235
+ transform: none;
236
+ }
237
+
238
+ .alert {
239
+ padding: 15px 20px;
240
+ border-radius: 10px;
241
+ margin-bottom: 20px;
242
+ display: none;
243
+ }
244
+
245
+ .alert-success {
246
+ background: rgba(82, 196, 38, 0.15);
247
+ border: 1px solid var(--success);
248
+ color: #b7eb8f;
249
+ }
250
+
251
+ .alert-error {
252
+ background: rgba(255, 77, 79, 0.15);
253
+ border: 1px solid var(--error);
254
+ color: #ffccc7;
255
+ }
256
+
257
+ .loading { text-align: center; padding: 40px; color: var(--text-muted); }
258
+ </style>
259
+ </head>
260
+ <body>
261
+ <div class="sidebar">
262
+ <div class="logo">
263
+ <img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2MCA2MCIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsPSJGYWVsaW5rIEljb24iPg0KICA8c3R5bGU+DQogICAgOnJvb3Qgew0KICAgICAgLS1jLTMwMDogIzkzQzVGRDsNCiAgICAgIC0tYy00MDA6ICM2MEE1RkE7DQogICAgICAtLWMtNTAwOiAjM0I4MkY2Ow0KICAgICAgLS1jLTYwMDogIzI1NjNFQjsNCiAgICAgIC0tYy03MDA6ICMxRDRFRDg7DQogICAgICAtLWMtODAwOiAjMUU0MEFGOw0KICAgIH0NCiAgPC9zdHlsZT4NCiAgDQogIDxnIGlkPSJmb3gtbG9nby12MyI+DQogICAgICA8IS0tID09PSBDRU5URVIgU1RSVUNUVVJFID09PSAtLT4NCiAgICAgIDwhLS0gTm9zZSBCcmlkZ2UgKERpYW1vbmQpIC0tPg0KICAgICAgPHBhdGggZD0iTSAzMCAxNSBMIDM2IDI1IEwgMzAgMzggTCAyNCAyNSBaIiBmaWxsPSJ2YXIoLS1jLTMwMCkiIC8+DQogICAgICANCiAgICAgIDwhLS0gU25vdXQgVGlwIChTaGFycCBWKSAtLT4NCiAgICAgIDxwYXRoIGQ9Ik0gMjQgMjUgTCAzMCAzOCBMIDMwIDU1IFoiIGZpbGw9InZhcigtLWMtNjAwKSIgLz4gPCEtLSBMZWZ0IFNoYWRvdyAtLT4NCiAgICAgIDxwYXRoIGQ9Ik0gMzYgMjUgTCAzMCAzOCBMIDMwIDU1IFoiIGZpbGw9InZhcigtLWMtNTAwKSIgLz4gPCEtLSBSaWdodCBCYXNlIC0tPg0KICAgICAgDQogICAgICA8IS0tID09PSBFQVJTIChMYXJnZSAmIEFsZXJ0KSA9PT0gLS0+DQogICAgICA8IS0tIExlZnQgRWFyIC0tPg0KICAgICAgPHBhdGggZD0iTSA4IDIgTCAyMCAyMCBMIDI2IDE1IFoiIGZpbGw9InZhcigtLWMtNTAwKSIgLz4NCiAgICAgIDxwYXRoIGQ9Ik0gMjYgMTUgTCAyMCAyMCBMIDI0IDI1IEwgMzAgMTUgWiIgZmlsbD0idmFyKC0tYy00MDApIiAvPiA8IS0tIElubmVyIGNvbm5lY3Rpb24gLS0+DQogICAgICANCiAgICAgIDwhLS0gUmlnaHQgRWFyIC0tPg0KICAgICAgPHBhdGggZD0iTSA1MiAyIEwgNDAgMjAgTCAzNCAxNSBaIiBmaWxsPSJ2YXIoLS1jLTQwMCkiIC8+DQogICAgICA8cGF0aCBkPSJNIDM0IDE1IEwgNDAgMjAgTCAzNiAyNSBMIDMwIDE1IFoiIGZpbGw9InZhcigtLWMtMzAwKSIgLz4gPCEtLSBJbm5lciBjb25uZWN0aW9uIC0tPg0KDQogICAgICA8IS0tID09PSBDSEVFS1MgKFRoZSAiRm94IiBTaGFwZSkgPT09IC0tPg0KICAgICAgPCEtLSBMZWZ0IENoZWVrIChXaWRlc3QgUG9pbnQpIC0tPg0KICAgICAgPHBhdGggZD0iTSA4IDIgTCAyIDIwIEwgMjQgMjUgTCAyMCAyMCBaIiBmaWxsPSJ2YXIoLS1jLTQwMCkiIC8+DQogICAgICA8cGF0aCBkPSJNIDIgMjAgTCAzMCA1NSBMIDI0IDI1IFoiIGZpbGw9InZhcigtLWMtNzAwKSIgLz4gPCEtLSBTaGFycCBKYXdsaW5lIFNoYWRvdyAtLT4NCiAgICAgIA0KICAgICAgPCEtLSBSaWdodCBDaGVlayAoV2lkZXN0IFBvaW50KSAtLT4NCiAgICAgIDxwYXRoIGQ9Ik0gNTIgMiBMIDU4IDIwIEwgMzYgMjUgTCA0MCAyMCBaIiBmaWxsPSJ2YXIoLS1jLTMwMCkiIC8+DQogICAgICA8cGF0aCBkPSJNIDU4IDIwIEwgMzAgNTUgTCAzNiAyNSBaIiBmaWxsPSJ2YXIoLS1jLTUwMCkiIC8+IDwhLS0gU2hhcnAgSmF3bGluZSBCYXNlIC0tPg0KICAgICAgDQogICAgICA8IS0tID09PSBFWUVTIChJbnRlZ3JhdGVkIERlcHRoKSA9PT0gLS0+DQogICAgICA8cGF0aCBkPSJNIDIwIDIwIEwgMjYgMTUgTCAyNCAyNSBaIiBmaWxsPSJ2YXIoLS1jLTgwMCkiIC8+DQogICAgICA8cGF0aCBkPSJNIDQwIDIwIEwgMzQgMTUgTCAzNiAyNSBaIiBmaWxsPSJ2YXIoLS1jLTcwMCkiIC8+DQogIDwvZz4NCjwvc3ZnPg==" class="logo-img" alt="Fox Logo">
264
+ </div>
265
+ <a href="foxcontroladmin_dashboard" class="nav-item">
266
+ <span class="nav-icon">📊</span>
267
+ <span>控制台</span>
268
+ </a>
269
+ <a href="foxcontroladmin_network" class="nav-item active">
270
+ <span class="nav-icon">🌐</span>
271
+ <span>网络管理</span>
272
+ </a>
273
+ <a href="foxcontroladmin_firewall" class="nav-item">
274
+ <span class="nav-icon">🔒</span>
275
+ <span>防火墙</span>
276
+ </a>
277
+ <a href="foxcontroladmin_deploy" class="nav-item">
278
+ <span class="nav-icon">🚀</span>
279
+ <span>站点部署</span>
280
+ </a>
281
+ <a href="../foxcontrol_edit" class="nav-item" target="_blank">
282
+ <span class="nav-icon">⚡</span>
283
+ <span>流程编辑器</span>
284
+ </a>
285
+ </div>
286
+
287
+ <div class="main-content">
288
+ <div class="header">
289
+ <h1>网络管理</h1>
290
+ <button class="logout-btn" onclick="logout()">退出登录</button>
291
+ </div>
292
+
293
+ <div id="alert-success" class="alert alert-success"></div>
294
+ <div id="alert-error" class="alert alert-error"></div>
295
+
296
+ <div class="card">
297
+ <h3>主机名设置</h3>
298
+ <div class="config-form">
299
+ <div class="form-group">
300
+ <label for="current-hostname">当前主机名</label>
301
+ <input type="text" id="current-hostname" readonly style="opacity: 0.6;">
302
+ </div>
303
+ <div class="form-group">
304
+ <label for="new-hostname">新主机名</label>
305
+ <input type="text" id="new-hostname" placeholder="请输入新的主机名">
306
+ </div>
307
+ </div>
308
+ <button class="btn btn-primary" id="update-hostname-btn" onclick="updateHostname()">更新主机名</button>
309
+
310
+ </div>
311
+ <div class="card">
312
+ <h3>网络接口</h3>
313
+ <div id="loading" class="loading">加载中...</div>
314
+ <div id="interfaces" class="interface-list" style="display: none;"></div>
315
+ </div>
316
+
317
+ <div class="card">
318
+ <h3>网络配置</h3>
319
+ <div class="config-form">
320
+ <div class="form-group">
321
+ <label for="interface-select">网络接口</label>
322
+ <select id="interface-select">
323
+ <option value="">选择接口</option>
324
+ </select>
325
+ </div>
326
+ <div class="form-group">
327
+ <label for="ip-address">IP 地址</label>
328
+ <input type="text" id="ip-address" placeholder="例如: 192.168.1.100">
329
+ </div>
330
+ <div class="form-group">
331
+ <label for="netmask">子网掩码</label>
332
+ <input type="text" id="netmask" placeholder="例如: 255.255.255.0">
333
+ </div>
334
+ <div class="form-group">
335
+ <label for="gateway">网关</label>
336
+ <input type="text" id="gateway" placeholder="例如: 192.168.1.1">
337
+ </div>
338
+ <div class="form-group">
339
+ <label for="dns">DNS 服务器</label>
340
+ <input type="text" id="dns" placeholder="例如: 8.8.8.8">
341
+ </div>
342
+ </div>
343
+ <button class="btn btn-primary" id="config-btn" onclick="configureNetwork()">应用配置</button>
344
+ </div>
345
+ </div>
346
+
347
+ <script>
348
+ let interfacesData = [];
349
+
350
+ async function loadSystemInfo() {
351
+ try {
352
+ const res = await fetch('/foxcontrol_api/admin/system/info');
353
+ const data = await res.json();
354
+
355
+ if (data.status === 'success') {
356
+ const { system } = data.data;
357
+ document.getElementById('current-hostname').value = system.hostname;
358
+ }
359
+ } catch (err) {
360
+ console.error('加载系统信息失败:', err);
361
+ }
362
+ }
363
+
364
+ async function updateHostname() {
365
+ const newHostname = document.getElementById('new-hostname').value.trim();
366
+ const btn = document.getElementById('update-hostname-btn');
367
+
368
+ if (!newHostname) {
369
+ showAlert('请输入新的主机名', 'error');
370
+ return;
371
+ }
372
+
373
+ if (newHostname.length < 2) {
374
+ showAlert('主机名至少需要 2 个字符', 'error');
375
+ return;
376
+ }
377
+
378
+ btn.disabled = true;
379
+ btn.textContent = '更新中...';
380
+
381
+ try {
382
+ const res = await fetch('/foxcontrol_api/admin/hostname', {
383
+ method: 'PUT',
384
+ headers: { 'Content-Type': 'application/json' },
385
+ body: JSON.stringify({ hostname: newHostname })
386
+ });
387
+ const data = await res.json();
388
+
389
+ if (data.status === 'success') {
390
+ showAlert('主机名更新成功!', 'success');
391
+ document.getElementById('new-hostname').value = '';
392
+ loadSystemInfo();
393
+ } else {
394
+ showAlert(data.message || '更新失败', 'error');
395
+ }
396
+ } catch (err) {
397
+ showAlert('更新失败: ' + err.message, 'error');
398
+ } finally {
399
+ btn.disabled = false;
400
+ btn.textContent = '更新主机名';
401
+ }
402
+ }
403
+
404
+ async function loadInterfaces() {
405
+ try {
406
+ const res = await fetch('/foxcontrol_api/admin/network/interfaces');
407
+ const data = await res.json();
408
+
409
+ if (data.status === 'success') {
410
+ interfacesData = data.data.filter(iface => !iface.internal && !iface.virtual);
411
+ renderInterfaces();
412
+ populateInterfaceSelect();
413
+
414
+ document.getElementById('loading').style.display = 'none';
415
+ document.getElementById('interfaces').style.display = 'grid';
416
+ }
417
+ } catch (err) {
418
+ document.getElementById('loading').textContent = '加载失败: ' + err.message;
419
+ }
420
+ }
421
+
422
+ function renderInterfaces() {
423
+ const container = document.getElementById('interfaces');
424
+ container.innerHTML = interfacesData.map(iface => `
425
+ <div class="interface-item">
426
+ <div class="interface-header">
427
+ <div class="interface-name">
428
+ 🌐 ${iface.iface}
429
+ </div>
430
+ <div class="interface-status ${iface.operstate === 'up' ? 'status-online' : 'status-offline'}">
431
+ ${iface.operstate === 'up' ? '在线' : '离线'}
432
+ </div>
433
+ </div>
434
+ <div class="interface-details">
435
+ <div class="detail-item">
436
+ <span class="detail-label">IPv4</span>
437
+ <span class="detail-value">${iface.ip4 || '-'}</span>
438
+ </div>
439
+ <div class="detail-item">
440
+ <span class="detail-label">子网掩码</span>
441
+ <span class="detail-value">${iface.ip4_subnet || '-'}</span>
442
+ </div>
443
+ <div class="detail-item">
444
+ <span class="detail-label">网关</span>
445
+ <span class="detail-value">${iface.gateway || '-'}</span>
446
+ </div>
447
+ <div class="detail-item">
448
+ <span class="detail-label">MAC 地址</span>
449
+ <span class="detail-value">${iface.mac || '-'}</span>
450
+ </div>
451
+ <div class="detail-item">
452
+ <span class="detail-label">类型</span>
453
+ <span class="detail-value">${iface.type || '-'}</span>
454
+ </div>
455
+ </div>
456
+ </div>
457
+ `).join('');
458
+ }
459
+
460
+ function populateInterfaceSelect() {
461
+ const select = document.getElementById('interface-select');
462
+ select.innerHTML = '<option value="">选择接口</option>' +
463
+ interfacesData.map(iface => `<option value="${iface.iface}">${iface.iface} (${iface.ip4 || '无 IP'})</option>`).join('');
464
+ }
465
+
466
+ function formatSpeed(bytes) {
467
+ if (!bytes) return '-';
468
+ if (bytes < 1024) return bytes + ' B/s';
469
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB/s';
470
+ return (bytes / 1024 / 1024).toFixed(2) + ' MB/s';
471
+ }
472
+
473
+ async function configureNetwork() {
474
+ const iface = document.getElementById('interface-select').value;
475
+ const ip4 = document.getElementById('ip-address').value.trim();
476
+ const netmask = document.getElementById('netmask').value.trim();
477
+ const gateway = document.getElementById('gateway').value.trim();
478
+ const dns = document.getElementById('dns').value.trim();
479
+ const btn = document.getElementById('config-btn');
480
+
481
+ if (!iface || !ip4 || !netmask || !gateway || !dns) {
482
+ showAlert('请填写所有必填项', 'error');
483
+ return;
484
+ }
485
+
486
+ if (!isValidIP(ip4) || !isValidIP(gateway) || !isValidIP(dns)) {
487
+ showAlert('IP 地址格式不正确', 'error');
488
+ return;
489
+ }
490
+
491
+ btn.disabled = true;
492
+ btn.textContent = '配置中...';
493
+
494
+ try {
495
+ const res = await fetch('/foxcontrol_api/admin/network/config', {
496
+ method: 'PUT',
497
+ headers: { 'Content-Type': 'application/json' },
498
+ body: JSON.stringify({ iface, ip4, netmask, gateway, dns })
499
+ });
500
+
501
+ const data = await res.json();
502
+
503
+ if (data.status === 'success') {
504
+ showAlert('网络配置已更新!', 'success');
505
+ setTimeout(loadInterfaces, 2000);
506
+ } else {
507
+ throw new Error(data.message || '配置失败');
508
+ }
509
+ } catch (err) {
510
+ showAlert('配置失败: ' + err.message, 'error');
511
+ } finally {
512
+ btn.disabled = false;
513
+ btn.textContent = '应用配置';
514
+ }
515
+ }
516
+
517
+ function isValidIP(ip) {
518
+ const pattern = /^(\d{1,3}\.){3}\d{1,3}$/;
519
+ if (!pattern.test(ip)) return false;
520
+ const parts = ip.split('.');
521
+ return parts.every(part => {
522
+ const num = parseInt(part);
523
+ return num >= 0 && num <= 255;
524
+ });
525
+ }
526
+
527
+ function showAlert(message, type) {
528
+ const alertId = type === 'success' ? 'alert-success' : 'alert-error';
529
+ const alertEl = document.getElementById(alertId);
530
+ alertEl.textContent = message;
531
+ alertEl.style.display = 'block';
532
+
533
+ setTimeout(() => {
534
+ alertEl.style.display = 'none';
535
+ }, 5000);
536
+ }
537
+
538
+ async function logout() {
539
+ if (confirm('确定要退出登录吗?')) {
540
+ await fetch('/foxcontrol_api/admin/logout', { method: 'POST' });
541
+ window.location.href = '..';
542
+ }
543
+ }
544
+
545
+ async function checkAuth() {
546
+ try {
547
+ const res = await fetch('/foxcontrol_api/admin/check-auth');
548
+ const data = await res.json();
549
+ if (data.status === 'error' || !data.authenticated) {
550
+ window.location.href = '/foxadmin';
551
+ }
552
+ } catch (err) {
553
+ console.error('[Fox Admin] 登录状态检查失败:', err);
554
+ }
555
+ }
556
+
557
+ loadSystemInfo();
558
+ loadInterfaces();
559
+ checkAuth();
560
+ setInterval(loadInterfaces, 30000);
561
+ </script>
562
+ </body>
563
+ </html>
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "node-red-contrib-fox-admin",
3
+ "version": "1.0.0",
4
+ "description": "Fox Control 系统管理控制台,支持设备管理、网络配置、防火墙管理",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "keywords": [
10
+ "node-red",
11
+ "fox-control",
12
+ "admin",
13
+ "system",
14
+ "network",
15
+ "firewall",
16
+ "runtime-plugin"
17
+ ],
18
+ "author": "Fox Control Team",
19
+ "license": "MIT",
20
+ "node-red": {
21
+ "plugins": {
22
+ "fox-admin": "index.js"
23
+ }
24
+ },
25
+ "dependencies": {
26
+ "systeminformation": "^5.31.5",
27
+ "multer": "^1.4.5-lts.2",
28
+ "adm-zip": "^0.5.16",
29
+ "body-parser": "^1.20.2",
30
+ "express-session": "^1.17.3"
31
+ },
32
+ "engines": {
33
+ "node": ">=14.0.0"
34
+ }
35
+ }