@jachy/multiport-proxy 0.0.1 → 0.0.2

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/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  一个功能强大的多端口代理服务,支持动态配置和实时日志查看。
4
4
 
5
+ ![multiport-proxy-screenshot](./ScreenShot.png)
6
+
5
7
  ## Features
6
8
 
7
9
  - 🚀 **Web 配置界面** - 启动时自动打开 Web UI,轻松配置代理规则
package/dist/index.js CHANGED
@@ -54,8 +54,11 @@ async function main() {
54
54
  // 创建 Web 服务器
55
55
  const app = (0, express_1.default)();
56
56
  app.use(express_1.default.json());
57
- // 静态文件服务
58
- const uiDir = path_1.default.join(__dirname, 'web', 'ui');
57
+ // 静态文件服务 - 支持开发和构建环境
58
+ const isDev = process.env.NODE_ENV !== 'production';
59
+ const uiDir = isDev
60
+ ? path_1.default.join(__dirname, 'web', 'ui')
61
+ : path_1.default.join(__dirname, '..', 'web', 'ui');
59
62
  app.use(express_1.default.static(uiDir));
60
63
  // API 路由
61
64
  app.use('/api', (0, api_routes_1.createApiRouter)(configManager, logger, proxyServer));
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,sDAA8B;AAC9B,gDAAwB;AACxB,4DAAwD;AACxD,4CAAyC;AACzC,wDAAoD;AACpD,iDAAmD;AAEnD,MAAM,QAAQ,GAAG,IAAI,CAAC;AAEtB,KAAK,UAAU,IAAI;IACjB,OAAO,CAAC,GAAG,CAAC,kCAAkC,CAAC,CAAC;IAEhD,UAAU;IACV,MAAM,aAAa,GAAG,IAAI,8BAAa,EAAE,CAAC;IAC1C,MAAM,MAAM,GAAG,IAAI,eAAM,EAAE,CAAC;IAC5B,MAAM,WAAW,GAAG,IAAI,0BAAW,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC;IAE3D,SAAS;IACT,WAAW,CAAC,YAAY,EAAE,CAAC;IAE3B,aAAa;IACb,MAAM,GAAG,GAAG,IAAA,iBAAO,GAAE,CAAC;IAEtB,GAAG,CAAC,GAAG,CAAC,iBAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IAExB,SAAS;IACT,MAAM,KAAK,GAAG,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;IAChD,GAAG,CAAC,GAAG,CAAC,iBAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IAE/B,SAAS;IACT,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,IAAA,4BAAe,EAAC,aAAa,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC;IAErE,aAAa;IACb,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;QACxB,GAAG,CAAC,QAAQ,CAAC,cAAI,CAAC,IAAI,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;IAEH,aAAa;IACb,GAAG,CAAC,MAAM,CAAC,QAAQ,EAAE,KAAK,IAAI,EAAE;QAC9B,OAAO,CAAC,GAAG,CAAC,wCAAwC,QAAQ,EAAE,CAAC,CAAC;QAChE,OAAO,CAAC,GAAG,CAAC,oBAAoB,WAAW,CAAC,eAAe,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,MAAM,IAAI,CAAC,CAAC;QAExF,UAAU;QACV,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,CAAC,wDAAa,MAAM,GAAC,CAAC,CAAC,OAAO,CAAC;YAC5C,MAAM,IAAI,CAAC,oBAAoB,QAAQ,EAAE,CAAC,CAAC;YAC3C,OAAO,CAAC,GAAG,CAAC,kCAAkC,CAAC,CAAC;QAClD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,GAAG,CAAC,gCAAgC,QAAQ,oBAAoB,CAAC,CAAC;QAC5E,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,OAAO;IACP,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE;QACxB,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC;QACvC,WAAW,CAAC,cAAc,EAAE,CAAC;QAC7B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;AACL,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,sDAA8B;AAC9B,gDAAwB;AACxB,4DAAwD;AACxD,4CAAyC;AACzC,wDAAoD;AACpD,iDAAmD;AAEnD,MAAM,QAAQ,GAAG,IAAI,CAAC;AAEtB,KAAK,UAAU,IAAI;IACjB,OAAO,CAAC,GAAG,CAAC,kCAAkC,CAAC,CAAC;IAEhD,UAAU;IACV,MAAM,aAAa,GAAG,IAAI,8BAAa,EAAE,CAAC;IAC1C,MAAM,MAAM,GAAG,IAAI,eAAM,EAAE,CAAC;IAC5B,MAAM,WAAW,GAAG,IAAI,0BAAW,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC;IAE3D,SAAS;IACT,WAAW,CAAC,YAAY,EAAE,CAAC;IAE3B,aAAa;IACb,MAAM,GAAG,GAAG,IAAA,iBAAO,GAAE,CAAC;IAEtB,GAAG,CAAC,GAAG,CAAC,iBAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IAExB,qBAAqB;IACrB,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,CAAC;IACpD,MAAM,KAAK,GAAG,KAAK;QACjB,CAAC,CAAC,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,EAAE,IAAI,CAAC;QACnC,CAAC,CAAC,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;IAC5C,GAAG,CAAC,GAAG,CAAC,iBAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IAE/B,SAAS;IACT,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,IAAA,4BAAe,EAAC,aAAa,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC;IAErE,aAAa;IACb,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;QACxB,GAAG,CAAC,QAAQ,CAAC,cAAI,CAAC,IAAI,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;IAEH,aAAa;IACb,GAAG,CAAC,MAAM,CAAC,QAAQ,EAAE,KAAK,IAAI,EAAE;QAC9B,OAAO,CAAC,GAAG,CAAC,wCAAwC,QAAQ,EAAE,CAAC,CAAC;QAChE,OAAO,CAAC,GAAG,CAAC,oBAAoB,WAAW,CAAC,eAAe,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,MAAM,IAAI,CAAC,CAAC;QAExF,UAAU;QACV,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,CAAC,wDAAa,MAAM,GAAC,CAAC,CAAC,OAAO,CAAC;YAC5C,MAAM,IAAI,CAAC,oBAAoB,QAAQ,EAAE,CAAC,CAAC;YAC3C,OAAO,CAAC,GAAG,CAAC,kCAAkC,CAAC,CAAC;QAClD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,GAAG,CAAC,gCAAgC,QAAQ,oBAAoB,CAAC,CAAC;QAC5E,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,OAAO;IACP,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE;QACxB,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC;QACvC,WAAW,CAAC,cAAc,EAAE,CAAC;QAC7B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;AACL,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC"}
@@ -0,0 +1,249 @@
1
+ let currentEditingId = null;
2
+
3
+ // 启用 CORS 复选框的事件监听
4
+ document.addEventListener('DOMContentLoaded', () => {
5
+ const corsCheckbox = document.getElementById('ruleCorsEnabled');
6
+ const corsOrigins = document.getElementById('corsOrigins');
7
+
8
+ corsCheckbox.addEventListener('change', (e) => {
9
+ corsOrigins.style.display = e.target.checked ? 'block' : 'none';
10
+ });
11
+
12
+ // 初始化
13
+ loadRules();
14
+ loadLogs();
15
+
16
+ // 定时刷新日志
17
+ setInterval(loadLogs, 2000);
18
+
19
+ // 日志筛选
20
+ document.getElementById('logFilter').addEventListener('input', filterLogs);
21
+ });
22
+
23
+ // 加载规则列表
24
+ async function loadRules() {
25
+ try {
26
+ const response = await fetch('/api/config');
27
+ const data = await response.json();
28
+ const rules = data.rules || [];
29
+
30
+ const rulesList = document.getElementById('rulesList');
31
+
32
+ if (rules.length === 0) {
33
+ rulesList.innerHTML = '<div class="loading">暂无规则,点击"新增规则"开始</div>';
34
+ return;
35
+ }
36
+
37
+ rulesList.innerHTML = rules.map(rule => `
38
+ <div class="rule-item" onclick="editRule('${rule.id}')">
39
+ <div class="rule-item-header">
40
+ <span class="rule-port">
41
+ :${rule.localPort}
42
+ <span class="rule-status ${rule.enabled ? 'enabled' : 'disabled'}"></span>
43
+ </span>
44
+ </div>
45
+ <div class="rule-target">${rule.targetUrl}</div>
46
+ </div>
47
+ `).join('');
48
+ } catch (error) {
49
+ console.error('Failed to load rules:', error);
50
+ }
51
+ }
52
+
53
+ // 打开新增规则表单
54
+ function showAddRuleForm() {
55
+ currentEditingId = null;
56
+ document.getElementById('modalTitle').textContent = '新增代理规则';
57
+ document.getElementById('deleteBtn').style.display = 'none';
58
+ document.getElementById('ruleForm').reset();
59
+ document.getElementById('ruleTimeout').value = '30000';
60
+ document.getElementById('ruleRetries').value = '0';
61
+ document.getElementById('ruleEnabled').checked = true;
62
+ document.getElementById('ruleCorsEnabled').checked = false;
63
+ document.getElementById('corsOrigins').style.display = 'none';
64
+ document.getElementById('ruleModal').classList.add('show');
65
+ }
66
+
67
+ // 编辑规则
68
+ async function editRule(id) {
69
+ try {
70
+ const response = await fetch('/api/config');
71
+ const data = await response.json();
72
+ const rule = data.rules.find(r => r.id === id);
73
+
74
+ if (!rule) return;
75
+
76
+ currentEditingId = id;
77
+ document.getElementById('modalTitle').textContent = '编辑代理规则';
78
+ document.getElementById('deleteBtn').style.display = 'inline-block';
79
+ document.getElementById('rulePort').value = rule.localPort;
80
+ document.getElementById('ruleTarget').value = rule.targetUrl;
81
+ document.getElementById('ruleTimeout').value = rule.timeout || 30000;
82
+ document.getElementById('ruleRetries').value = rule.retries || 0;
83
+ document.getElementById('ruleEnabled').checked = rule.enabled !== false;
84
+ document.getElementById('ruleCorsEnabled').checked = rule.cors?.enabled || false;
85
+ document.getElementById('ruleCorsOrigins').value = rule.cors?.origins?.join(', ') || '*';
86
+ document.getElementById('corsOrigins').style.display = rule.cors?.enabled ? 'block' : 'none';
87
+
88
+ document.getElementById('ruleModal').classList.add('show');
89
+ } catch (error) {
90
+ console.error('Failed to edit rule:', error);
91
+ }
92
+ }
93
+
94
+ // 保存规则
95
+ async function saveRule(e) {
96
+ e.preventDefault();
97
+
98
+ const rule = {
99
+ localPort: parseInt(document.getElementById('rulePort').value),
100
+ targetUrl: document.getElementById('ruleTarget').value,
101
+ timeout: parseInt(document.getElementById('ruleTimeout').value),
102
+ retries: parseInt(document.getElementById('ruleRetries').value),
103
+ enabled: document.getElementById('ruleEnabled').checked,
104
+ cors: {
105
+ enabled: document.getElementById('ruleCorsEnabled').checked,
106
+ origins: document.getElementById('ruleCorsOrigins').value
107
+ .split(',')
108
+ .map(o => o.trim())
109
+ .filter(o => o),
110
+ },
111
+ };
112
+
113
+ try {
114
+ if (currentEditingId) {
115
+ // 更新规则
116
+ const response = await fetch(`/api/config/rules/${currentEditingId}`, {
117
+ method: 'PUT',
118
+ headers: { 'Content-Type': 'application/json' },
119
+ body: JSON.stringify(rule),
120
+ });
121
+
122
+ if (!response.ok) {
123
+ throw new Error('Failed to update rule');
124
+ }
125
+ } else {
126
+ // 新增规则
127
+ const response = await fetch('/api/config/rules', {
128
+ method: 'POST',
129
+ headers: { 'Content-Type': 'application/json' },
130
+ body: JSON.stringify(rule),
131
+ });
132
+
133
+ if (!response.ok) {
134
+ throw new Error('Failed to add rule');
135
+ }
136
+ }
137
+
138
+ closeRuleForm();
139
+ loadRules();
140
+ alert('规则已保存!');
141
+ } catch (error) {
142
+ console.error('Error saving rule:', error);
143
+ alert('保存失败:' + error.message);
144
+ }
145
+ }
146
+
147
+ // 删除规则
148
+ async function deleteRule() {
149
+ if (!currentEditingId) return;
150
+
151
+ if (!confirm('确定删除此规则吗?')) return;
152
+
153
+ try {
154
+ const response = await fetch(`/api/config/rules/${currentEditingId}`, {
155
+ method: 'DELETE',
156
+ });
157
+
158
+ if (!response.ok) {
159
+ throw new Error('Failed to delete rule');
160
+ }
161
+
162
+ closeRuleForm();
163
+ loadRules();
164
+ alert('规则已删除!');
165
+ } catch (error) {
166
+ console.error('Error deleting rule:', error);
167
+ alert('删除失败:' + error.message);
168
+ }
169
+ }
170
+
171
+ // 关闭表单
172
+ function closeRuleForm() {
173
+ document.getElementById('ruleModal').classList.remove('show');
174
+ currentEditingId = null;
175
+ }
176
+
177
+ // 加载日志
178
+ async function loadLogs() {
179
+ try {
180
+ const response = await fetch('/api/logs');
181
+ const data = await response.json();
182
+ const logs = data.logs || [];
183
+
184
+ // 更新统计信息
185
+ if (data.stats) {
186
+ document.getElementById('totalLogs').textContent = data.stats.totalLogs;
187
+ document.getElementById('errorCount').textContent = data.stats.errorCount;
188
+ document.getElementById('avgDuration').textContent = data.stats.averageDuration + 'ms';
189
+ }
190
+
191
+ // 渲染日志
192
+ const logsContainer = document.getElementById('logsContainer');
193
+ if (logs.length === 0) {
194
+ logsContainer.innerHTML = '<div class="log-entry"><p style="text-align: center; color: #999;">暂无日志</p></div>';
195
+ return;
196
+ }
197
+
198
+ logsContainer.innerHTML = logs.map(log => {
199
+ const time = new Date(log.timestamp).toLocaleTimeString('zh-CN');
200
+ const statusClass = log.error ? 'error' : 'success';
201
+ const statusText = log.statusCode || (log.error ? 'ERROR' : '?');
202
+ const duration = log.duration.toFixed(0);
203
+
204
+ return `
205
+ <div class="log-entry">
206
+ <span class="log-time">${time}</span>
207
+ <span class="log-port">:${log.localPort}</span>
208
+ <span class="log-method ${log.method}">${log.method}</span>
209
+ <span>${log.path}</span>
210
+ <span class="log-status ${statusClass}">${statusText}</span>
211
+ <span class="log-duration">${duration}ms</span>
212
+ ${log.error ? `<span style="color: #ef4444; margin-left: 10px;">⚠️ ${log.error}</span>` : ''}
213
+ </div>
214
+ `;
215
+ }).join('');
216
+ } catch (error) {
217
+ console.error('Failed to load logs:', error);
218
+ }
219
+ }
220
+
221
+ // 筛选日志
222
+ function filterLogs() {
223
+ const filter = document.getElementById('logFilter').value.toLowerCase();
224
+ const entries = document.querySelectorAll('.log-entry');
225
+
226
+ entries.forEach(entry => {
227
+ const text = entry.textContent.toLowerCase();
228
+ entry.style.display = text.includes(filter) ? '' : 'none';
229
+ });
230
+ }
231
+
232
+ // 清空日志
233
+ async function clearLogs() {
234
+ if (!confirm('确定清空所有日志吗?')) return;
235
+
236
+ try {
237
+ const response = await fetch('/api/logs', {
238
+ method: 'DELETE',
239
+ });
240
+
241
+ if (!response.ok) {
242
+ throw new Error('Failed to clear logs');
243
+ }
244
+
245
+ loadLogs();
246
+ } catch (error) {
247
+ console.error('Error clearing logs:', error);
248
+ }
249
+ }
@@ -0,0 +1,124 @@
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>Multiport Proxy - 配置管理</title>
7
+ <link rel="stylesheet" href="/style.css">
8
+ </head>
9
+ <body>
10
+ <div class="container">
11
+ <header>
12
+ <h1>🚀 Multiport Proxy</h1>
13
+ <p class="subtitle">多端口代理配置管理</p>
14
+ </header>
15
+
16
+ <main>
17
+ <div class="layout">
18
+ <!-- 左侧:配置面板 -->
19
+ <section class="panel config-panel">
20
+ <div class="panel-header">
21
+ <h2>📋 代理规则</h2>
22
+ <button class="btn btn-primary" onclick="showAddRuleForm()">+ 新增规则</button>
23
+ </div>
24
+
25
+ <div class="rules-list" id="rulesList">
26
+ <div class="loading">加载中...</div>
27
+ </div>
28
+ </section>
29
+
30
+ <!-- 右侧:日志面板 -->
31
+ <section class="panel log-panel">
32
+ <div class="panel-header">
33
+ <h2>📊 实时日志</h2>
34
+ <div class="log-controls">
35
+ <input type="text" id="logFilter" placeholder="筛选日志..." class="filter-input">
36
+ <button class="btn btn-small" onclick="clearLogs()">清空</button>
37
+ </div>
38
+ </div>
39
+
40
+ <div class="stats" id="stats">
41
+ <div class="stat-item">
42
+ <span class="label">总请求数:</span>
43
+ <span class="value" id="totalLogs">0</span>
44
+ </div>
45
+ <div class="stat-item">
46
+ <span class="label">错误数:</span>
47
+ <span class="value" id="errorCount">0</span>
48
+ </div>
49
+ <div class="stat-item">
50
+ <span class="label">平均耗时:</span>
51
+ <span class="value" id="avgDuration">0ms</span>
52
+ </div>
53
+ </div>
54
+
55
+ <div class="logs-container" id="logsContainer">
56
+ <div class="log-entry">
57
+ <p style="text-align: center; color: #999;">暂无日志</p>
58
+ </div>
59
+ </div>
60
+ </section>
61
+ </div>
62
+ </main>
63
+ </div>
64
+
65
+ <!-- 新增/编辑规则 Modal -->
66
+ <div id="ruleModal" class="modal">
67
+ <div class="modal-content">
68
+ <div class="modal-header">
69
+ <h3 id="modalTitle">新增代理规则</h3>
70
+ <button class="close-btn" onclick="closeRuleForm()">&times;</button>
71
+ </div>
72
+
73
+ <form id="ruleForm" onsubmit="saveRule(event)">
74
+ <div class="form-group">
75
+ <label for="rulePort">本地监听端口 *</label>
76
+ <input type="number" id="rulePort" min="1" max="65535" required placeholder="3000">
77
+ </div>
78
+
79
+ <div class="form-group">
80
+ <label for="ruleTarget">目标服务地址 *</label>
81
+ <input type="url" id="ruleTarget" required placeholder="http://localhost:8000">
82
+ </div>
83
+
84
+ <div class="form-group">
85
+ <label for="ruleTimeout">超时时间 (ms)</label>
86
+ <input type="number" id="ruleTimeout" min="1000" value="30000" placeholder="30000">
87
+ </div>
88
+
89
+ <div class="form-group">
90
+ <label for="ruleRetries">重试次数</label>
91
+ <input type="number" id="ruleRetries" min="0" value="0" placeholder="0">
92
+ </div>
93
+
94
+ <div class="form-group">
95
+ <label class="checkbox">
96
+ <input type="checkbox" id="ruleCorsEnabled">
97
+ 启用 CORS
98
+ </label>
99
+ </div>
100
+
101
+ <div class="form-group" id="corsOrigins" style="display: none;">
102
+ <label for="ruleCorsOrigins">CORS 来源 (逗号分隔)</label>
103
+ <input type="text" id="ruleCorsOrigins" placeholder="* 或 http://localhost:3000, http://localhost:3001">
104
+ </div>
105
+
106
+ <div class="form-group">
107
+ <label class="checkbox">
108
+ <input type="checkbox" id="ruleEnabled" checked>
109
+ 启用此规则
110
+ </label>
111
+ </div>
112
+
113
+ <div class="form-actions">
114
+ <button type="submit" class="btn btn-primary">保存</button>
115
+ <button type="button" class="btn btn-secondary" onclick="closeRuleForm()">取消</button>
116
+ <button type="button" class="btn btn-danger" id="deleteBtn" style="display: none;" onclick="deleteRule()">删除</button>
117
+ </div>
118
+ </form>
119
+ </div>
120
+ </div>
121
+
122
+ <script src="/app.js"></script>
123
+ </body>
124
+ </html>
@@ -0,0 +1,449 @@
1
+ :root {
2
+ --primary-color: #3b82f6;
3
+ --danger-color: #ef4444;
4
+ --success-color: #10b981;
5
+ --warning-color: #f59e0b;
6
+ --secondary-color: #6b7280;
7
+ --bg-primary: #ffffff;
8
+ --bg-secondary: #f9fafb;
9
+ --border-color: #e5e7eb;
10
+ --text-primary: #111827;
11
+ --text-secondary: #6b7280;
12
+ --shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
13
+ --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
14
+ }
15
+
16
+ * {
17
+ margin: 0;
18
+ padding: 0;
19
+ box-sizing: border-box;
20
+ }
21
+
22
+ body {
23
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
24
+ background-color: var(--bg-secondary);
25
+ color: var(--text-primary);
26
+ }
27
+
28
+ .container {
29
+ max-width: 1400px;
30
+ margin: 0 auto;
31
+ padding: 15px;
32
+ }
33
+
34
+ header {
35
+ text-align: center;
36
+ margin-bottom: 15px;
37
+ padding: 15px 0;
38
+ border-bottom: 1px solid var(--border-color);
39
+ }
40
+
41
+ header h1 {
42
+ font-size: 1.5rem;
43
+ margin-bottom: 5px;
44
+ color: var(--primary-color);
45
+ }
46
+
47
+ .subtitle {
48
+ font-size: 0.85rem;
49
+ color: var(--text-secondary);
50
+ }
51
+
52
+ .layout {
53
+ display: grid;
54
+ grid-template-columns: 400px 1fr;
55
+ gap: 15px;
56
+ height: calc(100vh - 150px);
57
+ }
58
+
59
+ .panel {
60
+ background: var(--bg-primary);
61
+ border-radius: 8px;
62
+ box-shadow: var(--shadow);
63
+ display: flex;
64
+ flex-direction: column;
65
+ overflow: hidden;
66
+ }
67
+
68
+ .panel-header {
69
+ padding: 12px 15px;
70
+ border-bottom: 1px solid var(--border-color);
71
+ display: flex;
72
+ justify-content: space-between;
73
+ align-items: center;
74
+ flex-wrap: wrap;
75
+ gap: 10px;
76
+ }
77
+
78
+ .panel-header h2 {
79
+ font-size: 1rem;
80
+ margin: 0;
81
+ }
82
+
83
+ .config-panel .rules-list,
84
+ .log-panel .logs-container {
85
+ flex: 1;
86
+ overflow-y: auto;
87
+ padding: 0;
88
+ }
89
+
90
+ /* 规则列表 */
91
+ .rules-list {
92
+ padding: 0;
93
+ }
94
+
95
+ .rule-item {
96
+ padding: 15px 20px;
97
+ border-bottom: 1px solid var(--border-color);
98
+ cursor: pointer;
99
+ transition: background-color 0.2s;
100
+ }
101
+
102
+ .rule-item:hover {
103
+ background-color: var(--bg-secondary);
104
+ }
105
+
106
+ .rule-item-header {
107
+ display: flex;
108
+ justify-content: space-between;
109
+ align-items: center;
110
+ margin-bottom: 8px;
111
+ }
112
+
113
+ .rule-port {
114
+ font-weight: bold;
115
+ color: var(--primary-color);
116
+ font-size: 1.1rem;
117
+ }
118
+
119
+ .rule-status {
120
+ display: inline-block;
121
+ width: 8px;
122
+ height: 8px;
123
+ border-radius: 50%;
124
+ margin-left: 8px;
125
+ }
126
+
127
+ .rule-status.enabled {
128
+ background-color: var(--success-color);
129
+ }
130
+
131
+ .rule-status.disabled {
132
+ background-color: var(--danger-color);
133
+ }
134
+
135
+ .rule-target {
136
+ color: var(--text-secondary);
137
+ font-size: 0.9rem;
138
+ white-space: nowrap;
139
+ overflow: hidden;
140
+ text-overflow: ellipsis;
141
+ }
142
+
143
+ .loading {
144
+ padding: 20px;
145
+ text-align: center;
146
+ color: var(--text-secondary);
147
+ }
148
+
149
+ /* 日志面板 */
150
+ .stats {
151
+ display: grid;
152
+ grid-template-columns: repeat(3, 1fr);
153
+ gap: 10px;
154
+ padding: 15px 20px;
155
+ background-color: var(--bg-secondary);
156
+ border-bottom: 1px solid var(--border-color);
157
+ }
158
+
159
+ .stat-item .label {
160
+ font-size: 0.85rem;
161
+ color: var(--text-secondary);
162
+ }
163
+
164
+ .stat-item .value {
165
+ font-weight: bold;
166
+ color: var(--primary-color);
167
+ }
168
+
169
+ .log-controls {
170
+ display: flex;
171
+ gap: 10px;
172
+ }
173
+
174
+ .filter-input {
175
+ padding: 8px 12px;
176
+ border: 1px solid var(--border-color);
177
+ border-radius: 4px;
178
+ font-size: 0.9rem;
179
+ min-width: 200px;
180
+ }
181
+
182
+ .logs-container {
183
+ padding: 10px 20px;
184
+ background-color: #fafafa;
185
+ color: var(--text-primary);
186
+ }
187
+
188
+ .log-entry {
189
+ padding: 2px 0;
190
+ border-bottom: none;
191
+ font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
192
+ font-size: 0.8rem;
193
+ line-height: 1.4;
194
+ }
195
+
196
+ .log-entry:hover {
197
+ background-color: #f0f0f0;
198
+ }
199
+
200
+ .log-time {
201
+ color: #6b7280;
202
+ margin-right: 10px;
203
+ font-size: 0.75rem;
204
+ }
205
+
206
+ .log-port {
207
+ color: #2563eb;
208
+ font-weight: normal;
209
+ margin-right: 10px;
210
+ }
211
+
212
+ .log-method {
213
+ color: #6b7280;
214
+ margin-right: 10px;
215
+ font-weight: 600;
216
+ min-width: 45px;
217
+ display: inline-block;
218
+ }
219
+
220
+ .log-method.GET {
221
+ color: #059669;
222
+ }
223
+
224
+ .log-method.POST {
225
+ color: #d97706;
226
+ }
227
+
228
+ .log-method.PUT {
229
+ color: #7c3aed;
230
+ }
231
+
232
+ .log-method.DELETE {
233
+ color: #dc2626;
234
+ }
235
+
236
+ .log-status {
237
+ font-weight: normal;
238
+ margin-right: 10px;
239
+ min-width: 35px;
240
+ display: inline-block;
241
+ }
242
+
243
+ .log-status.success {
244
+ color: #059669;
245
+ }
246
+
247
+ .log-status.error {
248
+ color: #dc2626;
249
+ }
250
+
251
+ .log-duration {
252
+ color: #6b7280;
253
+ margin-left: 10px;
254
+ font-size: 0.75rem;
255
+ }
256
+
257
+ /* 按钮 */
258
+ .btn {
259
+ padding: 10px 16px;
260
+ border: none;
261
+ border-radius: 4px;
262
+ font-size: 0.95rem;
263
+ font-weight: 500;
264
+ cursor: pointer;
265
+ transition: all 0.2s;
266
+ }
267
+
268
+ .btn-primary {
269
+ background-color: var(--primary-color);
270
+ color: white;
271
+ }
272
+
273
+ .btn-primary:hover {
274
+ background-color: #2563eb;
275
+ }
276
+
277
+ .btn-secondary {
278
+ background-color: var(--secondary-color);
279
+ color: white;
280
+ }
281
+
282
+ .btn-secondary:hover {
283
+ background-color: #4b5563;
284
+ }
285
+
286
+ .btn-danger {
287
+ background-color: var(--danger-color);
288
+ color: white;
289
+ }
290
+
291
+ .btn-danger:hover {
292
+ background-color: #dc2626;
293
+ }
294
+
295
+ .btn-small {
296
+ padding: 6px 12px;
297
+ font-size: 0.85rem;
298
+ }
299
+
300
+ /* Modal */
301
+ .modal {
302
+ display: none;
303
+ position: fixed;
304
+ top: 0;
305
+ left: 0;
306
+ width: 100%;
307
+ height: 100%;
308
+ background-color: rgba(0, 0, 0, 0.5);
309
+ z-index: 1000;
310
+ justify-content: center;
311
+ align-items: center;
312
+ }
313
+
314
+ .modal.show {
315
+ display: flex;
316
+ }
317
+
318
+ .modal-content {
319
+ background: white;
320
+ border-radius: 8px;
321
+ box-shadow: var(--shadow-lg);
322
+ max-width: 500px;
323
+ width: 90%;
324
+ max-height: 90vh;
325
+ overflow-y: auto;
326
+ }
327
+
328
+ .modal-header {
329
+ padding: 20px;
330
+ border-bottom: 1px solid var(--border-color);
331
+ display: flex;
332
+ justify-content: space-between;
333
+ align-items: center;
334
+ }
335
+
336
+ .modal-header h3 {
337
+ margin: 0;
338
+ }
339
+
340
+ .close-btn {
341
+ background: none;
342
+ border: none;
343
+ font-size: 1.5rem;
344
+ cursor: pointer;
345
+ color: var(--text-secondary);
346
+ }
347
+
348
+ .close-btn:hover {
349
+ color: var(--text-primary);
350
+ }
351
+
352
+ form {
353
+ padding: 20px;
354
+ }
355
+
356
+ .form-group {
357
+ margin-bottom: 16px;
358
+ }
359
+
360
+ .form-group label {
361
+ display: block;
362
+ margin-bottom: 6px;
363
+ font-weight: 500;
364
+ font-size: 0.95rem;
365
+ }
366
+
367
+ .form-group input[type="text"],
368
+ .form-group input[type="url"],
369
+ .form-group input[type="number"] {
370
+ width: 100%;
371
+ padding: 10px 12px;
372
+ border: 1px solid var(--border-color);
373
+ border-radius: 4px;
374
+ font-size: 0.95rem;
375
+ font-family: inherit;
376
+ }
377
+
378
+ .form-group input:focus {
379
+ outline: none;
380
+ border-color: var(--primary-color);
381
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
382
+ }
383
+
384
+ .checkbox {
385
+ display: flex;
386
+ align-items: center;
387
+ cursor: pointer;
388
+ font-weight: 400;
389
+ }
390
+
391
+ .checkbox input[type="checkbox"] {
392
+ margin-right: 8px;
393
+ width: 16px;
394
+ height: 16px;
395
+ cursor: pointer;
396
+ }
397
+
398
+ .form-actions {
399
+ display: flex;
400
+ gap: 10px;
401
+ margin-top: 20px;
402
+ padding-top: 20px;
403
+ border-top: 1px solid var(--border-color);
404
+ }
405
+
406
+ .form-actions button {
407
+ flex: 1;
408
+ }
409
+
410
+ /* 响应式 */
411
+ @media (max-width: 1024px) {
412
+ .layout {
413
+ grid-template-columns: 1fr;
414
+ height: auto;
415
+ }
416
+
417
+ .panel {
418
+ min-height: 400px;
419
+ }
420
+
421
+ .stats {
422
+ grid-template-columns: 1fr;
423
+ }
424
+ }
425
+
426
+ @media (max-width: 768px) {
427
+ .container {
428
+ padding: 10px;
429
+ }
430
+
431
+ header h1 {
432
+ font-size: 1.8rem;
433
+ }
434
+
435
+ .panel-header {
436
+ flex-direction: column;
437
+ align-items: flex-start;
438
+ }
439
+
440
+ .log-controls {
441
+ width: 100%;
442
+ flex-direction: column;
443
+ }
444
+
445
+ .filter-input {
446
+ width: 100%;
447
+ min-width: auto;
448
+ }
449
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jachy/multiport-proxy",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "A multi-port proxy server with web UI configuration",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -13,7 +13,7 @@
13
13
  ],
14
14
  "scripts": {
15
15
  "dev": "ts-node src/cli.ts",
16
- "build": "tsc && chmod +x dist/cli.js",
16
+ "build": "tsc && cp -r src/web/ui dist/web/ && chmod +x dist/cli.js",
17
17
  "start": "node dist/cli.js",
18
18
  "prepublishOnly": "pnpm run build",
19
19
  "test": "echo \"Error: no test specified\" && exit 1"