@jachy/multiport-proxy 0.0.1 → 0.0.3
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 +2 -0
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/web/api-routes.d.ts.map +1 -1
- package/dist/web/api-routes.js +68 -2
- package/dist/web/api-routes.js.map +1 -1
- package/dist/web/ui/app.js +252 -0
- package/dist/web/ui/index.html +124 -0
- package/dist/web/ui/style.css +449 -0
- package/package.json +2 -2
package/README.md
CHANGED
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
|
|
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,
|
|
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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-routes.d.ts","sourceRoot":"","sources":["../../src/web/api-routes.ts"],"names":[],"mappings":"AAAA,OAAgB,EAAE,MAAM,EAAqB,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"api-routes.d.ts","sourceRoot":"","sources":["../../src/web/api-routes.ts"],"names":[],"mappings":"AAAA,OAAgB,EAAE,MAAM,EAAqB,MAAM,SAAS,CAAC;AAG7D,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAkDrD,wBAAgB,eAAe,CAC7B,aAAa,EAAE,aAAa,EAC5B,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,WAAW,GACvB,MAAM,CA0IR"}
|
package/dist/web/api-routes.js
CHANGED
|
@@ -5,6 +5,51 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.createApiRouter = createApiRouter;
|
|
7
7
|
const express_1 = __importDefault(require("express"));
|
|
8
|
+
const child_process_1 = require("child_process");
|
|
9
|
+
// 检查端口是否被占用
|
|
10
|
+
function checkPortAvailable(port) {
|
|
11
|
+
const isWindows = process.platform === 'win32';
|
|
12
|
+
try {
|
|
13
|
+
let command;
|
|
14
|
+
let output;
|
|
15
|
+
if (isWindows) {
|
|
16
|
+
// Windows 使用 netstat
|
|
17
|
+
command = `netstat -ano | findstr :${port}`;
|
|
18
|
+
output = (0, child_process_1.execSync)(command, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).toString();
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
// macOS 和 Linux 使用 lsof
|
|
22
|
+
command = `lsof -i :${port}`;
|
|
23
|
+
output = (0, child_process_1.execSync)(command, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).toString();
|
|
24
|
+
}
|
|
25
|
+
if (output.trim()) {
|
|
26
|
+
const lines = output.trim().split('\n');
|
|
27
|
+
const details = lines.slice(1).map(line => line.trim()).join('\n');
|
|
28
|
+
return {
|
|
29
|
+
available: false,
|
|
30
|
+
details: `端口 ${port} 已被占用\n\n程序信息:\n${details}`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
available: true,
|
|
35
|
+
details: `端口 ${port} 可用`,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
// 命令执行失败可能表示端口未被占用
|
|
40
|
+
if (error.status === 1 || error.code === 1) {
|
|
41
|
+
return {
|
|
42
|
+
available: true,
|
|
43
|
+
details: `端口 ${port} 可用`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// 其他错误
|
|
47
|
+
return {
|
|
48
|
+
available: false,
|
|
49
|
+
details: `端口 ${port} 检查失败: ${error.message}`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
8
53
|
function createApiRouter(configManager, logger, proxyServer) {
|
|
9
54
|
const router = express_1.default.Router();
|
|
10
55
|
// 获取所有配置
|
|
@@ -27,13 +72,21 @@ function createApiRouter(configManager, logger, proxyServer) {
|
|
|
27
72
|
}
|
|
28
73
|
});
|
|
29
74
|
// 添加规则
|
|
30
|
-
router.post('/config/rules', (req, res) => {
|
|
75
|
+
router.post('/config/rules', async (req, res) => {
|
|
31
76
|
try {
|
|
32
77
|
const rule = req.body;
|
|
33
78
|
if (!rule.id) {
|
|
34
79
|
rule.id = Math.random().toString(36).slice(2);
|
|
35
80
|
}
|
|
36
81
|
rule.enabled = rule.enabled !== false;
|
|
82
|
+
// 检查端口是否被占用
|
|
83
|
+
const portCheck = checkPortAvailable(rule.localPort);
|
|
84
|
+
if (!portCheck.available) {
|
|
85
|
+
return res.status(400).json({
|
|
86
|
+
error: 'Port unavailable',
|
|
87
|
+
details: portCheck.details,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
37
90
|
configManager.addRule(rule);
|
|
38
91
|
proxyServer.updateProxies();
|
|
39
92
|
res.json({ success: true, rule });
|
|
@@ -43,10 +96,23 @@ function createApiRouter(configManager, logger, proxyServer) {
|
|
|
43
96
|
}
|
|
44
97
|
});
|
|
45
98
|
// 更新规则
|
|
46
|
-
router.put('/config/rules/:id', (req, res) => {
|
|
99
|
+
router.put('/config/rules/:id', async (req, res) => {
|
|
47
100
|
try {
|
|
48
101
|
const { id } = req.params;
|
|
49
102
|
const updates = req.body;
|
|
103
|
+
// 获取旧规则
|
|
104
|
+
const oldRule = configManager.getConfig().rules.find(r => r.id === id);
|
|
105
|
+
const portChanged = oldRule && oldRule.localPort !== updates.localPort;
|
|
106
|
+
// 如果端口改变了,需要检查新端口是否可用
|
|
107
|
+
if (portChanged) {
|
|
108
|
+
const portCheck = checkPortAvailable(updates.localPort);
|
|
109
|
+
if (!portCheck.available) {
|
|
110
|
+
return res.status(400).json({
|
|
111
|
+
error: 'Port unavailable',
|
|
112
|
+
details: portCheck.details,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
50
116
|
configManager.updateRule(id, updates);
|
|
51
117
|
proxyServer.updateProxies();
|
|
52
118
|
res.json({ success: true, message: 'Rule updated' });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-routes.js","sourceRoot":"","sources":["../../src/web/api-routes.ts"],"names":[],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"api-routes.js","sourceRoot":"","sources":["../../src/web/api-routes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,252 @@
|
|
|
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 = true;
|
|
63
|
+
const corsOrigins = document.getElementById('corsOrigins');
|
|
64
|
+
corsOrigins.style.display = 'block';
|
|
65
|
+
document.getElementById('ruleModal').classList.add('show');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 编辑规则
|
|
69
|
+
async function editRule(id) {
|
|
70
|
+
try {
|
|
71
|
+
const response = await fetch('/api/config');
|
|
72
|
+
const data = await response.json();
|
|
73
|
+
const rule = data.rules.find(r => r.id === id);
|
|
74
|
+
|
|
75
|
+
if (!rule) return;
|
|
76
|
+
|
|
77
|
+
currentEditingId = id;
|
|
78
|
+
document.getElementById('modalTitle').textContent = '编辑代理规则';
|
|
79
|
+
document.getElementById('deleteBtn').style.display = 'inline-block';
|
|
80
|
+
document.getElementById('rulePort').value = rule.localPort;
|
|
81
|
+
document.getElementById('ruleTarget').value = rule.targetUrl;
|
|
82
|
+
document.getElementById('ruleTimeout').value = rule.timeout || 30000;
|
|
83
|
+
document.getElementById('ruleRetries').value = rule.retries || 0;
|
|
84
|
+
document.getElementById('ruleEnabled').checked = rule.enabled !== false;
|
|
85
|
+
document.getElementById('ruleCorsEnabled').checked = rule.cors?.enabled || false;
|
|
86
|
+
document.getElementById('ruleCorsOrigins').value = rule.cors?.origins?.join(', ') || '*';
|
|
87
|
+
document.getElementById('corsOrigins').style.display = rule.cors?.enabled ? 'block' : 'none';
|
|
88
|
+
|
|
89
|
+
document.getElementById('ruleModal').classList.add('show');
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.error('Failed to edit rule:', error);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 保存规则
|
|
96
|
+
async function saveRule(e) {
|
|
97
|
+
e.preventDefault();
|
|
98
|
+
|
|
99
|
+
const rule = {
|
|
100
|
+
localPort: parseInt(document.getElementById('rulePort').value),
|
|
101
|
+
targetUrl: document.getElementById('ruleTarget').value,
|
|
102
|
+
timeout: parseInt(document.getElementById('ruleTimeout').value),
|
|
103
|
+
retries: parseInt(document.getElementById('ruleRetries').value),
|
|
104
|
+
enabled: document.getElementById('ruleEnabled').checked,
|
|
105
|
+
cors: {
|
|
106
|
+
enabled: document.getElementById('ruleCorsEnabled').checked,
|
|
107
|
+
origins: document.getElementById('ruleCorsOrigins').value
|
|
108
|
+
.split(',')
|
|
109
|
+
.map(o => o.trim())
|
|
110
|
+
.filter(o => o),
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
if (currentEditingId) {
|
|
116
|
+
// 更新规则
|
|
117
|
+
const response = await fetch(`/api/config/rules/${currentEditingId}`, {
|
|
118
|
+
method: 'PUT',
|
|
119
|
+
headers: { 'Content-Type': 'application/json' },
|
|
120
|
+
body: JSON.stringify(rule),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
const errorData = await response.json();
|
|
125
|
+
throw new Error(errorData.details || errorData.error || 'Failed to update rule');
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
// 新增规则
|
|
129
|
+
const response = await fetch('/api/config/rules', {
|
|
130
|
+
method: 'POST',
|
|
131
|
+
headers: { 'Content-Type': 'application/json' },
|
|
132
|
+
body: JSON.stringify(rule),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
if (!response.ok) {
|
|
136
|
+
const errorData = await response.json();
|
|
137
|
+
throw new Error(errorData.details || errorData.error || 'Failed to add rule');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
closeRuleForm();
|
|
142
|
+
loadRules();
|
|
143
|
+
alert('规则已保存!');
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error('Error saving rule:', error);
|
|
146
|
+
alert('保存失败:' + error.message);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 删除规则
|
|
151
|
+
async function deleteRule() {
|
|
152
|
+
if (!currentEditingId) return;
|
|
153
|
+
|
|
154
|
+
if (!confirm('确定删除此规则吗?')) return;
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
const response = await fetch(`/api/config/rules/${currentEditingId}`, {
|
|
158
|
+
method: 'DELETE',
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (!response.ok) {
|
|
162
|
+
throw new Error('Failed to delete rule');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
closeRuleForm();
|
|
166
|
+
loadRules();
|
|
167
|
+
alert('规则已删除!');
|
|
168
|
+
} catch (error) {
|
|
169
|
+
console.error('Error deleting rule:', error);
|
|
170
|
+
alert('删除失败:' + error.message);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// 关闭表单
|
|
175
|
+
function closeRuleForm() {
|
|
176
|
+
document.getElementById('ruleModal').classList.remove('show');
|
|
177
|
+
currentEditingId = null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 加载日志
|
|
181
|
+
async function loadLogs() {
|
|
182
|
+
try {
|
|
183
|
+
const response = await fetch('/api/logs');
|
|
184
|
+
const data = await response.json();
|
|
185
|
+
const logs = data.logs || [];
|
|
186
|
+
|
|
187
|
+
// 更新统计信息
|
|
188
|
+
if (data.stats) {
|
|
189
|
+
document.getElementById('totalLogs').textContent = data.stats.totalLogs;
|
|
190
|
+
document.getElementById('errorCount').textContent = data.stats.errorCount;
|
|
191
|
+
document.getElementById('avgDuration').textContent = data.stats.averageDuration + 'ms';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 渲染日志
|
|
195
|
+
const logsContainer = document.getElementById('logsContainer');
|
|
196
|
+
if (logs.length === 0) {
|
|
197
|
+
logsContainer.innerHTML = '<div class="log-entry"><p style="text-align: center; color: #999;">暂无日志</p></div>';
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
logsContainer.innerHTML = logs.map(log => {
|
|
202
|
+
const time = new Date(log.timestamp).toLocaleTimeString('zh-CN');
|
|
203
|
+
const statusClass = log.error ? 'error' : 'success';
|
|
204
|
+
const statusText = log.statusCode || (log.error ? 'ERROR' : '?');
|
|
205
|
+
const duration = log.duration.toFixed(0);
|
|
206
|
+
|
|
207
|
+
return `
|
|
208
|
+
<div class="log-entry">
|
|
209
|
+
<span class="log-time">${time}</span>
|
|
210
|
+
<span class="log-port">:${log.localPort}</span>
|
|
211
|
+
<span class="log-method ${log.method}">${log.method}</span>
|
|
212
|
+
<span>${log.path}</span>
|
|
213
|
+
<span class="log-status ${statusClass}">${statusText}</span>
|
|
214
|
+
<span class="log-duration">${duration}ms</span>
|
|
215
|
+
${log.error ? `<span style="color: #ef4444; margin-left: 10px;">⚠️ ${log.error}</span>` : ''}
|
|
216
|
+
</div>
|
|
217
|
+
`;
|
|
218
|
+
}).join('');
|
|
219
|
+
} catch (error) {
|
|
220
|
+
console.error('Failed to load logs:', error);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// 筛选日志
|
|
225
|
+
function filterLogs() {
|
|
226
|
+
const filter = document.getElementById('logFilter').value.toLowerCase();
|
|
227
|
+
const entries = document.querySelectorAll('.log-entry');
|
|
228
|
+
|
|
229
|
+
entries.forEach(entry => {
|
|
230
|
+
const text = entry.textContent.toLowerCase();
|
|
231
|
+
entry.style.display = text.includes(filter) ? '' : 'none';
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 清空日志
|
|
236
|
+
async function clearLogs() {
|
|
237
|
+
if (!confirm('确定清空所有日志吗?')) return;
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
const response = await fetch('/api/logs', {
|
|
241
|
+
method: 'DELETE',
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
if (!response.ok) {
|
|
245
|
+
throw new Error('Failed to clear logs');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
loadLogs();
|
|
249
|
+
} catch (error) {
|
|
250
|
+
console.error('Error clearing logs:', error);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
@@ -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()">×</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" value="*" 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.
|
|
3
|
+
"version": "0.0.3",
|
|
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"
|