Compare commits
3 Commits
4c0aeeb96d
...
e34d8c600c
Author | SHA1 | Date |
---|---|---|
|
e34d8c600c | 2 weeks ago |
|
c28c786b15 | 2 weeks ago |
|
0376a5f39d | 2 weeks ago |
@ -0,0 +1,265 @@ |
||||
package gui |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
"html/template" |
||||
"net/http" |
||||
"path/filepath" |
||||
"runtime" |
||||
"strings" |
||||
|
||||
"github.com/azoic/wormhole-client/internal/config" |
||||
"github.com/azoic/wormhole-client/internal/proxy" |
||||
"github.com/azoic/wormhole-client/internal/routing" |
||||
"github.com/azoic/wormhole-client/internal/system" |
||||
"github.com/azoic/wormhole-client/pkg/logger" |
||||
) |
||||
|
||||
// GUIServer Web GUI 服务器
|
||||
type GUIServer struct { |
||||
config *config.Config |
||||
socks5Proxy *proxy.SOCKS5Proxy |
||||
systemProxyMgr *system.SystemProxyManager |
||||
routeMatcher *routing.RouteMatcher |
||||
mode string |
||||
templates *template.Template |
||||
} |
||||
|
||||
// NewGUIServer 创建 GUI 服务器
|
||||
func NewGUIServer(cfg *config.Config, socks5Proxy *proxy.SOCKS5Proxy, |
||||
systemProxyMgr *system.SystemProxyManager, routeMatcher *routing.RouteMatcher, mode string) *GUIServer { |
||||
|
||||
gui := &GUIServer{ |
||||
config: cfg, |
||||
socks5Proxy: socks5Proxy, |
||||
systemProxyMgr: systemProxyMgr, |
||||
routeMatcher: routeMatcher, |
||||
mode: mode, |
||||
} |
||||
|
||||
// 加载模板
|
||||
gui.loadTemplates() |
||||
|
||||
return gui |
||||
} |
||||
|
||||
// RegisterRoutes 注册 GUI 路由
|
||||
func (g *GUIServer) RegisterRoutes(mux *http.ServeMux) { |
||||
// 静态文件路由
|
||||
mux.HandleFunc("/static/", g.handleStatic) |
||||
|
||||
// Web 界面路由
|
||||
mux.HandleFunc("/gui", g.handleGUI) |
||||
mux.HandleFunc("/gui/", g.handleGUI) |
||||
|
||||
// API 路由
|
||||
mux.HandleFunc("/api/status", g.handleStatus) |
||||
mux.HandleFunc("/api/config", g.handleConfig) |
||||
mux.HandleFunc("/api/proxy/toggle", g.handleProxyToggle) |
||||
mux.HandleFunc("/api/routing/stats", g.handleRoutingStats) |
||||
mux.HandleFunc("/api/system/proxy", g.handleSystemProxy) |
||||
} |
||||
|
||||
// handleGUI 处理 GUI 主页面
|
||||
func (g *GUIServer) handleGUI(w http.ResponseWriter, r *http.Request) { |
||||
if r.Method != http.MethodGet { |
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
||||
return |
||||
} |
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8") |
||||
|
||||
data := g.getTemplateData() |
||||
|
||||
if err := g.templates.Execute(w, data); err != nil { |
||||
logger.Error("Failed to execute template: %v", err) |
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError) |
||||
return |
||||
} |
||||
} |
||||
|
||||
// handleStatic 处理静态文件
|
||||
func (g *GUIServer) handleStatic(w http.ResponseWriter, r *http.Request) { |
||||
path := strings.TrimPrefix(r.URL.Path, "/static/") |
||||
|
||||
var content string |
||||
var contentType string |
||||
|
||||
switch filepath.Ext(path) { |
||||
case ".css": |
||||
content = g.getCSS() |
||||
contentType = "text/css" |
||||
case ".js": |
||||
content = g.getJS() |
||||
contentType = "application/javascript" |
||||
default: |
||||
http.NotFound(w, r) |
||||
return |
||||
} |
||||
|
||||
w.Header().Set("Content-Type", contentType) |
||||
w.Write([]byte(content)) |
||||
} |
||||
|
||||
// handleStatus 处理状态 API
|
||||
func (g *GUIServer) handleStatus(w http.ResponseWriter, r *http.Request) { |
||||
if r.Method != http.MethodGet { |
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
||||
return |
||||
} |
||||
|
||||
var proxyStats interface{} |
||||
if g.socks5Proxy != nil { |
||||
proxyStats = g.socks5Proxy.GetStats() |
||||
} |
||||
|
||||
var routingStats interface{} |
||||
if g.routeMatcher != nil { |
||||
routingStats = g.routeMatcher.GetStats() |
||||
} |
||||
|
||||
status := map[string]interface{}{ |
||||
"mode": g.mode, |
||||
"system_proxy": g.systemProxyMgr.IsEnabled(), |
||||
"proxy_stats": proxyStats, |
||||
"routing_stats": routingStats, |
||||
"server_addr": g.config.GetServerAddr(), |
||||
"local_port": g.config.Proxy.LocalPort, |
||||
"os": runtime.GOOS, |
||||
} |
||||
|
||||
g.writeJSON(w, status) |
||||
} |
||||
|
||||
// handleConfig 处理配置 API
|
||||
func (g *GUIServer) handleConfig(w http.ResponseWriter, r *http.Request) { |
||||
switch r.Method { |
||||
case http.MethodGet: |
||||
g.writeJSON(w, g.config) |
||||
case http.MethodPost: |
||||
// TODO: 实现配置更新
|
||||
http.Error(w, "Configuration update not implemented", http.StatusNotImplemented) |
||||
default: |
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
||||
} |
||||
} |
||||
|
||||
// handleProxyToggle 处理代理开关
|
||||
func (g *GUIServer) handleProxyToggle(w http.ResponseWriter, r *http.Request) { |
||||
if r.Method != http.MethodPost { |
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
||||
return |
||||
} |
||||
|
||||
var req struct { |
||||
Enable bool `json:"enable"` |
||||
} |
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { |
||||
http.Error(w, "Invalid JSON", http.StatusBadRequest) |
||||
return |
||||
} |
||||
|
||||
if req.Enable && !g.systemProxyMgr.IsEnabled() { |
||||
// 启用系统代理
|
||||
httpProxy := fmt.Sprintf("127.0.0.1:%d", g.config.Proxy.LocalPort) |
||||
if err := g.systemProxyMgr.SetGlobalProxy(httpProxy, httpProxy, ""); err != nil { |
||||
g.writeJSON(w, map[string]interface{}{ |
||||
"success": false, |
||||
"error": err.Error(), |
||||
}) |
||||
return |
||||
} |
||||
} else if !req.Enable && g.systemProxyMgr.IsEnabled() { |
||||
// 禁用系统代理
|
||||
if err := g.systemProxyMgr.RestoreProxy(); err != nil { |
||||
g.writeJSON(w, map[string]interface{}{ |
||||
"success": false, |
||||
"error": err.Error(), |
||||
}) |
||||
return |
||||
} |
||||
} |
||||
|
||||
g.writeJSON(w, map[string]interface{}{ |
||||
"success": true, |
||||
"enabled": g.systemProxyMgr.IsEnabled(), |
||||
}) |
||||
} |
||||
|
||||
// handleRoutingStats 处理路由统计
|
||||
func (g *GUIServer) handleRoutingStats(w http.ResponseWriter, r *http.Request) { |
||||
if r.Method != http.MethodGet { |
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
||||
return |
||||
} |
||||
|
||||
var stats interface{} |
||||
if g.routeMatcher != nil { |
||||
stats = g.routeMatcher.GetStats() |
||||
} |
||||
|
||||
g.writeJSON(w, stats) |
||||
} |
||||
|
||||
// handleSystemProxy 处理系统代理信息
|
||||
func (g *GUIServer) handleSystemProxy(w http.ResponseWriter, r *http.Request) { |
||||
if r.Method != http.MethodGet { |
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) |
||||
return |
||||
} |
||||
|
||||
currentProxy, err := g.systemProxyMgr.GetCurrentProxy() |
||||
if err != nil { |
||||
g.writeJSON(w, map[string]interface{}{ |
||||
"error": err.Error(), |
||||
}) |
||||
return |
||||
} |
||||
|
||||
g.writeJSON(w, map[string]interface{}{ |
||||
"enabled": g.systemProxyMgr.IsEnabled(), |
||||
"current_proxy": currentProxy, |
||||
}) |
||||
} |
||||
|
||||
// writeJSON 写入 JSON 响应
|
||||
func (g *GUIServer) writeJSON(w http.ResponseWriter, data interface{}) { |
||||
w.Header().Set("Content-Type", "application/json") |
||||
w.Header().Set("Access-Control-Allow-Origin", "*") |
||||
|
||||
if err := json.NewEncoder(w).Encode(data); err != nil { |
||||
logger.Error("Failed to encode JSON: %v", err) |
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError) |
||||
} |
||||
} |
||||
|
||||
// getTemplateData 获取模板数据
|
||||
func (g *GUIServer) getTemplateData() map[string]interface{} { |
||||
var proxyStats interface{} |
||||
if g.socks5Proxy != nil { |
||||
proxyStats = g.socks5Proxy.GetStats() |
||||
} |
||||
|
||||
var routingStats interface{} |
||||
if g.routeMatcher != nil { |
||||
routingStats = g.routeMatcher.GetStats() |
||||
} |
||||
|
||||
return map[string]interface{}{ |
||||
"Title": "Wormhole SOCKS5 Client", |
||||
"Mode": g.mode, |
||||
"ServerAddr": g.config.GetServerAddr(), |
||||
"LocalPort": g.config.Proxy.LocalPort, |
||||
"SystemProxy": g.systemProxyMgr.IsEnabled(), |
||||
"ProxyStats": proxyStats, |
||||
"RoutingStats": routingStats, |
||||
"OS": runtime.GOOS, |
||||
} |
||||
} |
||||
|
||||
// loadTemplates 加载模板
|
||||
func (g *GUIServer) loadTemplates() { |
||||
g.templates = template.Must(template.New("index.html").Parse(g.getHTMLTemplate())) |
||||
} |
@ -0,0 +1,843 @@ |
||||
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(); |
||||
} |
||||
}); |
||||
` |
||||
} |
Loading…
Reference in new issue