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

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">
<!-- APIJS -->
</div>
</div>
<!-- WebSocket -->
<div id="tab-ws-tests" class="tab-content">
<div id="ws-tests-container">
<!-- WebSocketJS -->
</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>`