block-proxy 0.1.12 → 0.1.13

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.
Files changed (58) hide show
  1. package/.claude/settings.local.json +26 -1
  2. package/.claude/skills/build-client/skill.md +24 -0
  3. package/.claude/skills/release-client/skill.md +68 -0
  4. package/CLAUDE.md +69 -67
  5. package/Dockerfile +1 -1
  6. package/README.md +38 -24
  7. package/build/asset-manifest.json +6 -6
  8. package/build/index.html +1 -1
  9. package/build/static/css/main.3f317ce6.css +2 -0
  10. package/build/static/css/main.3f317ce6.css.map +1 -0
  11. package/build/static/js/{main.2247fb80.js → main.68f66be0.js} +3 -3
  12. package/build/static/js/main.68f66be0.js.map +1 -0
  13. package/client/app.py +312 -0
  14. package/client/build.sh +84 -0
  15. package/client/config.py +49 -0
  16. package/client/config_window.py +155 -0
  17. package/client/icons/app.icns +0 -0
  18. package/client/icons/app_example.png +0 -0
  19. package/client/icons/app_icon.png +0 -0
  20. package/client/icons/backup/app_example.png +0 -0
  21. package/client/icons/backup/christmas-sock_dark.png +0 -0
  22. package/client/icons/backup/christmas-sock_light.png +0 -0
  23. package/client/icons/backup/socks_on_G.png +0 -0
  24. package/client/icons/backup/socks_on_M.png +0 -0
  25. package/client/icons/christmas-sock_dark.png +0 -0
  26. package/client/icons/christmas-sock_light.png +0 -0
  27. package/client/icons/christmas-sock_light_bar.png +0 -0
  28. package/client/icons/socks_on_G.png +0 -0
  29. package/client/icons/socks_on_G_bar.png +0 -0
  30. package/client/icons/socks_on_M.png +0 -0
  31. package/client/icons/socks_on_M_bar.png +0 -0
  32. package/client/main.py +28 -0
  33. package/client/proxy_core.py +475 -0
  34. package/client/requirements.txt +3 -0
  35. package/client/scripts/download_xray.sh +30 -0
  36. package/client/setup.py +30 -0
  37. package/client/system_proxy.py +94 -0
  38. package/client/tests/__init__.py +0 -0
  39. package/client/tests/test_config.py +72 -0
  40. package/client/tests/test_system_proxy.py +69 -0
  41. package/client/watch-icons.js +31 -0
  42. package/config.json +28 -3
  43. package/docs/superpowers/plans/2026-05-27-blockproxyclient.md +1274 -0
  44. package/docs/superpowers/specs/2026-05-27-blockproxyclient-design.md +264 -0
  45. package/package.json +10 -4
  46. package/proxy/proxy.js +19 -15
  47. package/server/express.js +17 -1
  48. package/src/App.css +596 -276
  49. package/src/App.js +25 -22
  50. package/src/index.css +3 -4
  51. package/test/lib/mock-server.js +133 -0
  52. package/test/proxy-tests.js +708 -0
  53. package/test/run.js +330 -0
  54. package/build/static/css/main.8bfa3d5f.css +0 -2
  55. package/build/static/css/main.8bfa3d5f.css.map +0 -1
  56. package/build/static/js/main.2247fb80.js.map +0 -1
  57. package/hack-of-anyproxy/lib/requestHandler.js +0 -1060
  58. /package/build/static/js/{main.2247fb80.js.LICENSE.txt → main.68f66be0.js.LICENSE.txt} +0 -0
package/test/run.js ADDED
@@ -0,0 +1,330 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { spawn } = require('child_process');
4
+ const path = require('path');
5
+ const MockServer = require('./lib/mock-server');
6
+ const { runAllTests, checkProxyAccessible } = require('./proxy-tests');
7
+
8
+ // ── 配置 ──────────────────────────────────────────────────
9
+
10
+ const PROJECT_ROOT = path.resolve(__dirname, '..');
11
+ const DEFAULT_CONFIG = {
12
+ httpProxy: { host: '127.0.0.1', port: 8001, username: 'lijing', password: 'lijing' },
13
+ socks5: { host: '127.0.0.1', port: 8002, username: 'lijing', password: 'lijing' },
14
+ };
15
+
16
+ // ── CLI 参数解析 ──────────────────────────────────────────
17
+
18
+ function parseArgs() {
19
+ const args = process.argv.slice(2);
20
+ const opts = {
21
+ httpPort: 8001,
22
+ socks5Port: 8002,
23
+ httpUser: 'lijing',
24
+ httpPass: 'lijing',
25
+ skipExternal: false,
26
+ autoStart: false,
27
+ help: false,
28
+ };
29
+
30
+ for (let i = 0; i < args.length; i++) {
31
+ const a = args[i];
32
+ if (a === '--http-port' || a === '-p') opts.httpPort = parseInt(args[++i], 10);
33
+ else if (a === '--socks5-port' || a === '-s') opts.socks5Port = parseInt(args[++i], 10);
34
+ else if (a === '--http-user' || a === '-u') opts.httpUser = args[++i];
35
+ else if (a === '--http-pass') opts.httpPass = args[++i];
36
+ else if (a === '--skip-external') opts.skipExternal = true;
37
+ else if (a === '--auto-start') opts.autoStart = true;
38
+ else if (a === '--help' || a === '-h') opts.help = true;
39
+ }
40
+
41
+ return opts;
42
+ }
43
+
44
+ function printHelp() {
45
+ console.log(`
46
+ Block-Proxy 测试工具
47
+
48
+ 用法: node test/run.js [选项]
49
+
50
+ 选项:
51
+ -p, --http-port <port> HTTP 代理端口 (默认: 8001)
52
+ -s, --socks5-port <port> SOCKS5 代理端口 (默认: 8002)
53
+ -u, --http-user <user> 代理认证用户名 (默认: lijing)
54
+ --http-pass <pass> 代理认证密码 (默认: lijing)
55
+ --skip-external 跳过外部站点测试
56
+ --auto-start 自动启动代理服务 (如未运行)
57
+ -h, --help 显示帮助信息
58
+ `);
59
+ process.exit(0);
60
+ }
61
+
62
+ // ── 输出工具 ──────────────────────────────────────────────
63
+
64
+ const C = {
65
+ reset: '\x1b[0m',
66
+ bold: '\x1b[1m',
67
+ dim: '\x1b[2m',
68
+ green: '\x1b[32m',
69
+ red: '\x1b[31m',
70
+ yellow: '\x1b[33m',
71
+ cyan: '\x1b[36m',
72
+ gray: '\x1b[90m',
73
+ };
74
+
75
+ function icon(passed) {
76
+ return passed ? `${C.green}✓${C.reset}` : `${C.red}✗${C.reset}`;
77
+ }
78
+
79
+ function pad(str, len) {
80
+ return str.padEnd(len, ' ');
81
+ }
82
+
83
+ // ── 报告渲染 ──────────────────────────────────────────────
84
+
85
+ function printReport(data) {
86
+ const { categories, summary } = data;
87
+
88
+ console.log('');
89
+ console.log(`${C.bold}${C.cyan}══════════════════════════════════════════════════════════${C.reset}`);
90
+ console.log(`${C.bold}${C.cyan} Block-Proxy 代理测试报告${C.reset}`);
91
+ console.log(`${C.bold}${C.cyan}══════════════════════════════════════════════════════════${C.reset}`);
92
+ console.log('');
93
+
94
+ let maxNameLen = 0;
95
+ for (const cat of categories) {
96
+ for (const r of cat.results) {
97
+ if (r.name.length > maxNameLen) maxNameLen = r.name.length;
98
+ }
99
+ }
100
+ maxNameLen = Math.min(maxNameLen + 4, 70);
101
+
102
+ for (const cat of categories) {
103
+ const catIcon = icon(cat.passed);
104
+ console.log(`${C.bold}${catIcon} ${cat.category}${C.reset}`);
105
+ console.log(`${C.dim}${'─'.repeat(58)}${C.reset}`);
106
+
107
+ for (const r of cat.results) {
108
+ const statusIcon = icon(r.passed);
109
+ const name = pad(r.name, maxNameLen);
110
+ const dur = `${r.duration}ms`.padStart(8);
111
+ console.log(` ${statusIcon} ${C.gray}${name}${C.reset} ${dur} ${r.detail}`);
112
+ }
113
+ console.log('');
114
+ }
115
+
116
+ // ── 总结 ──
117
+ console.log(`${C.bold}${'─'.repeat(58)}${C.reset}`);
118
+ console.log(`${C.bold} 总结${C.reset}`);
119
+ console.log(`${C.bold}${'─'.repeat(58)}${C.reset}`);
120
+
121
+ const summaryIcon = summary.failed === 0 ? `${C.green}✓${C.reset}` : `${C.red}✗${C.reset}`;
122
+ console.log(` ${summaryIcon} 总测试项: ${summary.total} | 通过: ${summary.passed} | 失败: ${summary.failed}`);
123
+ console.log(` 分类通过: ${summary.categoriesPassed}/${summary.categoriesTotal}`);
124
+
125
+ // 关键指标
126
+ for (const cat of categories) {
127
+ if (cat.metrics) {
128
+ if (cat.metrics.p50 !== undefined) {
129
+ console.log(` ${C.dim}${cat.category}: P50=${cat.metrics.p50}ms P95=${cat.metrics.p95}ms P99=${cat.metrics.p99}ms${C.reset}`);
130
+ } else if (cat.metrics.qps !== undefined) {
131
+ console.log(` ${C.dim}${cat.category}: QPS=${cat.metrics.qps} 平均延迟=${cat.metrics.avgLatency}ms${C.reset}`);
132
+ } else if (cat.metrics.mbps !== undefined) {
133
+ console.log(` ${C.dim}${cat.category}: ${cat.metrics.mbps} MB/s${C.reset}`);
134
+ }
135
+ }
136
+ }
137
+
138
+ console.log('');
139
+
140
+ // ── 判定 ──
141
+ if (summary.failed === 0) {
142
+ console.log(`${C.green}${C.bold} ✓ 所有测试通过${C.reset}`);
143
+ } else {
144
+ console.log(`${C.red}${C.bold} ✗ 存在 ${summary.failed} 个失败项${C.reset}`);
145
+ }
146
+ console.log('');
147
+
148
+ return summary.failed === 0;
149
+ }
150
+
151
+ // ── 服务管理 ──────────────────────────────────────────────
152
+
153
+ async function waitForPort(host, port, timeoutMs = 15000) {
154
+ const start = Date.now();
155
+ while (Date.now() - start < timeoutMs) {
156
+ const { accessible } = await checkProxyAccessible(host, port);
157
+ if (accessible) return true;
158
+ await new Promise((r) => setTimeout(r, 500));
159
+ }
160
+ return false;
161
+ }
162
+
163
+ let childProcesses = [];
164
+
165
+ async function startProxyService(name, scriptPath) {
166
+ return new Promise((resolve, reject) => {
167
+ const proc = spawn('node', [scriptPath], {
168
+ cwd: PROJECT_ROOT,
169
+ stdio: 'pipe',
170
+ });
171
+
172
+ childProcesses.push(proc);
173
+
174
+ let started = false;
175
+ proc.stdout.on('data', (data) => {
176
+ const text = data.toString();
177
+ if (!started && (text.includes('start') || text.includes('启动') || text.includes('listen'))) {
178
+ started = true;
179
+ resolve(proc);
180
+ }
181
+ });
182
+
183
+ proc.stderr.on('data', (data) => {
184
+ const text = data.toString();
185
+ if (!started && (text.includes('start') || text.includes('启动') || text.includes('listen'))) {
186
+ started = true;
187
+ resolve(proc);
188
+ }
189
+ });
190
+
191
+ proc.on('error', reject);
192
+
193
+ // timeout fallback
194
+ setTimeout(() => {
195
+ if (!started) {
196
+ started = true;
197
+ resolve(proc);
198
+ }
199
+ }, 3000);
200
+ });
201
+ }
202
+
203
+ function cleanupChildProcesses() {
204
+ for (const proc of childProcesses) {
205
+ try { proc.kill('SIGTERM'); } catch (_) {}
206
+ }
207
+ childProcesses = [];
208
+ }
209
+
210
+ // ── 主流程 ────────────────────────────────────────────────
211
+
212
+ async function main() {
213
+ const opts = parseArgs();
214
+ if (opts.help) printHelp();
215
+
216
+ const httpProxyConfig = {
217
+ host: '127.0.0.1',
218
+ port: opts.httpPort,
219
+ username: opts.httpUser,
220
+ password: opts.httpPass,
221
+ };
222
+
223
+ const socks5Config = {
224
+ host: '127.0.0.1',
225
+ port: opts.socks5Port,
226
+ username: opts.httpUser,
227
+ password: opts.httpPass,
228
+ };
229
+
230
+ // ── 检查代理服务状态 ──
231
+ console.log(`${C.bold}检查代理服务...${C.reset}`);
232
+
233
+ const [httpStatus, socks5Status] = await Promise.all([
234
+ checkProxyAccessible(httpProxyConfig.host, httpProxyConfig.port, 'HTTP Proxy'),
235
+ checkProxyAccessible(socks5Config.host, socks5Config.port, 'SOCKS5 Proxy'),
236
+ ]);
237
+
238
+ console.log(` HTTP Proxy (${httpProxyConfig.port}): ${httpStatus.accessible ? `${C.green}运行中${C.reset}` : `${C.red}不可达${C.reset}`}`);
239
+ console.log(` SOCKS5 (${socks5Config.port}): ${socks5Status.accessible ? `${C.green}运行中${C.reset}` : `${C.red}不可达${C.reset}`}`);
240
+
241
+ if (!httpStatus.accessible && !socks5Status.accessible) {
242
+ if (opts.autoStart) {
243
+ console.log(`\n${C.yellow}代理服务未运行,自动启动...${C.reset}`);
244
+
245
+ startProxyService('proxy', path.join(PROJECT_ROOT, 'proxy/start.js'));
246
+ startProxyService('socks5', path.join(PROJECT_ROOT, 'socks5/start.js'));
247
+
248
+ const [httpReady, socks5Ready] = await Promise.all([
249
+ waitForPort(httpProxyConfig.host, httpProxyConfig.port),
250
+ waitForPort(socks5Config.host, socks5Config.port),
251
+ ]);
252
+
253
+ if (!httpReady) console.log(` ${C.red}HTTP Proxy 启动失败${C.reset}`);
254
+ else console.log(` HTTP Proxy ${C.green}已就绪${C.reset}`);
255
+
256
+ if (!socks5Ready) console.log(` ${C.red}SOCKS5 启动失败${C.reset}`);
257
+ else console.log(` SOCKS5 ${C.green}已就绪${C.reset}`);
258
+
259
+ if (!httpReady && !socks5Ready) {
260
+ console.log(`\n${C.red}无法启动代理服务,请手动启动后再运行测试。${C.reset}`);
261
+ console.log(`启动方式: npm run dev\n`);
262
+ process.exit(1);
263
+ }
264
+ } else {
265
+ console.log(`\n${C.red}代理服务未运行!请先启动代理服务:${C.reset}`);
266
+ console.log(` npm run dev # 启动全部服务`);
267
+ console.log(` npm run proxy # 仅 HTTP 代理`);
268
+ console.log(` npm run socks5 # 仅 SOCKS5`);
269
+ console.log(`\n或使用 --auto-start 自动启动\n`);
270
+ process.exit(1);
271
+ }
272
+ }
273
+
274
+ const effectiveHttpProxy = httpStatus.accessible ? httpProxyConfig : null;
275
+ const effectiveSocks5 = socks5Status.accessible ? socks5Config : null;
276
+
277
+ // ── 启动 Mock Server ──
278
+ console.log(`\n${C.bold}启动 Mock Server...${C.reset}`);
279
+ const mockServer = new MockServer();
280
+ await mockServer.start();
281
+ console.log(` Mock Server: ${mockServer.baseUrl} ${C.green}就绪${C.reset}`);
282
+
283
+ // ── 运行测试 ──
284
+ console.log(`\n${C.bold}开始测试...${C.reset}\n`);
285
+
286
+ const startTime = Date.now();
287
+ let allPassed = false;
288
+
289
+ try {
290
+ const results = await runAllTests({
291
+ mockBaseUrl: mockServer.baseUrl,
292
+ httpProxy: effectiveHttpProxy,
293
+ socks5: effectiveSocks5,
294
+ skipExternal: opts.skipExternal,
295
+ });
296
+
297
+ const elapsed = Date.now() - startTime;
298
+ results._elapsed = elapsed;
299
+
300
+ allPassed = printReport(results);
301
+ console.log(`${C.dim} 总耗时: ${(elapsed / 1000).toFixed(1)}s${C.reset}\n`);
302
+ } catch (err) {
303
+ console.error(`${C.red}测试执行异常: ${err.message}${C.reset}`);
304
+ console.error(err.stack);
305
+ } finally {
306
+ // ── 清理 ──
307
+ await mockServer.stop();
308
+ cleanupChildProcesses();
309
+ }
310
+
311
+ process.exit(allPassed ? 0 : 1);
312
+ }
313
+
314
+ // 优雅退出
315
+ process.on('SIGINT', () => {
316
+ cleanupChildProcesses();
317
+ process.exit(130);
318
+ });
319
+
320
+ process.on('SIGTERM', () => {
321
+ cleanupChildProcesses();
322
+ process.exit(143);
323
+ });
324
+
325
+ main().catch((err) => {
326
+ console.error(`Fatal: ${err.message}`);
327
+ console.error(err.stack);
328
+ cleanupChildProcesses();
329
+ process.exit(1);
330
+ });
@@ -1,2 +0,0 @@
1
- body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;margin:0}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}.server-info p{margin:5px 0}.ip-list{list-style:none;margin:10px 0;padding:0}.ip-item{border-bottom:1px solid #eee;display:flex;padding:8px 0}.ip-item:last-child{border-bottom:none}.interface-name{color:#555;font-weight:700}.ip-address{background-color:#f8f9fa;border-radius:3px;flex:1 1;font-family:Courier New,monospace;padding:2px 6px}.toast{align-items:center;animation:toastSlideIn .3s ease-out;border-radius:4px;box-shadow:0 4px 12px #00000026;color:#fff;display:flex;font-weight:500;min-width:250px;padding:16px 20px;position:fixed;right:20px;top:20px;z-index:1000}@keyframes toastSlideIn{0%{opacity:0;transform:translateX(100%)}to{opacity:1;transform:translateX(0)}}.toast.success{background-color:#28a745;border-left:4px solid #1e7e34}.toast.error{background-color:#dc3545;border-left:4px solid #bd2130}.toast.info{background-color:#17a2b8;border-left:4px solid #117a8b}.toast-close{align-items:center;background:none;border:none;color:#fff;cursor:pointer;display:flex;font-size:20px;font-weight:700;height:20px;justify-content:center;margin-left:15px;padding:0;width:20px}.toast-close:hover{opacity:.7}.App{background-color:#f5f5f5;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;min-height:100vh;padding:20px;text-align:center}.config-container{background:#fff;border-radius:8px;box-shadow:0 2px 10px #0000001a;margin:0 auto;max-width:1220px;padding:30px;position:relative;text-align:left}.config-container h1{color:#333;margin-top:0;text-align:center}.config-section{background-color:#fafafa;border:1px solid #eee;border-radius:5px;margin-bottom:30px;padding:20px}.config-section h2{border-bottom:1px solid #eee;color:#555;margin-top:0;padding-bottom:10px}.host-input{display:flex;flex-direction:row;gap:10px;margin-bottom:15px}.host-input input[type=text]{border:1px solid #ddd;border-radius:4px;flex:1 1;font-size:14px;padding:10px}.host-input button{align-self:flex-start;background-color:#007bff;border:none;border-radius:4px;color:#fff;cursor:pointer;font-size:14px}.host-input button:hover{background-color:#0069d9}.time-inputs{align-items:center;display:flex;gap:10px}.time-inputs label{flex-direction:column;font-size:14px}hr.simple-line{background-color:#e3e3e3;border-width:0;height:1px}input[type=time]{background-color:#fff;border:1px solid #ddd;border-radius:3px;padding:4px}input[type=time]:focus{border-color:#007bff;box-shadow:0 0 0 2px #007bff40;outline:none}.host-list{list-style:none;margin:0;padding:0}.host-item{align-items:flex-start;border-bottom:1px solid #eee;display:flex;justify-content:space-between;padding:12px 0}.host-item:last-child{border-bottom:none}.host-info{align-items:center;display:flex;flex-grow:1;font-size:15px}span.host-text{align-self:center;flex-grow:1;font-size:14px}.time-controls{align-items:center;display:flex;gap:8px}.time-controls label{display:flex;flex-direction:column;font-size:12px}.remove-btn{align-self:center;background-color:#dc3545;border:none;border-radius:3px;color:#fff;cursor:pointer;font-size:12px;height:-webkit-fit-content;height:fit-content;margin-left:10px;min-width:23px;padding:5px 7px}.remove-btn:hover{background-color:#c82333}.setting-row{align-items:center;display:flex;gap:15px;margin-bottom:15px}.setting-row label{color:#555;flex-shrink:0;font-weight:700;text-align:right;width:220px}.setting-row input{flex:1 1}.setting-row input,.setting-row input[type=number]{border:1px solid #ddd;border-radius:4px;padding:8px}.setting-row input[type=number]{font-size:14px}.setting-row input[type=number]:focus{border-color:#007bff;box-shadow:0 0 0 2px #007bff40;outline:none}.actions{display:flex;justify-content:space-between}.restart-btn,.save-btn{border:none;border-radius:4px;cursor:pointer;flex:1 1;font-size:16px;font-weight:700;padding:12px 20px}.save-btn{background-color:#28a745;color:#fff}.save-btn:hover:not(:disabled){background-color:#218838}.restart-btn{background-color:#ffc107;color:#212529}.restart-btn:hover:not(:disabled){background-color:#e0a800}.restart-btn:disabled,.save-btn:disabled{background-color:#6c757d;cursor:not-allowed}button{border:none;border-radius:3px;cursor:pointer;padding:8px 12px;transition:background-color .2s}button:hover{opacity:.9}button:disabled{cursor:not-allowed;opacity:.6}.docker-info{color:#007bff;font-size:.9em}.host-ip-info{border-top:1px solid #eee;margin-top:15px;padding-top:10px}.host-ip-list{list-style:none;margin:10px 0;padding:0}.host-ip-item{display:flex;padding:5px 0}.method-name{color:#555;font-weight:700;width:150px}.host-ip-address{background-color:#e9ecef;border-radius:3px;flex:1 1;font-family:Courier New,monospace;padding:2px 6px}.weekday-btn,.weekday-controls{align-items:center;display:flex}.weekday-btn{background-color:#e5e5e5;border-radius:0;color:#515b63;cursor:pointer;font-size:12px;height:24px;justify-content:center;padding:0;width:24px}.weekday-btn:hover{background-color:#e9ecef;border-color:#adb5bd}.weekday-btn.active{background-color:#007bff;color:#fff}.weekday-btn:focus{box-shadow:0 0 0 2px #007bff40;outline:none}.mac-input{align-items:center;display:flex;gap:5px;margin:0 10px}.mac-input label{color:#555;font-size:12px}.mac-input input[type=text]{border:1px solid #ddd;border-radius:3px;font-size:12px;padding:4px;width:106px}.mac-input input[type=text]:focus{border-color:#007bff;box-shadow:0 0 0 2px #007bff40;outline:none}.table-right-blank{min-width:30px}.title-mac-input{font-size:15px;min-width:131px;text-align:center}.title-time-controls{font-size:15px;min-width:183px;text-align:center}.title-weedkey-controls{font-size:15px;min-width:106px}@media (max-width:500px){.config-container{margin:10px;padding:20px}.time-controls{align-items:flex-start;flex-direction:column;gap:5px}.host-item{gap:10px}.remove-btn{align-self:flex-start;margin-left:0;margin-top:10px}.mac-input{margin:5px 0}.mac-input input[type=text]{width:100px}}
2
- /*# sourceMappingURL=main.8bfa3d5f.css.map*/
@@ -1 +0,0 @@
1
- {"version":3,"file":"static/css/main.8bfa3d5f.css","mappings":"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","sources":["index.css","App.css"],"sourcesContent":["body {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\n 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\n sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',\n monospace;\n}\n","/* 服务器信息样式 */\n.server-info {\n /*padding: 10px 0;*/\n}\n\n.server-info p {\n margin: 5px 0;\n}\n\n.ip-list {\n list-style: none;\n padding: 0;\n margin: 10px 0;\n}\n\n.ip-item {\n display: flex;\n padding: 8px 0;\n border-bottom: 1px solid #eee;\n}\n\n.ip-item:last-child {\n border-bottom: none;\n}\n\n.interface-name {\n font-weight: bold;\n color: #555;\n}\n\n.ip-address {\n flex: 1;\n font-family: 'Courier New', monospace;\n background-color: #f8f9fa;\n padding: 2px 6px;\n border-radius: 3px;\n}\n\n/* Toast 样式 */\n.toast {\n position: fixed;\n top: 20px;\n right: 20px;\n padding: 16px 20px;\n border-radius: 4px;\n color: white;\n font-weight: 500;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n z-index: 1000;\n display: flex;\n align-items: center;\n animation: toastSlideIn 0.3s ease-out;\n min-width: 250px;\n}\n\n@keyframes toastSlideIn {\n from {\n transform: translateX(100%);\n opacity: 0;\n }\n to {\n transform: translateX(0);\n opacity: 1;\n }\n}\n\n.toast.success {\n background-color: #28a745;\n border-left: 4px solid #1e7e34;\n}\n\n.toast.error {\n background-color: #dc3545;\n border-left: 4px solid #bd2130;\n}\n\n.toast.info {\n background-color: #17a2b8;\n border-left: 4px solid #117a8b;\n}\n\n.toast-close {\n background: none;\n border: none;\n color: white;\n font-size: 20px;\n font-weight: bold;\n margin-left: 15px;\n cursor: pointer;\n padding: 0;\n width: 20px;\n height: 20px;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.toast-close:hover {\n opacity: 0.7;\n}\n\n/* 其他原有样式保持不变 */\n.App {\n text-align: center;\n padding: 20px;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\n 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\n sans-serif;\n background-color: #f5f5f5;\n min-height: 100vh;\n}\n\n.config-container {\n max-width: 1220px;\n margin: 0 auto;\n background: white;\n border-radius: 8px;\n box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\n padding: 30px;\n text-align: left;\n position: relative;\n}\n\n.config-container h1 {\n text-align: center;\n color: #333;\n margin-top: 0;\n}\n\n.config-section {\n margin-bottom: 30px;\n padding: 20px;\n border: 1px solid #eee;\n border-radius: 5px;\n background-color: #fafafa;\n}\n\n.config-section h2 {\n margin-top: 0;\n color: #555;\n border-bottom: 1px solid #eee;\n padding-bottom: 10px;\n}\n\n.host-input {\n display: flex;\n margin-bottom: 15px;\n gap: 10px;\n flex-direction: row;\n}\n\n.host-input input[type=\"text\"] {\n flex: 1;\n padding: 10px;\n border: 1px solid #ddd;\n border-radius: 4px;\n font-size: 14px;\n}\n\n.host-input button {\n background-color: #007bff;\n color: white;\n border: none;\n border-radius: 4px;\n cursor: pointer;\n font-size: 14px;\n align-self: flex-start;\n}\n\n.host-input button:hover {\n background-color: #0069d9;\n}\n\n.time-inputs {\n display: flex;\n gap: 10px;\n align-items: center;\n}\n\n.time-inputs label {\n flex-direction: column;\n font-size: 14px;\n}\n\nhr.simple-line {\n height:1px;\n border-width:0px;\n background-color:#e3e3e3\n}\n\n/* 时间控件样式 */\ninput[type=\"time\"] {\n padding: 4px;\n border: 1px solid #ddd;\n border-radius: 3px;\n background-color: white;\n}\n\ninput[type=\"time\"]:focus {\n outline: none;\n border-color: #007bff;\n box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);\n}\n\n.host-list {\n list-style: none;\n padding: 0;\n margin: 0;\n}\n\n.host-item {\n display: flex;\n justify-content: space-between;\n align-items: flex-start;\n padding: 12px 0px;\n border-bottom: 1px solid #eee;\n}\n\n.host-item:last-child {\n border-bottom: none;\n}\n\n.host-info {\n display: flex;\n flex-grow: 1;\n align-items:center;\n font-size:15px;\n}\n\nspan.host-text {\n font-size: 14px;\n flex-grow: 1;\n align-self: center; /* 垂直居中 */\n}\n\n.time-controls {\n display: flex;\n gap: 8px;\n align-items: center;\n}\n\n.time-controls label {\n display: flex;\n flex-direction: column;\n font-size: 12px;\n}\n\n.remove-btn {\n background-color: #dc3545;\n color: white;\n border: none;\n padding: 5px 7px;\n min-width: 23px;\n cursor: pointer;\n border-radius: 3px;\n font-size: 12px;\n height: fit-content;\n align-self: center;\n margin-left: 10px;\n}\n\n.remove-btn:hover {\n background-color: #c82333;\n}\n\n.setting-row {\n display: flex;\n align-items: center;\n margin-bottom: 15px;\n gap: 15px;\n}\n\n.setting-row label {\n width: 220px; /* 固定宽度,足够容纳最长的标签 */\n text-align: right;\n font-weight: bold;\n color: #555;\n flex-shrink: 0; /* 防止被压缩 */\n}\n\n.setting-row input {\n flex: 1;\n padding: 8px;\n border: 1px solid #ddd;\n border-radius: 4px;\n}\n\n.setting-row input[type=\"number\"] {\n padding: 8px;\n border: 1px solid #ddd;\n border-radius: 4px;\n font-size: 14px;\n}\n\n.setting-row input[type=\"number\"]:focus {\n outline: none;\n border-color: #007bff;\n box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);\n}\n\n.actions {\n display: flex;\n justify-content: space-between;\n}\n\n.save-btn, .restart-btn {\n flex: 1;\n padding: 12px 20px;\n border: none;\n border-radius: 4px;\n cursor: pointer;\n font-size: 16px;\n font-weight: bold;\n}\n\n.save-btn {\n background-color: #28a745;\n color: white;\n}\n\n.save-btn:hover:not(:disabled) {\n background-color: #218838;\n}\n\n.restart-btn {\n background-color: #ffc107;\n color: #212529;\n}\n\n.restart-btn:hover:not(:disabled) {\n background-color: #e0a800;\n}\n\n.save-btn:disabled, .restart-btn:disabled {\n background-color: #6c757d;\n cursor: not-allowed;\n}\n\nbutton {\n padding: 8px 12px;\n border: none;\n border-radius: 3px;\n cursor: pointer;\n transition: background-color 0.2s;\n}\n\nbutton:hover {\n opacity: 0.9;\n}\n\nbutton:disabled {\n opacity: 0.6;\n cursor: not-allowed;\n}\n\n/* Docker信息样式 */\n.docker-info {\n color: #007bff;\n font-size: 0.9em;\n}\n\n.host-ip-info {\n margin-top: 15px;\n padding-top: 10px;\n border-top: 1px solid #eee;\n}\n\n.host-ip-list {\n list-style: none;\n padding: 0;\n margin: 10px 0;\n}\n\n.host-ip-item {\n display: flex;\n padding: 5px 0;\n}\n\n.method-name {\n font-weight: bold;\n width: 150px;\n color: #555;\n}\n\n.host-ip-address {\n flex: 1;\n font-family: 'Courier New', monospace;\n background-color: #e9ecef;\n padding: 2px 6px;\n border-radius: 3px;\n}\n\n/* 星期几控件样式 */\n.weekday-controls {\n display: flex;\n /*flex-wrap: wrap;*/\n align-items: center;\n}\n\n.weekday-btn {\n width: 24px;\n height: 24px;\n padding: 0;\n /*border: 1px solid #ddd;*/\n background-color: #e5e5e5;\n color: #515b63;\n font-size: 12px;\n border-radius: 0px;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.weekday-btn:hover {\n background-color: #e9ecef;\n border-color: #adb5bd;\n}\n\n.weekday-btn.active {\n background-color: #007bff;\n color: white;\n}\n\n.weekday-btn:focus {\n outline: none;\n box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);\n}\n\n/* MAC地址输入框样式 */\n.mac-input {\n display: flex;\n align-items: center;\n margin: 0 10px;\n gap: 5px;\n}\n\n.mac-input label {\n font-size: 12px;\n color: #555;\n}\n\n.mac-input input[type=\"text\"] {\n width: 106px;\n padding: 4px;\n border: 1px solid #ddd;\n border-radius: 3px;\n font-size: 12px;\n}\n\n.mac-input input[type=\"text\"]:focus {\n outline: none;\n border-color: #007bff;\n box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);\n}\n\n.table-right-blank {\n min-width:30px;\n}\n\n.title-mac-input {\n font-size:15px;\n text-align:center;\n min-width:131px;\n}\n.title-time-controls {\n font-size:15px;\n text-align:center;\n min-width:183px;\n}\n.title-weedkey-controls {\n font-size:15px;\n min-width:106px;\n}\n\n/* 响应式设计 */\n@media (max-width: 500px) {\n .config-container {\n margin: 10px;\n padding: 20px;\n }\n \n .time-controls {\n flex-direction: column;\n align-items: flex-start;\n gap: 5px;\n }\n\n .host-item {\n gap: 10px;\n }\n \n .remove-btn {\n align-self: flex-start;\n margin-left: 0;\n margin-top: 10px;\n }\n\n .mac-input {\n margin: 5px 0;\n }\n \n .mac-input input[type=\"text\"] {\n width: 100px;\n }\n}\n\n"],"names":[],"sourceRoot":""}