You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1431 lines
45 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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>`