mirror of
https://github.com/lzh-1625/go_process_manager.git
synced 2025-09-27 12:22:13 +08:00
Compare commits
6 Commits
8fd70be906
...
dev-web
Author | SHA1 | Date | |
---|---|---|---|
![]() |
6d0239fe60 | ||
![]() |
09ae980ab5 | ||
![]() |
c52f7e2097 | ||
![]() |
1844a843eb | ||
![]() |
593185a431 | ||
![]() |
ab7ef546ea |
@@ -4,34 +4,33 @@ var CF = new(configuration)
|
||||
|
||||
// 只支持 float64、int、int64、bool、string类型
|
||||
type configuration struct {
|
||||
LogLevel string `default:"debug" describe:"日志等级[debug,info]"`
|
||||
Listen string `default:":8797" describe:"监听端口"`
|
||||
StorgeType string `default:"sqlite" describe:"存储引擎[sqlite、es、bleve]"`
|
||||
EsUrl string `default:"" describe:"Elasticsearch url"`
|
||||
EsIndex string `default:"server_log_v1" describe:"Elasticsearch index"`
|
||||
EsUsername string `default:"" describe:"Elasticsearch用户名"`
|
||||
EsPassword string `default:"" describe:"Elasticsearch密码"`
|
||||
EsWindowLimit bool `default:"true" describe:"Es分页10000条限制"`
|
||||
FileSizeLimit float64 `default:"10.0" describe:"文件大小限制(MB)"`
|
||||
ProcessInputPrefix string `default:">" describe:"进程输入前缀"`
|
||||
ProcessRestartsLimit int `default:"2" describe:"进程重启次数限制"`
|
||||
ProcessMsgCacheLinesLimit int `default:"50" describe:"std进程缓存消息行数"`
|
||||
ProcessMsgCacheBufLimit int `default:"4096" describe:"pty进程缓存消息字节长度"`
|
||||
ProcessExpireTime int64 `default:"60" describe:"进程控制权过期时间(秒)"`
|
||||
PerformanceInfoListLength int `default:"30" describe:"性能信息存储长度"`
|
||||
PerformanceInfoInterval int `default:"60" describe:"监控获取间隔时间(秒)"`
|
||||
TerminalConnectTimeout int `default:"10" describe:"终端连接超时时间(分钟)"`
|
||||
UserPassWordMinLength int `default:"4" describe:"用户密码最小长度"`
|
||||
LogMinLenth int `default:"0" describe:"过滤日志最小长度"`
|
||||
LogHandlerPoolSize int `default:"10" describe:"日志处理并行数"`
|
||||
PprofEnable bool `default:"true" describe:"启用pprof分析工具"`
|
||||
KillWaitTime int `default:"5" describe:"kill信号等待时间(秒)"`
|
||||
TaskTimeout int `default:"60" describe:"任务执行超时时间(秒)"`
|
||||
TokenExpirationTime int64 `default:"720" describe:"token过期时间(小时)"`
|
||||
WsHealthCheckInterval int `default:"3" describe:"ws主动健康检查间隔(秒)"`
|
||||
CgroupPeriod int64 `default:"100000" describe:"CgroupPeriod"`
|
||||
CgroupSwapLimit bool `default:"false" describe:"cgroup swap限制"`
|
||||
CondWaitTime int `default:"30" describe:"长轮询等待时间(秒)"`
|
||||
PerformanceCapacityDisplay bool `default:"false" describe:"性能资源容量显示"`
|
||||
Tui bool `default:"-"`
|
||||
LogLevel string `default:"debug" describe:"日志等级[debug,info]"`
|
||||
Listen string `default:":8797" describe:"监听端口"`
|
||||
StorgeType string `default:"sqlite" describe:"存储引擎[sqlite、es、bleve]"`
|
||||
EsUrl string `default:"" describe:"Elasticsearch url"`
|
||||
EsIndex string `default:"server_log_v1" describe:"Elasticsearch index"`
|
||||
EsUsername string `default:"" describe:"Elasticsearch用户名"`
|
||||
EsPassword string `default:"" describe:"Elasticsearch密码"`
|
||||
EsWindowLimit bool `default:"true" describe:"Es分页10000条限制"`
|
||||
FileSizeLimit float64 `default:"10.0" describe:"文件大小限制(MB)"`
|
||||
ProcessInputPrefix string `default:">" describe:"进程输入前缀"`
|
||||
ProcessRestartsLimit int `default:"2" describe:"进程重启次数限制"`
|
||||
ProcessMsgCacheLinesLimit int `default:"50" describe:"std进程缓存消息行数"`
|
||||
ProcessMsgCacheBufLimit int `default:"4096" describe:"pty进程缓存消息字节长度"`
|
||||
ProcessExpireTime int64 `default:"60" describe:"进程控制权过期时间(秒)"`
|
||||
PerformanceInfoListLength int `default:"30" describe:"性能信息存储长度"`
|
||||
PerformanceInfoInterval int `default:"60" describe:"监控获取间隔时间(秒)"`
|
||||
TerminalConnectTimeout int `default:"10" describe:"终端连接超时时间(分钟)"`
|
||||
UserPassWordMinLength int `default:"4" describe:"用户密码最小长度"`
|
||||
LogMinLenth int `default:"0" describe:"过滤日志最小长度"`
|
||||
LogHandlerPoolSize int `default:"10" describe:"日志处理并行数"`
|
||||
PprofEnable bool `default:"true" describe:"启用pprof分析工具"`
|
||||
KillWaitTime int `default:"5" describe:"kill信号等待时间(秒)"`
|
||||
TaskTimeout int `default:"60" describe:"任务执行超时时间(秒)"`
|
||||
TokenExpirationTime int64 `default:"720" describe:"token过期时间(小时)"`
|
||||
WsHealthCheckInterval int `default:"3" describe:"ws主动健康检查间隔(秒)"`
|
||||
CgroupPeriod int64 `default:"100000" describe:"CgroupPeriod"`
|
||||
CgroupSwapLimit bool `default:"false" describe:"cgroup swap限制"`
|
||||
CondWaitTime int `default:"30" describe:"长轮询等待时间(秒)"`
|
||||
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 {
|
||||
Uuid int `form:"uuid" binding:"required"`
|
||||
}) (err error) {
|
||||
if !hasOprPermission(ctx, req.Uuid, eum.OperationStop) {
|
||||
return errors.New("not permission")
|
||||
}
|
||||
return logic.ProcessCtlLogic.KillProcess(req.Uuid)
|
||||
}
|
||||
|
||||
func (p *procApi) StartProcess(ctx *gin.Context, req struct {
|
||||
Uuid int `form:"uuid" binding:"required"`
|
||||
Uuid int `json:"uuid" binding:"required"`
|
||||
}) (err error) {
|
||||
if !hasOprPermission(ctx, req.Uuid, eum.OperationStart) {
|
||||
return errors.New("not permission")
|
||||
}
|
||||
prod, err := logic.ProcessCtlLogic.GetProcess(req.Uuid)
|
||||
if err != nil { // 进程不存在则创建
|
||||
proConfig, err := repository.ProcessRepository.GetProcessConfigById(req.Uuid)
|
||||
|
@@ -3,6 +3,7 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -46,9 +47,15 @@ func (w *WsConnetInstance) Cancel() {
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true // 允许所有跨域请求
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
proc, err := logic.ProcessCtlLogic.GetProcess(req.Uuid)
|
||||
if err != nil {
|
||||
@@ -64,7 +71,7 @@ func (w *wsApi) WebsocketHandle(ctx *gin.Context, req model.WebsocketHandleReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer conn.Close()
|
||||
log.Logger.Infow("ws连接成功")
|
||||
|
||||
wsCtx, cancel := context.WithCancel(context.Background())
|
||||
@@ -73,7 +80,9 @@ func (w *wsApi) WebsocketHandle(ctx *gin.Context, req model.WebsocketHandleReq)
|
||||
CancelFunc: cancel,
|
||||
wsLock: sync.Mutex{},
|
||||
}
|
||||
proc.ReadCache(wci)
|
||||
if err := proc.ReadCache(wci); err != nil {
|
||||
return nil
|
||||
}
|
||||
if proc.State.State == eum.ProcessStateRunning {
|
||||
proc.SetTerminalSize(req.Cols, req.Rows)
|
||||
w.startWsConnect(wci, cancel, proc, hasOprPermission(ctx, req.Uuid, eum.OperationTerminalWrite))
|
||||
@@ -92,7 +101,6 @@ func (w *wsApi) WebsocketHandle(ctx *gin.Context, req model.WebsocketHandleReq)
|
||||
case <-wsCtx.Done():
|
||||
log.Logger.Infow("ws连接断开", "操作类型", "tcp连接建立已被关闭")
|
||||
}
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -122,7 +130,7 @@ func (w *wsApi) WebsocketShareHandle(ctx *gin.Context, req model.WebsocketHandle
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer conn.Close()
|
||||
log.Logger.Infow("ws连接成功")
|
||||
data.UpdatedAt = time.Now()
|
||||
repository.WsShare.Edit(data)
|
||||
@@ -134,7 +142,9 @@ func (w *wsApi) WebsocketShareHandle(ctx *gin.Context, req model.WebsocketHandle
|
||||
CancelFunc: cancel,
|
||||
wsLock: sync.Mutex{},
|
||||
}
|
||||
proc.ReadCache(wci)
|
||||
if err := proc.ReadCache(wci); err != nil {
|
||||
return nil
|
||||
}
|
||||
w.startWsConnect(wci, cancel, proc, data.Write)
|
||||
proc.AddConn(guestName, wci)
|
||||
defer proc.DeleteConn(guestName)
|
||||
@@ -152,7 +162,6 @@ func (w *wsApi) WebsocketShareHandle(ctx *gin.Context, req model.WebsocketHandle
|
||||
case <-time.After(time.Until(data.ExpireTime)):
|
||||
log.Logger.Infow("ws连接断开", "操作类型", "分享时间已结束")
|
||||
}
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
|
@@ -21,7 +21,7 @@ import (
|
||||
)
|
||||
|
||||
type Process interface {
|
||||
ReadCache(ConnectInstance)
|
||||
ReadCache(ConnectInstance) error
|
||||
Write(string) error
|
||||
WriteBytes([]byte) error
|
||||
readInit()
|
||||
|
@@ -7,7 +7,6 @@ import (
|
||||
"sync"
|
||||
|
||||
"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/model"
|
||||
"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.Usage.Cpu = process.performanceStatus.cpu
|
||||
pi.Usage.Mem = process.performanceStatus.mem
|
||||
if config.CF.PerformanceCapacityDisplay {
|
||||
pi.Usage.CpuCapacity = float64(runtime.NumCPU()) * 100.0
|
||||
pi.Usage.MemCapacity = float64(utils.UnwarpIgnore(mem.VirtualMemory()).Total >> 10)
|
||||
}
|
||||
pi.Usage.CpuCapacity = float64(runtime.NumCPU()) * 100.0
|
||||
pi.Usage.MemCapacity = float64(utils.UnwarpIgnore(mem.VirtualMemory()).Total >> 10)
|
||||
pi.Usage.Time = process.performanceStatus.time
|
||||
pi.TermType = process.Type()
|
||||
pi.CgroupEnable = process.Config.cgroupEnable
|
||||
|
@@ -2,6 +2,7 @@ package logic
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
@@ -113,10 +114,12 @@ func (p *ProcessPty) readInit() {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ProcessPty) ReadCache(ws ConnectInstance) {
|
||||
if p.cacheBytesBuf != nil {
|
||||
ws.Write(p.cacheBytesBuf.Bytes())
|
||||
func (p *ProcessPty) ReadCache(ws ConnectInstance) error {
|
||||
if p.cacheBytesBuf == nil {
|
||||
return errors.New("cache is null")
|
||||
}
|
||||
ws.Write(p.cacheBytesBuf.Bytes())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *ProcessPty) bufHanle(b []byte) {
|
||||
|
@@ -122,8 +122,12 @@ func (p *ProcessPty) readInit() {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ProcessPty) ReadCache(ws ConnectInstance) {
|
||||
func (p *ProcessPty) ReadCache(ws ConnectInstance) error {
|
||||
if p.cacheBytesBuf == nil {
|
||||
return errors.New("cache is null")
|
||||
}
|
||||
ws.Write(p.cacheBytesBuf.Bytes())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *ProcessPty) bufHanle(b []byte) {
|
||||
|
@@ -87,10 +87,14 @@ func (p *ProcessStd) doOnInit() {
|
||||
p.cacheLine = make([]string, config.CF.ProcessMsgCacheLinesLimit)
|
||||
}
|
||||
|
||||
func (p *ProcessStd) ReadCache(ws ConnectInstance) {
|
||||
func (p *ProcessStd) ReadCache(ws ConnectInstance) error {
|
||||
if len(p.cacheLine) == 0 {
|
||||
return errors.New("cache is null")
|
||||
}
|
||||
for _, line := range p.cacheLine {
|
||||
ws.WriteString(line)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *ProcessStd) doOnKilled() {
|
||||
|
@@ -1,11 +1,7 @@
|
||||
package middle
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strconv"
|
||||
|
||||
"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"
|
||||
)
|
||||
@@ -20,24 +16,3 @@ func RolePermission(needPermission eum.Role) func(ctx *gin.Context) {
|
||||
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) {
|
||||
reqUser := c.GetHeader("token")
|
||||
reqUser := c.GetHeader("Uuid")
|
||||
defer p.timeMap.Store(reqUser, p.ts)
|
||||
if ts, ok := p.timeMap.Load(reqUser); !ok || ts.(int64) > p.ts {
|
||||
c.Next()
|
||||
|
@@ -9,7 +9,7 @@ type Process struct {
|
||||
Cwd string `gorm:"column:cwd" json:"cwd"`
|
||||
AutoRestart bool `gorm:"column:auto_restart" json:"autoRestart"`
|
||||
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"`
|
||||
TermType eum.TerminalType `gorm:"column:term_type" json:"termType"`
|
||||
CgroupEnable bool `gorm:"column:cgroup_enable" json:"cgroupEnable"`
|
||||
|
@@ -41,7 +41,7 @@ func InitDb() {
|
||||
}
|
||||
sqlDB.SetConnMaxLifetime(time.Hour)
|
||||
db = gdb.Session(&defaultConfig)
|
||||
// db = db.Debug()
|
||||
db = db.Debug()
|
||||
db.AutoMigrate(
|
||||
&model.Process{},
|
||||
&model.User{},
|
||||
|
@@ -26,7 +26,7 @@ func (t *taskRepository) GetTaskByKey(key string) (result model.Task, err error)
|
||||
}
|
||||
|
||||
func (t *taskRepository) AddTask(data model.Task) (taskId int, err error) {
|
||||
err = db.Create(&data).Error
|
||||
err = query.Task.Create(&data)
|
||||
taskId = data.Id
|
||||
return
|
||||
}
|
||||
|
@@ -58,16 +58,16 @@ func routePathInit(r *gin.Engine) {
|
||||
{
|
||||
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))
|
||||
}
|
||||
|
||||
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("/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.DELETE("/all", bind(api.ProcApi.KillAllProcess, None))
|
||||
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",
|
||||
"@vueuse/core": "^10.11.0",
|
||||
"@vueuse/integrations": "^10.11.0",
|
||||
"@xterm/addon-canvas": "^0.7.0",
|
||||
"@yeger/vue-masonry-wall": "^5.0.14",
|
||||
"apexcharts": "^3.52.0",
|
||||
"axios": "^1.7.5",
|
||||
@@ -44,7 +45,10 @@
|
||||
"vue3-perfect-scrollbar": "^2.0.0",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"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": {
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
@@ -2810,6 +2814,22 @@
|
||||
"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": {
|
||||
"version": "2.0.13",
|
||||
"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==",
|
||||
"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": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.8.1.tgz",
|
||||
|
@@ -18,6 +18,7 @@
|
||||
"@vueup/vue-quill": "^1.2.0",
|
||||
"@vueuse/core": "^10.11.0",
|
||||
"@vueuse/integrations": "^10.11.0",
|
||||
"@xterm/addon-canvas": "^0.7.0",
|
||||
"@yeger/vue-masonry-wall": "^5.0.14",
|
||||
"apexcharts": "^3.52.0",
|
||||
"axios": "^1.7.5",
|
||||
@@ -46,7 +47,10 @@
|
||||
"vue3-perfect-scrollbar": "^2.0.0",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"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": {
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
|
@@ -4,7 +4,6 @@ import LandingLayout from "@/layouts/LandingLayout.vue";
|
||||
import DefaultLayout from "@/layouts/DefaultLayout.vue";
|
||||
import AuthLayout from "@/layouts/AuthLayout.vue";
|
||||
|
||||
import BackToTop from "@/components/common/BackToTop.vue";
|
||||
import Snackbar from "@/components/common/Snackbar.vue";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
import { useTheme } from "vuetify";
|
||||
@@ -45,7 +44,6 @@ onMounted(() => {
|
||||
<component :is="currentLayout" v-if="isRouterLoaded">
|
||||
<router-view> </router-view>
|
||||
</component>
|
||||
<BackToTop />
|
||||
<Snackbar />
|
||||
</v-app>
|
||||
</template>
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { ProcessItem } from "../types/process/process";
|
||||
import { ProcessConfig, ProcessItem } from "../types/process/process";
|
||||
import api from "./api";
|
||||
|
||||
export function getProcessList() {
|
||||
@@ -9,12 +9,12 @@ export function getProcessListWait() {
|
||||
return api.get<ProcessItem[]>("/process/wait", undefined).then((res) => res);
|
||||
}
|
||||
|
||||
export function killProcessAll(uuid) {
|
||||
return api.delete("/process/all", { uuid }).then((res) => res);
|
||||
export function killProcessAll() {
|
||||
return api.delete("/process/all", { }).then((res) => res);
|
||||
}
|
||||
|
||||
export function startProcessAll(uuid) {
|
||||
return api.put("/process/all", { uuid }).then((res) => res);
|
||||
export function startProcessAll() {
|
||||
return api.put("/process/all", { }).then((res) => res);
|
||||
}
|
||||
|
||||
export function killProcess(uuid) {
|
||||
@@ -30,7 +30,7 @@ export function getContorl(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) {
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { PushItem } from "../types/push/push";
|
||||
import api from "./api";
|
||||
|
||||
export function createPush(data) {
|
||||
@@ -5,7 +6,7 @@ export function createPush(data) {
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@@ -1,7 +1,8 @@
|
||||
import { TaskItem } from "../types/tassk/task";
|
||||
import api from "./api";
|
||||
|
||||
export function getTaskAll() {
|
||||
return api.get("/task/all", undefined).then((res) => res);
|
||||
return api.get<TaskItem[]>("/task/all", undefined).then((res) => res);
|
||||
}
|
||||
|
||||
export function getTaskAllWait() {
|
||||
@@ -9,7 +10,7 @@ export function getTaskAllWait() {
|
||||
}
|
||||
|
||||
export function getTaskById(id) {
|
||||
return api.get("/task", { id }).then((res) => res);
|
||||
return api.get<TaskItem>("/task", { id }).then((res) => res);
|
||||
}
|
||||
|
||||
export function startTaskById(id) {
|
||||
|
45
resources/src/components/ConfirmButton.vue
Normal file
45
resources/src/components/ConfirmButton.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- 触发按钮 -->
|
||||
<v-btn :color="color" size="small" variant="tonal" @click="dialog = true">
|
||||
<slot>{{ label }}</slot>
|
||||
</v-btn>
|
||||
|
||||
<!-- 确认弹窗 -->
|
||||
<v-dialog v-model="dialog" max-width="400">
|
||||
<v-card>
|
||||
<v-card-title class="text-h6">{{ title }}</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
{{ message }}
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="dialog = false">取消</v-btn>
|
||||
<v-btn :color="color" variant="flat" @click="confirm"> 确认 </v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
label: { type: String, default: "操作" },
|
||||
title: { type: String, default: "确认" },
|
||||
message: { type: String, default: "确定要执行此操作吗?" },
|
||||
color: { type: String, default: "primary" },
|
||||
});
|
||||
|
||||
const emits = defineEmits(["confirm"]);
|
||||
|
||||
const dialog = ref(false);
|
||||
|
||||
const confirm = () => {
|
||||
dialog.value = false;
|
||||
emits("confirm");
|
||||
};
|
||||
</script>
|
@@ -1,6 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { ProcessItem } from "~/src/types/process/process";
|
||||
import { init } from "echarts";
|
||||
import TerminalPty from "./TerminalPty.vue";
|
||||
import {
|
||||
deleteProcessConfig,
|
||||
getContorl,
|
||||
killProcess,
|
||||
startProcess,
|
||||
} from "~/src/api/process";
|
||||
import { useSnackbarStore } from "~/src/stores/snackbarStore";
|
||||
import ProcessConfig from "./ProcessConfig.vue";
|
||||
let chartInstance;
|
||||
|
||||
const snackbarStore = useSnackbarStore();
|
||||
const initEChart = () => {
|
||||
props.data.usage.cpu = (props.data.usage.cpu ?? [0, 0]).map((num) =>
|
||||
parseFloat(num.toFixed(2))
|
||||
@@ -10,7 +22,7 @@ const initEChart = () => {
|
||||
);
|
||||
const cpu = props.data.usage.cpu[props.data.usage.cpu.length - 1] ?? "-";
|
||||
const mem = props.data.usage.mem[props.data.usage.mem.length - 1] ?? "-";
|
||||
var myChart = init(document.getElementById("echarts" + props.index));
|
||||
var myChart = init(document.getElementById("echarts" + props.data.uuid));
|
||||
var option = {
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
@@ -20,8 +32,8 @@ const initEChart = () => {
|
||||
},
|
||||
animationDuration: 2000,
|
||||
grid: {
|
||||
left: "3%",
|
||||
right: "4%",
|
||||
left: "0%",
|
||||
right: "0%", // 原来 4%,改成更大
|
||||
bottom: "3%",
|
||||
containLabel: true,
|
||||
},
|
||||
@@ -34,7 +46,7 @@ const initEChart = () => {
|
||||
yAxis: [
|
||||
{
|
||||
type: "value",
|
||||
name: " CPU(" + cpu + "%)",
|
||||
name: "CPU(" + cpu + "%)",
|
||||
min: 0, // 设置CPU的y轴最小值为10
|
||||
max: props.data.usage.cpuCapacity,
|
||||
minInterval: 0.1,
|
||||
@@ -46,7 +58,7 @@ const initEChart = () => {
|
||||
},
|
||||
{
|
||||
type: "value",
|
||||
name: " 内存(" + mem + "MB)",
|
||||
name: "内存(" + mem + "MB)",
|
||||
max: parseFloat((props.data.usage.memCapacity / 1024).toFixed(2)),
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
@@ -88,7 +100,7 @@ const initEChart = () => {
|
||||
name: "CPU限制(%)",
|
||||
type: "line",
|
||||
yAxisIndex: 0,
|
||||
data: new Array(props.data.usage.time!.length).fill(
|
||||
data: new Array(props.data.usage.time?.length ?? 0).fill(
|
||||
props.data.cpuLimit
|
||||
),
|
||||
lineStyle: {
|
||||
@@ -103,7 +115,7 @@ const initEChart = () => {
|
||||
name: "内存限制(MB)",
|
||||
type: "line",
|
||||
yAxisIndex: 1,
|
||||
data: new Array(props.data.usage.time!.length).fill(
|
||||
data: new Array(props.data.usage.time?.length ?? 0).fill(
|
||||
props.data.memoryLimit
|
||||
),
|
||||
lineStyle: {
|
||||
@@ -114,47 +126,177 @@ const initEChart = () => {
|
||||
});
|
||||
}
|
||||
}
|
||||
console.log(option);
|
||||
myChart.setOption(option);
|
||||
chartInstance = myChart;
|
||||
};
|
||||
|
||||
type WsHandle = {
|
||||
wsConnect: () => void;
|
||||
};
|
||||
const terminalComponent = ref<WsHandle | null>(null);
|
||||
|
||||
type ConfigHandle = {
|
||||
openConfigDialog: () => void;
|
||||
};
|
||||
const processConfigComponent = ref<ConfigHandle | null>(null);
|
||||
|
||||
const buttons = [
|
||||
{ label: "按钮1", action: () => console.log("按钮1点击") },
|
||||
{ label: "按钮2", action: () => console.log("按钮2点击") },
|
||||
{ label: "按钮3", action: () => console.log("按钮3点击") },
|
||||
{
|
||||
icon: "mdi-console",
|
||||
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(() => {
|
||||
initEChart();
|
||||
window.addEventListener("resize", handleResize);
|
||||
});
|
||||
|
||||
const props = defineProps<{
|
||||
data: ProcessItem;
|
||||
index: Number;
|
||||
}>();
|
||||
|
||||
const control = () => {
|
||||
getContorl(props.data.uuid).then((e) => {
|
||||
if (e.code === 0) {
|
||||
snackbarStore.showSuccessMessage("sucess");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const del = () => {
|
||||
deleteProcessConfig(props.data.uuid).then((e) => {
|
||||
if (e.code === 0) {
|
||||
snackbarStore.showSuccessMessage("sucess");
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="chart-container">
|
||||
<!-- 顶部:进程名字 + 菜单 -->
|
||||
<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">
|
||||
<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="control"> 获取控制权 </v-list-item>
|
||||
<v-list-item @click="del"> 删除进程 </v-list-item>
|
||||
<v-list-item @click=""> 创建分享链接 </v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 中间:ECharts -->
|
||||
<div :id="'echarts' + props.index" class="chart"></div>
|
||||
<div :id="'echarts' + props.data.uuid" class="chart"></div>
|
||||
|
||||
<!-- 底部:按钮组 + 时间 -->
|
||||
<div class="footer">
|
||||
<div class="bottom-left">
|
||||
<button v-for="(btn, idx) in buttons" :key="idx" @click="">
|
||||
{{ btn.label }}
|
||||
</button>
|
||||
<v-chip
|
||||
size="small"
|
||||
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 class="bottom-right">{{ props.data.startTime }}</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>
|
||||
</template>
|
||||
|
||||
@@ -163,10 +305,8 @@ const props = defineProps<{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 260px; /* 可根据实际容器调整 */
|
||||
height: 250px; /* 可根据实际容器调整 */
|
||||
background: #fff;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -175,7 +315,7 @@ const props = defineProps<{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 5px 10px;
|
||||
padding: 5px 5px;
|
||||
font-weight: bold;
|
||||
height: 30px; /* 顶部固定高度 */
|
||||
}
|
||||
@@ -183,7 +323,9 @@ const props = defineProps<{
|
||||
/* 中间图表自适应 */
|
||||
.chart {
|
||||
flex: 1; /* 占满剩余空间 */
|
||||
width: 100%;
|
||||
width: 90%;
|
||||
margin-left: 5%;
|
||||
margin-right: 5%;
|
||||
}
|
||||
|
||||
/* 底部 footer */
|
||||
|
220
resources/src/components/process/ProcessConfig.vue
Normal file
220
resources/src/components/process/ProcessConfig.vue
Normal file
@@ -0,0 +1,220 @@
|
||||
<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-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-row>
|
||||
|
||||
<v-divider class="my-4"></v-divider>
|
||||
<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-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>
|
197
resources/src/components/process/ProcessCreate.vue
Normal file
197
resources/src/components/process/ProcessCreate.vue
Normal file
@@ -0,0 +1,197 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { postProcessConfig } from "~/src/api/process";
|
||||
import { getPushList } from "~/src/api/push";
|
||||
import { useSnackbarStore } from "~/src/stores/snackbarStore";
|
||||
import { ProcessConfig } 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 }
|
||||
);
|
||||
defineExpose({
|
||||
createProcessDialog: () => {
|
||||
initPushItem();
|
||||
dialog.value = true;
|
||||
},
|
||||
});
|
||||
|
||||
const updateJsonString = () => {
|
||||
configForm.value.pushIds = JSON.stringify(pushSelectedValues);
|
||||
};
|
||||
|
||||
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}]`,
|
||||
}));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const create = () => {
|
||||
postProcessConfig(configForm.value).then((e) => {
|
||||
if (e.code === 0) {
|
||||
snackbarStore.showSuccessMessage("sucess");
|
||||
dialog.value = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
</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-select
|
||||
label="终端类型"
|
||||
v-model="configForm.termType"
|
||||
:items="['pty', 'std']"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" md="12">
|
||||
<v-text-field
|
||||
label="工作目录"
|
||||
v-model="configForm.cwd"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="12">
|
||||
<v-textarea
|
||||
label="启动命令"
|
||||
rows="2"
|
||||
v-model="configForm.cmd"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
></v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-divider class="my-4"></v-divider>
|
||||
<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-divider class="my-4"></v-divider>
|
||||
|
||||
<v-row align="center">
|
||||
<v-col cols="12" sm="3">
|
||||
<v-switch
|
||||
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"
|
||||
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"
|
||||
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="create">
|
||||
<v-icon left>mdi-check</v-icon>
|
||||
确认
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
180
resources/src/components/process/TerminalPty.vue
Normal file
180
resources/src/components/process/TerminalPty.vue
Normal file
@@ -0,0 +1,180 @@
|
||||
<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("终端连接断开");
|
||||
dialog.value = false;
|
||||
};
|
||||
|
||||
socket.onerror = (err) => {
|
||||
snackbarStore.showErrorMessage("终端连接发生错误");
|
||||
console.error("WebSocket Error:", err);
|
||||
};
|
||||
};
|
||||
|
||||
const initTerm = () => {
|
||||
if (!socket || !xtermEl.value) return;
|
||||
const showCursor = props.data.state.state === 3;
|
||||
term = new Terminal({
|
||||
convertEol: true,
|
||||
disableStdin: false,
|
||||
cursorBlink: showCursor,
|
||||
cursorStyle: "block",
|
||||
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 toolbarColor = computed(() => {
|
||||
if (props.data.state.state == 3) {
|
||||
return;
|
||||
}
|
||||
return "red";
|
||||
});
|
||||
|
||||
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
|
||||
:color="toolbarColor"
|
||||
dark
|
||||
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>
|
8
resources/src/configs/menus/task.menus.ts
Normal file
8
resources/src/configs/menus/task.menus.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export default [
|
||||
{
|
||||
icon: "mdi-application-braces",
|
||||
name: "task-page",
|
||||
key: "menu.task",
|
||||
link: "/task",
|
||||
},
|
||||
];
|
@@ -5,8 +5,8 @@ import menuCharts from "./menus/charts.menu";
|
||||
import menuUML from "./menus/uml.menu";
|
||||
import menuLanding from "./menus/landing.menu";
|
||||
import menuData from "./menus/data.menu";
|
||||
import menuAi from "./menus/ai.menu";
|
||||
import menuProcess from "./menus/process.menus"
|
||||
import menuProcess from "./menus/process.menus";
|
||||
import menuTask from "./menus/task.menus";
|
||||
|
||||
export default {
|
||||
menu: [
|
||||
@@ -26,6 +26,10 @@ export default {
|
||||
text: "process",
|
||||
items: menuProcess,
|
||||
},
|
||||
{
|
||||
text: "task",
|
||||
items: menuTask,
|
||||
},
|
||||
{
|
||||
text: "Apps",
|
||||
items: menuApps,
|
||||
|
@@ -14,7 +14,6 @@ import VueVirtualScroller from "vue-virtual-scroller";
|
||||
import "vue-virtual-scroller/dist/vue-virtual-scroller.css";
|
||||
import VueApexCharts from "vue3-apexcharts";
|
||||
import piniaPersist from "pinia-plugin-persist";
|
||||
import { PerfectScrollbarPlugin } from 'vue3-perfect-scrollbar';
|
||||
import 'vue3-perfect-scrollbar/style.css';
|
||||
import "@/styles/main.scss";
|
||||
import router from "./router";
|
||||
@@ -31,7 +30,6 @@ const app = createApp(App);
|
||||
app.config.globalProperties.$echarts = echarts;
|
||||
app.directive('permission', permission);
|
||||
app.use(router);
|
||||
app.use(PerfectScrollbarPlugin);
|
||||
app.use(MasonryWall);
|
||||
app.use(VueVirtualScroller);
|
||||
app.use(VueApexCharts);
|
||||
|
@@ -10,6 +10,7 @@ import UmlRoutes from "./uml.routes";
|
||||
import AppsRoutes from "./apps.routes";
|
||||
import DataRoutes from "./data.routes";
|
||||
import ProcessRoutes from "./process.routes";
|
||||
import TaskRoutes from "./task.routes";
|
||||
|
||||
export const routes = [
|
||||
{
|
||||
@@ -41,7 +42,8 @@ export const routes = [
|
||||
...UmlRoutes,
|
||||
...AppsRoutes,
|
||||
...DataRoutes,
|
||||
...ProcessRoutes
|
||||
...ProcessRoutes,
|
||||
...TaskRoutes
|
||||
];
|
||||
|
||||
// 动态路由,基于用户权限动态去加载
|
||||
|
12
resources/src/router/task.routes.ts
Normal file
12
resources/src/router/task.routes.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
// users Data Page
|
||||
export default [
|
||||
{
|
||||
path: "/task",
|
||||
component: () => import("@/views/task/Task.vue"),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
layout: "landing",
|
||||
category: "Data",
|
||||
},
|
||||
},
|
||||
];
|
@@ -1,34 +1,49 @@
|
||||
export interface ProcessItem {
|
||||
name: string;
|
||||
uuid: number;
|
||||
startTime: Date;
|
||||
user: string;
|
||||
usage: Usage;
|
||||
state: State;
|
||||
termType: TermType;
|
||||
cgroupEnable: boolean;
|
||||
memoryLimit: number | null;
|
||||
cpuLimit: number | null;
|
||||
name: string;
|
||||
uuid: number;
|
||||
startTime: Date;
|
||||
user: string;
|
||||
usage: Usage;
|
||||
state: State;
|
||||
termType: TermType;
|
||||
cgroupEnable: boolean;
|
||||
memoryLimit: number | null;
|
||||
cpuLimit: number | null;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
state: number;
|
||||
info: Info;
|
||||
state: number;
|
||||
info: Info;
|
||||
}
|
||||
|
||||
export enum Info {
|
||||
Empty = "",
|
||||
重启次数异常 = "重启次数异常",
|
||||
Empty = "",
|
||||
重启次数异常 = "重启次数异常",
|
||||
}
|
||||
|
||||
export enum TermType {
|
||||
Pty = "pty",
|
||||
Pty = "pty",
|
||||
}
|
||||
|
||||
export interface Usage {
|
||||
cpuCapacity: number;
|
||||
memCapacity: number;
|
||||
cpu: number[] | null;
|
||||
mem: number[] | null;
|
||||
time: string[] | null;
|
||||
cpuCapacity: number;
|
||||
memCapacity: number;
|
||||
cpu: number[] | null;
|
||||
mem: number[] | 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;
|
||||
}
|
20
resources/src/types/tassk/task.ts
Normal file
20
resources/src/types/tassk/task.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export interface TaskItem {
|
||||
id: number;
|
||||
name: string;
|
||||
processId: number;
|
||||
condition: number;
|
||||
nextId: null;
|
||||
operation: number;
|
||||
triggerEvent: null;
|
||||
triggerTarget: null;
|
||||
operationTarget: number;
|
||||
cron: string;
|
||||
enable: boolean;
|
||||
apiEnable: boolean;
|
||||
key: string;
|
||||
processName: string;
|
||||
targetName: string;
|
||||
triggerName: string;
|
||||
startTime: Date;
|
||||
running: boolean;
|
||||
}
|
@@ -1,48 +1,139 @@
|
||||
<template>
|
||||
<!-- <div class="toolbar">
|
||||
<ConfirmButton @confirm="startAll" color="#3CB371">全部启动</ConfirmButton>
|
||||
<ConfirmButton @confirm="killAll" color="#CD5555">全部停止</ConfirmButton>
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="tonal"
|
||||
color="blue"
|
||||
@click="processCreateComponent?.createProcessDialog()"
|
||||
>创建<v-icon dark right> mdi-plus-circle </v-icon>
|
||||
</v-btn>
|
||||
</div> -->
|
||||
<v-container>
|
||||
<!-- 顶部工具栏 -->
|
||||
|
||||
<!-- 主体网格 -->
|
||||
<div class="flex-grid">
|
||||
<div v-for="(i, v) in processData" class="responsive-box">
|
||||
<ProcessCard :data="i" :index="v"></ProcessCard>
|
||||
<div v-for="(i, v) in processData" :key="i.uuid" class="responsive-box">
|
||||
<ProcessCard :data="i" :index="v" />
|
||||
</div>
|
||||
</div>
|
||||
</v-container>
|
||||
<ProcessCreate ref="processCreateComponent"></ProcessCreate>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ProcessCard from "@/components/process/ProcessCard.vue";
|
||||
import { getProcessList } from "~/src/api/process";
|
||||
import axios from "axios";
|
||||
import {
|
||||
getProcessList,
|
||||
killProcessAll,
|
||||
startProcessAll,
|
||||
} from "~/src/api/process";
|
||||
import ProcessCreate from "~/src/components/process/ProcessCreate.vue";
|
||||
import { useSnackbarStore } from "~/src/stores/snackbarStore";
|
||||
import { ProcessItem } from "~/src/types/process/process";
|
||||
|
||||
type CreateHandle = {
|
||||
createProcessDialog: () => void;
|
||||
test: () => void;
|
||||
};
|
||||
const processCreateComponent = ref<CreateHandle | null>(null);
|
||||
const processData = ref<ProcessItem[]>();
|
||||
|
||||
const uuid: string = crypto.randomUUID();
|
||||
const snackbarStore = useSnackbarStore();
|
||||
const initProcessData = () => {
|
||||
getProcessList().then((e) => {
|
||||
processData.value = e.data!;
|
||||
console.log(e.data);
|
||||
processData.value = e.data!.sort((a, b) => a.name.localeCompare(b.name));
|
||||
getProcessListWait();
|
||||
});
|
||||
};
|
||||
|
||||
const startAll = () => {
|
||||
startProcessAll().then((e) => {
|
||||
if (e.code === 0) {
|
||||
snackbarStore.showSuccessMessage("sucess");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const killAll = () => {
|
||||
killProcessAll().then((e) => {
|
||||
if (e.code === 0) {
|
||||
snackbarStore.showSuccessMessage("sucess");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
let cancelTokenSource: any;
|
||||
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(() => {
|
||||
initProcessData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 工具栏样式 */
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end; /* 靠右对齐 */
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
/* 原来的网格样式 */
|
||||
.flex-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap; /* 自动换行 */
|
||||
justify-content: space-between; /* 两边与中间间距均匀 */
|
||||
gap: 50px; /* 每个 div 之间的间距 */
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 80px;
|
||||
}
|
||||
|
||||
.responsive-box {
|
||||
flex: 1 1 300px; /* 最小宽度 500px */
|
||||
min-width: 300px; /* 强制最小宽度 */
|
||||
max-width: 100%; /* 不超过容器宽度 */
|
||||
background: #f5f5f5;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
flex: 1 1 300px;
|
||||
min-width: 300px;
|
||||
max-width: 100%;
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
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);
|
||||
}
|
||||
|
||||
.fab {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
z-index: 999;
|
||||
border-radius: 50%;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
</style>
|
||||
|
469
resources/src/views/task/Task.vue
Normal file
469
resources/src/views/task/Task.vue
Normal file
@@ -0,0 +1,469 @@
|
||||
<template>
|
||||
<v-card class="pa-4">
|
||||
<v-card-title class="d-flex justify-space-between align-center">
|
||||
<span>任务</span>
|
||||
<v-btn color="primary" variant="tonal" @click="addTaskBefore">
|
||||
<v-icon left>mdi-plus</v-icon> 新建任务
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="taskData"
|
||||
:items-per-page="10"
|
||||
item-key="id"
|
||||
class="elevation-1"
|
||||
>
|
||||
<!-- 自定义列渲染 -->
|
||||
<template #item.nextId="{ item }">
|
||||
<span>{{ item.nextId === null ? "-" : item.nextId }}</span>
|
||||
</template>
|
||||
|
||||
<template #item.enable="{ item }">
|
||||
<v-switch
|
||||
color="primary"
|
||||
@change="changeEnable(item)"
|
||||
v-model="item.enable"
|
||||
></v-switch>
|
||||
</template>
|
||||
|
||||
<template #item.apiEnable="{ item }">
|
||||
<v-switch
|
||||
color="primary"
|
||||
@change="edit(item)"
|
||||
v-model="item.apiEnable"
|
||||
></v-switch>
|
||||
</template>
|
||||
|
||||
<template #item.running="{ item }">
|
||||
<svg
|
||||
v-if="item.running"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 48 48"
|
||||
fill="#000000"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M25 34a1 1 0 011 1v10a1 1 0 01-1 1h-2a1 1 0 01-1-1V35a1 1 0 011-1h2zm8.192-3.636l7.072 7.071a1 1 0 010 1.414l-1.415 1.415a1 1 0 01-1.414 0l-7.071-7.072a1 1 0 010-1.414l1.414-1.414a1 1 0 011.414 0zm-16.97 0l1.414 1.414a1 1 0 010 1.414l-7.071 7.072a1 1 0 01-1.414 0l-1.414-1.415a1 1 0 010-1.414l7.07-7.071a1 1 0 011.415 0zM45 22a1 1 0 011 1v2a1 1 0 01-1 1H35a1 1 0 01-1-1v-2a1 1 0 011-1h10zm-32 0a1 1 0 011 1v2a1 1 0 01-1 1H3a1 1 0 01-1-1v-2a1 1 0 011-1h10zM10.565 7.737l7.071 7.07a1 1 0 010 1.415l-1.414 1.414a1 1 0 01-1.414 0l-7.071-7.071a1 1 0 010-1.414L9.15 7.737a1 1 0 011.414 0zm28.284 0l1.415 1.414a1 1 0 010 1.414l-7.072 7.071a1 1 0 01-1.414 0l-1.414-1.414a1 1 0 010-1.414l7.071-7.071a1 1 0 011.414 0zM25 2a1 1 0 011 1v10a1 1 0 01-1 1h-2a1 1 0 01-1-1V3a1 1 0 011-1h2z"
|
||||
fill="#000000"
|
||||
/>
|
||||
</svg>
|
||||
<svg v-else width="20" height="20" viewBox="0 0 48 48" fill="#000000">
|
||||
<path
|
||||
d="M42.02 12.71l-1.38-1.42a1 1 0 00-1.41 0L18.01 32.5l-9.9-9.89a1 1 0 00-1.41 0l-1.41 1.41a1 1 0 000 1.41l10.6 10.61 1.42 1.41a1 1 0 001.41 0l1.41-1.41 21.92-21.92a1 1 0 00-.03-1.41z"
|
||||
fill="#000000"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<template #item.startTime="{ item }">
|
||||
<span>{{ formatStartTime(item.startTime) }}</span>
|
||||
</template>
|
||||
|
||||
<template #item.key="{ item }">
|
||||
<code>{{ item.key }}</code>
|
||||
</template>
|
||||
|
||||
<!-- 如果需要可以加 actions 列 -->
|
||||
<template #item.operate="{ item }">
|
||||
<!-- 这里可以放底部说明或按钮 -->
|
||||
<v-icon class="mr-2" v-if="!item.running"> mdi-play </v-icon>
|
||||
<v-icon class="mr-2" v-else> mdi-stop </v-icon>
|
||||
<v-icon class="mr-2" @click="editTaskBefore(item)"> mdi-pencil </v-icon>
|
||||
<v-icon> mdi-delete </v-icon>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card>
|
||||
|
||||
<v-dialog v-model="taskDialog" max-width="600px">
|
||||
<v-card class="rounded-xl">
|
||||
<!-- 标题 -->
|
||||
<v-card-title class="text-h6 font-weight-medium">
|
||||
{{ isAdd ? "添加任务" : "修改任务" }}
|
||||
</v-card-title>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<!-- 表单内容 -->
|
||||
<v-card-text class="pt-6">
|
||||
<v-container fluid>
|
||||
<v-row dense>
|
||||
<v-col cols="12" sm="12">
|
||||
<v-text-field
|
||||
label="任务名"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
v-model="taskForm.name"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6">
|
||||
<v-autocomplete
|
||||
label="判断目标"
|
||||
item-title="name"
|
||||
item-value="value"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
:items="processSelect"
|
||||
v-model="taskForm.processId"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6">
|
||||
<v-autocomplete
|
||||
:disabled="taskForm.processId == null"
|
||||
label="判断条件"
|
||||
item-title="name"
|
||||
item-value="value"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
:items="conditionSelect"
|
||||
v-model="taskForm.condition"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6">
|
||||
<v-autocomplete
|
||||
label="操作目标"
|
||||
item-title="name"
|
||||
item-value="value"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
:items="processSelect"
|
||||
v-model="taskForm.operationTarget"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6">
|
||||
<v-autocomplete
|
||||
:disabled="taskForm.operationTarget == null"
|
||||
label="执行操作"
|
||||
item-title="name"
|
||||
item-value="value"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
:items="operationSelect"
|
||||
v-model="taskForm.operation"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6">
|
||||
<v-autocomplete
|
||||
label="触发目标"
|
||||
item-title="name"
|
||||
item-value="value"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
:items="processSelect"
|
||||
v-model="taskForm.triggerTarget"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6">
|
||||
<v-autocomplete
|
||||
:disabled="taskForm.triggerTarget == null"
|
||||
label="触发事件"
|
||||
item-title="name"
|
||||
item-value="value"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
:items="eventSelect"
|
||||
v-model="taskForm.triggerEvent"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6">
|
||||
<v-autocomplete
|
||||
label="后续任务"
|
||||
item-title="name"
|
||||
item-value="value"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
:items="taskSelect"
|
||||
v-model="taskForm.nextId"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
label="定时任务"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
v-model="taskForm.cron"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" v-if="!isAdd">
|
||||
<v-text-field
|
||||
label="API"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
readonly
|
||||
v-model="apiUrl"
|
||||
append-inner-icon="mdi-content-copy"
|
||||
@click:append-inner="copyToClipboard"
|
||||
>
|
||||
<!-- 把按钮放进输入框右侧 -->
|
||||
<template v-slot:append>
|
||||
<v-btn
|
||||
v-if="taskForm?.key == null"
|
||||
@click="changeApi"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
icon="mdi-plus"
|
||||
>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-else
|
||||
@click="changeApi"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
icon="mdi-refresh"
|
||||
>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<v-card-actions class="justify-end pa-4">
|
||||
<v-btn text @click="taskDialog = false">取消</v-btn>
|
||||
<v-btn color="primary" @click="submit">确认</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { ref, onMounted } from "vue";
|
||||
import { getProcessList } from "~/src/api/process";
|
||||
import {
|
||||
addTask,
|
||||
changeTaskKey,
|
||||
editTask,
|
||||
editTaskEnable,
|
||||
getTaskAll,
|
||||
getTaskById,
|
||||
} from "~/src/api/task";
|
||||
import { useSnackbarStore } from "~/src/stores/snackbarStore";
|
||||
import { TaskItem } from "~/src/types/tassk/task";
|
||||
|
||||
const snackbarStore = useSnackbarStore();
|
||||
|
||||
// 弹窗 & 表单
|
||||
const taskDialog = ref(false);
|
||||
const taskForm = ref<Partial<TaskItem>>({});
|
||||
const isAdd = ref(false);
|
||||
|
||||
// 下拉框选项
|
||||
const taskSelect = ref<any[]>([]);
|
||||
const processSelect = ref<any[]>([]);
|
||||
const eventSelect = ref<any[]>([]);
|
||||
const operationSelect = ref<any[]>([]);
|
||||
const conditionSelect = ref<any[]>([]);
|
||||
|
||||
// 映射表
|
||||
const conditionMap = {
|
||||
0: "运行中",
|
||||
1: "已停止",
|
||||
2: "错误",
|
||||
3: "无条件",
|
||||
};
|
||||
const operationMap = {
|
||||
0: "异步启动",
|
||||
1: "异步停止",
|
||||
2: "完成启动",
|
||||
3: "完成停止",
|
||||
};
|
||||
const eventMap = {
|
||||
0: "停止",
|
||||
1: "启动",
|
||||
2: "异常",
|
||||
};
|
||||
|
||||
const urlBase = ref(`${window.location.origin}/api/task/api-key/`);
|
||||
|
||||
// 表头
|
||||
const headers = [
|
||||
{ title: "任务ID", key: "id" },
|
||||
{ title: "任务名", key: "name" },
|
||||
{ title: "下一步 ID", key: "nextId" },
|
||||
{ title: "定时任务", key: "cron" },
|
||||
{ title: "开始时间", key: "startTime" },
|
||||
{ title: "状态", key: "running" },
|
||||
{ title: "启用定时任务", key: "enable" },
|
||||
{ title: "启用API", key: "apiEnable" },
|
||||
{
|
||||
title: "操作",
|
||||
key: "operate",
|
||||
headerProps: { style: "min-width: 150px;" },
|
||||
},
|
||||
];
|
||||
const apiUrl = computed(() =>
|
||||
taskForm.value?.key ? urlBase.value + taskForm.value.key : "未创建api"
|
||||
);
|
||||
// 任务数据
|
||||
const taskData = ref<TaskItem[]>([]);
|
||||
|
||||
// 格式化时间
|
||||
const formatStartTime = (v: Date) => {
|
||||
if (!v) return "-";
|
||||
try {
|
||||
if (Number.isNaN(v.getTime())) return v;
|
||||
return v.toLocaleString();
|
||||
} catch {
|
||||
return v;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initTask();
|
||||
});
|
||||
|
||||
// 打开添加任务弹窗
|
||||
const addTaskBefore = () => {
|
||||
isAdd.value = true;
|
||||
taskForm.value = {}; // 清空表单
|
||||
taskDialog.value = true;
|
||||
};
|
||||
|
||||
// 打开编辑任务弹窗
|
||||
const editTaskBefore = (row: TaskItem) => {
|
||||
isAdd.value = false;
|
||||
getTaskById(row.id).then((res) => {
|
||||
taskForm.value = res.data ?? {};
|
||||
});
|
||||
taskDialog.value = true;
|
||||
};
|
||||
|
||||
// 复制 API 地址
|
||||
const copyToClipboard = () => {
|
||||
if (!taskForm.value?.key) return;
|
||||
navigator.clipboard
|
||||
.writeText(urlBase.value + taskForm.value.key)
|
||||
.then(() => {
|
||||
snackbarStore.showSuccessMessage("复制成功");
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("复制失败:", err);
|
||||
});
|
||||
};
|
||||
|
||||
// 初始化任务 & 下拉框选项
|
||||
const initTask = () => {
|
||||
getTaskAll().then((res) => {
|
||||
const list = res.data ?? [];
|
||||
taskData.value = list;
|
||||
|
||||
// 任务选择
|
||||
taskSelect.value = list.map((t) => ({
|
||||
name: t.id,
|
||||
value: t.id,
|
||||
}));
|
||||
taskSelect.value.push({ name: "无", value: null });
|
||||
});
|
||||
|
||||
// 操作/事件/条件
|
||||
operationSelect.value = Object.entries(operationMap).map(([value, name]) => ({
|
||||
name,
|
||||
value: parseInt(value),
|
||||
}));
|
||||
|
||||
eventSelect.value = Object.entries(eventMap).map(([value, name]) => ({
|
||||
name,
|
||||
value: parseInt(value),
|
||||
}));
|
||||
|
||||
conditionSelect.value = Object.entries(conditionMap).map(([value, name]) => ({
|
||||
name,
|
||||
value: parseInt(value),
|
||||
}));
|
||||
getProcessList().then((resp) => {
|
||||
if (resp.code == 0) {
|
||||
processSelect.value = resp.data!.map((e) => {
|
||||
return {
|
||||
name: e.name,
|
||||
value: e.uuid,
|
||||
};
|
||||
});
|
||||
processSelect.value.push({
|
||||
name: "无",
|
||||
value: null,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 修改任务
|
||||
const edit = (item: TaskItem) => {
|
||||
editTask(item).then((res) => {
|
||||
if (res.code === 0) {
|
||||
snackbarStore.showSuccessMessage("修改成功");
|
||||
initTask();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 切换启用状态
|
||||
const changeEnable = (item: TaskItem) => {
|
||||
editTaskEnable({ id: item.id, enable: item.enable }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
snackbarStore.showSuccessMessage("修改成功");
|
||||
initTask();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 生成/刷新 API Key
|
||||
const changeApi = () => {
|
||||
if (!taskForm.value?.id) return;
|
||||
changeTaskKey(taskForm.value.id).then((res) => {
|
||||
if (res.code === 0) {
|
||||
snackbarStore.showSuccessMessage("API 更新成功");
|
||||
getTaskById(taskForm.value?.id).then((e) => {
|
||||
Object.assign(taskForm.value, e.data);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 提交
|
||||
const submit = () => {
|
||||
if (isAdd.value) {
|
||||
addTask(taskForm.value).then((res) => {
|
||||
if (res.code === 0) {
|
||||
taskDialog.value = false;
|
||||
snackbarStore.showSuccessMessage("添加成功");
|
||||
initTask();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
editTask(taskForm.value).then((res) => {
|
||||
if (res.code === 0) {
|
||||
taskDialog.value = false;
|
||||
snackbarStore.showSuccessMessage("修改成功");
|
||||
initTask();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
code {
|
||||
font-size: 0.85rem;
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user