You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
844 lines
23 KiB
844 lines
23 KiB
2 weeks ago
|
package gui
|
||
|
|
||
|
// getHTMLTemplate 返回 HTML 模板
|
||
|
func (g *GUIServer) getHTMLTemplate() string {
|
||
|
return `
|
||
|
<!DOCTYPE html>
|
||
|
<html lang="zh-CN">
|
||
|
<head>
|
||
|
<meta charset="UTF-8">
|
||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
|
<title>{{.Title}}</title>
|
||
|
<link rel="stylesheet" href="/static/style.css">
|
||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||
|
</head>
|
||
|
<body>
|
||
|
<div class="container">
|
||
|
<!-- 头部 -->
|
||
|
<header class="header">
|
||
|
<div class="header-content">
|
||
|
<div class="logo">
|
||
|
<i class="fas fa-globe"></i>
|
||
|
<h1>{{.Title}}</h1>
|
||
|
</div>
|
||
|
<div class="status-indicator">
|
||
|
<span class="status-dot" id="statusDot"></span>
|
||
|
<span id="statusText">正在检查...</span>
|
||
|
</div>
|
||
|
</div>
|
||
|
</header>
|
||
|
|
||
|
<!-- 主要内容 -->
|
||
|
<main class="main-content">
|
||
|
<!-- 代理控制面板 -->
|
||
|
<section class="panel proxy-panel">
|
||
|
<div class="panel-header">
|
||
|
<h2><i class="fas fa-network-wired"></i> 代理控制</h2>
|
||
|
</div>
|
||
|
<div class="panel-content">
|
||
|
<div class="proxy-info">
|
||
|
<div class="info-item">
|
||
|
<label>运行模式:</label>
|
||
|
<span class="mode-badge" id="modeText">{{.Mode}}</span>
|
||
|
</div>
|
||
|
<div class="info-item">
|
||
|
<label>服务器地址:</label>
|
||
|
<span>{{.ServerAddr}}</span>
|
||
|
</div>
|
||
|
<div class="info-item">
|
||
|
<label>本地端口:</label>
|
||
|
<span>{{.LocalPort}}</span>
|
||
|
</div>
|
||
|
<div class="info-item">
|
||
|
<label>操作系统:</label>
|
||
|
<span>{{.OS}}</span>
|
||
|
</div>
|
||
|
</div>
|
||
|
<div class="proxy-controls">
|
||
|
<button id="toggleProxy" class="btn btn-primary">
|
||
|
<i class="fas fa-power-off"></i>
|
||
|
<span id="toggleText">启用系统代理</span>
|
||
|
</button>
|
||
|
<button id="refreshStatus" class="btn btn-secondary">
|
||
|
<i class="fas fa-sync-alt"></i>
|
||
|
刷新状态
|
||
|
</button>
|
||
|
</div>
|
||
|
</div>
|
||
|
</section>
|
||
|
|
||
|
<!-- 统计信息面板 -->
|
||
|
<section class="panel stats-panel">
|
||
|
<div class="panel-header">
|
||
|
<h2><i class="fas fa-chart-line"></i> 连接统计</h2>
|
||
|
</div>
|
||
|
<div class="panel-content">
|
||
|
<div class="stats-grid">
|
||
|
<div class="stat-item">
|
||
|
<div class="stat-value" id="totalConnections">-</div>
|
||
|
<div class="stat-label">总连接数</div>
|
||
|
</div>
|
||
|
<div class="stat-item">
|
||
|
<div class="stat-value" id="activeConnections">-</div>
|
||
|
<div class="stat-label">活跃连接</div>
|
||
|
</div>
|
||
|
<div class="stat-item">
|
||
|
<div class="stat-value" id="successRate">-</div>
|
||
|
<div class="stat-label">成功率</div>
|
||
|
</div>
|
||
|
<div class="stat-item">
|
||
|
<div class="stat-value" id="uptime">-</div>
|
||
|
<div class="stat-label">运行时间</div>
|
||
|
</div>
|
||
|
<div class="stat-item">
|
||
|
<div class="stat-value" id="bytesTransferred">-</div>
|
||
|
<div class="stat-label">传输流量</div>
|
||
|
</div>
|
||
|
<div class="stat-item">
|
||
|
<div class="stat-value" id="failedRequests">-</div>
|
||
|
<div class="stat-label">失败请求</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
</section>
|
||
|
|
||
|
<!-- 路由配置面板 -->
|
||
|
<section class="panel routing-panel">
|
||
|
<div class="panel-header">
|
||
|
<h2><i class="fas fa-route"></i> 路由配置</h2>
|
||
|
</div>
|
||
|
<div class="panel-content">
|
||
|
<div class="routing-stats">
|
||
|
<div class="routing-item">
|
||
|
<label>绕过域名数量:</label>
|
||
|
<span id="bypassDomainsCount">-</span>
|
||
|
</div>
|
||
|
<div class="routing-item">
|
||
|
<label>强制代理域名:</label>
|
||
|
<span id="forceDomainsCount">-</span>
|
||
|
</div>
|
||
|
<div class="routing-item">
|
||
|
<label>绕过本地网络:</label>
|
||
|
<span id="bypassLocal">-</span>
|
||
|
</div>
|
||
|
<div class="routing-item">
|
||
|
<label>绕过私有网络:</label>
|
||
|
<span id="bypassPrivate">-</span>
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
</section>
|
||
|
|
||
|
<!-- 系统信息面板 -->
|
||
|
<section class="panel system-panel">
|
||
|
<div class="panel-header">
|
||
|
<h2><i class="fas fa-cog"></i> 系统信息</h2>
|
||
|
</div>
|
||
|
<div class="panel-content">
|
||
|
<div class="system-info">
|
||
|
<div class="info-item">
|
||
|
<label>系统代理状态:</label>
|
||
|
<span id="systemProxyStatus" class="status-badge">检查中...</span>
|
||
|
</div>
|
||
|
<div class="info-item">
|
||
|
<label>当前代理设置:</label>
|
||
|
<div id="currentProxySettings">-</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
</section>
|
||
|
</main>
|
||
|
|
||
|
<!-- 底部 -->
|
||
|
<footer class="footer">
|
||
|
<p>© 2024 Wormhole SOCKS5 Client | <a href="/stats" target="_blank">JSON API</a> |
|
||
|
<a href="/health" target="_blank">健康检查</a></p>
|
||
|
</footer>
|
||
|
</div>
|
||
|
|
||
|
<!-- 加载脚本 -->
|
||
|
<script src="/static/app.js"></script>
|
||
|
</body>
|
||
|
</html>`
|
||
|
}
|
||
|
|
||
|
// getCSS 返回 CSS 样式
|
||
|
func (g *GUIServer) getCSS() string {
|
||
|
return `
|
||
|
/* 全局样式 */
|
||
|
* {
|
||
|
margin: 0;
|
||
|
padding: 0;
|
||
|
box-sizing: border-box;
|
||
|
}
|
||
|
|
||
|
body {
|
||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
|
min-height: 100vh;
|
||
|
color: #333;
|
||
|
}
|
||
|
|
||
|
.container {
|
||
|
max-width: 1200px;
|
||
|
margin: 0 auto;
|
||
|
padding: 20px;
|
||
|
}
|
||
|
|
||
|
/* 头部样式 */
|
||
|
.header {
|
||
|
background: rgba(255, 255, 255, 0.95);
|
||
|
backdrop-filter: blur(10px);
|
||
|
border-radius: 12px;
|
||
|
padding: 20px;
|
||
|
margin-bottom: 30px;
|
||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||
|
}
|
||
|
|
||
|
.header-content {
|
||
|
display: flex;
|
||
|
justify-content: space-between;
|
||
|
align-items: center;
|
||
|
}
|
||
|
|
||
|
.logo {
|
||
|
display: flex;
|
||
|
align-items: center;
|
||
|
gap: 15px;
|
||
|
}
|
||
|
|
||
|
.logo i {
|
||
|
font-size: 2.5em;
|
||
|
color: #667eea;
|
||
|
}
|
||
|
|
||
|
.logo h1 {
|
||
|
font-size: 2em;
|
||
|
color: #333;
|
||
|
font-weight: 600;
|
||
|
}
|
||
|
|
||
|
.status-indicator {
|
||
|
display: flex;
|
||
|
align-items: center;
|
||
|
gap: 10px;
|
||
|
font-weight: 500;
|
||
|
}
|
||
|
|
||
|
.status-dot {
|
||
|
width: 12px;
|
||
|
height: 12px;
|
||
|
border-radius: 50%;
|
||
|
background: #ffd700;
|
||
|
animation: pulse 2s infinite;
|
||
|
}
|
||
|
|
||
|
.status-dot.connected {
|
||
|
background: #00ff00;
|
||
|
}
|
||
|
|
||
|
.status-dot.disconnected {
|
||
|
background: #ff4444;
|
||
|
}
|
||
|
|
||
|
@keyframes pulse {
|
||
|
0%, 100% { opacity: 1; }
|
||
|
50% { opacity: 0.5; }
|
||
|
}
|
||
|
|
||
|
/* 主要内容样式 */
|
||
|
.main-content {
|
||
|
display: grid;
|
||
|
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||
|
gap: 25px;
|
||
|
margin-bottom: 30px;
|
||
|
}
|
||
|
|
||
|
/* 面板样式 */
|
||
|
.panel {
|
||
|
background: rgba(255, 255, 255, 0.95);
|
||
|
backdrop-filter: blur(10px);
|
||
|
border-radius: 12px;
|
||
|
overflow: hidden;
|
||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||
|
}
|
||
|
|
||
|
.panel:hover {
|
||
|
transform: translateY(-5px);
|
||
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
|
||
|
}
|
||
|
|
||
|
.panel-header {
|
||
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
||
|
color: white;
|
||
|
padding: 20px;
|
||
|
}
|
||
|
|
||
|
.panel-header h2 {
|
||
|
font-size: 1.3em;
|
||
|
font-weight: 600;
|
||
|
display: flex;
|
||
|
align-items: center;
|
||
|
gap: 10px;
|
||
|
}
|
||
|
|
||
|
.panel-content {
|
||
|
padding: 25px;
|
||
|
}
|
||
|
|
||
|
/* 代理面板样式 */
|
||
|
.proxy-info {
|
||
|
margin-bottom: 25px;
|
||
|
}
|
||
|
|
||
|
.info-item {
|
||
|
display: flex;
|
||
|
justify-content: space-between;
|
||
|
align-items: center;
|
||
|
padding: 12px 0;
|
||
|
border-bottom: 1px solid #eee;
|
||
|
}
|
||
|
|
||
|
.info-item:last-child {
|
||
|
border-bottom: none;
|
||
|
}
|
||
|
|
||
|
.info-item label {
|
||
|
font-weight: 600;
|
||
|
color: #555;
|
||
|
}
|
||
|
|
||
|
.mode-badge {
|
||
|
background: #667eea;
|
||
|
color: white;
|
||
|
padding: 4px 12px;
|
||
|
border-radius: 20px;
|
||
|
font-size: 0.9em;
|
||
|
font-weight: 500;
|
||
|
text-transform: uppercase;
|
||
|
}
|
||
|
|
||
|
.proxy-controls {
|
||
|
display: flex;
|
||
|
gap: 15px;
|
||
|
flex-wrap: wrap;
|
||
|
}
|
||
|
|
||
|
/* 按钮样式 */
|
||
|
.btn {
|
||
|
padding: 12px 20px;
|
||
|
border: none;
|
||
|
border-radius: 8px;
|
||
|
font-size: 1em;
|
||
|
font-weight: 500;
|
||
|
cursor: pointer;
|
||
|
display: flex;
|
||
|
align-items: center;
|
||
|
gap: 8px;
|
||
|
transition: all 0.3s ease;
|
||
|
min-width: 140px;
|
||
|
justify-content: center;
|
||
|
}
|
||
|
|
||
|
.btn-primary {
|
||
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
||
|
color: white;
|
||
|
}
|
||
|
|
||
|
.btn-primary:hover {
|
||
|
background: linear-gradient(135deg, #5a6fd8, #6a4190);
|
||
|
transform: translateY(-2px);
|
||
|
}
|
||
|
|
||
|
.btn-secondary {
|
||
|
background: #f8f9fa;
|
||
|
color: #333;
|
||
|
border: 2px solid #dee2e6;
|
||
|
}
|
||
|
|
||
|
.btn-secondary:hover {
|
||
|
background: #e9ecef;
|
||
|
border-color: #adb5bd;
|
||
|
transform: translateY(-2px);
|
||
|
}
|
||
|
|
||
|
.btn:disabled {
|
||
|
opacity: 0.6;
|
||
|
cursor: not-allowed;
|
||
|
transform: none !important;
|
||
|
}
|
||
|
|
||
|
/* 统计面板样式 */
|
||
|
.stats-grid {
|
||
|
display: grid;
|
||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||
|
gap: 20px;
|
||
|
}
|
||
|
|
||
|
.stat-item {
|
||
|
text-align: center;
|
||
|
padding: 20px;
|
||
|
background: #f8f9fa;
|
||
|
border-radius: 8px;
|
||
|
transition: transform 0.3s ease;
|
||
|
}
|
||
|
|
||
|
.stat-item:hover {
|
||
|
transform: scale(1.05);
|
||
|
}
|
||
|
|
||
|
.stat-value {
|
||
|
font-size: 2em;
|
||
|
font-weight: 700;
|
||
|
color: #667eea;
|
||
|
margin-bottom: 8px;
|
||
|
}
|
||
|
|
||
|
.stat-label {
|
||
|
font-size: 0.9em;
|
||
|
color: #666;
|
||
|
font-weight: 500;
|
||
|
}
|
||
|
|
||
|
/* 路由面板样式 */
|
||
|
.routing-stats {
|
||
|
display: flex;
|
||
|
flex-direction: column;
|
||
|
gap: 15px;
|
||
|
}
|
||
|
|
||
|
.routing-item {
|
||
|
display: flex;
|
||
|
justify-content: space-between;
|
||
|
align-items: center;
|
||
|
padding: 15px;
|
||
|
background: #f8f9fa;
|
||
|
border-radius: 8px;
|
||
|
}
|
||
|
|
||
|
.routing-item label {
|
||
|
font-weight: 600;
|
||
|
color: #555;
|
||
|
}
|
||
|
|
||
|
/* 系统面板样式 */
|
||
|
.system-info {
|
||
|
display: flex;
|
||
|
flex-direction: column;
|
||
|
gap: 20px;
|
||
|
}
|
||
|
|
||
|
.status-badge {
|
||
|
padding: 4px 12px;
|
||
|
border-radius: 20px;
|
||
|
font-size: 0.9em;
|
||
|
font-weight: 500;
|
||
|
}
|
||
|
|
||
|
.status-badge.enabled {
|
||
|
background: #d4edda;
|
||
|
color: #155724;
|
||
|
}
|
||
|
|
||
|
.status-badge.disabled {
|
||
|
background: #f8d7da;
|
||
|
color: #721c24;
|
||
|
}
|
||
|
|
||
|
#currentProxySettings {
|
||
|
font-family: monospace;
|
||
|
background: #f8f9fa;
|
||
|
padding: 10px;
|
||
|
border-radius: 4px;
|
||
|
font-size: 0.9em;
|
||
|
margin-top: 5px;
|
||
|
}
|
||
|
|
||
|
/* 底部样式 */
|
||
|
.footer {
|
||
|
background: rgba(255, 255, 255, 0.95);
|
||
|
backdrop-filter: blur(10px);
|
||
|
border-radius: 12px;
|
||
|
padding: 20px;
|
||
|
text-align: center;
|
||
|
color: #666;
|
||
|
}
|
||
|
|
||
|
.footer a {
|
||
|
color: #667eea;
|
||
|
text-decoration: none;
|
||
|
font-weight: 500;
|
||
|
}
|
||
|
|
||
|
.footer a:hover {
|
||
|
text-decoration: underline;
|
||
|
}
|
||
|
|
||
|
/* 响应式设计 */
|
||
|
@media (max-width: 768px) {
|
||
|
.container {
|
||
|
padding: 15px;
|
||
|
}
|
||
|
|
||
|
.header-content {
|
||
|
flex-direction: column;
|
||
|
gap: 15px;
|
||
|
text-align: center;
|
||
|
}
|
||
|
|
||
|
.main-content {
|
||
|
grid-template-columns: 1fr;
|
||
|
gap: 20px;
|
||
|
}
|
||
|
|
||
|
.proxy-controls {
|
||
|
flex-direction: column;
|
||
|
}
|
||
|
|
||
|
.stats-grid {
|
||
|
grid-template-columns: repeat(2, 1fr);
|
||
|
gap: 15px;
|
||
|
}
|
||
|
|
||
|
.stat-item {
|
||
|
padding: 15px;
|
||
|
}
|
||
|
|
||
|
.stat-value {
|
||
|
font-size: 1.5em;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@media (max-width: 480px) {
|
||
|
.stats-grid {
|
||
|
grid-template-columns: 1fr;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/* 加载动画 */
|
||
|
.loading {
|
||
|
opacity: 0.6;
|
||
|
pointer-events: none;
|
||
|
}
|
||
|
|
||
|
.spin {
|
||
|
animation: spin 1s linear infinite;
|
||
|
}
|
||
|
|
||
|
@keyframes spin {
|
||
|
from { transform: rotate(0deg); }
|
||
|
to { transform: rotate(360deg); }
|
||
|
}
|
||
|
`
|
||
|
}
|
||
|
|
||
|
// getJS 返回 JavaScript 代码
|
||
|
func (g *GUIServer) getJS() string {
|
||
|
return `
|
||
|
// GUI 应用主类
|
||
|
class WormholeGUI {
|
||
|
constructor() {
|
||
|
this.isLoading = false;
|
||
|
this.updateInterval = null;
|
||
|
this.init();
|
||
|
}
|
||
|
|
||
|
// 初始化应用
|
||
|
init() {
|
||
|
this.bindEvents();
|
||
|
this.startAutoUpdate();
|
||
|
this.updateStatus();
|
||
|
}
|
||
|
|
||
|
// 绑定事件
|
||
|
bindEvents() {
|
||
|
const toggleBtn = document.getElementById('toggleProxy');
|
||
|
const refreshBtn = document.getElementById('refreshStatus');
|
||
|
|
||
|
if (toggleBtn) {
|
||
|
toggleBtn.addEventListener('click', () => this.toggleProxy());
|
||
|
}
|
||
|
|
||
|
if (refreshBtn) {
|
||
|
refreshBtn.addEventListener('click', () => this.updateStatus());
|
||
|
}
|
||
|
|
||
|
// 页面可见性变化时处理
|
||
|
document.addEventListener('visibilitychange', () => {
|
||
|
if (document.hidden) {
|
||
|
this.stopAutoUpdate();
|
||
|
} else {
|
||
|
this.startAutoUpdate();
|
||
|
this.updateStatus();
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// 开始自动更新
|
||
|
startAutoUpdate() {
|
||
|
if (this.updateInterval) return;
|
||
|
|
||
|
this.updateInterval = setInterval(() => {
|
||
|
if (!document.hidden) {
|
||
|
this.updateStatus();
|
||
|
}
|
||
|
}, 5000); // 每5秒更新一次
|
||
|
}
|
||
|
|
||
|
// 停止自动更新
|
||
|
stopAutoUpdate() {
|
||
|
if (this.updateInterval) {
|
||
|
clearInterval(this.updateInterval);
|
||
|
this.updateInterval = null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 更新状态
|
||
|
async updateStatus() {
|
||
|
if (this.isLoading) return;
|
||
|
|
||
|
try {
|
||
|
this.setLoading(true);
|
||
|
|
||
|
// 并行获取所有数据
|
||
|
const [status, systemProxy, routingStats] = await Promise.all([
|
||
|
this.fetchAPI('/api/status'),
|
||
|
this.fetchAPI('/api/system/proxy'),
|
||
|
this.fetchAPI('/api/routing/stats')
|
||
|
]);
|
||
|
|
||
|
this.updateStatusDisplay(status);
|
||
|
this.updateProxyStats(status.proxy_stats);
|
||
|
this.updateSystemProxy(systemProxy);
|
||
|
this.updateRoutingStats(routingStats);
|
||
|
|
||
|
} catch (error) {
|
||
|
console.error('更新状态失败:', error);
|
||
|
this.showError('无法连接到服务器');
|
||
|
} finally {
|
||
|
this.setLoading(false);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 切换代理状态
|
||
|
async toggleProxy() {
|
||
|
const toggleBtn = document.getElementById('toggleProxy');
|
||
|
const toggleText = document.getElementById('toggleText');
|
||
|
|
||
|
if (!toggleBtn || this.isLoading) return;
|
||
|
|
||
|
try {
|
||
|
this.setLoading(true);
|
||
|
toggleBtn.disabled = true;
|
||
|
|
||
|
// 获取当前状态
|
||
|
const currentStatus = await this.fetchAPI('/api/system/proxy');
|
||
|
const isEnabled = currentStatus.enabled;
|
||
|
|
||
|
// 切换状态
|
||
|
const response = await this.fetchAPI('/api/proxy/toggle', {
|
||
|
method: 'POST',
|
||
|
headers: {
|
||
|
'Content-Type': 'application/json',
|
||
|
},
|
||
|
body: JSON.stringify({ enable: !isEnabled })
|
||
|
});
|
||
|
|
||
|
if (response.success) {
|
||
|
toggleText.textContent = response.enabled ? '禁用系统代理' : '启用系统代理';
|
||
|
toggleBtn.className = response.enabled ? 'btn btn-secondary' : 'btn btn-primary';
|
||
|
|
||
|
// 立即更新状态
|
||
|
setTimeout(() => this.updateStatus(), 500);
|
||
|
} else {
|
||
|
this.showError(response.error || '操作失败');
|
||
|
}
|
||
|
|
||
|
} catch (error) {
|
||
|
console.error('切换代理失败:', error);
|
||
|
this.showError('操作失败,请检查权限');
|
||
|
} finally {
|
||
|
toggleBtn.disabled = false;
|
||
|
this.setLoading(false);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 更新状态显示
|
||
|
updateStatusDisplay(status) {
|
||
|
const statusDot = document.getElementById('statusDot');
|
||
|
const statusText = document.getElementById('statusText');
|
||
|
const modeText = document.getElementById('modeText');
|
||
|
|
||
|
if (statusDot && statusText) {
|
||
|
if (status.proxy_stats) {
|
||
|
statusDot.className = 'status-dot connected';
|
||
|
statusText.textContent = '服务运行中';
|
||
|
} else {
|
||
|
statusDot.className = 'status-dot disconnected';
|
||
|
statusText.textContent = '服务未运行';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (modeText) {
|
||
|
modeText.textContent = status.mode || 'unknown';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 更新代理统计
|
||
|
updateProxyStats(stats) {
|
||
|
if (!stats) return;
|
||
|
|
||
|
this.updateElement('totalConnections', stats.total_connections);
|
||
|
this.updateElement('activeConnections', stats.active_connections);
|
||
|
this.updateElement('successRate', this.formatSuccessRate(stats));
|
||
|
this.updateElement('uptime', this.formatDuration(stats.uptime));
|
||
|
this.updateElement('bytesTransferred', this.formatBytes(stats.bytes_sent + stats.bytes_received));
|
||
|
this.updateElement('failedRequests', stats.failed_requests);
|
||
|
}
|
||
|
|
||
|
// 更新系统代理信息
|
||
|
updateSystemProxy(proxyInfo) {
|
||
|
const statusElement = document.getElementById('systemProxyStatus');
|
||
|
const settingsElement = document.getElementById('currentProxySettings');
|
||
|
const toggleBtn = document.getElementById('toggleProxy');
|
||
|
const toggleText = document.getElementById('toggleText');
|
||
|
|
||
|
if (statusElement) {
|
||
|
if (proxyInfo.enabled) {
|
||
|
statusElement.textContent = '已启用';
|
||
|
statusElement.className = 'status-badge enabled';
|
||
|
} else {
|
||
|
statusElement.textContent = '已禁用';
|
||
|
statusElement.className = 'status-badge disabled';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (settingsElement && proxyInfo.current_proxy) {
|
||
|
const settings = Object.entries(proxyInfo.current_proxy)
|
||
|
.map(([key, value]) => key + ': ' + value)
|
||
|
.join('\n');
|
||
|
settingsElement.textContent = settings || '无代理设置';
|
||
|
}
|
||
|
|
||
|
if (toggleBtn && toggleText) {
|
||
|
toggleText.textContent = proxyInfo.enabled ? '禁用系统代理' : '启用系统代理';
|
||
|
toggleBtn.className = proxyInfo.enabled ? 'btn btn-secondary' : 'btn btn-primary';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 更新路由统计
|
||
|
updateRoutingStats(stats) {
|
||
|
if (!stats) return;
|
||
|
|
||
|
this.updateElement('bypassDomainsCount', stats.bypass_domains_count);
|
||
|
this.updateElement('forceDomainsCount', stats.force_domains_count);
|
||
|
this.updateElement('bypassLocal', stats.bypass_local ? '是' : '否');
|
||
|
this.updateElement('bypassPrivate', stats.bypass_private ? '是' : '否');
|
||
|
}
|
||
|
|
||
|
// 通用元素更新
|
||
|
updateElement(id, value) {
|
||
|
const element = document.getElementById(id);
|
||
|
if (element) {
|
||
|
element.textContent = value !== undefined ? value : '-';
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 设置加载状态
|
||
|
setLoading(loading) {
|
||
|
this.isLoading = loading;
|
||
|
const refreshBtn = document.getElementById('refreshStatus');
|
||
|
|
||
|
if (refreshBtn) {
|
||
|
const icon = refreshBtn.querySelector('i');
|
||
|
if (loading) {
|
||
|
icon.classList.add('spin');
|
||
|
refreshBtn.disabled = true;
|
||
|
} else {
|
||
|
icon.classList.remove('spin');
|
||
|
refreshBtn.disabled = false;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// API 请求
|
||
|
async fetchAPI(url, options = {}) {
|
||
|
const response = await fetch(url, {
|
||
|
timeout: 10000,
|
||
|
...options
|
||
|
});
|
||
|
|
||
|
if (!response.ok) {
|
||
|
throw new Error(response.status + ': ' + response.statusText);
|
||
|
}
|
||
|
|
||
|
return await response.json();
|
||
|
}
|
||
|
|
||
|
// 显示错误
|
||
|
showError(message) {
|
||
|
const statusDot = document.getElementById('statusDot');
|
||
|
const statusText = document.getElementById('statusText');
|
||
|
|
||
|
if (statusDot && statusText) {
|
||
|
statusDot.className = 'status-dot disconnected';
|
||
|
statusText.textContent = message;
|
||
|
}
|
||
|
|
||
|
console.error('GUI Error:', message);
|
||
|
}
|
||
|
|
||
|
// 格式化成功率
|
||
|
formatSuccessRate(stats) {
|
||
|
const total = stats.successful_requests + stats.failed_requests;
|
||
|
if (total === 0) return '0%';
|
||
|
return ((stats.successful_requests / total) * 100).toFixed(1) + '%';
|
||
|
}
|
||
|
|
||
|
// 格式化持续时间
|
||
|
formatDuration(durationNs) {
|
||
|
if (!durationNs) return '0s';
|
||
|
|
||
|
const seconds = Math.floor(durationNs / 1000000000);
|
||
|
const minutes = Math.floor(seconds / 60);
|
||
|
const hours = Math.floor(minutes / 60);
|
||
|
const days = Math.floor(hours / 24);
|
||
|
|
||
|
if (days > 0) return days + 'd ' + (hours % 24) + 'h';
|
||
|
if (hours > 0) return hours + 'h ' + (minutes % 60) + 'm';
|
||
|
if (minutes > 0) return minutes + 'm ' + (seconds % 60) + 's';
|
||
|
return seconds + 's';
|
||
|
}
|
||
|
|
||
|
// 格式化字节数
|
||
|
formatBytes(bytes) {
|
||
|
if (!bytes) return '0 B';
|
||
|
|
||
|
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||
|
let size = bytes;
|
||
|
let unitIndex = 0;
|
||
|
|
||
|
while (size >= 1024 && unitIndex < units.length - 1) {
|
||
|
size /= 1024;
|
||
|
unitIndex++;
|
||
|
}
|
||
|
|
||
|
return size.toFixed(1) + ' ' + units[unitIndex];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 页面加载完成后启动应用
|
||
|
document.addEventListener('DOMContentLoaded', () => {
|
||
|
window.wormholeGUI = new WormholeGUI();
|
||
|
});
|
||
|
|
||
|
// 页面卸载时清理
|
||
|
window.addEventListener('beforeunload', () => {
|
||
|
if (window.wormholeGUI) {
|
||
|
window.wormholeGUI.stopAutoUpdate();
|
||
|
}
|
||
|
});
|
||
|
`
|
||
|
}
|