|
|
|
|
|
package api
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"net/http"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
|
"github.com/gin-gonic/gin/binding"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// AdminRouter 管理后台路由
|
|
|
|
|
|
type AdminRouter struct {
|
|
|
|
|
|
handler Handler
|
|
|
|
|
|
adminHandler AdminHandler
|
|
|
|
|
|
configHandler ConfigHandler
|
|
|
|
|
|
adapterHandler AdapterHandler
|
|
|
|
|
|
testHandler TestHandler
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ConfigHandler 配置管理Handler接口
|
|
|
|
|
|
type ConfigHandler interface {
|
|
|
|
|
|
GetConfigList(ctx context.Context, req *ConfigListRequest) (*Response, error)
|
|
|
|
|
|
UpdateConfig(ctx context.Context, req *ConfigUpdateRequest) (*Response, error)
|
|
|
|
|
|
ReloadConfig(ctx context.Context, req *ReloadRequest) (*Response, error)
|
|
|
|
|
|
GetSystemStatus(ctx context.Context) (*Response, error)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// AdapterHandler 适配器管理Handler接口
|
|
|
|
|
|
type AdapterHandler interface {
|
|
|
|
|
|
GetAdapterList(ctx context.Context) (*Response, error)
|
|
|
|
|
|
ToggleAdapter(ctx context.Context, req *AdapterToggleRequest) (*Response, error)
|
|
|
|
|
|
UpdateAdapterConfig(ctx context.Context, req *AdapterConfigUpdateRequest) (*Response, error)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TestHandler 测试管理Handler接口
|
|
|
|
|
|
type TestHandler interface {
|
|
|
|
|
|
GetAPITestList(ctx context.Context) (*Response, error)
|
|
|
|
|
|
RunAPITest(ctx context.Context, req *APITestRequest) (*Response, error)
|
|
|
|
|
|
GetWSTestList(ctx context.Context) (*Response, error)
|
|
|
|
|
|
RunWSTest(ctx context.Context, req *WSTestRequest) (*Response, error)
|
|
|
|
|
|
GetTestHistory(ctx context.Context, req *TestHistoryRequest) (*Response, error)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// NewAdminRouter 创建管理后台路由
|
|
|
|
|
|
func NewAdminRouter(
|
|
|
|
|
|
handler Handler,
|
|
|
|
|
|
configHandler ConfigHandler,
|
|
|
|
|
|
adapterHandler AdapterHandler,
|
|
|
|
|
|
testHandler TestHandler,
|
|
|
|
|
|
) *AdminRouter {
|
|
|
|
|
|
return &AdminRouter{
|
|
|
|
|
|
handler: handler,
|
|
|
|
|
|
adminHandler: handler,
|
|
|
|
|
|
configHandler: configHandler,
|
|
|
|
|
|
adapterHandler: adapterHandler,
|
|
|
|
|
|
testHandler: testHandler,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Register 注册管理后台路由
|
|
|
|
|
|
func (r *AdminRouter) Register(engine *gin.Engine) {
|
|
|
|
|
|
// 管理后台页面路由
|
|
|
|
|
|
admin := engine.Group("/admin")
|
|
|
|
|
|
{
|
|
|
|
|
|
admin.GET("", r.serveAdminPage)
|
|
|
|
|
|
admin.GET("/", r.serveAdminPage)
|
|
|
|
|
|
admin.StaticFS("/static", http.Dir("./web/admin/static"))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 管理后台API路由(需要认证)
|
|
|
|
|
|
api := engine.Group("/v1/admin")
|
|
|
|
|
|
api.Use(r.authMiddleware())
|
|
|
|
|
|
{
|
|
|
|
|
|
// 系统管理
|
|
|
|
|
|
api.GET("/system/status", r.getSystemStatus)
|
|
|
|
|
|
api.POST("/system/reload", r.reloadConfig)
|
|
|
|
|
|
api.POST("/system/restart", r.restartService)
|
|
|
|
|
|
|
|
|
|
|
|
// 配置管理
|
|
|
|
|
|
api.GET("/config", r.getConfigList)
|
|
|
|
|
|
api.PUT("/config", r.updateConfig)
|
|
|
|
|
|
api.POST("/config/reload", r.reloadConfig)
|
|
|
|
|
|
|
|
|
|
|
|
// 适配器管理
|
|
|
|
|
|
api.GET("/adapters", r.getAdapterList)
|
|
|
|
|
|
api.POST("/adapters/toggle", r.toggleAdapter)
|
|
|
|
|
|
api.PUT("/adapters/config", r.updateAdapterConfig)
|
|
|
|
|
|
|
|
|
|
|
|
// 测试管理
|
|
|
|
|
|
api.GET("/tests/api", r.getAPITestList)
|
|
|
|
|
|
api.POST("/tests/api/run", r.runAPITest)
|
|
|
|
|
|
api.GET("/tests/ws", r.getWSTestList)
|
|
|
|
|
|
api.POST("/tests/ws/run", r.runWSTest)
|
|
|
|
|
|
api.GET("/tests/history", r.getTestHistory)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// authMiddleware 认证中间件
|
|
|
|
|
|
func (r *AdminRouter) authMiddleware() gin.HandlerFunc {
|
|
|
|
|
|
return func(c *gin.Context) {
|
|
|
|
|
|
// 检查Admin Token
|
|
|
|
|
|
token := c.GetHeader("X-Admin-Token")
|
|
|
|
|
|
if token == "" {
|
|
|
|
|
|
token = c.Query("token")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TODO: 验证Token有效性
|
|
|
|
|
|
// 暂时允许所有请求通过
|
|
|
|
|
|
c.Set("admin_token", token)
|
|
|
|
|
|
c.Next()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// serveAdminPage 服务管理后台页面
|
|
|
|
|
|
func (r *AdminRouter) serveAdminPage(c *gin.Context) {
|
|
|
|
|
|
c.Header("Content-Type", "text/html; charset=utf-8")
|
|
|
|
|
|
c.String(http.StatusOK, adminHTML)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
// 系统管理接口
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
|
|
|
|
|
|
func (r *AdminRouter) getSystemStatus(c *gin.Context) {
|
|
|
|
|
|
resp, err := r.configHandler.GetSystemStatus(c.Request.Context())
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
|
|
|
|
Code: 500,
|
|
|
|
|
|
Message: "获取系统状态失败",
|
|
|
|
|
|
Detail: err.Error(),
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, resp)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (r *AdminRouter) reloadConfig(c *gin.Context) {
|
|
|
|
|
|
var req ReloadRequest
|
|
|
|
|
|
if err := c.ShouldBindBodyWith(&req, binding.JSON); err != nil {
|
|
|
|
|
|
// 如果没有请求体,使用默认值
|
|
|
|
|
|
req.ConfigType = ""
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
resp, err := r.configHandler.ReloadConfig(c.Request.Context(), &req)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
|
|
|
|
Code: 500,
|
|
|
|
|
|
Message: "热加载配置失败",
|
|
|
|
|
|
Detail: err.Error(),
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, resp)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (r *AdminRouter) restartService(c *gin.Context) {
|
|
|
|
|
|
// TODO: 实现服务重启逻辑
|
|
|
|
|
|
// 可以通过发送信号或调用外部脚本来实现
|
|
|
|
|
|
c.JSON(http.StatusOK, SuccessResponse{
|
|
|
|
|
|
Code: 0,
|
|
|
|
|
|
Message: "重启命令已发送",
|
|
|
|
|
|
Data: map[string]string{
|
|
|
|
|
|
"status": "restarting",
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
// 配置管理接口
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
|
|
|
|
|
|
func (r *AdminRouter) getConfigList(c *gin.Context) {
|
|
|
|
|
|
var req ConfigListRequest
|
|
|
|
|
|
req.Type = ConfigType(c.Query("type"))
|
|
|
|
|
|
|
|
|
|
|
|
resp, err := r.configHandler.GetConfigList(c.Request.Context(), &req)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
|
|
|
|
Code: 500,
|
|
|
|
|
|
Message: "获取配置列表失败",
|
|
|
|
|
|
Detail: err.Error(),
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, resp)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (r *AdminRouter) updateConfig(c *gin.Context) {
|
|
|
|
|
|
var req ConfigUpdateRequest
|
|
|
|
|
|
if err := c.ShouldBindBodyWith(&req, binding.JSON); err != nil {
|
|
|
|
|
|
c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
|
|
|
|
Code: 400,
|
|
|
|
|
|
Message: "参数错误",
|
|
|
|
|
|
Detail: err.Error(),
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
resp, err := r.configHandler.UpdateConfig(c.Request.Context(), &req)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
|
|
|
|
Code: 500,
|
|
|
|
|
|
Message: "更新配置失败",
|
|
|
|
|
|
Detail: err.Error(),
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, resp)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
// 适配器管理接口
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
|
|
|
|
|
|
func (r *AdminRouter) getAdapterList(c *gin.Context) {
|
|
|
|
|
|
resp, err := r.adapterHandler.GetAdapterList(c.Request.Context())
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
|
|
|
|
Code: 500,
|
|
|
|
|
|
Message: "获取适配器列表失败",
|
|
|
|
|
|
Detail: err.Error(),
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, resp)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (r *AdminRouter) toggleAdapter(c *gin.Context) {
|
|
|
|
|
|
var req AdapterToggleRequest
|
|
|
|
|
|
if err := c.ShouldBindBodyWith(&req, binding.JSON); err != nil {
|
|
|
|
|
|
c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
|
|
|
|
Code: 400,
|
|
|
|
|
|
Message: "参数错误",
|
|
|
|
|
|
Detail: err.Error(),
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
resp, err := r.adapterHandler.ToggleAdapter(c.Request.Context(), &req)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
|
|
|
|
Code: 500,
|
|
|
|
|
|
Message: "切换适配器状态失败",
|
|
|
|
|
|
Detail: err.Error(),
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, resp)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (r *AdminRouter) updateAdapterConfig(c *gin.Context) {
|
|
|
|
|
|
var req AdapterConfigUpdateRequest
|
|
|
|
|
|
if err := c.ShouldBindBodyWith(&req, binding.JSON); err != nil {
|
|
|
|
|
|
c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
|
|
|
|
Code: 400,
|
|
|
|
|
|
Message: "参数错误",
|
|
|
|
|
|
Detail: err.Error(),
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
resp, err := r.adapterHandler.UpdateAdapterConfig(c.Request.Context(), &req)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
|
|
|
|
Code: 500,
|
|
|
|
|
|
Message: "更新适配器配置失败",
|
|
|
|
|
|
Detail: err.Error(),
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, resp)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
// 测试管理接口
|
|
|
|
|
|
// ============================================
|
|
|
|
|
|
|
|
|
|
|
|
func (r *AdminRouter) getAPITestList(c *gin.Context) {
|
|
|
|
|
|
resp, err := r.testHandler.GetAPITestList(c.Request.Context())
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
|
|
|
|
Code: 500,
|
|
|
|
|
|
Message: "获取API测试列表失败",
|
|
|
|
|
|
Detail: err.Error(),
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, resp)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (r *AdminRouter) runAPITest(c *gin.Context) {
|
|
|
|
|
|
var req APITestRequest
|
|
|
|
|
|
if err := c.ShouldBindBodyWith(&req, binding.JSON); err != nil {
|
|
|
|
|
|
c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
|
|
|
|
Code: 400,
|
|
|
|
|
|
Message: "参数错误",
|
|
|
|
|
|
Detail: err.Error(),
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
resp, err := r.testHandler.RunAPITest(c.Request.Context(), &req)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
|
|
|
|
Code: 500,
|
|
|
|
|
|
Message: "执行API测试失败",
|
|
|
|
|
|
Detail: err.Error(),
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, resp)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (r *AdminRouter) getWSTestList(c *gin.Context) {
|
|
|
|
|
|
resp, err := r.testHandler.GetWSTestList(c.Request.Context())
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
|
|
|
|
Code: 500,
|
|
|
|
|
|
Message: "获取WebSocket测试列表失败",
|
|
|
|
|
|
Detail: err.Error(),
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, resp)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (r *AdminRouter) runWSTest(c *gin.Context) {
|
|
|
|
|
|
var req WSTestRequest
|
|
|
|
|
|
if err := c.ShouldBindBodyWith(&req, binding.JSON); err != nil {
|
|
|
|
|
|
c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
|
|
|
|
Code: 400,
|
|
|
|
|
|
Message: "参数错误",
|
|
|
|
|
|
Detail: err.Error(),
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
resp, err := r.testHandler.RunWSTest(c.Request.Context(), &req)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
|
|
|
|
Code: 500,
|
|
|
|
|
|
Message: "执行WebSocket测试失败",
|
|
|
|
|
|
Detail: err.Error(),
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, resp)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (r *AdminRouter) getTestHistory(c *gin.Context) {
|
|
|
|
|
|
var req TestHistoryRequest
|
|
|
|
|
|
req.Type = c.Query("type")
|
|
|
|
|
|
req.Limit = 20 // 默认值
|
|
|
|
|
|
|
|
|
|
|
|
resp, err := r.testHandler.GetTestHistory(c.Request.Context(), &req)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
|
|
|
|
Code: 500,
|
|
|
|
|
|
Message: "获取测试历史失败",
|
|
|
|
|
|
Detail: err.Error(),
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, resp)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// AdminHTML 管理后台页面HTML
|
|
|
|
|
|
const adminHTML = `<!DOCTYPE html>
|
|
|
|
|
|
<html lang="zh-CN">
|
|
|
|
|
|
<head>
|
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
|
|
<title>行情数据服务 - 管理后台</title>
|
|
|
|
|
|
<style>
|
|
|
|
|
|
* {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
body {
|
|
|
|
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
|
|
|
|
|
background: #f0f2f5;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.layout {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar {
|
|
|
|
|
|
width: 200px;
|
|
|
|
|
|
background: #001529;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
height: 100vh;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.logo {
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
border-bottom: 1px solid rgba(255,255,255,0.1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.nav-menu {
|
|
|
|
|
|
padding: 16px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.nav-item {
|
|
|
|
|
|
padding: 12px 24px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.3s;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.nav-item:hover {
|
|
|
|
|
|
background: rgba(255,255,255,0.05);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.nav-item.active {
|
|
|
|
|
|
background: #1890ff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.main-content {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
margin-left: 200px;
|
|
|
|
|
|
padding: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.header {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
padding: 16px 24px;
|
|
|
|
|
|
margin: -24px -24px 24px -24px;
|
|
|
|
|
|
box-shadow: 0 1px 4px rgba(0,0,0,0.1);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.page-title {
|
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 24px;
|
|
|
|
|
|
margin-bottom: 24px;
|
|
|
|
|
|
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-title {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
padding-bottom: 12px;
|
|
|
|
|
|
border-bottom: 1px solid #f0f0f0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stats-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
margin-bottom: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stat-card {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stat-value {
|
|
|
|
|
|
font-size: 32px;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
color: #1890ff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stat-label {
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.config-section {
|
|
|
|
|
|
margin-bottom: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.config-section-title {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-row {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-group {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-group label {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-group input,
|
|
|
|
|
|
.form-group select {
|
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
|
border: 1px solid #d9d9d9;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
transition: all 0.3s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-group input:focus,
|
|
|
|
|
|
.form-group select:focus {
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
border-color: #1890ff;
|
|
|
|
|
|
box-shadow: 0 0 0 2px rgba(24,144,255,0.2);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn {
|
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.3s;
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn-primary {
|
|
|
|
|
|
background: #1890ff;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn-primary:hover {
|
|
|
|
|
|
background: #40a9ff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn-danger {
|
|
|
|
|
|
background: #ff4d4f;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn-danger:hover {
|
|
|
|
|
|
background: #ff7875;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn-success {
|
|
|
|
|
|
background: #52c41a;
|
|
|
|
|
|
color: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn-success:hover {
|
|
|
|
|
|
background: #73d13d;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn-default {
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border: 1px solid #d9d9d9;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn-default:hover {
|
|
|
|
|
|
border-color: #1890ff;
|
|
|
|
|
|
color: #1890ff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn-sm {
|
|
|
|
|
|
padding: 4px 8px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.table {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
border-collapse: collapse;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.table th,
|
|
|
|
|
|
.table td {
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
border-bottom: 1px solid #f0f0f0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.table th {
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
background: #fafafa;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.table tr:hover {
|
|
|
|
|
|
background: #fafafa;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tag {
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
padding: 2px 8px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tag-success {
|
|
|
|
|
|
background: #f6ffed;
|
|
|
|
|
|
color: #52c41a;
|
|
|
|
|
|
border: 1px solid #b7eb8f;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tag-warning {
|
|
|
|
|
|
background: #fffbe6;
|
|
|
|
|
|
color: #faad14;
|
|
|
|
|
|
border: 1px solid #ffe58f;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tag-error {
|
|
|
|
|
|
background: #fff2f0;
|
|
|
|
|
|
color: #ff4d4f;
|
|
|
|
|
|
border: 1px solid #ffccc7;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tag-default {
|
|
|
|
|
|
background: #f5f5f5;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
border: 1px solid #d9d9d9;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.test-item {
|
|
|
|
|
|
border: 1px solid #e8e8e8;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.test-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.test-name {
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.test-desc {
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.test-params {
|
|
|
|
|
|
background: #f5f5f5;
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
font-family: monospace;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.test-result {
|
|
|
|
|
|
margin-top: 12px;
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
background: #f6ffed;
|
|
|
|
|
|
border: 1px solid #b7eb8f;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.test-result.error {
|
|
|
|
|
|
background: #fff2f0;
|
|
|
|
|
|
border-color: #ffccc7;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hidden {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tab-bar {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
border-bottom: 1px solid #e8e8e8;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tab-item {
|
|
|
|
|
|
padding: 12px 24px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
border-bottom: 2px solid transparent;
|
|
|
|
|
|
transition: all 0.3s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tab-item:hover {
|
|
|
|
|
|
color: #1890ff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tab-item.active {
|
|
|
|
|
|
color: #1890ff;
|
|
|
|
|
|
border-bottom-color: #1890ff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tab-content {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tab-content.active {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.loading {
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
width: 16px;
|
|
|
|
|
|
height: 16px;
|
|
|
|
|
|
border: 2px solid #f3f3f3;
|
|
|
|
|
|
border-top: 2px solid #1890ff;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
animation: spin 1s linear infinite;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes spin {
|
|
|
|
|
|
0% { transform: rotate(0deg); }
|
|
|
|
|
|
100% { transform: rotate(360deg); }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.alert {
|
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.alert-success {
|
|
|
|
|
|
background: #f6ffed;
|
|
|
|
|
|
border: 1px solid #b7eb8f;
|
|
|
|
|
|
color: #52c41a;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.alert-error {
|
|
|
|
|
|
background: #fff2f0;
|
|
|
|
|
|
border: 1px solid #ffccc7;
|
|
|
|
|
|
color: #ff4d4f;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.actions-bar {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|
|
|
|
|
|
</head>
|
|
|
|
|
|
<body>
|
|
|
|
|
|
<div class="layout">
|
|
|
|
|
|
<aside class="sidebar">
|
|
|
|
|
|
<div class="logo">📊 行情数据服务</div>
|
|
|
|
|
|
<nav class="nav-menu">
|
|
|
|
|
|
<div class="nav-item active" data-page="dashboard">
|
|
|
|
|
|
<span>📈</span> 系统概览
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="nav-item" data-page="config">
|
|
|
|
|
|
<span>⚙️</span> 配置管理
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="nav-item" data-page="adapters">
|
|
|
|
|
|
<span>🔌</span> 数据源适配
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="nav-item" data-page="tests">
|
|
|
|
|
|
<span>🧪</span> 接口测试
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</nav>
|
|
|
|
|
|
</aside>
|
|
|
|
|
|
|
|
|
|
|
|
<main class="main-content">
|
|
|
|
|
|
<!-- 系统概览页面 -->
|
|
|
|
|
|
<div id="page-dashboard" class="page">
|
|
|
|
|
|
<div class="header">
|
|
|
|
|
|
<h1 class="page-title">系统概览</h1>
|
|
|
|
|
|
<div class="actions-bar">
|
|
|
|
|
|
<button class="btn btn-success" onclick="reloadConfig()">
|
|
|
|
|
|
<span>🔄</span> 热加载配置
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="btn btn-danger" onclick="restartService()">
|
|
|
|
|
|
<span>🔁</span> 重启服务
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div id="dashboard-alert"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="stats-grid" id="system-stats">
|
|
|
|
|
|
<div class="stat-card">
|
|
|
|
|
|
<div class="stat-value" id="stat-status">-</div>
|
|
|
|
|
|
<div class="stat-label">运行状态</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="stat-card">
|
|
|
|
|
|
<div class="stat-value" id="stat-uptime">-</div>
|
|
|
|
|
|
<div class="stat-label">运行时长</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="stat-card">
|
|
|
|
|
|
<div class="stat-value" id="stat-version">-</div>
|
|
|
|
|
|
<div class="stat-label">系统版本</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="stat-card">
|
|
|
|
|
|
<div class="stat-value" id="stat-goroutines">-</div>
|
|
|
|
|
|
<div class="stat-label">Goroutines</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="card">
|
|
|
|
|
|
<div class="card-title">内存使用</div>
|
|
|
|
|
|
<div class="form-row">
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label>已分配内存</label>
|
|
|
|
|
|
<input type="text" id="mem-alloc" readonly>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label>系统内存</label>
|
|
|
|
|
|
<input type="text" id="mem-sys" readonly>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label>GC次数</label>
|
|
|
|
|
|
<input type="text" id="mem-gc" readonly>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 配置管理页面 -->
|
|
|
|
|
|
<div id="page-config" class="page hidden">
|
|
|
|
|
|
<div class="header">
|
|
|
|
|
|
<h1 class="page-title">配置管理</h1>
|
|
|
|
|
|
<div class="actions-bar">
|
|
|
|
|
|
<button class="btn btn-primary" onclick="saveConfig()">
|
|
|
|
|
|
<span>💾</span> 保存配置
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div id="config-alert"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<div id="config-container">
|
|
|
|
|
|
<!-- 配置项将通过JS动态加载 -->
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 数据源适配页面 -->
|
|
|
|
|
|
<div id="page-adapters" class="page hidden">
|
|
|
|
|
|
<div class="header">
|
|
|
|
|
|
<h1 class="page-title">数据源适配</h1>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div id="adapter-alert"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="card">
|
|
|
|
|
|
<div class="card-title">适配器列表</div>
|
|
|
|
|
|
<table class="table">
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>名称</th>
|
|
|
|
|
|
<th>类型</th>
|
|
|
|
|
|
<th>版本</th>
|
|
|
|
|
|
<th>描述</th>
|
|
|
|
|
|
<th>状态</th>
|
|
|
|
|
|
<th>操作</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody id="adapter-list">
|
|
|
|
|
|
<!-- 适配器列表将通过JS动态加载 -->
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 接口测试页面 -->
|
|
|
|
|
|
<div id="page-tests" class="page hidden">
|
|
|
|
|
|
<div class="header">
|
|
|
|
|
|
<h1 class="page-title">接口测试</h1>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="tab-bar">
|
|
|
|
|
|
<div class="tab-item active" data-tab="api-tests">API测试</div>
|
|
|
|
|
|
<div class="tab-item" data-tab="ws-tests">WebSocket测试</div>
|
|
|
|
|
|
<div class="tab-item" data-tab="test-history">测试历史</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- API测试 -->
|
|
|
|
|
|
<div id="tab-api-tests" class="tab-content active">
|
|
|
|
|
|
<div id="api-tests-container">
|
|
|
|
|
|
<!-- API测试项将通过JS动态加载 -->
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- WebSocket测试 -->
|
|
|
|
|
|
<div id="tab-ws-tests" class="tab-content">
|
|
|
|
|
|
<div id="ws-tests-container">
|
|
|
|
|
|
<!-- WebSocket测试项将通过JS动态加载 -->
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 测试历史 -->
|
|
|
|
|
|
<div id="tab-test-history" class="tab-content">
|
|
|
|
|
|
<div class="card">
|
|
|
|
|
|
<div class="card-title">最近测试记录</div>
|
|
|
|
|
|
<table class="table">
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<th>时间</th>
|
|
|
|
|
|
<th>测试项</th>
|
|
|
|
|
|
<th>状态</th>
|
|
|
|
|
|
<th>延迟</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody id="test-history-list">
|
|
|
|
|
|
<!-- 历史记录将通过JS动态加载 -->
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</main>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
// 全局状态
|
|
|
|
|
|
const state = {
|
|
|
|
|
|
currentPage: 'dashboard',
|
|
|
|
|
|
config: {},
|
|
|
|
|
|
adapters: [],
|
|
|
|
|
|
apiTests: [],
|
|
|
|
|
|
wsTests: [],
|
|
|
|
|
|
baseURL: window.location.origin
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// API请求封装
|
|
|
|
|
|
async function apiRequest(method, path, data = null) {
|
|
|
|
|
|
const options = {
|
|
|
|
|
|
method,
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
'X-Admin-Token': localStorage.getItem('adminToken') || ''
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (data) {
|
|
|
|
|
|
options.body = JSON.stringify(data);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const response = await fetch(state.baseURL + path, options);
|
|
|
|
|
|
return response.json();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 页面切换
|
|
|
|
|
|
document.querySelectorAll('.nav-item').forEach(item => {
|
|
|
|
|
|
item.addEventListener('click', () => {
|
|
|
|
|
|
const page = item.dataset.page;
|
|
|
|
|
|
switchPage(page);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
function switchPage(page) {
|
|
|
|
|
|
state.currentPage = page;
|
|
|
|
|
|
|
|
|
|
|
|
// 更新导航状态
|
|
|
|
|
|
document.querySelectorAll('.nav-item').forEach(item => {
|
|
|
|
|
|
item.classList.toggle('active', item.dataset.page === page);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 切换页面显示
|
|
|
|
|
|
document.querySelectorAll('.page').forEach(p => {
|
|
|
|
|
|
p.classList.toggle('hidden', p.id !== 'page-' + page);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 加载页面数据
|
|
|
|
|
|
switch(page) {
|
|
|
|
|
|
case 'dashboard':
|
|
|
|
|
|
loadDashboard();
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'config':
|
|
|
|
|
|
loadConfig();
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'adapters':
|
|
|
|
|
|
loadAdapters();
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 'tests':
|
|
|
|
|
|
loadTests();
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Tab切换
|
|
|
|
|
|
document.querySelectorAll('.tab-item').forEach(item => {
|
|
|
|
|
|
item.addEventListener('click', () => {
|
|
|
|
|
|
const tab = item.dataset.tab;
|
|
|
|
|
|
|
|
|
|
|
|
document.querySelectorAll('.tab-item').forEach(t => {
|
|
|
|
|
|
t.classList.toggle('active', t.dataset.tab === tab);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
document.querySelectorAll('.tab-content').forEach(c => {
|
|
|
|
|
|
c.classList.toggle('active', c.id === 'tab-' + tab);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (tab === 'test-history') {
|
|
|
|
|
|
loadTestHistory();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 加载系统概览
|
|
|
|
|
|
async function loadDashboard() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await apiRequest('GET', '/v1/admin/system/status');
|
|
|
|
|
|
if (res.code === 0 || res.status) {
|
|
|
|
|
|
const data = res.data || res;
|
|
|
|
|
|
document.getElementById('stat-status').textContent = data.status === 'running' ? '运行中' : '异常';
|
|
|
|
|
|
document.getElementById('stat-uptime').textContent = data.uptime;
|
|
|
|
|
|
document.getElementById('stat-version').textContent = data.version;
|
|
|
|
|
|
document.getElementById('stat-goroutines').textContent = data.goroutines;
|
|
|
|
|
|
|
|
|
|
|
|
if (data.memory) {
|
|
|
|
|
|
document.getElementById('mem-alloc').value = formatBytes(data.memory.alloc);
|
|
|
|
|
|
document.getElementById('mem-sys').value = formatBytes(data.memory.sys);
|
|
|
|
|
|
document.getElementById('mem-gc').value = data.memory.num_gc;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('加载系统状态失败:', e);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 加载配置
|
|
|
|
|
|
async function loadConfig() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await apiRequest('GET', '/v1/admin/config');
|
|
|
|
|
|
if (res.code === 0) {
|
|
|
|
|
|
renderConfig(res.data);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('加载配置失败:', e);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderConfig(data) {
|
|
|
|
|
|
const container = document.getElementById('config-container');
|
|
|
|
|
|
container.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
|
|
state.config = data;
|
|
|
|
|
|
|
|
|
|
|
|
data.sections.forEach(section => {
|
|
|
|
|
|
const sectionEl = document.createElement('div');
|
|
|
|
|
|
sectionEl.className = 'card';
|
|
|
|
|
|
sectionEl.innerHTML = `
|
|
|
|
|
|
<div class="card-title">${section.name}</div>
|
|
|
|
|
|
<div class="config-section">
|
|
|
|
|
|
<div class="config-section-title">${section.description}</div>
|
|
|
|
|
|
<div class="form-row" id="config-${section.type}">
|
|
|
|
|
|
${section.items.map(item => `
|
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
|
<label>${item.description}</label>
|
|
|
|
|
|
<input type="${item.type === 'password' ? 'password' : 'text'}"
|
|
|
|
|
|
id="cfg-${section.type}-${item.key}"
|
|
|
|
|
|
value="${item.value}"
|
|
|
|
|
|
data-type="${section.type}"
|
|
|
|
|
|
data-key="${item.key}"
|
|
|
|
|
|
${!item.editable ? 'readonly' : ''}>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`).join('')}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
container.appendChild(sectionEl);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function saveConfig() {
|
|
|
|
|
|
const updates = {};
|
|
|
|
|
|
|
|
|
|
|
|
document.querySelectorAll('[id^="cfg-"]').forEach(input => {
|
|
|
|
|
|
const type = input.dataset.type;
|
|
|
|
|
|
const key = input.dataset.key;
|
|
|
|
|
|
|
|
|
|
|
|
if (!updates[type]) {
|
|
|
|
|
|
updates[type] = { type, items: {} };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let value = input.value;
|
|
|
|
|
|
if (input.type === 'number') {
|
|
|
|
|
|
value = parseInt(value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
updates[type].items[key] = value;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
for (const type in updates) {
|
|
|
|
|
|
const res = await apiRequest('PUT', '/v1/admin/config', updates[type]);
|
|
|
|
|
|
if (res.code !== 0 && !res.success) {
|
|
|
|
|
|
showAlert('config-alert', 'error', res.message || '保存失败');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
showAlert('config-alert', 'success', '配置保存成功');
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
showAlert('config-alert', 'error', '保存失败: ' + e.message);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 加载适配器列表
|
|
|
|
|
|
async function loadAdapters() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await apiRequest('GET', '/v1/admin/adapters');
|
|
|
|
|
|
if (res.code === 0) {
|
|
|
|
|
|
renderAdapters(res.data.adapters);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('加载适配器列表失败:', e);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderAdapters(adapters) {
|
|
|
|
|
|
state.adapters = adapters;
|
|
|
|
|
|
const tbody = document.getElementById('adapter-list');
|
|
|
|
|
|
tbody.innerHTML = adapters.map(adapter => `
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td>${adapter.name}</td>
|
|
|
|
|
|
<td>${adapter.type}</td>
|
|
|
|
|
|
<td>${adapter.version}</td>
|
|
|
|
|
|
<td>${adapter.description}</td>
|
|
|
|
|
|
<td>
|
|
|
|
|
|
<span class="tag ${getStatusClass(adapter.status)}">
|
|
|
|
|
|
${getStatusText(adapter.status)}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
<td>
|
|
|
|
|
|
<button class="btn btn-sm ${adapter.status === 'active' ? 'btn-danger' : 'btn-success'}"
|
|
|
|
|
|
onclick="toggleAdapter('${adapter.name}', ${adapter.status !== 'active'})">
|
|
|
|
|
|
${adapter.status === 'active' ? '禁用' : '启用'}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
`).join('');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getStatusClass(status) {
|
|
|
|
|
|
switch(status) {
|
|
|
|
|
|
case 'active': return 'tag-success';
|
|
|
|
|
|
case 'standby': return 'tag-warning';
|
|
|
|
|
|
case 'disabled': return 'tag-default';
|
|
|
|
|
|
default: return 'tag-error';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getStatusText(status) {
|
|
|
|
|
|
switch(status) {
|
|
|
|
|
|
case 'active': return '已激活';
|
|
|
|
|
|
case 'standby': return '待命';
|
|
|
|
|
|
case 'disabled': return '已禁用';
|
|
|
|
|
|
default: return '异常';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function toggleAdapter(name, enable) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await apiRequest('POST', '/v1/admin/adapters/toggle', { name, enable });
|
|
|
|
|
|
if (res.code === 0) {
|
|
|
|
|
|
showAlert('adapter-alert', 'success', enable ? '启用成功' : '禁用成功');
|
|
|
|
|
|
loadAdapters();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showAlert('adapter-alert', 'error', res.message || '操作失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
showAlert('adapter-alert', 'error', '操作失败: ' + e.message);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 加载测试
|
|
|
|
|
|
async function loadTests() {
|
|
|
|
|
|
await loadAPITests();
|
|
|
|
|
|
await loadWSTests();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadAPITests() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await apiRequest('GET', '/v1/admin/tests/api');
|
|
|
|
|
|
if (res.code === 0) {
|
|
|
|
|
|
renderAPITests(res.data);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('加载API测试失败:', e);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderAPITests(data) {
|
|
|
|
|
|
state.apiTests = data;
|
|
|
|
|
|
const container = document.getElementById('api-tests-container');
|
|
|
|
|
|
|
|
|
|
|
|
container.innerHTML = data.categories.map(cat => `
|
|
|
|
|
|
<div class="card">
|
|
|
|
|
|
<div class="card-title">${cat.name}</div>
|
|
|
|
|
|
${cat.items.map(item => `
|
|
|
|
|
|
<div class="test-item" id="test-api-${item.id}">
|
|
|
|
|
|
<div class="test-header">
|
|
|
|
|
|
<span class="test-name">${item.name}</span>
|
|
|
|
|
|
<button class="btn btn-primary btn-sm" onclick="runAPITest('${item.id}')">
|
|
|
|
|
|
运行测试
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="test-desc">${item.description}</div>
|
|
|
|
|
|
<div class="test-params">
|
|
|
|
|
|
<div>${item.method} ${item.path}</div>
|
|
|
|
|
|
${item.params ? '<div>参数: ' + JSON.stringify(item.params) + '</div>' : ''}
|
|
|
|
|
|
${item.body ? '<div>Body: ' + JSON.stringify(item.body) + '</div>' : ''}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="test-result hidden" id="result-api-${item.id}"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`).join('')}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`).join('');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadWSTests() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await apiRequest('GET', '/v1/admin/tests/ws');
|
|
|
|
|
|
if (res.code === 0) {
|
|
|
|
|
|
renderWSTests(res.data);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('加载WebSocket测试失败:', e);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderWSTests(data) {
|
|
|
|
|
|
state.wsTests = data;
|
|
|
|
|
|
const container = document.getElementById('ws-tests-container');
|
|
|
|
|
|
|
|
|
|
|
|
container.innerHTML = `
|
|
|
|
|
|
<div class="card">
|
|
|
|
|
|
<div class="card-title">WebSocket连接测试</div>
|
|
|
|
|
|
${data.cases.map(item => `
|
|
|
|
|
|
<div class="test-item" id="test-ws-${item.id}">
|
|
|
|
|
|
<div class="test-header">
|
|
|
|
|
|
<span class="test-name">${item.name}</span>
|
|
|
|
|
|
<button class="btn btn-primary btn-sm" onclick="runWSTest('${item.id}')">
|
|
|
|
|
|
运行测试
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="test-desc">${item.description}</div>
|
|
|
|
|
|
<div class="test-params">
|
|
|
|
|
|
<div>Action: ${item.action}</div>
|
|
|
|
|
|
<div>Symbols: ${item.symbols.join(', ')}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="test-result hidden" id="result-ws-${item.id}"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`).join('')}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function runAPITest(id) {
|
|
|
|
|
|
const resultEl = document.getElementById('result-api-' + id);
|
|
|
|
|
|
resultEl.classList.remove('hidden');
|
|
|
|
|
|
resultEl.innerHTML = '<div class="loading"></div> 测试中...';
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await apiRequest('POST', '/v1/admin/tests/api/run', { id });
|
|
|
|
|
|
if (res.code === 0) {
|
|
|
|
|
|
const data = res.data;
|
|
|
|
|
|
const isSuccess = data.success;
|
|
|
|
|
|
resultEl.className = 'test-result ' + (isSuccess ? '' : 'error');
|
|
|
|
|
|
resultEl.innerHTML = `
|
|
|
|
|
|
<div>状态: ${isSuccess ? '✅ 成功' : '❌ 失败'}</div>
|
|
|
|
|
|
<div>HTTP状态: ${data.status_code}</div>
|
|
|
|
|
|
<div>延迟: ${data.latency}ms</div>
|
|
|
|
|
|
${data.error ? '<div style="color:red">错误: ' + data.error + '</div>' : ''}
|
|
|
|
|
|
${data.response ? '<div style="margin-top:8px"><pre>' + JSON.stringify(data.response, null, 2) + '</pre></div>' : ''}
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
resultEl.className = 'test-result error';
|
|
|
|
|
|
resultEl.innerHTML = '<div>❌ 测试执行失败: ' + e.message + '</div>';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function runWSTest(id) {
|
|
|
|
|
|
const resultEl = document.getElementById('result-ws-' + id);
|
|
|
|
|
|
resultEl.classList.remove('hidden');
|
|
|
|
|
|
resultEl.innerHTML = '<div class="loading"></div> 测试中...';
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await apiRequest('POST', '/v1/admin/tests/ws/run', { id });
|
|
|
|
|
|
if (res.code === 0) {
|
|
|
|
|
|
const data = res.data;
|
|
|
|
|
|
const isSuccess = data.success;
|
|
|
|
|
|
resultEl.className = 'test-result ' + (isSuccess ? '' : 'error');
|
|
|
|
|
|
resultEl.innerHTML = `
|
|
|
|
|
|
<div>状态: ${isSuccess ? '✅ 成功' : '❌ 失败'}</div>
|
|
|
|
|
|
<div>连接延迟: ${data.latency}ms</div>
|
|
|
|
|
|
${data.error ? '<div style="color:red">错误: ' + data.error + '</div>' : ''}
|
|
|
|
|
|
${data.messages && data.messages.length > 0 ? `
|
|
|
|
|
|
<div style="margin-top:8px">
|
|
|
|
|
|
<div>收到消息:</div>
|
|
|
|
|
|
${data.messages.map(m => '<pre>' + JSON.stringify(m.data, null, 2) + '</pre>').join('')}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
` : ''}
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
resultEl.className = 'test-result error';
|
|
|
|
|
|
resultEl.innerHTML = '<div>❌ 测试执行失败: ' + e.message + '</div>';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadTestHistory() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await apiRequest('GET', '/v1/admin/tests/history?limit=20');
|
|
|
|
|
|
if (res.code === 0) {
|
|
|
|
|
|
const tbody = document.getElementById('test-history-list');
|
|
|
|
|
|
const allTests = [];
|
|
|
|
|
|
|
|
|
|
|
|
if (res.data.api_tests) {
|
|
|
|
|
|
res.data.api_tests.forEach(t => {
|
|
|
|
|
|
allTests.push({ ...t, type: 'API' });
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
if (res.data.ws_tests) {
|
|
|
|
|
|
res.data.ws_tests.forEach(t => {
|
|
|
|
|
|
allTests.push({ ...t, type: 'WebSocket' });
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
allTests.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
|
|
|
|
|
|
|
|
|
|
|
tbody.innerHTML = allTests.slice(0, 20).map(t => `
|
|
|
|
|
|
<tr>
|
|
|
|
|
|
<td>${new Date(t.timestamp).toLocaleString()}</td>
|
|
|
|
|
|
<td>${t.name || t.case_id} (${t.type})</td>
|
|
|
|
|
|
<td><span class="tag ${t.success ? 'tag-success' : 'tag-error'}">${t.success ? '成功' : '失败'}</span></td>
|
|
|
|
|
|
<td>${t.latency}ms</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
`).join('') || '<tr><td colspan="4" style="text-align:center">暂无测试记录</td></tr>';
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('加载测试历史失败:', e);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 系统操作
|
|
|
|
|
|
async function reloadConfig() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await apiRequest('POST', '/v1/admin/system/reload', {});
|
|
|
|
|
|
if (res.code === 0 || res.success) {
|
|
|
|
|
|
showAlert('dashboard-alert', 'success', '配置热加载成功');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showAlert('dashboard-alert', 'error', res.message || '热加载失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
showAlert('dashboard-alert', 'error', '热加载失败: ' + e.message);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function restartService() {
|
|
|
|
|
|
if (!confirm('确定要重启服务吗?这将导致短暂的服务中断。')) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await apiRequest('POST', '/v1/admin/system/restart', {});
|
|
|
|
|
|
if (res.code === 0) {
|
|
|
|
|
|
showAlert('dashboard-alert', 'success', '重启命令已发送,请等待服务恢复...');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
showAlert('dashboard-alert', 'error', '重启失败: ' + e.message);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 工具函数
|
|
|
|
|
|
function showAlert(containerId, type, message) {
|
|
|
|
|
|
const container = document.getElementById(containerId);
|
|
|
|
|
|
container.innerHTML = '<div class="alert alert-' + type + '">' + message + '</div>';
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
container.innerHTML = '';
|
|
|
|
|
|
}, 5000);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatBytes(bytes) {
|
|
|
|
|
|
if (bytes === 0) return '0 B';
|
|
|
|
|
|
const k = 1024;
|
|
|
|
|
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
|
|
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
|
|
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
|
loadDashboard();
|
|
|
|
|
|
|
|
|
|
|
|
// 定时刷新系统状态
|
|
|
|
|
|
setInterval(() => {
|
|
|
|
|
|
if (state.currentPage === 'dashboard') {
|
|
|
|
|
|
loadDashboard();
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 5000);
|
|
|
|
|
|
});
|
|
|
|
|
|
</script>
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>`
|