diff --git a/api.go b/api.go index 809d438..2f746f4 100644 --- a/api.go +++ b/api.go @@ -6,11 +6,13 @@ import ( "fmt" "io" "io/fs" + "net" "net/http" "os" "strconv" "strings" "text/template" + "time" "github.com/gin-gonic/gin" "golang.org/x/crypto/bcrypt" @@ -64,7 +66,7 @@ func SetupRouter() *gin.Engine { auth.POST("/frpc/stop", stopFrpcHandler) auth.GET("/frpc/status", getFrpcStatusHandler) auth.GET("/frpc/log", getFrpcLogHandler) - + auth.GET("/ping", pingHandler) auth.POST("/import/toml", importTomlHandler) auth.GET("/export/toml", ExportTomlHandler) @@ -143,6 +145,36 @@ func registerHandler(c *gin.Context) { }) } +// Ping 延迟检测 +func pingHandler(c *gin.Context) { + target := c.Query("target") + if target == "" { + c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "缺少 target 参数"}) + return + } + + // 获取全局配置(获取服务端端口) + cfg, err := GetGlobalConfig() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "读取配置失败"}) + return + } + + port := cfg.ServerPort + address := net.JoinHostPort(target, strconv.Itoa(port)) + + start := time.Now() + conn, err := net.DialTimeout("tcp", address, 5*time.Second) + if err != nil { + c.JSON(http.StatusOK, gin.H{"code": 1, "msg": "ping 失败", "latency": -1}) + return + } + conn.Close() + + latency := time.Since(start).Milliseconds() + c.JSON(http.StatusOK, gin.H{"code": 0, "latency": latency}) +} + func loginHandler(c *gin.Context) { var req struct { Username string `json:"username"` diff --git a/static/app.js b/static/app.js index 7d3082d..6a04455 100644 --- a/static/app.js +++ b/static/app.js @@ -205,6 +205,93 @@ async function fetchLogsApi() { } } +// ---- 新增:Ping 延迟检测 ---- +const pingLatency = ref(null); // null 表示未接入/失败 +const pingInterval = ref(null); +const PING_INTERVAL_MS = 30000; // 30 秒轮询一次 +const PING_TIMEOUT_MS = 5000; // 5 秒超时 + +// 计算 ping 状态 class +const pingStatusClass = computed(() => { + if (pingLatency.value === null) return 'ping-fail'; + if (pingLatency.value < 1000) return 'ping-good'; + if (pingLatency.value < 5000) return 'ping-slow'; + return 'ping-fail'; +}); + +// 根据状态返回对应 SVG 图标(内联) +const pingIcon = computed(() => { + if (pingLatency.value === null) { + // 未接入/失败 —— 断开图标 + return ``; + } + if (pingLatency.value < 1000) { + // 延迟好 —— 实心信号图标(青色/绿色) + return ``; + } + // 延迟一般(1000-5000ms)—— 空心信号图标(黄色/橙色) + return ``; +}); + +// 执行 Ping 检测 +async function doPing() { + if (!frpcRunning.value) { + pingLatency.value = null; + return; + } + const addr = globalConfig.serverAddr || 'frp.example.com'; + const start = performance.now(); + try { + const res = await fetch(`/api/ping?target=${encodeURIComponent(addr)}`, { + signal: AbortSignal.timeout(PING_TIMEOUT_MS), + }); + if (!res.ok) throw new Error('Ping failed'); + const end = performance.now(); + pingLatency.value = Math.round(end - start); + } catch { + pingLatency.value = null; + } +} + +// 启动 Ping 轮询 +function startPingPolling() { + if (pingInterval.value) return; + doPing(); + pingInterval.value = setInterval(doPing, PING_INTERVAL_MS); +} + +function stopPingPolling() { + if (pingInterval.value) { + clearInterval(pingInterval.value); + pingInterval.value = null; + } +} + +// 在登录后启动,退出时停止 +// 在 loadAllData 成功后调用 startPingPolling() +// 在 doLogout 中调用 stopPingPolling() +// 监听 frpcRunning 变化:如果为 false,清空 pingLatency +watch(frpcRunning, (running) => { + if (!running) { + pingLatency.value = null; + stopPingPolling(); + } else if (loggedIn.value) { + startPingPolling(); + } +}); + // ============================================================ // 7. UI State // ============================================================ diff --git a/static/index.html b/static/index.html index bef17dc..63de43e 100644 --- a/static/index.html +++ b/static/index.html @@ -87,7 +87,6 @@
{{ passwordChangeError }}
-{{ passwordChangeSuccess }}
-修改当前账户的登录密码
+{{ passwordChangeError }}
+{{ passwordChangeSuccess }}
+