```
feat(admin): add TCP proxy management and server operations - Add restart and delete server API endpoints with proper error handling - Implement TCP proxy management including list, create, update, delete, start, and stop operations - Add TCP proxy configuration model and manager integration - Create admin API routes for both server and TCP proxy management feat(adminui): redesign UI with tabs for servers and TCP proxies - Replace old site management panel with tabbed interface for servers and TCP proxies - Add table views for managing HTTP servers and TCP proxies with status indicators - Implement CRUD operations for both server and TCP proxy configurations - Update styling and layout for better user experience refactor(adminui): clean up HTML structure and remove unused features - Remove particle background and theme switching functionality - Simplify HTML template by removing unused chart and logger panels - Rename application title to "GoHttp Management Console" - Update refresh button and remove export/import configuration options ``` Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
parent
ca5515387a
commit
50c48e8f07
174
admin/admin.go
174
admin/admin.go
|
|
@ -5,6 +5,7 @@ import (
|
|||
"net/http"
|
||||
"runtime"
|
||||
|
||||
"git.kingecg.top/kingecg/gohttpd/handler"
|
||||
"git.kingecg.top/kingecg/gohttpd/model"
|
||||
"git.kingecg.top/kingecg/gohttpd/server"
|
||||
"github.com/gin-gonic/gin" // 添加Gin框架导入
|
||||
|
|
@ -131,6 +132,166 @@ func start(c *gin.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
func restart(c *gin.Context) {
|
||||
name := c.Param("name")
|
||||
if name != "" {
|
||||
serverConf := model.GetServerConfig(name)
|
||||
if serverConf == nil {
|
||||
c.Writer.WriteHeader(http.StatusNotFound)
|
||||
c.Writer.Write(server.NewErrorResult(errors.New("server not found")))
|
||||
return
|
||||
}
|
||||
// 先停止再启动
|
||||
server.StopServer(name, serverConf.Port)
|
||||
server.StartServer(name, serverConf.Port)
|
||||
c.Writer.WriteHeader(http.StatusOK)
|
||||
data := "restarted"
|
||||
c.Writer.Write(server.NewSuccessResult(data))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func deleteServer(c *gin.Context) {
|
||||
name := c.Param("name")
|
||||
if name != "" {
|
||||
serverConf := model.GetServerConfig(name)
|
||||
if serverConf == nil {
|
||||
c.Writer.WriteHeader(http.StatusNotFound)
|
||||
c.Writer.Write(server.NewErrorResult(errors.New("server not found")))
|
||||
return
|
||||
}
|
||||
// 先停止服务器
|
||||
server.StopServer(name, serverConf.Port)
|
||||
// 从配置中删除
|
||||
err := model.DeleteServerConfig(name)
|
||||
if err != nil {
|
||||
c.Writer.WriteHeader(http.StatusInternalServerError)
|
||||
c.Writer.Write(server.NewErrorResult(err))
|
||||
return
|
||||
}
|
||||
c.Writer.WriteHeader(http.StatusOK)
|
||||
c.Writer.Write(server.NewSuccessResult("deleted"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// TCP代理相关API
|
||||
|
||||
func listTcpProxy(c *gin.Context) {
|
||||
data := model.GetConfig().TcpProxys
|
||||
c.JSON(http.StatusOK, server.NewResultObj(data))
|
||||
}
|
||||
|
||||
func setTcpProxy(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
ctxData := ctx.Value(server.RequestCtxKey("data")).(map[string]interface{})
|
||||
data, ok := ctxData["data"]
|
||||
if !ok {
|
||||
c.Writer.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
tcpProxy, ok := data.(model.TcpProxyConfig)
|
||||
if !ok {
|
||||
c.Writer.WriteHeader(http.StatusBadRequest)
|
||||
c.Writer.Write(server.NewErrorResult(errors.New("invalid tcp proxy config")))
|
||||
return
|
||||
}
|
||||
er := model.SetTcpProxyConfig(&tcpProxy)
|
||||
if er != nil {
|
||||
c.Writer.WriteHeader(http.StatusBadRequest)
|
||||
c.Writer.Write(server.NewErrorResult(er))
|
||||
return
|
||||
}
|
||||
c.Writer.WriteHeader(http.StatusOK)
|
||||
c.Writer.Write(server.NewSuccessResult(tcpProxy))
|
||||
}
|
||||
|
||||
func getTcpProxy(c *gin.Context) {
|
||||
name := c.Param("name")
|
||||
if name != "" {
|
||||
data := model.GetTcpProxyConfig(name)
|
||||
if data == nil {
|
||||
c.Writer.WriteHeader(http.StatusNotFound)
|
||||
c.Writer.Write(server.NewErrorResult(errors.New("tcp proxy not found")))
|
||||
return
|
||||
}
|
||||
c.Writer.WriteHeader(http.StatusOK)
|
||||
c.Writer.Write(server.NewSuccessResult(data))
|
||||
return
|
||||
}
|
||||
http.NotFound(c.Writer, c.Request)
|
||||
}
|
||||
|
||||
func deleteTcpProxy(c *gin.Context) {
|
||||
name := c.Param("name")
|
||||
if name != "" {
|
||||
tcpProxy := model.GetTcpProxyConfig(name)
|
||||
if tcpProxy == nil {
|
||||
c.Writer.WriteHeader(http.StatusNotFound)
|
||||
c.Writer.Write(server.NewErrorResult(errors.New("tcp proxy not found")))
|
||||
return
|
||||
}
|
||||
// 如果正在运行,先停止
|
||||
if proxy, ok := server.TcpProxyManager[name]; ok {
|
||||
proxy.Stop()
|
||||
delete(server.TcpProxyManager, name)
|
||||
}
|
||||
// 从配置中删除
|
||||
err := model.DeleteTcpProxyConfig(name)
|
||||
if err != nil {
|
||||
c.Writer.WriteHeader(http.StatusInternalServerError)
|
||||
c.Writer.Write(server.NewErrorResult(err))
|
||||
return
|
||||
}
|
||||
c.Writer.WriteHeader(http.StatusOK)
|
||||
c.Writer.Write(server.NewSuccessResult("deleted"))
|
||||
return
|
||||
}
|
||||
http.NotFound(c.Writer, c.Request)
|
||||
}
|
||||
|
||||
func startTcpProxy(c *gin.Context) {
|
||||
name := c.Param("name")
|
||||
if name != "" {
|
||||
tcpProxy := model.GetTcpProxyConfig(name)
|
||||
if tcpProxy == nil {
|
||||
c.Writer.WriteHeader(http.StatusNotFound)
|
||||
c.Writer.Write(server.NewErrorResult(errors.New("tcp proxy not found")))
|
||||
return
|
||||
}
|
||||
if _, ok := server.TcpProxyManager[name]; ok {
|
||||
c.Writer.WriteHeader(http.StatusOK)
|
||||
c.Writer.Write(server.NewSuccessResult("already started"))
|
||||
return
|
||||
}
|
||||
proxy := handler.NewTcpProxyHandler(tcpProxy.Name, tcpProxy.Listen, tcpProxy.Target)
|
||||
server.AddTcpProxy(name, proxy)
|
||||
proxy.Start()
|
||||
c.Writer.WriteHeader(http.StatusOK)
|
||||
c.Writer.Write(server.NewSuccessResult("started"))
|
||||
return
|
||||
}
|
||||
http.NotFound(c.Writer, c.Request)
|
||||
}
|
||||
|
||||
func stopTcpProxy(c *gin.Context) {
|
||||
name := c.Param("name")
|
||||
if name != "" {
|
||||
proxy, ok := server.TcpProxyManager[name]
|
||||
if !ok {
|
||||
c.Writer.WriteHeader(http.StatusNotFound)
|
||||
c.Writer.Write(server.NewErrorResult(errors.New("tcp proxy not found or not running")))
|
||||
return
|
||||
}
|
||||
proxy.Stop()
|
||||
delete(server.TcpProxyManager, name)
|
||||
c.Writer.WriteHeader(http.StatusOK)
|
||||
c.Writer.Write(server.NewSuccessResult("stopped"))
|
||||
return
|
||||
}
|
||||
http.NotFound(c.Writer, c.Request)
|
||||
}
|
||||
|
||||
var AdminServerMux *gin.Engine
|
||||
|
||||
// InitAdminApi 初始化管理API路由
|
||||
|
|
@ -170,6 +331,19 @@ func InitAdminApi(conf *model.HttpServerConfig) {
|
|||
{
|
||||
serverGroup.POST("stop/:name", stop)
|
||||
serverGroup.POST("start/:name", start)
|
||||
serverGroup.POST("restart/:name", restart)
|
||||
serverGroup.DELETE(":name", deleteServer)
|
||||
}
|
||||
|
||||
// TCP代理路由
|
||||
tcpGroup := api.Group("tcp")
|
||||
{
|
||||
tcpGroup.GET("", listTcpProxy)
|
||||
tcpGroup.POST("", setTcpProxy)
|
||||
tcpGroup.GET(":name", getTcpProxy)
|
||||
tcpGroup.DELETE(":name", deleteTcpProxy)
|
||||
tcpGroup.POST("start/:name", startTcpProxy)
|
||||
tcpGroup.POST("stop/:name", stopTcpProxy)
|
||||
}
|
||||
|
||||
api.POST("login", login)
|
||||
|
|
|
|||
|
|
@ -152,20 +152,30 @@ body {
|
|||
|
||||
/* 表格样式 */
|
||||
.el-table {
|
||||
background-color: transparent;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
color: var(--light);
|
||||
}
|
||||
|
||||
.el-table th {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
color: var(--light);
|
||||
border-bottom: 1px solid var(--glass-border);
|
||||
}
|
||||
|
||||
.el-table tr {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.el-table td {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
border-bottom: 1px solid var(--glass-border);
|
||||
}
|
||||
|
||||
.el-table--enable-row-hover .el-table__body tr:hover>td {
|
||||
background-color: rgba(64, 158, 255, 0.15);
|
||||
}
|
||||
|
||||
.el-table__empty-block {
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
|
|
@ -297,3 +307,12 @@ body {
|
|||
box-shadow: 0 10px 40px 0 rgba(31, 38, 135, 0.37);
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
.el-table th.el-table__cell, .el-table--striped .el-table__body tr.el-table__row--striped td.el-table__cell {
|
||||
background-color: transparent;
|
||||
|
||||
}
|
||||
|
||||
.el-table--enable-row-hover .el-table__body tr:hover>td.el-table__cell {
|
||||
background-color: #02270c;
|
||||
}
|
||||
|
|
@ -1,353 +1,264 @@
|
|||
|
||||
<!-- http_server_management_console/frontend/index.html -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>HTTP服务器管理控制台</title>
|
||||
<!-- Element UI CSS -->
|
||||
<title>GoHttp 管理控制台</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/element-ui/2.15.13/theme-chalk/index.min.css">
|
||||
<!-- Vue & Element UI JS -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.14/vue.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/element-ui/2.15.13/index.min.js"></script>
|
||||
<!-- Tailwind CSS -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet">
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
<!-- Google Fonts -->
|
||||
<link href="https://fonts.loli.net/css?family=Roboto+Mono:400,700&display=swap" rel="stylesheet">
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
<style>
|
||||
.particles {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div class="particles" id="particles-js"></div>
|
||||
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<!-- Header -->
|
||||
<el-header class="flex justify-between items-center mb-8">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-server text-3xl mr-3 text-blue-400"></i>
|
||||
<h1 class="text-2xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-blue-400 to-purple-500">
|
||||
HTTP服务器管理控制台
|
||||
GoHttp 管理控制台
|
||||
</h1>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<el-button type="primary" @click="exportConfig">
|
||||
<i class="fas fa-file-export mr-2"></i>导出配置
|
||||
<el-button type="primary" @click="refreshAll" :loading="loading">
|
||||
<i class="fas fa-sync mr-2"></i>刷新
|
||||
</el-button>
|
||||
<el-button type="primary" @click="importConfig">
|
||||
<i class="fas fa-file-import mr-2"></i>导入配置
|
||||
</el-button>
|
||||
<el-dropdown trigger="click" @command="changeTheme">
|
||||
<el-button type="primary" icon="el-icon-brush"></el-button>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item command="theme-dark">暗色主题</el-dropdown-item>
|
||||
<el-dropdown-item command="theme-light">亮色主题</el-dropdown-item>
|
||||
<el-dropdown-item command="theme-tech">科技主题</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</el-header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="16">
|
||||
<!-- Server Config Panel -->
|
||||
<!-- Tabs -->
|
||||
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
|
||||
<!-- 服务器配置 -->
|
||||
<el-tab-pane label="服务器配置" name="servers">
|
||||
<el-card class="glass-card mb-6">
|
||||
<div slot="header" class="flex justify-between items-center">
|
||||
<span class="text-xl font-semibold text-blue-300">
|
||||
<i class="fas fa-cog mr-2"></i>服务器配置
|
||||
<i class="fas fa-cog mr-2"></i>HTTP服务器
|
||||
</span>
|
||||
<el-button type="primary" @click="addSite" size="small">
|
||||
<i class="fas fa-plus mr-1"></i>添加网站
|
||||
<el-button type="primary" @click="showServerDialog()" size="small">
|
||||
<i class="fas fa-plus mr-1"></i>添加服务器
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div v-for="(site, index) in serverConfig.sites" :key="site.id" class="glass-card p-4 mb-4">
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<div>
|
||||
<h3 class="font-medium text-blue-200">{{ site.name }}</h3>
|
||||
<p class="text-xs text-gray-400">{{ site.domain }}:{{ site.port }}</p>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<el-button type="primary" icon="el-icon-edit" circle size="mini" @click="editSite(index)"></el-button>
|
||||
<el-button type="danger" icon="el-icon-delete" circle size="mini" @click="deleteSite(index)"></el-button>
|
||||
<el-button type="info" icon="el-icon-arrow-down" circle size="mini"
|
||||
@click="toggleCollapse(index)"
|
||||
:class="{'toggle-icon': true, 'collapsed': site.collapsed}"></el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-form :model="site" label-width="100px" class="mb-4">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="域名">
|
||||
<el-input v-model="site.domain"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="端口">
|
||||
<el-input-number v-model="site.port" :min="1" :max="65535"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="网站名称">
|
||||
<el-input v-model="site.name"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="启用SSL">
|
||||
<el-switch v-model="site.ssl"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
|
||||
<div class="border-t border-gray-700 pt-3">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<h4 class="text-sm font-medium text-purple-300">
|
||||
<i class="fas fa-route mr-1"></i>URL路由配置
|
||||
</h4>
|
||||
<div>
|
||||
<el-button type="primary" size="mini" @click="addRoute(index)">
|
||||
<i class="fas fa-plus mr-1"></i>添加路由
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="route-list" :class="site.collapsed ? 'collapsed' : 'expanded'">
|
||||
<div v-for="(route, routeIndex) in site.routes" :key="routeIndex" class="glass-card p-3 text-sm mb-2">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<span class="font-medium">{{ route.path }}</span>
|
||||
<span class="text-gray-400 ml-2">→ {{ route.target }}</span>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<el-button type="primary" icon="el-icon-edit" circle size="mini" @click="editRoute(index, routeIndex)"></el-button>
|
||||
<el-button type="danger" icon="el-icon-delete" circle size="mini" @click="deleteRoute(index, routeIndex)"></el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table :data="servers" stripe style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="name" label="名称" width="150"></el-table-column>
|
||||
<el-table-column prop="server" label="域名" width="200">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.server ? scope.row.server.join(', ') : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="port" label="端口" width="100"></el-table-column>
|
||||
<el-table-column label="SSL" width="80">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="scope.row.enable_ssl ? 'success' : 'info'" size="small">
|
||||
{{ scope.row.enable_ssl ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="路径数" width="80">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.paths ? scope.row.paths.length : 0 }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="280">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="small" @click="showServerDialog(scope.row)" class="mr-2">
|
||||
<i class="fas fa-edit mr-1"></i>编辑
|
||||
</el-button>
|
||||
<el-button type="text" size="small" @click="restartServer(scope.row.name)" class="mr-2">
|
||||
<i class="fas fa-redo mr-1"></i>重启
|
||||
</el-button>
|
||||
<el-button type="text" size="small" @click="deleteServer(scope.row.name)" class="text-red-400">
|
||||
<i class="fas fa-trash mr-1"></i>删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- Logger Panel -->
|
||||
<el-card class="glass-card">
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- TCP代理 -->
|
||||
<el-tab-pane label="TCP代理" name="tcp">
|
||||
<el-card class="glass-card mb-6">
|
||||
<div slot="header" class="flex justify-between items-center">
|
||||
<span class="text-xl font-semibold text-blue-300">
|
||||
<i class="fas fa-history mr-2"></i>操作日志
|
||||
<i class="fas fa-exchange-alt mr-2"></i>TCP代理
|
||||
</span>
|
||||
<div>
|
||||
<el-button type="info" size="mini" @click="exportLogs" title="导出日志">
|
||||
<i class="fas fa-download"></i>
|
||||
</el-button>
|
||||
<el-button type="danger" size="mini" @click="clearLogs" title="清空日志">
|
||||
<i class="fas fa-trash"></i>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4 flex items-center space-x-4">
|
||||
<el-select v-model="filterType" placeholder="日志类型" size="small">
|
||||
<el-option
|
||||
v-for="level in logLevels"
|
||||
:key="level.value"
|
||||
:label="level.label"
|
||||
:value="level.value">
|
||||
</el-option>
|
||||
</el-select>
|
||||
<el-input
|
||||
placeholder="搜索日志"
|
||||
prefix-icon="el-icon-search"
|
||||
v-model="searchQuery"
|
||||
size="small">
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 max-h-60 overflow-y-auto">
|
||||
<div v-for="(log, index) in filteredLogs" :key="index" class="text-sm p-2 bg-gray-800/30 rounded">
|
||||
<span class="text-gray-400">[{{ log.timestamp }}]</span>
|
||||
<span :class="[getTypeColor(log.type)]">
|
||||
{{ log.message }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="filteredLogs.length === 0" class="text-center text-gray-400 p-4">
|
||||
无匹配日志记录
|
||||
</div>
|
||||
<el-button type="primary" @click="showTcpDialog()" size="small">
|
||||
<i class="fas fa-plus mr-1"></i>添加代理
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="tcpProxies" stripe style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="name" label="名称" width="150"></el-table-column>
|
||||
<el-table-column prop="listen" label="监听" width="200"></el-table-column>
|
||||
<el-table-column prop="target" label="目标"></el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="isTcpRunning(scope.row.name) ? 'success' : 'info'" size="small">
|
||||
{{ isTcpRunning(scope.row.name) ? '运行中' : '已停止' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="200">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="small" @click="showTcpDialog(scope.row)" class="mr-2">
|
||||
<i class="fas fa-edit mr-1"></i>编辑
|
||||
</el-button>
|
||||
<el-button type="text" size="small" @click="toggleTcpProxy(scope.row)" class="mr-2">
|
||||
<i :class="isTcpRunning(scope.row.name) ? 'fas fa-stop mr-1' : 'fas fa-play mr-1'"></i>
|
||||
{{ isTcpRunning(scope.row.name) ? '停止' : '启动' }}
|
||||
</el-button>
|
||||
<el-button type="text" size="small" @click="deleteTcpProxy(scope.row.name)" class="text-red-400">
|
||||
<i class="fas fa-trash mr-1"></i>删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- Server Status Panel -->
|
||||
<el-col :span="8">
|
||||
<el-card class="glass-card mb-6">
|
||||
<div slot="header" class="text-xl font-semibold text-blue-300">
|
||||
<i class="fas fa-chart-line mr-2"></i>服务器状态
|
||||
</div>
|
||||
|
||||
<el-row :gutter="20" class="mb-6">
|
||||
<el-col :span="12">
|
||||
<el-card class="glass-card text-center">
|
||||
<div class="text-3xl font-bold text-green-400 mb-1">{{ requestCount }}</div>
|
||||
<div class="text-xs text-gray-400">访问量</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card class="glass-card text-center">
|
||||
<div class="text-3xl font-bold text-purple-400 mb-1">{{ goroutineCount }}</div>
|
||||
<div class="text-xs text-gray-400">Goroutines</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<div class="mb-4">
|
||||
<h3 class="text-sm font-medium text-blue-200 mb-2">访问量趋势</h3>
|
||||
<canvas id="request-chart" class="w-full h-32"></canvas>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-blue-200 mb-2">Goroutine变化</h3>
|
||||
<canvas id="goroutine-chart" class="w-full h-32"></canvas>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="glass-card mb-6">
|
||||
<div slot="header" class="text-xl font-semibold text-blue-300">
|
||||
<i class="fas fa-terminal mr-2"></i>快速操作
|
||||
</div>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">
|
||||
<el-button type="primary" class="w-full mb-2" @click="startServer">
|
||||
<i class="fas fa-play mr-1"></i>启动
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-button type="danger" class="w-full mb-2" @click="stopServer">
|
||||
<i class="fas fa-stop mr-1"></i>停止
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-button type="warning" class="w-full" @click="restartServer">
|
||||
<i class="fas fa-redo mr-1"></i>重启
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-button type="info" class="w-full" @click="showLogs">
|
||||
<i class="fas fa-file-alt mr-1"></i>日志
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
|
||||
<el-card class="glass-card">
|
||||
<div slot="header" class="text-xl font-semibold text-blue-300">
|
||||
<i class="fas fa-server mr-2"></i>性能监控
|
||||
</div>
|
||||
<div class="text-center p-8 text-gray-400">
|
||||
<i class="fas fa-tools text-4xl mb-4"></i>
|
||||
<p>性能监控功能正在开发中</p>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 系统状态 -->
|
||||
<el-tab-pane label="系统状态" name="status">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-card class="glass-card text-center">
|
||||
<div class="text-3xl font-bold text-green-400 mb-1">{{ goroutines }}</div>
|
||||
<div class="text-xs text-gray-400">Goroutines</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<!-- Site Dialog -->
|
||||
<el-dialog :title="siteDialog.title" :visible.sync="siteDialog.visible" width="50%">
|
||||
<el-form :model="siteDialog.form" label-width="100px">
|
||||
<el-form-item label="网站名称">
|
||||
<el-input v-model="siteDialog.form.name"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="域名">
|
||||
<el-input v-model="siteDialog.form.domain"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="端口">
|
||||
<el-input-number v-model="siteDialog.form.port" :min="1" :max="65535"></el-input-number>
|
||||
</el-form-item>
|
||||
<el-form-item label="启用SSL">
|
||||
<el-switch v-model="siteDialog.form.ssl"></el-switch>
|
||||
<!-- Server Dialog -->
|
||||
<el-dialog :title="serverDialog.title" :visible.sync="serverDialog.visible" width="70%" @closed="resetServerForm">
|
||||
<el-form :model="serverDialog.form" label-width="120px" ref="serverForm">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="名称" prop="name" :rules="[{ required: true, message: '请输入名称', trigger: 'blur' }]">
|
||||
<el-input v-model="serverDialog.form.name" :disabled="serverDialog.isEdit"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="端口" prop="port" :rules="[{ required: true, message: '请输入端口', trigger: 'blur' }]">
|
||||
<el-input-number v-model="serverDialog.form.port" :min="1" :max="65535"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="域名" prop="server">
|
||||
<el-select v-model="serverDialog.form.server" multiple filterable allow-create placeholder="输入域名后回车" style="width: 100%">
|
||||
<el-option v-for="s in serverDialog.form.server" :key="s" :label="s" :value="s"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="启用SSL">
|
||||
<el-switch v-model="serverDialog.form.enable_ssl"></el-switch>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12" v-if="serverDialog.form.enable_ssl">
|
||||
<el-form-item label="证书文件">
|
||||
<el-input v-model="serverDialog.form.certfile" placeholder="/path/to/cert.pem"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" v-if="serverDialog.form.enable_ssl">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="密钥文件">
|
||||
<el-input v-model="serverDialog.form.keyfile" placeholder="/path/to/key.pem"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- Paths -->
|
||||
<el-divider><i class="fas fa-route mr-2"></i>路由配置</el-divider>
|
||||
|
||||
<div v-for="(path, index) in serverDialog.form.paths" :key="index" class="glass-card p-4 mb-4">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-form-item label="路径">
|
||||
<el-input v-model="path.path" placeholder="/api"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="类型">
|
||||
<el-select v-model="path.type" @change="onPathTypeChange(path)">
|
||||
<el-option label="静态文件" value="static"></el-option>
|
||||
<el-option label="反向代理" value="proxy"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="操作">
|
||||
<el-button type="danger" icon="el-icon-delete" circle @click="removePath(index)"></el-button>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" v-if="path.type === 'static'">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="根目录">
|
||||
<el-input v-model="path.root" placeholder="/var/www/html"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="默认文件">
|
||||
<el-input v-model="path.default" placeholder="index.html"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" v-if="path.type === 'proxy'">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="上游服务器">
|
||||
<el-select v-model="path.upstreams" multiple filterable allow-create placeholder="输入地址后回车" style="width: 100%">
|
||||
<el-option v-for="u in path.upstreams" :key="u" :label="u" :value="u"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<el-button type="primary" plain @click="addPath" class="mb-4">
|
||||
<i class="fas fa-plus mr-1"></i>添加路由
|
||||
</el-button>
|
||||
</el-form>
|
||||
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="siteDialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveSite">确定</el-button>
|
||||
<el-button @click="serverDialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveServer" :loading="serverDialog.saving">保存</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
|
||||
<!-- Route Dialog -->
|
||||
<el-dialog :title="routeDialog.title" :visible.sync="routeDialog.visible" width="50%">
|
||||
<el-form :model="routeDialog.form" label-width="100px">
|
||||
<el-form-item label="路由路径">
|
||||
<el-input v-model="routeDialog.form.path" placeholder="/api"></el-input>
|
||||
<!-- TCP Proxy Dialog -->
|
||||
<el-dialog :title="tcpDialog.title" :visible.sync="tcpDialog.visible" width="500px" @closed="resetTcpForm">
|
||||
<el-form :model="tcpDialog.form" label-width="100px" ref="tcpForm">
|
||||
<el-form-item label="名称" prop="name" :rules="[{ required: true, message: '请输入名称', trigger: 'blur' }]">
|
||||
<el-input v-model="tcpDialog.form.name" :disabled="tcpDialog.isEdit"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="路由类型">
|
||||
<el-radio-group v-model="routeDialog.form.type">
|
||||
<el-radio label="static">静态资源</el-radio>
|
||||
<el-radio label="proxy">反向代理</el-radio>
|
||||
</el-radio-group>
|
||||
<el-form-item label="监听地址" prop="listen" :rules="[{ required: true, message: '请输入监听地址', trigger: 'blur' }]">
|
||||
<el-input v-model="tcpDialog.form.listen" placeholder="0.0.0.0:3306"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="目标地址" v-if="routeDialog.form.type === 'proxy'">
|
||||
<el-input v-model="routeDialog.form.target" placeholder="http://backend:3000"></el-input>
|
||||
<el-form-item label="目标地址" prop="target" :rules="[{ required: true, message: '请输入目标地址', trigger: 'blur' }]">
|
||||
<el-input v-model="tcpDialog.form.target" placeholder="192.168.1.100:3306"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="routeDialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveRoute">确定</el-button>
|
||||
<el-button @click="tcpDialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveTcpProxy" :loading="tcpDialog.saving">保存</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.1/chart.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/particles.js/2.0.0/particles.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.14/vue.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/element-ui/2.15.13/index.min.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
<script src="js/chart.js"></script>
|
||||
<script src="js/logger.js"></script>
|
||||
|
||||
<script>
|
||||
// 初始化粒子效果
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (typeof particlesJS !== 'undefined') {
|
||||
particlesJS('particles-js', {
|
||||
particles: {
|
||||
number: { value: 80, density: { enable: true, value_area: 800 } },
|
||||
color: { value: "#3a86ff" },
|
||||
shape: { type: "circle" },
|
||||
opacity: { value: 0.5, random: true },
|
||||
size: { value: 3, random: true },
|
||||
line_linked: { enable: true, distance: 150, color: "#3a86ff", opacity: 0.4, width: 1 },
|
||||
move: { enable: true, speed: 2, direction: "none", random: true, straight: false, out_mode: "out" }
|
||||
},
|
||||
interactivity: {
|
||||
detect_on: "canvas",
|
||||
events: {
|
||||
onhover: { enable: true, mode: "repulse" },
|
||||
onclick: { enable: true, mode: "push" }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 应用保存的主题
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
if (savedTheme) {
|
||||
document.body.className = savedTheme;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
|
@ -1,535 +1,353 @@
|
|||
|
||||
// http_server_management_console/frontend/js/app.js
|
||||
// GoHttp 管理控制台 - 前端应用
|
||||
new Vue({
|
||||
el: '#app',
|
||||
data() {
|
||||
return {
|
||||
serverConfig: JSON.parse(localStorage.getItem('serverConfig')) || {
|
||||
sites: [
|
||||
{
|
||||
|
||||
name: '示例网站',
|
||||
server: 'example.com',
|
||||
port: 8080,
|
||||
enable_ssl: false,
|
||||
collapsed: false,
|
||||
paths: [
|
||||
{
|
||||
path: '/static',
|
||||
type: 'static',
|
||||
target: '静态资源'
|
||||
},
|
||||
{
|
||||
path: '/api',
|
||||
type: 'proxy',
|
||||
target: 'http://backend:3000'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
logs: JSON.parse(localStorage.getItem('serverLogs')) || [
|
||||
{ timestamp: '2025-05-06 23:13:22', message: '添加了路由配置 /api → http://backend:3000', type: 'success' },
|
||||
{ timestamp: '2025-05-06 23:12:15', message: '修改了网站端口 8080 → 8443', type: 'warning' },
|
||||
{ timestamp: '2025-05-06 23:10:07', message: '创建了网站配置 example.com', type: 'info' }
|
||||
],
|
||||
requestCount: 0,
|
||||
goroutineCount: 10,
|
||||
filterType: 'all',
|
||||
searchQuery: '',
|
||||
logLevels: [
|
||||
{ value: 'all', label: '全部' },
|
||||
{ value: 'info', label: '信息' },
|
||||
{ value: 'success', label: '成功' },
|
||||
{ value: 'warning', label: '警告' },
|
||||
{ value: 'danger', label: '错误' }
|
||||
],
|
||||
siteDialog: {
|
||||
loading: false,
|
||||
activeTab: 'servers',
|
||||
servers: [],
|
||||
tcpProxies: [],
|
||||
runningTcpProxies: [],
|
||||
goroutines: 0,
|
||||
serverDialog: {
|
||||
visible: false,
|
||||
title: '添加网站',
|
||||
form: {
|
||||
id: null,
|
||||
name: '',
|
||||
domain: '',
|
||||
port: 80,
|
||||
ssl: false,
|
||||
collapsed: false
|
||||
},
|
||||
editIndex: null
|
||||
title: '添加服务器',
|
||||
isEdit: false,
|
||||
saving: false,
|
||||
form: this.getDefaultServerForm()
|
||||
},
|
||||
routeDialog: {
|
||||
tcpDialog: {
|
||||
visible: false,
|
||||
title: '添加路由',
|
||||
form: {
|
||||
path: '',
|
||||
type: 'static',
|
||||
target: ''
|
||||
},
|
||||
siteIndex: null,
|
||||
editIndex: null
|
||||
},
|
||||
requestChart: null,
|
||||
goroutineChart: null,
|
||||
chartInterval: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredLogs() {
|
||||
let result = this.logs;
|
||||
|
||||
if (this.filterType !== 'all') {
|
||||
result = result.filter(log => log.type === this.filterType);
|
||||
title: '添加TCP代理',
|
||||
isEdit: false,
|
||||
saving: false,
|
||||
form: this.getDefaultTcpForm()
|
||||
}
|
||||
|
||||
if (this.searchQuery) {
|
||||
const query = this.searchQuery.toLowerCase();
|
||||
result = result.filter(log =>
|
||||
log.message.toLowerCase().includes(query) ||
|
||||
log.timestamp.includes(query)
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.requestCount = Math.floor(Math.random() * 1000) + 500;
|
||||
|
||||
// 模拟数据更新
|
||||
setInterval(() => {
|
||||
this.requestCount += Math.floor(Math.random() * 10);
|
||||
this.goroutineCount = Math.max(5, this.goroutineCount + Math.floor(Math.random() * 5) - 2);
|
||||
}, 3000);
|
||||
|
||||
// 应用已保存的主题
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
if (savedTheme) {
|
||||
document.body.className = savedTheme;
|
||||
}
|
||||
|
||||
// 初始化图表
|
||||
this.initCharts();
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.chartInterval) {
|
||||
clearInterval(this.chartInterval);
|
||||
}
|
||||
this.refreshAll()
|
||||
this.startStatusPolling()
|
||||
},
|
||||
methods: {
|
||||
// 站点配置管理
|
||||
toggleCollapse(index) {
|
||||
this.serverConfig.sites[index].collapsed = !this.serverConfig.sites[index].collapsed;
|
||||
this.saveConfig();
|
||||
// 获取默认服务器表单
|
||||
getDefaultServerForm() {
|
||||
return {
|
||||
name: '',
|
||||
server: [],
|
||||
port: 8080,
|
||||
enable_ssl: false,
|
||||
certfile: '',
|
||||
keyfile: '',
|
||||
paths: []
|
||||
}
|
||||
},
|
||||
saveConfig() {
|
||||
localStorage.setItem('serverConfig', JSON.stringify(this.serverConfig));
|
||||
this.logAction('保存了服务器配置', 'success');
|
||||
|
||||
// 获取默认TCP代理表单
|
||||
getDefaultTcpForm() {
|
||||
return {
|
||||
name: '',
|
||||
listen: '',
|
||||
target: '',
|
||||
enabled: true
|
||||
}
|
||||
},
|
||||
addSite() {
|
||||
this.siteDialog = {
|
||||
visible: true,
|
||||
title: '添加网站',
|
||||
form: {
|
||||
id: Date.now(),
|
||||
name: '新网站',
|
||||
domain: 'new-site.com',
|
||||
port: 80,
|
||||
ssl: false,
|
||||
collapsed: false
|
||||
},
|
||||
editIndex: null
|
||||
};
|
||||
|
||||
// 重置服务器表单
|
||||
resetServerForm() {
|
||||
this.serverDialog.form = this.getDefaultServerForm()
|
||||
this.serverDialog.isEdit = false
|
||||
},
|
||||
editSite(index) {
|
||||
const site = this.serverConfig.sites[index];
|
||||
this.siteDialog = {
|
||||
visible: true,
|
||||
title: '编辑网站',
|
||||
form: JSON.parse(JSON.stringify(site)),
|
||||
editIndex: index
|
||||
};
|
||||
|
||||
// 重置TCP表单
|
||||
resetTcpForm() {
|
||||
this.tcpDialog.form = this.getDefaultTcpForm()
|
||||
this.tcpDialog.isEdit = false
|
||||
},
|
||||
saveSite() {
|
||||
if (this.siteDialog.editIndex !== null) {
|
||||
this.serverConfig.sites.splice(this.siteDialog.editIndex, 1, this.siteDialog.form);
|
||||
this.logAction(`更新了网站 ${this.siteDialog.form.name} 的配置`, 'success');
|
||||
|
||||
// API请求封装
|
||||
async request(method, url, data) {
|
||||
try {
|
||||
const options = {
|
||||
method: method,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}
|
||||
if (data && (method === 'POST' || method === 'PUT' || method === 'DELETE')) {
|
||||
options.body = JSON.stringify({ data })
|
||||
}
|
||||
const response = await fetch(url, options)
|
||||
const result = await response.json()
|
||||
if (result.code !== 200) {
|
||||
this.$message.error(result.error || '操作失败')
|
||||
return null
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('Request error:', error)
|
||||
this.$message.error('网络错误: ' + error.message)
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
// 刷新所有数据
|
||||
async refreshAll() {
|
||||
this.loading = true
|
||||
await Promise.all([
|
||||
this.fetchServers(),
|
||||
this.fetchTcpProxies(),
|
||||
this.fetchStatus()
|
||||
])
|
||||
this.loading = false
|
||||
},
|
||||
|
||||
// 获取服务器列表
|
||||
async fetchServers() {
|
||||
const result = await this.request('GET', '/api/config')
|
||||
if (result) {
|
||||
this.servers = result.data || []
|
||||
}
|
||||
},
|
||||
|
||||
// 获取TCP代理列表
|
||||
async fetchTcpProxies() {
|
||||
const result = await this.request('GET', '/api/tcp')
|
||||
if (result) {
|
||||
this.tcpProxies = result.data || []
|
||||
}
|
||||
},
|
||||
|
||||
// 获取系统状态
|
||||
async fetchStatus() {
|
||||
const result = await this.request('GET', '/api/status')
|
||||
if (result && result.data) {
|
||||
this.goroutines = result.data.goroutines || 0
|
||||
}
|
||||
},
|
||||
|
||||
// 定时获取状态
|
||||
startStatusPolling() {
|
||||
setInterval(() => {
|
||||
if (this.activeTab === 'status') {
|
||||
this.fetchStatus()
|
||||
}
|
||||
}, 5000)
|
||||
},
|
||||
|
||||
// Tab切换
|
||||
handleTabClick(tab) {
|
||||
if (tab.name === 'status') {
|
||||
this.fetchStatus()
|
||||
}
|
||||
},
|
||||
|
||||
// 判断TCP代理是否运行中
|
||||
isTcpRunning(name) {
|
||||
return this.runningTcpProxies.includes(name)
|
||||
},
|
||||
|
||||
// 显示服务器对话框
|
||||
showServerDialog(server) {
|
||||
if (server) {
|
||||
this.serverDialog.title = '编辑服务器'
|
||||
this.serverDialog.isEdit = true
|
||||
// 转换后端数据格式到前端
|
||||
this.serverDialog.form = {
|
||||
name: server.name,
|
||||
server: server.server || [],
|
||||
port: server.port,
|
||||
enable_ssl: server.enable_ssl || false,
|
||||
certfile: server.certfile || '',
|
||||
keyfile: server.keyfile || '',
|
||||
paths: (server.paths || []).map(p => ({
|
||||
path: p.path,
|
||||
type: p.root ? 'static' : (p.upstreams && p.upstreams.length > 0 ? 'proxy' : 'static'),
|
||||
root: p.root || '',
|
||||
default: p.default || '',
|
||||
upstreams: p.upstreams || []
|
||||
}))
|
||||
}
|
||||
} else {
|
||||
this.serverConfig.sites.push({
|
||||
...this.siteDialog.form,
|
||||
routes: []
|
||||
});
|
||||
this.logAction(`添加了网站 ${this.siteDialog.form.name}`, 'success');
|
||||
this.serverDialog.title = '添加服务器'
|
||||
this.serverDialog.isEdit = false
|
||||
this.serverDialog.form = this.getDefaultServerForm()
|
||||
}
|
||||
|
||||
this.saveConfig();
|
||||
this.siteDialog.visible = false;
|
||||
},
|
||||
deleteSite(index) {
|
||||
this.$confirm('确定要删除这个网站吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
const site = this.serverConfig.sites[index];
|
||||
this.serverConfig.sites.splice(index, 1);
|
||||
this.saveConfig();
|
||||
this.logAction(`删除了网站 ${site.name}`, 'warning');
|
||||
this.$message({
|
||||
type: 'success',
|
||||
message: '删除成功!'
|
||||
});
|
||||
}).catch(() => {
|
||||
this.$message({
|
||||
type: 'info',
|
||||
message: '已取消删除'
|
||||
});
|
||||
});
|
||||
this.serverDialog.visible = true
|
||||
},
|
||||
|
||||
//get site
|
||||
getSites() {
|
||||
//visit REST API to get sites, url is /api/config
|
||||
//return this.serverConfig.sites;
|
||||
this.$axios.get('/api/config').then(response => {
|
||||
this.sites = response.data;
|
||||
this.siteIndex = siteIndex;
|
||||
this.getRoutes(siteIndex);
|
||||
});
|
||||
// 添加路由
|
||||
addPath() {
|
||||
this.serverDialog.form.paths.push({
|
||||
path: '',
|
||||
type: 'static',
|
||||
root: '',
|
||||
default: 'index.html',
|
||||
upstreams: []
|
||||
})
|
||||
},
|
||||
|
||||
// 移除路由
|
||||
removePath(index) {
|
||||
this.serverDialog.form.paths.splice(index, 1)
|
||||
},
|
||||
// 路由管理
|
||||
addRoute(siteIndex) {
|
||||
this.routeDialog = {
|
||||
visible: true,
|
||||
title: '添加路由',
|
||||
form: {
|
||||
path: '',
|
||||
type: 'static',
|
||||
target: ''
|
||||
},
|
||||
siteIndex,
|
||||
editIndex: null
|
||||
};
|
||||
},
|
||||
editRoute(siteIndex, routeIndex) {
|
||||
const route = this.serverConfig.sites[siteIndex].routes[routeIndex];
|
||||
this.routeDialog = {
|
||||
visible: true,
|
||||
title: '编辑路由',
|
||||
form: JSON.parse(JSON.stringify(route)),
|
||||
siteIndex,
|
||||
editIndex: routeIndex
|
||||
};
|
||||
},
|
||||
saveRoute() {
|
||||
const site = this.serverConfig.sites[this.routeDialog.siteIndex];
|
||||
|
||||
if (this.routeDialog.editIndex !== null) {
|
||||
site.routes.splice(this.routeDialog.editIndex, 1, this.routeDialog.form);
|
||||
this.logAction(`更新了路由 ${this.routeDialog.form.path}`, 'success');
|
||||
|
||||
// 路由类型变更
|
||||
onPathTypeChange(path) {
|
||||
if (path.type === 'static') {
|
||||
path.upstreams = []
|
||||
} else {
|
||||
site.routes.push(this.routeDialog.form);
|
||||
this.logAction(`添加了路由 ${this.routeDialog.form.path}`, 'success');
|
||||
path.root = ''
|
||||
path.default = ''
|
||||
}
|
||||
|
||||
this.saveConfig();
|
||||
this.routeDialog.visible = false;
|
||||
},
|
||||
deleteRoute(siteIndex, routeIndex) {
|
||||
this.$confirm('确定要删除这个路由吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
const route = this.serverConfig.sites[siteIndex].routes[routeIndex];
|
||||
this.serverConfig.sites[siteIndex].routes.splice(routeIndex, 1);
|
||||
this.saveConfig();
|
||||
this.logAction(`删除了路由 ${route.path}`, 'warning');
|
||||
this.$message({
|
||||
type: 'success',
|
||||
message: '删除成功!'
|
||||
});
|
||||
}).catch(() => {
|
||||
this.$message({
|
||||
type: 'info',
|
||||
message: '已取消删除'
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// 配置导入导出
|
||||
exportConfig() {
|
||||
const dataStr = JSON.stringify(this.serverConfig, null, 2);
|
||||
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
|
||||
|
||||
const exportFileDefaultName = `server-config-${new Date().toISOString().slice(0,10)}.json`;
|
||||
|
||||
const linkElement = document.createElement('a');
|
||||
linkElement.setAttribute('href', dataUri);
|
||||
linkElement.setAttribute('download', exportFileDefaultName);
|
||||
linkElement.click();
|
||||
|
||||
this.logAction('导出了服务器配置', 'success');
|
||||
},
|
||||
importConfig() {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.json';
|
||||
|
||||
input.onchange = e => {
|
||||
const file = e.target.files[0];
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = event => {
|
||||
try {
|
||||
const importedConfig = JSON.parse(event.target.result);
|
||||
this.$confirm('确定要导入此配置吗?当前配置将被覆盖。', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.serverConfig = importedConfig;
|
||||
this.saveConfig();
|
||||
this.logAction('导入了服务器配置', 'success');
|
||||
this.$message({
|
||||
type: 'success',
|
||||
message: '导入成功!'
|
||||
});
|
||||
}).catch(() => {
|
||||
this.$message({
|
||||
type: 'info',
|
||||
message: '已取消导入'
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
this.$message.error('配置文件格式错误: ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
reader.readAsText(file);
|
||||
};
|
||||
|
||||
input.click();
|
||||
},
|
||||
|
||||
// 日志管理
|
||||
logAction(message, type = 'info') {
|
||||
const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19);
|
||||
this.logs.unshift({ timestamp, message, type });
|
||||
|
||||
if (this.logs.length > 100) {
|
||||
this.logs = this.logs.slice(0, 100);
|
||||
// 保存服务器
|
||||
async saveServer() {
|
||||
const form = this.serverDialog.form
|
||||
if (!form.name || !form.port) {
|
||||
this.$message.warning('请填写必填项')
|
||||
return
|
||||
}
|
||||
|
||||
localStorage.setItem('serverLogs', JSON.stringify(this.logs));
|
||||
},
|
||||
clearLogs() {
|
||||
this.$confirm('确定要清空所有操作日志吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.logs = [];
|
||||
localStorage.setItem('serverLogs', JSON.stringify(this.logs));
|
||||
this.logAction('清空了所有操作日志', 'warning');
|
||||
this.$message.success('日志已清空');
|
||||
}).catch(() => {
|
||||
this.$message.info('已取消清空操作');
|
||||
});
|
||||
},
|
||||
exportLogs() {
|
||||
const dataStr = JSON.stringify(this.logs, null, 2);
|
||||
const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
|
||||
|
||||
const exportFileDefaultName = `server-logs-${new Date().toISOString().slice(0,10)}.json`;
|
||||
|
||||
const linkElement = document.createElement('a');
|
||||
linkElement.setAttribute('href', dataUri);
|
||||
linkElement.setAttribute('download', exportFileDefaultName);
|
||||
linkElement.click();
|
||||
|
||||
this.logAction('导出了操作日志', 'info');
|
||||
},
|
||||
getTypeColor(type) {
|
||||
const colors = {
|
||||
info: 'text-blue-400',
|
||||
success: 'text-green-400',
|
||||
warning: 'text-yellow-400',
|
||||
danger: 'text-red-400'
|
||||
};
|
||||
return colors[type] || 'text-gray-400';
|
||||
},
|
||||
|
||||
// 主题管理
|
||||
changeTheme(theme) {
|
||||
document.body.className = theme;
|
||||
localStorage.setItem('theme', theme);
|
||||
|
||||
// 更新图表颜色以匹配主题
|
||||
this.updateChartColors(theme);
|
||||
|
||||
let themeName = '未知';
|
||||
switch (theme) {
|
||||
case 'theme-dark':
|
||||
themeName = '暗色';
|
||||
break;
|
||||
case 'theme-light':
|
||||
themeName = '亮色';
|
||||
break;
|
||||
case 'theme-tech':
|
||||
themeName = '科技';
|
||||
break;
|
||||
// 转换前端数据格式到后端
|
||||
const serverConfig = {
|
||||
name: form.name,
|
||||
server: form.server,
|
||||
port: form.port,
|
||||
enable_ssl: form.enable_ssl,
|
||||
certfile: form.certfile,
|
||||
keyfile: form.keyfile,
|
||||
paths: form.paths.map(p => ({
|
||||
path: p.path,
|
||||
root: p.type === 'static' ? p.root : '',
|
||||
default: p.type === 'static' ? p.default : '',
|
||||
upstreams: p.type === 'proxy' ? p.upstreams : [],
|
||||
directives: p.directives || []
|
||||
}))
|
||||
}
|
||||
|
||||
this.serverDialog.saving = true
|
||||
const result = await this.request('POST', '/api/config', serverConfig)
|
||||
this.serverDialog.saving = false
|
||||
|
||||
if (result) {
|
||||
this.$message.success('保存成功')
|
||||
this.serverDialog.visible = false
|
||||
await this.fetchServers()
|
||||
}
|
||||
|
||||
this.logAction(`切换为${themeName}主题`, 'info');
|
||||
},
|
||||
|
||||
// 服务器控制
|
||||
startServer() {
|
||||
this.logAction('启动了HTTP服务器', 'success');
|
||||
this.$message.success('服务器已启动');
|
||||
},
|
||||
stopServer() {
|
||||
this.logAction('停止了HTTP服务器', 'warning');
|
||||
this.$message.warning('服务器已停止');
|
||||
},
|
||||
restartServer() {
|
||||
this.logAction('重启了HTTP服务器', 'info');
|
||||
this.$message.info('服务器正在重启');
|
||||
},
|
||||
showLogs() {
|
||||
this.$message.info('正在显示日志');
|
||||
// 重启服务器
|
||||
async restartServer(name) {
|
||||
try {
|
||||
const result = await this.request('POST', `/api/server/restart/${name}`)
|
||||
if (result) {
|
||||
this.$message.success(`服务器 ${name} 已重启`)
|
||||
}
|
||||
} catch (e) {
|
||||
this.$message.error('重启失败')
|
||||
}
|
||||
},
|
||||
|
||||
// 图表功能
|
||||
initCharts() {
|
||||
// 初始化请求图表
|
||||
const requestCtx = document.getElementById('request-chart').getContext('2d');
|
||||
this.requestChart = new Chart(requestCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: Array.from({length: 12}, (_, i) => `${i*5}秒`),
|
||||
datasets: [{
|
||||
label: '访问量',
|
||||
data: Array.from({length: 12}, () => Math.floor(Math.random() * 100) + 50),
|
||||
borderColor: 'rgba(58, 134, 255, 0.8)',
|
||||
backgroundColor: 'rgba(58, 134, 255, 0.1)',
|
||||
borderWidth: 2,
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
pointRadius: 0
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
display: false,
|
||||
grid: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
y: {
|
||||
display: false,
|
||||
grid: {
|
||||
display: false
|
||||
}
|
||||
// 删除服务器
|
||||
async deleteServer(name) {
|
||||
try {
|
||||
await this.$confirm(`确定要删除服务器 "${name}" 吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
const result = await this.request('DELETE', `/api/server/${name}`)
|
||||
if (result) {
|
||||
this.$message.success('删除成功')
|
||||
await this.fetchServers()
|
||||
}
|
||||
} catch (e) {
|
||||
if (e !== 'cancel') {
|
||||
this.$message.error('删除失败')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 显示TCP代理对话框
|
||||
showTcpDialog(tcp) {
|
||||
if (tcp) {
|
||||
this.tcpDialog.title = '编辑TCP代理'
|
||||
this.tcpDialog.isEdit = true
|
||||
this.tcpDialog.form = {
|
||||
name: tcp.name,
|
||||
listen: tcp.listen,
|
||||
target: tcp.target,
|
||||
enabled: tcp.enabled !== false
|
||||
}
|
||||
} else {
|
||||
this.tcpDialog.title = '添加TCP代理'
|
||||
this.tcpDialog.isEdit = false
|
||||
this.tcpDialog.form = this.getDefaultTcpForm()
|
||||
}
|
||||
this.tcpDialog.visible = true
|
||||
},
|
||||
|
||||
// 保存TCP代理
|
||||
async saveTcpProxy() {
|
||||
const form = this.tcpDialog.form
|
||||
if (!form.name || !form.listen || !form.target) {
|
||||
this.$message.warning('请填写必填项')
|
||||
return
|
||||
}
|
||||
|
||||
this.tcpDialog.saving = true
|
||||
const result = await this.request('POST', '/api/tcp', form)
|
||||
this.tcpDialog.saving = false
|
||||
|
||||
if (result) {
|
||||
this.$message.success('保存成功')
|
||||
this.tcpDialog.visible = false
|
||||
await this.fetchTcpProxies()
|
||||
}
|
||||
},
|
||||
|
||||
// 切换TCP代理状态
|
||||
async toggleTcpProxy(tcp) {
|
||||
const action = this.isTcpRunning(tcp.name) ? 'stop' : 'start'
|
||||
try {
|
||||
const result = await this.request('POST', `/api/tcp/${action}/${tcp.name}`)
|
||||
if (result) {
|
||||
this.$message.success(`TCP代理已${action === 'stop' ? '停止' : '启动'}`)
|
||||
// 更新运行状态
|
||||
if (action === 'start') {
|
||||
if (!this.runningTcpProxies.includes(tcp.name)) {
|
||||
this.runningTcpProxies.push(tcp.name)
|
||||
}
|
||||
} else {
|
||||
this.runningTcpProxies = this.runningTcpProxies.filter(n => n !== tcp.name)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化Goroutine图表
|
||||
const goroutineCtx = document.getElementById('goroutine-chart').getContext('2d');
|
||||
this.goroutineChart = new Chart(goroutineCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: Array.from({length: 12}, (_, i) => `${i*5}秒`),
|
||||
datasets: [{
|
||||
label: 'Goroutines',
|
||||
data: Array.from({length: 12}, () => Math.floor(Math.random() * 20) + 10),
|
||||
borderColor: 'rgba(131, 56, 236, 0.8)',
|
||||
backgroundColor: 'rgba(131, 56, 236, 0.1)',
|
||||
borderWidth: 2,
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
pointRadius: 0
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
display: false,
|
||||
grid: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
y: {
|
||||
display: false,
|
||||
grid: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 模拟实时数据更新
|
||||
this.chartInterval = setInterval(() => {
|
||||
// 更新请求图表数据
|
||||
const requestData = this.requestChart.data.datasets[0].data;
|
||||
requestData.shift();
|
||||
requestData.push(Math.floor(Math.random() * 30) + requestData[requestData.length - 1] - 15);
|
||||
this.requestChart.update();
|
||||
|
||||
// 更新Goroutine图表数据
|
||||
const goroutineData = this.goroutineChart.data.datasets[0].data;
|
||||
goroutineData.shift();
|
||||
goroutineData.push(Math.floor(Math.random() * 5) + goroutineData[goroutineData.length - 1] - 2);
|
||||
this.goroutineChart.update();
|
||||
}, 5000);
|
||||
} catch (e) {
|
||||
this.$message.error('操作失败')
|
||||
}
|
||||
},
|
||||
updateChartColors(theme) {
|
||||
let requestColor, goroutineColor;
|
||||
|
||||
switch(theme) {
|
||||
case 'theme-tech':
|
||||
requestColor = 'rgba(0, 247, 255, 0.8)';
|
||||
goroutineColor = 'rgba(0, 200, 255, 0.8)';
|
||||
break;
|
||||
case 'theme-light':
|
||||
requestColor = 'rgba(0, 102, 255, 0.8)';
|
||||
goroutineColor = 'rgba(98, 0, 238, 0.8)';
|
||||
break;
|
||||
default: // dark theme
|
||||
requestColor = 'rgba(58, 134, 255, 0.8)';
|
||||
goroutineColor = 'rgba(131, 56, 236, 0.8)';
|
||||
}
|
||||
|
||||
if (this.requestChart) {
|
||||
this.requestChart.data.datasets[0].borderColor = requestColor;
|
||||
this.requestChart.data.datasets[0].backgroundColor = requestColor.replace('0.8', '0.1');
|
||||
this.requestChart.update();
|
||||
}
|
||||
// 删除TCP代理
|
||||
async deleteTcpProxy(name) {
|
||||
try {
|
||||
await this.$confirm(`确定要删除TCP代理 "${name}" 吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
if (this.goroutineChart) {
|
||||
this.goroutineChart.data.datasets[0].borderColor = goroutineColor;
|
||||
this.goroutineChart.data.datasets[0].backgroundColor = goroutineColor.replace('0.8', '0.1');
|
||||
this.goroutineChart.update();
|
||||
const result = await this.request('DELETE', `/api/tcp/${name}`)
|
||||
if (result) {
|
||||
this.$message.success('删除成功')
|
||||
this.runningTcpProxies = this.runningTcpProxies.filter(n => n !== name)
|
||||
await this.fetchTcpProxies()
|
||||
}
|
||||
} catch (e) {
|
||||
if (e !== 'cancel') {
|
||||
this.$message.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
12
config.json
12
config.json
|
|
@ -21,6 +21,18 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"admin":{
|
||||
"name":"admin",
|
||||
"server":["localhost"],
|
||||
"port":8080,
|
||||
"paths":[
|
||||
{
|
||||
"path":"/",
|
||||
"root":"./adminui",
|
||||
"default":"index.html"
|
||||
}
|
||||
]
|
||||
},
|
||||
"includs":"./include",
|
||||
"servers": [
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package model
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"git.kingecg.top/kingecg/gohttpd/utils"
|
||||
|
|
@ -149,3 +150,56 @@ func GetServerConfig(name string) *HttpServerConfig {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteServerConfig(name string) error {
|
||||
l := gologger.GetLogger("model")
|
||||
for i, s := range Config.Servers {
|
||||
if s.Name == name {
|
||||
Config.Servers = append(Config.Servers[:i], Config.Servers[i+1:]...)
|
||||
configFile := utils.NormalizePath("./config.json")
|
||||
configsStr, err := json.Marshal(Config)
|
||||
if err != nil {
|
||||
l.Error("Save config error:", err)
|
||||
return err
|
||||
}
|
||||
werr := os.WriteFile(configFile, configsStr, 0644)
|
||||
if werr != nil {
|
||||
l.Error("Save config error:", werr)
|
||||
return werr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.New("server not found")
|
||||
}
|
||||
|
||||
func GetTcpProxyConfig(name string) *TcpProxyConfig {
|
||||
for _, s := range Config.TcpProxys {
|
||||
if s.Name == name {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteTcpProxyConfig(name string) error {
|
||||
l := gologger.GetLogger("model")
|
||||
for i, s := range Config.TcpProxys {
|
||||
if s.Name == name {
|
||||
Config.TcpProxys = append(Config.TcpProxys[:i], Config.TcpProxys[i+1:]...)
|
||||
configFile := utils.NormalizePath("./config.json")
|
||||
configsStr, err := json.Marshal(Config)
|
||||
if err != nil {
|
||||
l.Error("Save config error:", err)
|
||||
return err
|
||||
}
|
||||
werr := os.WriteFile(configFile, configsStr, 0644)
|
||||
if werr != nil {
|
||||
l.Error("Save config error:", werr)
|
||||
return werr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.New("tcp proxy not found")
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue