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