tanyu_admin 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,656 @@
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>虚拟主机管理系统</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600;700&family=Fira+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
10
+ <style>
11
+ * {
12
+ margin: 0;
13
+ padding: 0;
14
+ box-sizing: border-box;
15
+ }
16
+
17
+ body {
18
+ font-family: 'Fira Sans', -apple-system, BlinkMacSystemFont, sans-serif;
19
+ background: #FAF5FF;
20
+ min-height: 100vh;
21
+ padding: 20px;
22
+ }
23
+
24
+ .container {
25
+ max-width: 1400px;
26
+ margin: 0 auto;
27
+ background: white;
28
+ border-radius: 10px;
29
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
30
+ overflow: hidden;
31
+ }
32
+
33
+ .header {
34
+ background: #4C1D95;
35
+ color: white;
36
+ padding: 30px;
37
+ text-align: center;
38
+ }
39
+
40
+ .header h1 {
41
+ font-family: 'Fira Code', monospace;
42
+ font-size: 32px;
43
+ margin-bottom: 10px;
44
+ font-weight: 600;
45
+ }
46
+
47
+ .header p {
48
+ opacity: 0.9;
49
+ font-size: 14px;
50
+ }
51
+
52
+ .toolbar {
53
+ padding: 20px 30px;
54
+ background: #f8f9fa;
55
+ border-bottom: 1px solid #e9ecef;
56
+ display: flex;
57
+ justify-content: space-between;
58
+ align-items: center;
59
+ }
60
+
61
+ .btn {
62
+ padding: 10px 20px;
63
+ border: none;
64
+ border-radius: 5px;
65
+ cursor: pointer;
66
+ font-size: 14px;
67
+ transition: all 200ms;
68
+ font-weight: 500;
69
+ }
70
+
71
+ .btn-primary {
72
+ background: #7C3AED;
73
+ color: white;
74
+ cursor: pointer;
75
+ }
76
+
77
+ .btn-primary:hover {
78
+ background: #6D28D9;
79
+ transform: translateY(-1px);
80
+ box-shadow: 0 4px 12px rgba(124, 58, 237, 0.3);
81
+ }
82
+
83
+ .btn-danger {
84
+ background: #dc3545;
85
+ color: white;
86
+ cursor: pointer;
87
+ }
88
+
89
+ .btn-danger:hover {
90
+ background: #c82333;
91
+ transform: translateY(-1px);
92
+ }
93
+
94
+ .btn-success {
95
+ background: #F97316;
96
+ color: white;
97
+ cursor: pointer;
98
+ }
99
+
100
+ .btn-success:hover {
101
+ background: #EA580C;
102
+ transform: translateY(-1px);
103
+ }
104
+
105
+ .btn-secondary {
106
+ background: #A78BFA;
107
+ color: white;
108
+ cursor: pointer;
109
+ }
110
+
111
+ .btn-secondary:hover {
112
+ background: #8B5CF6;
113
+ transform: translateY(-1px);
114
+ }
115
+
116
+ .content {
117
+ padding: 30px;
118
+ }
119
+
120
+ .stats {
121
+ display: grid;
122
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
123
+ gap: 20px;
124
+ margin-bottom: 30px;
125
+ }
126
+
127
+ .stat-card {
128
+ background: white;
129
+ color: #4C1D95;
130
+ padding: 20px;
131
+ border-radius: 10px;
132
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
133
+ border-left: 4px solid #7C3AED;
134
+ cursor: default;
135
+ }
136
+
137
+ .stat-card h3 {
138
+ font-size: 14px;
139
+ color: #6b7280;
140
+ margin-bottom: 10px;
141
+ font-weight: 500;
142
+ }
143
+
144
+ .stat-card .number {
145
+ font-family: 'Fira Code', monospace;
146
+ font-size: 32px;
147
+ font-weight: bold;
148
+ color: #7C3AED;
149
+ }
150
+
151
+ table {
152
+ width: 100%;
153
+ border-collapse: collapse;
154
+ background: white;
155
+ border-radius: 10px;
156
+ overflow: hidden;
157
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
158
+ }
159
+
160
+ thead {
161
+ background: #f8f9fa;
162
+ }
163
+
164
+ th {
165
+ padding: 15px;
166
+ text-align: left;
167
+ font-weight: 600;
168
+ color: #495057;
169
+ border-bottom: 2px solid #dee2e6;
170
+ }
171
+
172
+ td {
173
+ padding: 15px;
174
+ border-bottom: 1px solid #e9ecef;
175
+ }
176
+
177
+ tbody tr {
178
+ transition: background-color 200ms;
179
+ }
180
+
181
+ tbody tr:hover {
182
+ background: #F3E8FF;
183
+ cursor: pointer;
184
+ }
185
+
186
+ .badge {
187
+ display: inline-block;
188
+ padding: 5px 10px;
189
+ border-radius: 20px;
190
+ font-size: 12px;
191
+ font-weight: 500;
192
+ }
193
+
194
+ .badge-success {
195
+ background: #d4edda;
196
+ color: #155724;
197
+ }
198
+
199
+ .badge-danger {
200
+ background: #f8d7da;
201
+ color: #721c24;
202
+ }
203
+
204
+ .badge-primary {
205
+ background: #cce5ff;
206
+ color: #004085;
207
+ }
208
+
209
+ .modal {
210
+ display: none;
211
+ position: fixed;
212
+ top: 0;
213
+ left: 0;
214
+ width: 100%;
215
+ height: 100%;
216
+ background: rgba(0, 0, 0, 0.5);
217
+ z-index: 1000;
218
+ align-items: center;
219
+ justify-content: center;
220
+ }
221
+
222
+ .modal.active {
223
+ display: flex;
224
+ }
225
+
226
+ .modal-content {
227
+ background: white;
228
+ border-radius: 10px;
229
+ padding: 30px;
230
+ max-width: 600px;
231
+ width: 90%;
232
+ max-height: 90vh;
233
+ overflow-y: auto;
234
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
235
+ }
236
+
237
+ .modal-header {
238
+ margin-bottom: 20px;
239
+ padding-bottom: 15px;
240
+ border-bottom: 2px solid #e9ecef;
241
+ }
242
+
243
+ .modal-header h2 {
244
+ color: #333;
245
+ font-size: 24px;
246
+ }
247
+
248
+ .form-group {
249
+ margin-bottom: 20px;
250
+ }
251
+
252
+ .form-group label {
253
+ display: block;
254
+ margin-bottom: 8px;
255
+ color: #495057;
256
+ font-weight: 500;
257
+ }
258
+
259
+ .form-group input,
260
+ .form-group select {
261
+ width: 100%;
262
+ padding: 10px;
263
+ border: 1px solid #ced4da;
264
+ border-radius: 5px;
265
+ font-size: 14px;
266
+ transition: border-color 0.3s;
267
+ }
268
+
269
+ .form-group input:focus,
270
+ .form-group select:focus {
271
+ outline: none;
272
+ border-color: #7C3AED;
273
+ box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.1);
274
+ }
275
+
276
+ .form-actions {
277
+ display: flex;
278
+ gap: 10px;
279
+ justify-content: flex-end;
280
+ margin-top: 30px;
281
+ padding-top: 20px;
282
+ border-top: 1px solid #e9ecef;
283
+ }
284
+
285
+ .empty-state {
286
+ text-align: center;
287
+ padding: 60px 20px;
288
+ color: #6c757d;
289
+ }
290
+
291
+ .empty-state svg {
292
+ width: 100px;
293
+ height: 100px;
294
+ margin-bottom: 20px;
295
+ opacity: 0.5;
296
+ }
297
+
298
+ .loading {
299
+ text-align: center;
300
+ padding: 40px;
301
+ color: #6c757d;
302
+ }
303
+
304
+ .action-buttons {
305
+ display: flex;
306
+ gap: 10px;
307
+ }
308
+
309
+ .btn-sm {
310
+ padding: 5px 15px;
311
+ font-size: 12px;
312
+ }
313
+ </style>
314
+ </head>
315
+ <body>
316
+ <div class="container">
317
+ <div class="header">
318
+ <h1>
319
+ <svg style="display: inline-block; width: 32px; height: 32px; vertical-align: middle; margin-right: 8px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
320
+ <circle cx="12" cy="12" r="10"></circle>
321
+ <line x1="2" y1="12" x2="22" y2="12"></line>
322
+ <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"></path>
323
+ </svg>
324
+ 虚拟主机管理系统
325
+ </h1>
326
+ <p>端口映射配置管理 - 查看、新增、删除虚拟主机配置</p>
327
+ </div>
328
+
329
+ <div class="toolbar">
330
+ <div class="stats-summary">
331
+ <span id="totalCount">总计: 0 个配置</span>
332
+ </div>
333
+ <div style="display: flex; gap: 10px;">
334
+ <button class="btn btn-secondary" onclick="refreshFromModem()">
335
+ <svg style="display: inline-block; width: 16px; height: 16px; vertical-align: middle; margin-right: 4px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
336
+ <path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"/>
337
+ </svg>
338
+ 从光猫刷新
339
+ </button>
340
+ <button class="btn btn-primary" onclick="showAddModal()">
341
+ <svg style="display: inline-block; width: 16px; height: 16px; vertical-align: middle; margin-right: 4px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
342
+ <line x1="12" y1="5" x2="12" y2="19"></line>
343
+ <line x1="5" y1="12" x2="19" y2="12"></line>
344
+ </svg>
345
+ 新增配置
346
+ </button>
347
+ </div>
348
+ </div>
349
+
350
+ <div class="content">
351
+ <div class="stats">
352
+ <div class="stat-card">
353
+ <h3>总配置数</h3>
354
+ <div class="number" id="statTotal">0</div>
355
+ </div>
356
+ <div class="stat-card">
357
+ <h3>启用中</h3>
358
+ <div class="number" id="statEnabled">0</div>
359
+ </div>
360
+ <div class="stat-card">
361
+ <h3>TCP协议</h3>
362
+ <div class="number" id="statTCP">0</div>
363
+ </div>
364
+ <div class="stat-card">
365
+ <h3>TCP/UDP协议</h3>
366
+ <div class="number" id="statTCPUDP">0</div>
367
+ </div>
368
+ </div>
369
+
370
+ <div id="tableContainer">
371
+ <div class="loading">加载中...</div>
372
+ </div>
373
+ </div>
374
+ </div>
375
+
376
+ <!-- 新增/编辑模态框 -->
377
+ <div id="hostModal" class="modal">
378
+ <div class="modal-content">
379
+ <div class="modal-header">
380
+ <h2 id="modalTitle">新增虚拟主机配置</h2>
381
+ </div>
382
+ <form id="hostForm">
383
+ <div class="form-group">
384
+ <label>名称 *</label>
385
+ <input type="text" id="formName" required placeholder="例如: web服务器">
386
+ </div>
387
+ <div class="form-group">
388
+ <label>协议 *</label>
389
+ <select id="formProtocol" required>
390
+ <option value="0">TCP</option>
391
+ <option value="1">UDP</option>
392
+ <option value="2">TCP/UDP</option>
393
+ </select>
394
+ </div>
395
+ <div class="form-group">
396
+ <label>外部端口 (起始) *</label>
397
+ <input type="number" id="formMinExtPort" required placeholder="例如: 8080" min="1" max="65535">
398
+ </div>
399
+ <div class="form-group">
400
+ <label>外部端口 (结束) *</label>
401
+ <input type="number" id="formMaxExtPort" required placeholder="例如: 8080" min="1" max="65535">
402
+ </div>
403
+ <div class="form-group">
404
+ <label>内部主机IP *</label>
405
+ <input type="text" id="formInternalHost" required placeholder="例如: 192.168.1.100">
406
+ </div>
407
+ <div class="form-group">
408
+ <label>内部端口 (起始) *</label>
409
+ <input type="number" id="formMinIntPort" required placeholder="例如: 8080" min="1" max="65535">
410
+ </div>
411
+ <div class="form-group">
412
+ <label>内部端口 (结束) *</label>
413
+ <input type="number" id="formMaxIntPort" required placeholder="例如: 8080" min="1" max="65535">
414
+ </div>
415
+ <div class="form-group">
416
+ <label>启用状态</label>
417
+ <select id="formEnable">
418
+ <option value="1">启用</option>
419
+ <option value="0">禁用</option>
420
+ </select>
421
+ </div>
422
+ <div class="form-group">
423
+ <label>描述</label>
424
+ <input type="text" id="formDescription" placeholder="可选">
425
+ </div>
426
+ <div class="form-actions">
427
+ <button type="button" class="btn btn-secondary" onclick="closeModal()">取消</button>
428
+ <button type="submit" class="btn btn-success">保存</button>
429
+ </div>
430
+ </form>
431
+ </div>
432
+ </div>
433
+
434
+ <script>
435
+ let hosts = [];
436
+ let editingId = null;
437
+
438
+ // 加载数据
439
+ async function loadHosts() {
440
+ try {
441
+ const response = await fetch('/api/hosts');
442
+ const result = await response.json();
443
+ if (result.success) {
444
+ hosts = result.data;
445
+ renderTable();
446
+ updateStats();
447
+ }
448
+ } catch (error) {
449
+ console.error('加载失败:', error);
450
+ document.getElementById('tableContainer').innerHTML =
451
+ '<div class="empty-state">加载失败,请刷新页面重试</div>';
452
+ }
453
+ }
454
+
455
+ // 渲染表格
456
+ function renderTable() {
457
+ const container = document.getElementById('tableContainer');
458
+
459
+ if (hosts.length === 0) {
460
+ container.innerHTML = `
461
+ <div class="empty-state">
462
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
463
+ <path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
464
+ </svg>
465
+ <h3>暂无配置</h3>
466
+ <p>点击上方"新增配置"按钮添加第一个虚拟主机配置</p>
467
+ </div>
468
+ `;
469
+ return;
470
+ }
471
+
472
+ const table = `
473
+ <table>
474
+ <thead>
475
+ <tr>
476
+ <th>名称</th>
477
+ <th>状态</th>
478
+ <th>协议</th>
479
+ <th>外部端口</th>
480
+ <th>内部主机</th>
481
+ <th>内部端口</th>
482
+ <th>操作</th>
483
+ </tr>
484
+ </thead>
485
+ <tbody>
486
+ ${hosts.map(host => `
487
+ <tr>
488
+ <td><strong>${host.Name}</strong></td>
489
+ <td>
490
+ <span class="badge ${host.Enable === '1' ? 'badge-success' : 'badge-danger'}">
491
+ ${host.EnableText}
492
+ </span>
493
+ </td>
494
+ <td>
495
+ <span class="badge badge-primary">${host.ProtocolText}</span>
496
+ </td>
497
+ <td>${host.MinExtPort}${host.MinExtPort !== host.MaxExtPort ? '-' + host.MaxExtPort : ''}</td>
498
+ <td>${host.InternalHost}</td>
499
+ <td>${host.MinIntPort}${host.MinIntPort !== host.MaxIntPort ? '-' + host.MaxIntPort : ''}</td>
500
+ <td>
501
+ <div class="action-buttons">
502
+ <button class="btn btn-primary btn-sm" onclick="editHost(${host.index})">编辑</button>
503
+ <button class="btn btn-danger btn-sm" onclick="deleteHost(${host.index}, '${host.Name}')">删除</button>
504
+ </div>
505
+ </td>
506
+ </tr>
507
+ `).join('')}
508
+ </tbody>
509
+ </table>
510
+ `;
511
+ container.innerHTML = table;
512
+ }
513
+
514
+ // 更新统计
515
+ function updateStats() {
516
+ document.getElementById('totalCount').textContent = `总计: ${hosts.length} 个配置`;
517
+ document.getElementById('statTotal').textContent = hosts.length;
518
+ document.getElementById('statEnabled').textContent = hosts.filter(h => h.Enable === '1').length;
519
+ document.getElementById('statTCP').textContent = hosts.filter(h => h.Protocol === '0').length;
520
+ document.getElementById('statTCPUDP').textContent = hosts.filter(h => h.Protocol === '2').length;
521
+ }
522
+
523
+ // 显示新增模态框
524
+ function showAddModal() {
525
+ editingId = null;
526
+ document.getElementById('modalTitle').textContent = '新增虚拟主机配置';
527
+ document.getElementById('hostForm').reset();
528
+ document.getElementById('hostModal').classList.add('active');
529
+ }
530
+
531
+ // 编辑
532
+ function editHost(id) {
533
+ const host = hosts.find(h => h.index === id);
534
+ if (!host) return;
535
+
536
+ editingId = id;
537
+ document.getElementById('modalTitle').textContent = '编辑虚拟主机配置';
538
+ document.getElementById('formName').value = host.Name || '';
539
+ document.getElementById('formProtocol').value = host.Protocol || '2';
540
+ document.getElementById('formMinExtPort').value = host.MinExtPort || '';
541
+ document.getElementById('formMaxExtPort').value = host.MaxExtPort || '';
542
+ document.getElementById('formInternalHost').value = host.InternalHost || '';
543
+ document.getElementById('formMinIntPort').value = host.MinIntPort || '';
544
+ document.getElementById('formMaxIntPort').value = host.MaxIntPort || '';
545
+ document.getElementById('formEnable').value = host.Enable || '1';
546
+ document.getElementById('formDescription').value = host.Description || '';
547
+ document.getElementById('hostModal').classList.add('active');
548
+ }
549
+
550
+ // 删除
551
+ async function deleteHost(id, name) {
552
+ if (!confirm(`确定要删除配置"${name}"吗?`)) return;
553
+
554
+ try {
555
+ const response = await fetch(`/api/hosts/${id}`, {
556
+ method: 'DELETE'
557
+ });
558
+ const result = await response.json();
559
+ if (result.success) {
560
+ alert('删除成功');
561
+ loadHosts();
562
+ } else {
563
+ alert('删除失败: ' + result.message);
564
+ }
565
+ } catch (error) {
566
+ alert('删除失败: ' + error.message);
567
+ }
568
+ }
569
+
570
+ // 从光猫刷新配置
571
+ async function refreshFromModem() {
572
+ if (!confirm('确定要从光猫刷新配置吗?这将覆盖本地数据。')) return;
573
+
574
+ try {
575
+ const response = await fetch('/api/hosts/refresh', {
576
+ method: 'POST'
577
+ });
578
+ const result = await response.json();
579
+ if (result.success) {
580
+ alert(`刷新成功!共获取 ${result.total} 个配置`);
581
+ loadHosts();
582
+ } else {
583
+ alert('刷新失败: ' + result.message);
584
+ }
585
+ } catch (error) {
586
+ alert('刷新失败: ' + error.message);
587
+ }
588
+ }
589
+
590
+ // 关闭模态框
591
+ function closeModal() {
592
+ document.getElementById('hostModal').classList.remove('active');
593
+ }
594
+
595
+ // 表单提交
596
+ document.getElementById('hostForm').addEventListener('submit', async (e) => {
597
+ e.preventDefault();
598
+
599
+ const data = {
600
+ Name: document.getElementById('formName').value,
601
+ Protocol: document.getElementById('formProtocol').value,
602
+ MinExtPort: document.getElementById('formMinExtPort').value,
603
+ MaxExtPort: document.getElementById('formMaxExtPort').value,
604
+ InternalHost: document.getElementById('formInternalHost').value,
605
+ MinIntPort: document.getElementById('formMinIntPort').value,
606
+ MaxIntPort: document.getElementById('formMaxIntPort').value,
607
+ Enable: document.getElementById('formEnable').value,
608
+ Description: document.getElementById('formDescription').value,
609
+ WANCViewName: 'IGD.WD1.WCD2.WCPPP1',
610
+ ViewName: '',
611
+ WANCName: '',
612
+ LeaseDuration: '0',
613
+ PortMappCreator: '',
614
+ MinRemoteHost: '0.0.0.0',
615
+ MaxRemoteHost: '0.0.0.0',
616
+ InternalMacHost: '00:00:00:00:00:00',
617
+ MacEnable: '0'
618
+ };
619
+
620
+ try {
621
+ const url = editingId !== null ? `/api/hosts/${editingId}` : '/api/hosts';
622
+ const method = editingId !== null ? 'PUT' : 'POST';
623
+
624
+ const response = await fetch(url, {
625
+ method: method,
626
+ headers: {
627
+ 'Content-Type': 'application/json'
628
+ },
629
+ body: JSON.stringify(data)
630
+ });
631
+
632
+ const result = await response.json();
633
+ if (result.success) {
634
+ alert(editingId !== null ? '更新成功' : '添加成功');
635
+ closeModal();
636
+ loadHosts();
637
+ } else {
638
+ alert('操作失败: ' + result.message);
639
+ }
640
+ } catch (error) {
641
+ alert('操作失败: ' + error.message);
642
+ }
643
+ });
644
+
645
+ // 点击模态框外部关闭
646
+ document.getElementById('hostModal').addEventListener('click', (e) => {
647
+ if (e.target.id === 'hostModal') {
648
+ closeModal();
649
+ }
650
+ });
651
+
652
+ // 初始加载
653
+ loadHosts();
654
+ </script>
655
+ </body>
656
+ </html>
package/readme.md ADDED
@@ -0,0 +1 @@
1
+ 联通光猫-模拟登录