mirror of
https://github.com/lzh-1625/go_process_manager.git
synced 2025-10-05 07:56:50 +08:00
update
This commit is contained in:
@@ -4,34 +4,33 @@ var CF = new(configuration)
|
|||||||
|
|
||||||
// 只支持 float64、int、int64、bool、string类型
|
// 只支持 float64、int、int64、bool、string类型
|
||||||
type configuration struct {
|
type configuration struct {
|
||||||
LogLevel string `default:"debug" describe:"日志等级[debug,info]"`
|
LogLevel string `default:"debug" describe:"日志等级[debug,info]"`
|
||||||
Listen string `default:":8797" describe:"监听端口"`
|
Listen string `default:":8797" describe:"监听端口"`
|
||||||
StorgeType string `default:"sqlite" describe:"存储引擎[sqlite、es、bleve]"`
|
StorgeType string `default:"sqlite" describe:"存储引擎[sqlite、es、bleve]"`
|
||||||
EsUrl string `default:"" describe:"Elasticsearch url"`
|
EsUrl string `default:"" describe:"Elasticsearch url"`
|
||||||
EsIndex string `default:"server_log_v1" describe:"Elasticsearch index"`
|
EsIndex string `default:"server_log_v1" describe:"Elasticsearch index"`
|
||||||
EsUsername string `default:"" describe:"Elasticsearch用户名"`
|
EsUsername string `default:"" describe:"Elasticsearch用户名"`
|
||||||
EsPassword string `default:"" describe:"Elasticsearch密码"`
|
EsPassword string `default:"" describe:"Elasticsearch密码"`
|
||||||
EsWindowLimit bool `default:"true" describe:"Es分页10000条限制"`
|
EsWindowLimit bool `default:"true" describe:"Es分页10000条限制"`
|
||||||
FileSizeLimit float64 `default:"10.0" describe:"文件大小限制(MB)"`
|
FileSizeLimit float64 `default:"10.0" describe:"文件大小限制(MB)"`
|
||||||
ProcessInputPrefix string `default:">" describe:"进程输入前缀"`
|
ProcessInputPrefix string `default:">" describe:"进程输入前缀"`
|
||||||
ProcessRestartsLimit int `default:"2" describe:"进程重启次数限制"`
|
ProcessRestartsLimit int `default:"2" describe:"进程重启次数限制"`
|
||||||
ProcessMsgCacheLinesLimit int `default:"50" describe:"std进程缓存消息行数"`
|
ProcessMsgCacheLinesLimit int `default:"50" describe:"std进程缓存消息行数"`
|
||||||
ProcessMsgCacheBufLimit int `default:"4096" describe:"pty进程缓存消息字节长度"`
|
ProcessMsgCacheBufLimit int `default:"4096" describe:"pty进程缓存消息字节长度"`
|
||||||
ProcessExpireTime int64 `default:"60" describe:"进程控制权过期时间(秒)"`
|
ProcessExpireTime int64 `default:"60" describe:"进程控制权过期时间(秒)"`
|
||||||
PerformanceInfoListLength int `default:"30" describe:"性能信息存储长度"`
|
PerformanceInfoListLength int `default:"30" describe:"性能信息存储长度"`
|
||||||
PerformanceInfoInterval int `default:"60" describe:"监控获取间隔时间(秒)"`
|
PerformanceInfoInterval int `default:"60" describe:"监控获取间隔时间(秒)"`
|
||||||
TerminalConnectTimeout int `default:"10" describe:"终端连接超时时间(分钟)"`
|
TerminalConnectTimeout int `default:"10" describe:"终端连接超时时间(分钟)"`
|
||||||
UserPassWordMinLength int `default:"4" describe:"用户密码最小长度"`
|
UserPassWordMinLength int `default:"4" describe:"用户密码最小长度"`
|
||||||
LogMinLenth int `default:"0" describe:"过滤日志最小长度"`
|
LogMinLenth int `default:"0" describe:"过滤日志最小长度"`
|
||||||
LogHandlerPoolSize int `default:"10" describe:"日志处理并行数"`
|
LogHandlerPoolSize int `default:"10" describe:"日志处理并行数"`
|
||||||
PprofEnable bool `default:"true" describe:"启用pprof分析工具"`
|
PprofEnable bool `default:"true" describe:"启用pprof分析工具"`
|
||||||
KillWaitTime int `default:"5" describe:"kill信号等待时间(秒)"`
|
KillWaitTime int `default:"5" describe:"kill信号等待时间(秒)"`
|
||||||
TaskTimeout int `default:"60" describe:"任务执行超时时间(秒)"`
|
TaskTimeout int `default:"60" describe:"任务执行超时时间(秒)"`
|
||||||
TokenExpirationTime int64 `default:"720" describe:"token过期时间(小时)"`
|
TokenExpirationTime int64 `default:"720" describe:"token过期时间(小时)"`
|
||||||
WsHealthCheckInterval int `default:"3" describe:"ws主动健康检查间隔(秒)"`
|
WsHealthCheckInterval int `default:"3" describe:"ws主动健康检查间隔(秒)"`
|
||||||
CgroupPeriod int64 `default:"100000" describe:"CgroupPeriod"`
|
CgroupPeriod int64 `default:"100000" describe:"CgroupPeriod"`
|
||||||
CgroupSwapLimit bool `default:"false" describe:"cgroup swap限制"`
|
CgroupSwapLimit bool `default:"false" describe:"cgroup swap限制"`
|
||||||
CondWaitTime int `default:"30" describe:"长轮询等待时间(秒)"`
|
CondWaitTime int `default:"30" describe:"长轮询等待时间(秒)"`
|
||||||
PerformanceCapacityDisplay bool `default:"false" describe:"性能资源容量显示"`
|
Tui bool `default:"-"`
|
||||||
Tui bool `default:"-"`
|
|
||||||
}
|
}
|
||||||
|
@@ -45,12 +45,18 @@ func (p *procApi) DeleteNewProcess(ctx *gin.Context, req struct {
|
|||||||
func (p *procApi) KillProcess(ctx *gin.Context, req struct {
|
func (p *procApi) KillProcess(ctx *gin.Context, req struct {
|
||||||
Uuid int `form:"uuid" binding:"required"`
|
Uuid int `form:"uuid" binding:"required"`
|
||||||
}) (err error) {
|
}) (err error) {
|
||||||
|
if !hasOprPermission(ctx, req.Uuid, eum.OperationStop) {
|
||||||
|
return errors.New("not permission")
|
||||||
|
}
|
||||||
return logic.ProcessCtlLogic.KillProcess(req.Uuid)
|
return logic.ProcessCtlLogic.KillProcess(req.Uuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *procApi) StartProcess(ctx *gin.Context, req struct {
|
func (p *procApi) StartProcess(ctx *gin.Context, req struct {
|
||||||
Uuid int `form:"uuid" binding:"required"`
|
Uuid int `json:"uuid" binding:"required"`
|
||||||
}) (err error) {
|
}) (err error) {
|
||||||
|
if !hasOprPermission(ctx, req.Uuid, eum.OperationStart) {
|
||||||
|
return errors.New("not permission")
|
||||||
|
}
|
||||||
prod, err := logic.ProcessCtlLogic.GetProcess(req.Uuid)
|
prod, err := logic.ProcessCtlLogic.GetProcess(req.Uuid)
|
||||||
if err != nil { // 进程不存在则创建
|
if err != nil { // 进程不存在则创建
|
||||||
proConfig, err := repository.ProcessRepository.GetProcessConfigById(req.Uuid)
|
proConfig, err := repository.ProcessRepository.GetProcessConfigById(req.Uuid)
|
||||||
|
@@ -3,6 +3,7 @@ package api
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -46,9 +47,15 @@ func (w *WsConnetInstance) Cancel() {
|
|||||||
var upgrader = websocket.Upgrader{
|
var upgrader = websocket.Upgrader{
|
||||||
ReadBufferSize: 1024,
|
ReadBufferSize: 1024,
|
||||||
WriteBufferSize: 1024,
|
WriteBufferSize: 1024,
|
||||||
|
CheckOrigin: func(r *http.Request) bool {
|
||||||
|
return true // 允许所有跨域请求
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *wsApi) WebsocketHandle(ctx *gin.Context, req model.WebsocketHandleReq) (err error) {
|
func (w *wsApi) WebsocketHandle(ctx *gin.Context, req model.WebsocketHandleReq) (err error) {
|
||||||
|
if !hasOprPermission(ctx, req.Uuid, eum.OperationTerminal) {
|
||||||
|
return errors.New("not permission")
|
||||||
|
}
|
||||||
reqUser := getUserName(ctx)
|
reqUser := getUserName(ctx)
|
||||||
proc, err := logic.ProcessCtlLogic.GetProcess(req.Uuid)
|
proc, err := logic.ProcessCtlLogic.GetProcess(req.Uuid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@@ -7,7 +7,6 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/google/shlex"
|
"github.com/google/shlex"
|
||||||
"github.com/lzh-1625/go_process_manager/config"
|
|
||||||
"github.com/lzh-1625/go_process_manager/internal/app/eum"
|
"github.com/lzh-1625/go_process_manager/internal/app/eum"
|
||||||
"github.com/lzh-1625/go_process_manager/internal/app/model"
|
"github.com/lzh-1625/go_process_manager/internal/app/model"
|
||||||
"github.com/lzh-1625/go_process_manager/internal/app/repository"
|
"github.com/lzh-1625/go_process_manager/internal/app/repository"
|
||||||
@@ -116,10 +115,8 @@ func (p *processCtlLogic) getProcessInfoList(processConfiglist []model.Process)
|
|||||||
pi.User = process.GetUserString()
|
pi.User = process.GetUserString()
|
||||||
pi.Usage.Cpu = process.performanceStatus.cpu
|
pi.Usage.Cpu = process.performanceStatus.cpu
|
||||||
pi.Usage.Mem = process.performanceStatus.mem
|
pi.Usage.Mem = process.performanceStatus.mem
|
||||||
if config.CF.PerformanceCapacityDisplay {
|
pi.Usage.CpuCapacity = float64(runtime.NumCPU()) * 100.0
|
||||||
pi.Usage.CpuCapacity = float64(runtime.NumCPU()) * 100.0
|
pi.Usage.MemCapacity = float64(utils.UnwarpIgnore(mem.VirtualMemory()).Total >> 10)
|
||||||
pi.Usage.MemCapacity = float64(utils.UnwarpIgnore(mem.VirtualMemory()).Total >> 10)
|
|
||||||
}
|
|
||||||
pi.Usage.Time = process.performanceStatus.time
|
pi.Usage.Time = process.performanceStatus.time
|
||||||
pi.TermType = process.Type()
|
pi.TermType = process.Type()
|
||||||
pi.CgroupEnable = process.Config.cgroupEnable
|
pi.CgroupEnable = process.Config.cgroupEnable
|
||||||
|
@@ -1,11 +1,7 @@
|
|||||||
package middle
|
package middle
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/lzh-1625/go_process_manager/internal/app/eum"
|
"github.com/lzh-1625/go_process_manager/internal/app/eum"
|
||||||
"github.com/lzh-1625/go_process_manager/internal/app/repository"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@@ -20,24 +16,3 @@ func RolePermission(needPermission eum.Role) func(ctx *gin.Context) {
|
|||||||
ctx.Next()
|
ctx.Next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func OprPermission(op eum.OprPermission) func(ctx *gin.Context) {
|
|
||||||
return func(ctx *gin.Context) {
|
|
||||||
uuid, err := strconv.Atoi(ctx.Query("uuid"))
|
|
||||||
if err != nil {
|
|
||||||
rErr(ctx, -1, "Invalid parameters!", nil)
|
|
||||||
ctx.Abort()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if v, ok := ctx.Get(eum.CtxRole); !ok || v.(eum.Role) <= eum.RoleAdmin {
|
|
||||||
ctx.Next()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !reflect.ValueOf(repository.PermissionRepository.GetPermission(ctx.GetString(eum.CtxUserName), uuid)).FieldByName(string(op)).Bool() {
|
|
||||||
rErr(ctx, -1, "Insufficient permissions; please check your access rights!", nil)
|
|
||||||
ctx.Abort()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx.Next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@@ -43,7 +43,7 @@ func (p *waitCond) Trigger() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *waitCond) WaitGetMiddel(c *gin.Context) {
|
func (p *waitCond) WaitGetMiddel(c *gin.Context) {
|
||||||
reqUser := c.GetHeader("token")
|
reqUser := c.GetHeader("Uuid")
|
||||||
defer p.timeMap.Store(reqUser, p.ts)
|
defer p.timeMap.Store(reqUser, p.ts)
|
||||||
if ts, ok := p.timeMap.Load(reqUser); !ok || ts.(int64) > p.ts {
|
if ts, ok := p.timeMap.Load(reqUser); !ok || ts.(int64) > p.ts {
|
||||||
c.Next()
|
c.Next()
|
||||||
|
@@ -9,7 +9,7 @@ type Process struct {
|
|||||||
Cwd string `gorm:"column:cwd" json:"cwd"`
|
Cwd string `gorm:"column:cwd" json:"cwd"`
|
||||||
AutoRestart bool `gorm:"column:auto_restart" json:"autoRestart"`
|
AutoRestart bool `gorm:"column:auto_restart" json:"autoRestart"`
|
||||||
CompulsoryRestart bool `gorm:"column:compulsory_restart" json:"compulsoryRestart"`
|
CompulsoryRestart bool `gorm:"column:compulsory_restart" json:"compulsoryRestart"`
|
||||||
PushIds string `gorm:"column:push_ids" json:"pushIds"`
|
PushIds string `gorm:"column:push_ids;type:json" json:"pushIds"`
|
||||||
LogReport bool `gorm:"column:log_report" json:"logReport"`
|
LogReport bool `gorm:"column:log_report" json:"logReport"`
|
||||||
TermType eum.TerminalType `gorm:"column:term_type" json:"termType"`
|
TermType eum.TerminalType `gorm:"column:term_type" json:"termType"`
|
||||||
CgroupEnable bool `gorm:"column:cgroup_enable" json:"cgroupEnable"`
|
CgroupEnable bool `gorm:"column:cgroup_enable" json:"cgroupEnable"`
|
||||||
|
@@ -58,16 +58,16 @@ func routePathInit(r *gin.Engine) {
|
|||||||
{
|
{
|
||||||
wsGroup := apiGroup.Group("/ws")
|
wsGroup := apiGroup.Group("/ws")
|
||||||
{
|
{
|
||||||
wsGroup.GET("", middle.OprPermission(eum.OperationTerminal), bind(api.WsApi.WebsocketHandle, Query))
|
wsGroup.GET("", bind(api.WsApi.WebsocketHandle, Query))
|
||||||
wsGroup.GET("/share", bind(api.WsApi.WebsocketShareHandle, Query))
|
wsGroup.GET("/share", bind(api.WsApi.WebsocketShareHandle, Query))
|
||||||
}
|
}
|
||||||
|
|
||||||
processGroup := apiGroup.Group("/process")
|
processGroup := apiGroup.Group("/process")
|
||||||
{
|
{
|
||||||
processGroup.DELETE("", middle.OprPermission(eum.OperationStop), bind(api.ProcApi.KillProcess, Query))
|
processGroup.DELETE("", bind(api.ProcApi.KillProcess, Query))
|
||||||
processGroup.GET("", bind(api.ProcApi.GetProcessList, None))
|
processGroup.GET("", bind(api.ProcApi.GetProcessList, None))
|
||||||
processGroup.GET("/wait", middle.ProcessWaitCond.WaitGetMiddel, bind(api.ProcApi.GetProcessList, None))
|
processGroup.GET("/wait", middle.ProcessWaitCond.WaitGetMiddel, bind(api.ProcApi.GetProcessList, None))
|
||||||
processGroup.PUT("", middle.OprPermission(eum.OperationStart), bind(api.ProcApi.StartProcess, Query))
|
processGroup.PUT("", bind(api.ProcApi.StartProcess, Body))
|
||||||
processGroup.PUT("/all", bind(api.ProcApi.StartAllProcess, None))
|
processGroup.PUT("/all", bind(api.ProcApi.StartAllProcess, None))
|
||||||
processGroup.DELETE("/all", bind(api.ProcApi.KillAllProcess, None))
|
processGroup.DELETE("/all", bind(api.ProcApi.KillAllProcess, None))
|
||||||
processGroup.POST("/share", middle.RolePermission(eum.RoleAdmin), bind(api.ProcApi.ProcessCreateShare, Body))
|
processGroup.POST("/share", middle.RolePermission(eum.RoleAdmin), bind(api.ProcApi.ProcessCreateShare, Body))
|
||||||
|
49
resources/package-lock.json
generated
49
resources/package-lock.json
generated
@@ -16,6 +16,7 @@
|
|||||||
"@vueup/vue-quill": "^1.2.0",
|
"@vueup/vue-quill": "^1.2.0",
|
||||||
"@vueuse/core": "^10.11.0",
|
"@vueuse/core": "^10.11.0",
|
||||||
"@vueuse/integrations": "^10.11.0",
|
"@vueuse/integrations": "^10.11.0",
|
||||||
|
"@xterm/addon-canvas": "^0.7.0",
|
||||||
"@yeger/vue-masonry-wall": "^5.0.14",
|
"@yeger/vue-masonry-wall": "^5.0.14",
|
||||||
"apexcharts": "^3.52.0",
|
"apexcharts": "^3.52.0",
|
||||||
"axios": "^1.7.5",
|
"axios": "^1.7.5",
|
||||||
@@ -44,7 +45,10 @@
|
|||||||
"vue3-perfect-scrollbar": "^2.0.0",
|
"vue3-perfect-scrollbar": "^2.0.0",
|
||||||
"vuedraggable": "^4.1.0",
|
"vuedraggable": "^4.1.0",
|
||||||
"vuetify": "^3.6.14",
|
"vuetify": "^3.6.14",
|
||||||
"webfontloader": "^1.6.28"
|
"webfontloader": "^1.6.28",
|
||||||
|
"xterm": "^5.3.0",
|
||||||
|
"xterm-addon-attach": "^0.9.0",
|
||||||
|
"xterm-addon-fit": "^0.8.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@faker-js/faker": "^8.4.1",
|
"@faker-js/faker": "^8.4.1",
|
||||||
@@ -2810,6 +2814,22 @@
|
|||||||
"url": "https://github.com/sponsors/antfu"
|
"url": "https://github.com/sponsors/antfu"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@xterm/addon-canvas": {
|
||||||
|
"version": "0.7.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@xterm/addon-canvas/-/addon-canvas-0.7.0.tgz",
|
||||||
|
"integrity": "sha512-LF5LYcfvefJuJ7QotNRdRSPc9YASAVDeoT5uyXS/nZshZXjYplGXRECBGiznwvhNL2I8bq1Lf5MzRwstsYQ2Iw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@xterm/xterm": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@xterm/xterm": {
|
||||||
|
"version": "5.5.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@xterm/xterm/-/xterm-5.5.0.tgz",
|
||||||
|
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/@yeger/debounce": {
|
"node_modules/@yeger/debounce": {
|
||||||
"version": "2.0.13",
|
"version": "2.0.13",
|
||||||
"resolved": "https://registry.npmmirror.com/@yeger/debounce/-/debounce-2.0.13.tgz",
|
"resolved": "https://registry.npmmirror.com/@yeger/debounce/-/debounce-2.0.13.tgz",
|
||||||
@@ -7371,6 +7391,33 @@
|
|||||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/xterm": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/xterm/-/xterm-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==",
|
||||||
|
"deprecated": "This package is now deprecated. Move to @xterm/xterm instead.",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/xterm-addon-attach": {
|
||||||
|
"version": "0.9.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/xterm-addon-attach/-/xterm-addon-attach-0.9.0.tgz",
|
||||||
|
"integrity": "sha512-NykWWOsobVZPPK3P9eFkItrnBK9Lw0f94uey5zhqIVB1bhswdVBfl+uziEzSOhe2h0rT9wD0wOeAYsdSXeavPw==",
|
||||||
|
"deprecated": "This package is now deprecated. Move to @xterm/addon-attach instead.",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"xterm": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xterm-addon-fit": {
|
||||||
|
"version": "0.8.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/xterm-addon-fit/-/xterm-addon-fit-0.8.0.tgz",
|
||||||
|
"integrity": "sha512-yj3Np7XlvxxhYF/EJ7p3KHaMt6OdwQ+HDu573Vx1lRXsVxOcnVJs51RgjZOouIZOczTsskaS+CpXspK81/DLqw==",
|
||||||
|
"deprecated": "This package is now deprecated. Move to @xterm/addon-fit instead.",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"xterm": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yaml": {
|
"node_modules/yaml": {
|
||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.8.1.tgz",
|
"resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.8.1.tgz",
|
||||||
|
@@ -18,6 +18,7 @@
|
|||||||
"@vueup/vue-quill": "^1.2.0",
|
"@vueup/vue-quill": "^1.2.0",
|
||||||
"@vueuse/core": "^10.11.0",
|
"@vueuse/core": "^10.11.0",
|
||||||
"@vueuse/integrations": "^10.11.0",
|
"@vueuse/integrations": "^10.11.0",
|
||||||
|
"@xterm/addon-canvas": "^0.7.0",
|
||||||
"@yeger/vue-masonry-wall": "^5.0.14",
|
"@yeger/vue-masonry-wall": "^5.0.14",
|
||||||
"apexcharts": "^3.52.0",
|
"apexcharts": "^3.52.0",
|
||||||
"axios": "^1.7.5",
|
"axios": "^1.7.5",
|
||||||
@@ -46,7 +47,10 @@
|
|||||||
"vue3-perfect-scrollbar": "^2.0.0",
|
"vue3-perfect-scrollbar": "^2.0.0",
|
||||||
"vuedraggable": "^4.1.0",
|
"vuedraggable": "^4.1.0",
|
||||||
"vuetify": "^3.6.14",
|
"vuetify": "^3.6.14",
|
||||||
"webfontloader": "^1.6.28"
|
"webfontloader": "^1.6.28",
|
||||||
|
"xterm": "^5.3.0",
|
||||||
|
"xterm-addon-attach": "^0.9.0",
|
||||||
|
"xterm-addon-fit": "^0.8.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@faker-js/faker": "^8.4.1",
|
"@faker-js/faker": "^8.4.1",
|
||||||
|
@@ -45,7 +45,6 @@ onMounted(() => {
|
|||||||
<component :is="currentLayout" v-if="isRouterLoaded">
|
<component :is="currentLayout" v-if="isRouterLoaded">
|
||||||
<router-view> </router-view>
|
<router-view> </router-view>
|
||||||
</component>
|
</component>
|
||||||
<BackToTop />
|
|
||||||
<Snackbar />
|
<Snackbar />
|
||||||
</v-app>
|
</v-app>
|
||||||
</template>
|
</template>
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { ProcessItem } from "../types/process/process";
|
import { ProcessConfig, ProcessItem } from "../types/process/process";
|
||||||
import api from "./api";
|
import api from "./api";
|
||||||
|
|
||||||
export function getProcessList() {
|
export function getProcessList() {
|
||||||
@@ -30,7 +30,7 @@ export function getContorl(uuid) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getProcessConfig(uuid) {
|
export function getProcessConfig(uuid) {
|
||||||
return api.get("/process/config", { uuid }).then((res) => res);
|
return api.get<ProcessConfig>("/process/config", { uuid }).then((res) => res);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteProcessConfig(uuid) {
|
export function deleteProcessConfig(uuid) {
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import { PushItem } from "../types/push/push";
|
||||||
import api from "./api";
|
import api from "./api";
|
||||||
|
|
||||||
export function createPush(data) {
|
export function createPush(data) {
|
||||||
@@ -5,7 +6,7 @@ export function createPush(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getPushList() {
|
export function getPushList() {
|
||||||
return api.get("/push/list", undefined).then((res) => res);
|
return api.get<PushItem[]>("/push/list", undefined).then((res) => res);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deletePush(id) {
|
export function deletePush(id) {
|
||||||
|
@@ -1,6 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ProcessItem } from "~/src/types/process/process";
|
import { ProcessItem } from "~/src/types/process/process";
|
||||||
import { init } from "echarts";
|
import { init } from "echarts";
|
||||||
|
import TerminalPty from "./TerminalPty.vue";
|
||||||
|
import { killProcess, startProcess } from "~/src/api/process";
|
||||||
|
import { useSnackbarStore } from "~/src/stores/snackbarStore";
|
||||||
|
import ProcessConfig from "./ProcessConfig.vue";
|
||||||
|
let chartInstance;
|
||||||
|
|
||||||
|
const snackbarStore = useSnackbarStore();
|
||||||
const initEChart = () => {
|
const initEChart = () => {
|
||||||
props.data.usage.cpu = (props.data.usage.cpu ?? [0, 0]).map((num) =>
|
props.data.usage.cpu = (props.data.usage.cpu ?? [0, 0]).map((num) =>
|
||||||
parseFloat(num.toFixed(2))
|
parseFloat(num.toFixed(2))
|
||||||
@@ -20,8 +27,8 @@ const initEChart = () => {
|
|||||||
},
|
},
|
||||||
animationDuration: 2000,
|
animationDuration: 2000,
|
||||||
grid: {
|
grid: {
|
||||||
left: "3%",
|
left: "0%",
|
||||||
right: "4%",
|
right: "0%", // 原来 4%,改成更大
|
||||||
bottom: "3%",
|
bottom: "3%",
|
||||||
containLabel: true,
|
containLabel: true,
|
||||||
},
|
},
|
||||||
@@ -34,7 +41,7 @@ const initEChart = () => {
|
|||||||
yAxis: [
|
yAxis: [
|
||||||
{
|
{
|
||||||
type: "value",
|
type: "value",
|
||||||
name: " CPU(" + cpu + "%)",
|
name: "CPU(" + cpu + "%)",
|
||||||
min: 0, // 设置CPU的y轴最小值为10
|
min: 0, // 设置CPU的y轴最小值为10
|
||||||
max: props.data.usage.cpuCapacity,
|
max: props.data.usage.cpuCapacity,
|
||||||
minInterval: 0.1,
|
minInterval: 0.1,
|
||||||
@@ -46,7 +53,7 @@ const initEChart = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "value",
|
type: "value",
|
||||||
name: " 内存(" + mem + "MB)",
|
name: "内存(" + mem + "MB)",
|
||||||
max: parseFloat((props.data.usage.memCapacity / 1024).toFixed(2)),
|
max: parseFloat((props.data.usage.memCapacity / 1024).toFixed(2)),
|
||||||
axisLine: { show: false },
|
axisLine: { show: false },
|
||||||
axisTick: { show: false },
|
axisTick: { show: false },
|
||||||
@@ -114,18 +121,63 @@ const initEChart = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log(option);
|
|
||||||
myChart.setOption(option);
|
myChart.setOption(option);
|
||||||
|
chartInstance = myChart;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type WsHandle = {
|
||||||
|
wsConnect: () => void;
|
||||||
|
};
|
||||||
|
const terminalComponent = ref<WsHandle | null>(null);
|
||||||
|
|
||||||
|
type ConfigHandle = {
|
||||||
|
openConfigDialog: () => void;
|
||||||
|
test: () => void;
|
||||||
|
};
|
||||||
|
const processConfigComponent = ref<ConfigHandle | null>(null);
|
||||||
|
|
||||||
const buttons = [
|
const buttons = [
|
||||||
{ label: "按钮1", action: () => console.log("按钮1点击") },
|
{
|
||||||
{ label: "按钮2", action: () => console.log("按钮2点击") },
|
icon: "mdi-console",
|
||||||
{ label: "按钮3", action: () => console.log("按钮3点击") },
|
action: () => {
|
||||||
|
terminalComponent.value?.wsConnect();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "mdi-play",
|
||||||
|
action: () => {
|
||||||
|
startProcess(props.data.uuid).then((e) => {
|
||||||
|
if (e.code === 0) {
|
||||||
|
snackbarStore.showSuccessMessage("success");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "mdi-stop",
|
||||||
|
action: () => {
|
||||||
|
killProcess(props.data.uuid).then((e) => {
|
||||||
|
if (e.code === 0) {
|
||||||
|
snackbarStore.showSuccessMessage("success");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "mdi-pencil",
|
||||||
|
action: () => {
|
||||||
|
processConfigComponent.value?.openConfigDialog();
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
chartInstance.resize();
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initEChart();
|
initEChart();
|
||||||
|
window.addEventListener("resize", handleResize);
|
||||||
});
|
});
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -137,9 +189,53 @@ const props = defineProps<{
|
|||||||
<div class="chart-container">
|
<div class="chart-container">
|
||||||
<!-- 顶部:进程名字 + 菜单 -->
|
<!-- 顶部:进程名字 + 菜单 -->
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="top-left">{{ props.data.name }}</div>
|
<div class="top-left">
|
||||||
|
<v-icon
|
||||||
|
color="green"
|
||||||
|
v-if="props.data.state.state == 3 || props.data.state.state == 1"
|
||||||
|
x-large
|
||||||
|
style="float: left"
|
||||||
|
>
|
||||||
|
mdi-checkbox-marked-circle</v-icon
|
||||||
|
>
|
||||||
|
<v-icon
|
||||||
|
color="red"
|
||||||
|
v-if="props.data.state.state == 0"
|
||||||
|
x-large
|
||||||
|
style="float: left"
|
||||||
|
>
|
||||||
|
mdi-stop-circle</v-icon
|
||||||
|
>
|
||||||
|
<div v-if="props.data.state.state == 2" style="float: left">
|
||||||
|
<v-tooltip top color="warning">
|
||||||
|
<template>
|
||||||
|
<v-icon color="yellow accent-4" x-large> mdi-alert-circle</v-icon>
|
||||||
|
</template>
|
||||||
|
<span>{{ props.data.state.info }}</span>
|
||||||
|
</v-tooltip>
|
||||||
|
</div>
|
||||||
|
{{ props.data.name }}
|
||||||
|
</div>
|
||||||
<div class="top-right">
|
<div class="top-right">
|
||||||
<button @click="">菜单</button>
|
<v-menu bottom left>
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn
|
||||||
|
variant="text"
|
||||||
|
@click=""
|
||||||
|
density="compact"
|
||||||
|
class="px-1 min-w-0"
|
||||||
|
v-bind="props"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-dots-vertical</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-list nav dense>
|
||||||
|
<v-list-item @click=""> 获取控制权 </v-list-item>
|
||||||
|
<v-list-item @click=""> 删除进程 </v-list-item>
|
||||||
|
<v-list-item @click=""> 创建分享链接 </v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -149,12 +245,36 @@ const props = defineProps<{
|
|||||||
<!-- 底部:按钮组 + 时间 -->
|
<!-- 底部:按钮组 + 时间 -->
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<div class="bottom-left">
|
<div class="bottom-left">
|
||||||
<button v-for="(btn, idx) in buttons" :key="idx" @click="">
|
<v-chip
|
||||||
{{ btn.label }}
|
size="small"
|
||||||
</button>
|
variant="outlined"
|
||||||
|
style="border-color: grey; color: black"
|
||||||
|
class="d-flex align-center"
|
||||||
|
>
|
||||||
|
<v-btn
|
||||||
|
v-for="(btn, idx) in buttons"
|
||||||
|
:key="idx"
|
||||||
|
@click="btn.action"
|
||||||
|
size="small"
|
||||||
|
:icon="btn.icon"
|
||||||
|
variant="text"
|
||||||
|
density="comfortable"
|
||||||
|
/>
|
||||||
|
</v-chip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bottom-right text-caption">
|
||||||
|
<span>{{ props.data.startTime }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="bottom-right">{{ props.data.startTime }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<TerminalPty
|
||||||
|
v-if="props.data.termType == 'pty'"
|
||||||
|
:data="props.data"
|
||||||
|
ref="terminalComponent"
|
||||||
|
></TerminalPty>
|
||||||
|
<TerminalPty v-else :data="props.data"></TerminalPty>
|
||||||
|
<ProcessConfig :data="props.data" ref="processConfigComponent"></ProcessConfig>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -163,10 +283,8 @@ const props = defineProps<{
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 260px; /* 可根据实际容器调整 */
|
height: 250px; /* 可根据实际容器调整 */
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,7 +293,7 @@ const props = defineProps<{
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 5px 10px;
|
padding: 5px 5px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
height: 30px; /* 顶部固定高度 */
|
height: 30px; /* 顶部固定高度 */
|
||||||
}
|
}
|
||||||
@@ -183,7 +301,9 @@ const props = defineProps<{
|
|||||||
/* 中间图表自适应 */
|
/* 中间图表自适应 */
|
||||||
.chart {
|
.chart {
|
||||||
flex: 1; /* 占满剩余空间 */
|
flex: 1; /* 占满剩余空间 */
|
||||||
width: 100%;
|
width: 90%;
|
||||||
|
margin-left: 5%;
|
||||||
|
margin-right: 5%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 底部 footer */
|
/* 底部 footer */
|
||||||
|
224
resources/src/components/process/ProcessConfig.vue
Normal file
224
resources/src/components/process/ProcessConfig.vue
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { getProcessConfig, putProcessConfig } from "~/src/api/process";
|
||||||
|
import { getPushList } from "~/src/api/push";
|
||||||
|
import { useSnackbarStore } from "~/src/stores/snackbarStore";
|
||||||
|
import { ProcessConfig, ProcessItem } from "~/src/types/process/process";
|
||||||
|
|
||||||
|
const snackbarStore = useSnackbarStore();
|
||||||
|
const dialog = ref(false);
|
||||||
|
const configForm = ref<Partial<ProcessConfig>>({});
|
||||||
|
const pushItems = ref<{ value: any; label: string }[]>([]);
|
||||||
|
const pushSelectedValues = ref([]);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
pushSelectedValues,
|
||||||
|
(newValues) => {
|
||||||
|
configForm.value.pushIds = JSON.stringify(newValues);
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
data: ProcessItem;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
openConfigDialog: () => {
|
||||||
|
getConfig();
|
||||||
|
initPushItem();
|
||||||
|
dialog.value = true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const getConfig = () => {
|
||||||
|
getProcessConfig(props.data.uuid).then((e) => {
|
||||||
|
// 使用 Object.assign 来更新响应式对象,而不是替换它
|
||||||
|
if (e.data) {
|
||||||
|
Object.assign(configForm.value, e.data);
|
||||||
|
pushSelectedValues.value = JSON.parse(
|
||||||
|
(e.data!.pushIds as string) == "" ? "[]" : (e.data!.pushIds as string)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateJsonString = () => {
|
||||||
|
configForm.value.pushIds = JSON.stringify(pushSelectedValues);
|
||||||
|
};
|
||||||
|
|
||||||
|
const editConfig = () => {
|
||||||
|
putProcessConfig(configForm.value).then((e) => {
|
||||||
|
if (e.code === 0) {
|
||||||
|
snackbarStore.showSuccessMessage("sucess");
|
||||||
|
dialog.value = false; // 成功后通常会关闭对话框
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const initPushItem = () => {
|
||||||
|
getPushList().then((resp) => {
|
||||||
|
// 3. 更新 ref 的 .value
|
||||||
|
if (resp.data) {
|
||||||
|
pushItems.value = resp.data.map((e) => ({
|
||||||
|
value: e.id,
|
||||||
|
label: `${e.remark} [${e.id}]`,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-dialog v-model="dialog" width="700">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="text-h5 grey lighten-2">
|
||||||
|
<v-icon left>mdi-cog</v-icon>
|
||||||
|
设置
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-card-text>
|
||||||
|
<v-container>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
label="进程名称"
|
||||||
|
v-model="configForm.name"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
label="工作目录"
|
||||||
|
v-model="configForm.cwd"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
label="启动命令"
|
||||||
|
v-model="configForm.cmd"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-select
|
||||||
|
label="终端类型"
|
||||||
|
disabled
|
||||||
|
v-model="configForm.termType"
|
||||||
|
:items="['pty', 'std']"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
></v-select>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-select
|
||||||
|
v-model="pushSelectedValues"
|
||||||
|
@change="updateJsonString"
|
||||||
|
:items="pushItems"
|
||||||
|
item-title="label"
|
||||||
|
item-value="value"
|
||||||
|
chips
|
||||||
|
label="状态推送"
|
||||||
|
multiple
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
></v-select>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-divider class="my-4"></v-divider>
|
||||||
|
|
||||||
|
<v-row align="center">
|
||||||
|
<v-col cols="12" sm="3">
|
||||||
|
<v-switch
|
||||||
|
:disabled="props.data.state?.state === 1"
|
||||||
|
v-model="configForm.cgroupEnable"
|
||||||
|
label="资源限制"
|
||||||
|
color="primary"
|
||||||
|
hide-details
|
||||||
|
></v-switch>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="4">
|
||||||
|
<v-text-field
|
||||||
|
:disabled="
|
||||||
|
!configForm.cgroupEnable || props.data.state?.state === 3
|
||||||
|
"
|
||||||
|
label="CPU 限制 (%)"
|
||||||
|
type="number"
|
||||||
|
v-model.number="configForm.cpuLimit"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
hide-details="auto"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="4">
|
||||||
|
<v-text-field
|
||||||
|
:disabled="
|
||||||
|
!configForm.cgroupEnable || props.data.state?.state === 3
|
||||||
|
"
|
||||||
|
label="内存限制 (MB)"
|
||||||
|
type="number"
|
||||||
|
v-model.number="configForm.memoryLimit"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
hide-details="auto"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-divider class="my-4"></v-divider>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" sm="4">
|
||||||
|
<v-switch
|
||||||
|
v-model="configForm.autoRestart"
|
||||||
|
label="自动重启"
|
||||||
|
color="primary"
|
||||||
|
hide-details
|
||||||
|
></v-switch>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="4">
|
||||||
|
<v-switch
|
||||||
|
:disabled="!configForm.autoRestart"
|
||||||
|
v-model="configForm.compulsoryRestart"
|
||||||
|
label="强制重启"
|
||||||
|
color="primary"
|
||||||
|
hide-details
|
||||||
|
></v-switch>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="4">
|
||||||
|
<v-switch
|
||||||
|
v-model="configForm.logReport"
|
||||||
|
label="日志上报"
|
||||||
|
color="primary"
|
||||||
|
hide-details
|
||||||
|
></v-switch>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-divider></v-divider>
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn variant="text" color="grey-darken-1" @click="dialog = false">
|
||||||
|
<v-icon left>mdi-close</v-icon>
|
||||||
|
取消
|
||||||
|
</v-btn>
|
||||||
|
<v-btn variant="flat" color="primary" @click="editConfig">
|
||||||
|
<v-icon left>mdi-check</v-icon>
|
||||||
|
确认
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
167
resources/src/components/process/TerminalPty.vue
Normal file
167
resources/src/components/process/TerminalPty.vue
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, nextTick, onUnmounted } from "vue"; // 引入 watch, nextTick 和 onUnmounted
|
||||||
|
import { useSnackbarStore } from "~/src/stores/snackbarStore";
|
||||||
|
import { ProcessItem } from "~/src/types/process/process";
|
||||||
|
import { Terminal } from "xterm";
|
||||||
|
import { FitAddon } from "xterm-addon-fit";
|
||||||
|
import { AttachAddon } from "xterm-addon-attach";
|
||||||
|
import { CanvasAddon } from '@xterm/addon-canvas';
|
||||||
|
import 'xterm/css/xterm.css';
|
||||||
|
|
||||||
|
const snackbarStore = useSnackbarStore();
|
||||||
|
const dialog = ref(false);
|
||||||
|
const props = defineProps<{
|
||||||
|
data: ProcessItem;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const xtermEl = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
let socket: WebSocket | null = null;
|
||||||
|
let term: Terminal | null = null;
|
||||||
|
const fitAddon = new FitAddon();
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
wsConnect: () => {
|
||||||
|
dialog.value = true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 使用 watch 监听 dialog 的状态变化
|
||||||
|
watch(dialog, (newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
nextTick(() => {
|
||||||
|
initWebSocketPty();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const initWebSocketPty = () => {
|
||||||
|
if (!xtermEl.value) {
|
||||||
|
snackbarStore.showErrorMessage("终端容器初始化失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 在这里计算初始尺寸更准确
|
||||||
|
const initialCols = Math.floor(xtermEl.value.clientWidth / 9);
|
||||||
|
const initialRows = Math.floor(xtermEl.value.clientHeight / 19);
|
||||||
|
|
||||||
|
const baseUrl = `ws://${window.location.hostname}:8797/api/ws`;
|
||||||
|
const url = `${baseUrl}?uuid=${props.data.uuid}&token=${localStorage.getItem("token")}&cols=${initialCols}&rows=${initialRows}`;
|
||||||
|
|
||||||
|
initSocket(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
const initSocket = (url: string) => {
|
||||||
|
socket = new WebSocket(url);
|
||||||
|
|
||||||
|
socket.onopen = () => {
|
||||||
|
// WebSocket 连接成功后,初始化 Terminal
|
||||||
|
initTerm();
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onclose = () => {
|
||||||
|
snackbarStore.showErrorMessage("终端连接断开");
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onerror = (err) => {
|
||||||
|
snackbarStore.showErrorMessage("终端连接发生错误");
|
||||||
|
console.error("WebSocket Error:", err);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const initTerm = () => {
|
||||||
|
if (!socket || !xtermEl.value) return;
|
||||||
|
|
||||||
|
term = new Terminal({
|
||||||
|
// rendererType: "canvas", // 已通过插件方式加载,此处无需设置
|
||||||
|
convertEol: true,
|
||||||
|
disableStdin: false,
|
||||||
|
cursorBlink: true,
|
||||||
|
theme: {
|
||||||
|
foreground: "#ECECEC",
|
||||||
|
cursor: "help",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const attachAddon = new AttachAddon(socket);
|
||||||
|
|
||||||
|
term.loadAddon(new CanvasAddon()); // 推荐先加载渲染器
|
||||||
|
term.loadAddon(attachAddon);
|
||||||
|
term.loadAddon(fitAddon);
|
||||||
|
|
||||||
|
term.open(xtermEl.value);
|
||||||
|
|
||||||
|
// 在打开后执行 fit() 来适配尺寸
|
||||||
|
fitAddon.fit();
|
||||||
|
term.focus();
|
||||||
|
window.addEventListener("resize", handleResize);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
fitAddon.fit();
|
||||||
|
};
|
||||||
|
|
||||||
|
const wsClose = () => {
|
||||||
|
dialog.value = false;
|
||||||
|
cleanup();
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
window.removeEventListener("resize", handleResize);
|
||||||
|
if (term) {
|
||||||
|
term.dispose();
|
||||||
|
term = null;
|
||||||
|
}
|
||||||
|
if (socket) {
|
||||||
|
socket.close();
|
||||||
|
socket = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-dialog
|
||||||
|
fullscreen
|
||||||
|
hide-overlay
|
||||||
|
transition="dialog-bottom-transition"
|
||||||
|
v-model="dialog"
|
||||||
|
@update:modelValue="val => !val && cleanup()"
|
||||||
|
>
|
||||||
|
<v-card
|
||||||
|
style="
|
||||||
|
height: 100%;
|
||||||
|
background-color: black;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<v-toolbar
|
||||||
|
dense
|
||||||
|
dark
|
||||||
|
color="blue-grey darken-4"
|
||||||
|
style="height: 35px; flex-grow: 0"
|
||||||
|
>
|
||||||
|
<v-toolbar-title style="height: 100%"
|
||||||
|
>{{ props.data.name }} ({{ props.data.termType }})</v-toolbar-title
|
||||||
|
>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-toolbar-items style="height: 35px">
|
||||||
|
<v-btn icon dense dark @click="wsClose">
|
||||||
|
<v-icon>mdi-close</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-toolbar-items>
|
||||||
|
</v-toolbar>
|
||||||
|
<div id="xterm" ref="xtermEl" style="flex-grow: 1; height: 100%; width: 100%;"></div>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#xterm .terminal {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
@@ -1,34 +1,49 @@
|
|||||||
export interface ProcessItem {
|
export interface ProcessItem {
|
||||||
name: string;
|
name: string;
|
||||||
uuid: number;
|
uuid: number;
|
||||||
startTime: Date;
|
startTime: Date;
|
||||||
user: string;
|
user: string;
|
||||||
usage: Usage;
|
usage: Usage;
|
||||||
state: State;
|
state: State;
|
||||||
termType: TermType;
|
termType: TermType;
|
||||||
cgroupEnable: boolean;
|
cgroupEnable: boolean;
|
||||||
memoryLimit: number | null;
|
memoryLimit: number | null;
|
||||||
cpuLimit: number | null;
|
cpuLimit: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
state: number;
|
state: number;
|
||||||
info: Info;
|
info: Info;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Info {
|
export enum Info {
|
||||||
Empty = "",
|
Empty = "",
|
||||||
重启次数异常 = "重启次数异常",
|
重启次数异常 = "重启次数异常",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum TermType {
|
export enum TermType {
|
||||||
Pty = "pty",
|
Pty = "pty",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Usage {
|
export interface Usage {
|
||||||
cpuCapacity: number;
|
cpuCapacity: number;
|
||||||
memCapacity: number;
|
memCapacity: number;
|
||||||
cpu: number[] | null;
|
cpu: number[] | null;
|
||||||
mem: number[] | null;
|
mem: number[] | null;
|
||||||
time: string[] | null;
|
time: string[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProcessConfig {
|
||||||
|
uuid: number;
|
||||||
|
name: string;
|
||||||
|
cmd: string;
|
||||||
|
cwd: string;
|
||||||
|
autoRestart: boolean;
|
||||||
|
compulsoryRestart: boolean;
|
||||||
|
pushIds: number[] | string;
|
||||||
|
logReport: boolean;
|
||||||
|
termType: string;
|
||||||
|
cgroupEnable: boolean;
|
||||||
|
memoryLimit: null;
|
||||||
|
cpuLimit: null;
|
||||||
}
|
}
|
||||||
|
8
resources/src/types/push/push.ts
Normal file
8
resources/src/types/push/push.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export interface PushItem {
|
||||||
|
id: number;
|
||||||
|
method: string;
|
||||||
|
url: string;
|
||||||
|
body: string;
|
||||||
|
remark: string;
|
||||||
|
enable: boolean;
|
||||||
|
}
|
@@ -10,18 +10,41 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ProcessCard from "@/components/process/ProcessCard.vue";
|
import ProcessCard from "@/components/process/ProcessCard.vue";
|
||||||
|
import axios from "axios";
|
||||||
import { getProcessList } from "~/src/api/process";
|
import { getProcessList } from "~/src/api/process";
|
||||||
import { ProcessItem } from "~/src/types/process/process";
|
import { ProcessItem } from "~/src/types/process/process";
|
||||||
|
|
||||||
const processData = ref<ProcessItem[]>();
|
const processData = ref<ProcessItem[]>();
|
||||||
|
const uuid: string = crypto.randomUUID();
|
||||||
|
|
||||||
const initProcessData = () => {
|
const initProcessData = () => {
|
||||||
getProcessList().then((e) => {
|
getProcessList().then((e) => {
|
||||||
processData.value = e.data!;
|
processData.value = e.data!.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
console.log(e.data);
|
getProcessListWait();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var cancelTokenSource;
|
||||||
|
const getProcessListWait = () => {
|
||||||
|
cancelTokenSource = axios.CancelToken.source();
|
||||||
|
axios
|
||||||
|
.get("api/process/wait", {
|
||||||
|
cancelToken: cancelTokenSource.token,
|
||||||
|
headers: {
|
||||||
|
Authorization: "bearer " + localStorage.getItem("token"),
|
||||||
|
Uuid: uuid,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
processData.value = response.data.data.sort((a, b) =>
|
||||||
|
a.name.localeCompare(b.name)
|
||||||
|
);
|
||||||
|
getProcessListWait();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("请求错误:", error);
|
||||||
|
});
|
||||||
|
};
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initProcessData();
|
initProcessData();
|
||||||
});
|
});
|
||||||
@@ -32,17 +55,24 @@ onMounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap; /* 自动换行 */
|
flex-wrap: wrap; /* 自动换行 */
|
||||||
justify-content: space-between; /* 两边与中间间距均匀 */
|
justify-content: space-between; /* 两边与中间间距均匀 */
|
||||||
gap: 50px; /* 每个 div 之间的间距 */
|
gap: 80px; /* 每个 div 之间的间距 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.responsive-box {
|
.responsive-box {
|
||||||
flex: 1 1 300px; /* 最小宽度 500px */
|
flex: 1 1 300px; /* 最小宽度 300px */
|
||||||
min-width: 300px; /* 强制最小宽度 */
|
min-width: 300px;
|
||||||
max-width: 100%; /* 不超过容器宽度 */
|
max-width: 100%;
|
||||||
background: #f5f5f5;
|
background: #ffffff; /* 改为白色背景 */
|
||||||
padding: 16px;
|
border-radius: 16px; /* 圆角 */
|
||||||
border-radius: 12px;
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); /* 柔和阴影 */
|
||||||
text-align: center;
|
text-align: center;
|
||||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
padding: 10px; /* 内边距,让内容不贴边 */
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease; /* 交互动画 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 悬停效果 */
|
||||||
|
.responsive-box:hover {
|
||||||
|
transform: translateY(-6px);
|
||||||
|
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
Reference in New Issue
Block a user