mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-09-12 09:34:58 +08:00
feat: 终端管理 tabs 多终端实现
This commit is contained in:
parent
555bcd4316
commit
31c71b1d62
24 changed files with 790 additions and 35 deletions
|
@ -10,5 +10,6 @@ var ApiGroupApp = new(ApiGroup)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
userService = service.ServiceGroupApp.UserService
|
userService = service.ServiceGroupApp.UserService
|
||||||
|
hostService = service.ServiceGroupApp.HostService
|
||||||
operationService = service.ServiceGroupApp.OperationService
|
operationService = service.ServiceGroupApp.OperationService
|
||||||
)
|
)
|
||||||
|
|
95
backend/app/api/v1/host.go
Normal file
95
backend/app/api/v1/host.go
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
package v1
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/1Panel-dev/1Panel/app/api/v1/helper"
|
||||||
|
"github.com/1Panel-dev/1Panel/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/global"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (b *BaseApi) Create(c *gin.Context) {
|
||||||
|
var req dto.HostCreate
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := global.VALID.Struct(req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
host, err := hostService.Create(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, host)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseApi) PageHosts(c *gin.Context) {
|
||||||
|
var req dto.PageInfo
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
total, list, err := hostService.Page(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
helper.SuccessWithData(c, dto.PageResult{
|
||||||
|
Items: list,
|
||||||
|
Total: total,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseApi) DeleteHost(c *gin.Context) {
|
||||||
|
var req dto.BatchDeleteReq
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := global.VALID.Struct(req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := hostService.BatchDelete(req.Ids); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseApi) UpdateHost(c *gin.Context) {
|
||||||
|
var req dto.HostUpdate
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := global.VALID.Struct(req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, err := helper.GetParamID(c)
|
||||||
|
if err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
upMap := make(map[string]interface{})
|
||||||
|
upMap["name"] = req.Name
|
||||||
|
upMap["addr"] = req.Addr
|
||||||
|
upMap["port"] = req.Port
|
||||||
|
upMap["user"] = req.User
|
||||||
|
upMap["auth_mode"] = req.AuthMode
|
||||||
|
upMap["password"] = req.Password
|
||||||
|
upMap["private_key"] = req.PrivateKey
|
||||||
|
if err := hostService.Update(id, upMap); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, nil)
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/global"
|
"github.com/1Panel-dev/1Panel/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/utils/copier"
|
||||||
"github.com/1Panel-dev/1Panel/utils/ssh"
|
"github.com/1Panel-dev/1Panel/utils/ssh"
|
||||||
"github.com/1Panel-dev/1Panel/utils/terminal"
|
"github.com/1Panel-dev/1Panel/utils/terminal"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
@ -13,14 +14,6 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (b *BaseApi) WsSsh(c *gin.Context) {
|
func (b *BaseApi) WsSsh(c *gin.Context) {
|
||||||
host := ssh.ConnInfo{
|
|
||||||
Addr: "172.16.10.111",
|
|
||||||
Port: 22,
|
|
||||||
User: "root",
|
|
||||||
AuthMode: "password",
|
|
||||||
Password: "Calong@2015",
|
|
||||||
}
|
|
||||||
|
|
||||||
wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil)
|
wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
global.LOG.Errorf("gin context http handler failed, err: %v", err)
|
global.LOG.Errorf("gin context http handler failed, err: %v", err)
|
||||||
|
@ -28,6 +21,20 @@ func (b *BaseApi) WsSsh(c *gin.Context) {
|
||||||
}
|
}
|
||||||
defer wsConn.Close()
|
defer wsConn.Close()
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(c.Query("id"))
|
||||||
|
if wshandleError(wsConn, err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
host, err := hostService.GetConnInfo(uint(id))
|
||||||
|
if wshandleError(wsConn, err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var connInfo ssh.ConnInfo
|
||||||
|
err = copier.Copy(&connInfo, &host)
|
||||||
|
if wshandleError(wsConn, err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
cols, err := strconv.Atoi(c.DefaultQuery("cols", "80"))
|
cols, err := strconv.Atoi(c.DefaultQuery("cols", "80"))
|
||||||
if wshandleError(wsConn, err) {
|
if wshandleError(wsConn, err) {
|
||||||
return
|
return
|
||||||
|
@ -37,18 +44,18 @@ func (b *BaseApi) WsSsh(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
client, err := host.NewClient()
|
client, err := connInfo.NewClient()
|
||||||
if wshandleError(wsConn, err) {
|
if wshandleError(wsConn, err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
ssConn, err := host.NewSshConn(cols, rows)
|
ssConn, err := connInfo.NewSshConn(cols, rows)
|
||||||
if wshandleError(wsConn, err) {
|
if wshandleError(wsConn, err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer ssConn.Close()
|
defer ssConn.Close()
|
||||||
|
|
||||||
sws, err := terminal.NewLogicSshWsSession(cols, rows, true, host.Client, wsConn)
|
sws, err := terminal.NewLogicSshWsSession(cols, rows, true, connInfo.Client, wsConn)
|
||||||
if wshandleError(wsConn, err) {
|
if wshandleError(wsConn, err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
39
backend/app/dto/host.go
Normal file
39
backend/app/dto/host.go
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
package dto
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type HostCreate struct {
|
||||||
|
Name string `json:"name" validate:"required,name"`
|
||||||
|
Addr string `json:"addr" validate:"required,ip"`
|
||||||
|
Port uint `json:"port" validate:"required,number,max=65535,min=1"`
|
||||||
|
User string `json:"user" validate:"required"`
|
||||||
|
AuthMode string `json:"authMode" validate:"oneof=password key"`
|
||||||
|
PrivateKey string `json:"privateKey"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HostInfo struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Addr string `json:"addr"`
|
||||||
|
Port uint `json:"port"`
|
||||||
|
User string `json:"user"`
|
||||||
|
AuthMode string `json:"authMode"`
|
||||||
|
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HostUpdate struct {
|
||||||
|
Name string `json:"name" validate:"required,name"`
|
||||||
|
Addr string `json:"addr" validate:"required,ip"`
|
||||||
|
Port uint `json:"port" validate:"required,number,max=65535,min=1"`
|
||||||
|
User string `json:"user" validate:"required"`
|
||||||
|
AuthMode string `json:"authMode" validate:"oneof=password key"`
|
||||||
|
PrivateKey string `json:"privateKey"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
|
@ -26,7 +26,7 @@ type UserUpdate struct {
|
||||||
Email string `json:"email" validate:"required,email"`
|
Email string `json:"email" validate:"required,email"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserBack struct {
|
type UserInfo struct {
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
|
|
16
backend/app/model/host.go
Normal file
16
backend/app/model/host.go
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
import "gorm.io/gorm"
|
||||||
|
|
||||||
|
type Host struct {
|
||||||
|
gorm.Model
|
||||||
|
Name string `gorm:"type:varchar(64);unique;not null" json:"name"`
|
||||||
|
Addr string `gorm:"type:varchar(16);unique;not null" json:"addr"`
|
||||||
|
Port int `gorm:"type:varchar(5);not null" json:"port"`
|
||||||
|
User string `gorm:"type:varchar(64);not null" json:"user"`
|
||||||
|
AuthMode string `gorm:"type:varchar(16);not null" json:"authMode"`
|
||||||
|
Password string `gorm:"type:varchar(64)" json:"password"`
|
||||||
|
PrivateKey string `gorm:"type:varchar(256)" json:"privateKey"`
|
||||||
|
|
||||||
|
Description string `gorm:"type:varchar(256)" json:"description"`
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ package repo
|
||||||
|
|
||||||
type RepoGroup struct {
|
type RepoGroup struct {
|
||||||
UserRepo
|
UserRepo
|
||||||
|
HostRepo
|
||||||
OperationRepo
|
OperationRepo
|
||||||
CommonRepo
|
CommonRepo
|
||||||
}
|
}
|
||||||
|
|
58
backend/app/repo/host.go
Normal file
58
backend/app/repo/host.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/1Panel-dev/1Panel/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/global"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HostRepo struct{}
|
||||||
|
|
||||||
|
type IHostRepo interface {
|
||||||
|
Get(opts ...DBOption) (model.Host, error)
|
||||||
|
Page(limit, offset int, opts ...DBOption) (int64, []model.Host, error)
|
||||||
|
Create(host *model.Host) error
|
||||||
|
Update(id uint, vars map[string]interface{}) error
|
||||||
|
Delete(opts ...DBOption) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIHostService() IHostRepo {
|
||||||
|
return &HostRepo{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *HostRepo) Get(opts ...DBOption) (model.Host, error) {
|
||||||
|
var host model.Host
|
||||||
|
db := global.DB
|
||||||
|
for _, opt := range opts {
|
||||||
|
db = opt(db)
|
||||||
|
}
|
||||||
|
err := db.First(&host).Error
|
||||||
|
return host, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *HostRepo) Page(page, size int, opts ...DBOption) (int64, []model.Host, error) {
|
||||||
|
var hosts []model.Host
|
||||||
|
db := global.DB.Model(&model.Host{})
|
||||||
|
for _, opt := range opts {
|
||||||
|
db = opt(db)
|
||||||
|
}
|
||||||
|
count := int64(0)
|
||||||
|
db = db.Count(&count)
|
||||||
|
err := db.Limit(size).Offset(size * (page - 1)).Find(&hosts).Error
|
||||||
|
return count, hosts, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *HostRepo) Create(host *model.Host) error {
|
||||||
|
return global.DB.Create(host).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *HostRepo) Update(id uint, vars map[string]interface{}) error {
|
||||||
|
return global.DB.Model(&model.Host{}).Where("id = ?", id).Updates(vars).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *HostRepo) Delete(opts ...DBOption) error {
|
||||||
|
db := global.DB
|
||||||
|
for _, opt := range opts {
|
||||||
|
db = opt(db)
|
||||||
|
}
|
||||||
|
return db.Delete(&model.Host{}).Error
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import "github.com/1Panel-dev/1Panel/app/repo"
|
||||||
|
|
||||||
type ServiceGroup struct {
|
type ServiceGroup struct {
|
||||||
UserService
|
UserService
|
||||||
|
HostService
|
||||||
OperationService
|
OperationService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,6 +12,7 @@ var ServiceGroupApp = new(ServiceGroup)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
userRepo = repo.RepoGroupApp.UserRepo
|
userRepo = repo.RepoGroupApp.UserRepo
|
||||||
|
hostRepo = repo.RepoGroupApp.HostRepo
|
||||||
operationRepo = repo.RepoGroupApp.OperationRepo
|
operationRepo = repo.RepoGroupApp.OperationRepo
|
||||||
commonRepo = repo.RepoGroupApp.CommonRepo
|
commonRepo = repo.RepoGroupApp.CommonRepo
|
||||||
)
|
)
|
||||||
|
|
76
backend/app/service/host.go
Normal file
76
backend/app/service/host.go
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/1Panel-dev/1Panel/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/constant"
|
||||||
|
"github.com/jinzhu/copier"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HostService struct{}
|
||||||
|
|
||||||
|
type IHostService interface {
|
||||||
|
GetConnInfo(id uint) (*model.Host, error)
|
||||||
|
Page(search dto.PageInfo) (int64, interface{}, error)
|
||||||
|
Create(hostDto dto.HostCreate) (*dto.HostInfo, error)
|
||||||
|
Update(id uint, upMap map[string]interface{}) error
|
||||||
|
BatchDelete(ids []uint) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIHostService() IHostService {
|
||||||
|
return &HostService{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *HostService) GetConnInfo(id uint) (*model.Host, error) {
|
||||||
|
host, err := hostRepo.Get(commonRepo.WithByID(id))
|
||||||
|
if err != nil {
|
||||||
|
return nil, constant.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
return &host, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *HostService) Page(search dto.PageInfo) (int64, interface{}, error) {
|
||||||
|
total, hosts, err := hostRepo.Page(search.Page, search.PageSize)
|
||||||
|
var dtoHosts []dto.HostInfo
|
||||||
|
for _, host := range hosts {
|
||||||
|
var item dto.HostInfo
|
||||||
|
if err := copier.Copy(&item, &host); err != nil {
|
||||||
|
return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error())
|
||||||
|
}
|
||||||
|
dtoHosts = append(dtoHosts, item)
|
||||||
|
}
|
||||||
|
return total, dtoHosts, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *HostService) Create(hostDto dto.HostCreate) (*dto.HostInfo, error) {
|
||||||
|
host, _ := hostRepo.Get(commonRepo.WithByName(hostDto.Name))
|
||||||
|
if host.ID != 0 {
|
||||||
|
return nil, constant.ErrRecordExist
|
||||||
|
}
|
||||||
|
if err := copier.Copy(&host, &hostDto); err != nil {
|
||||||
|
return nil, errors.WithMessage(constant.ErrStructTransform, err.Error())
|
||||||
|
}
|
||||||
|
if err := hostRepo.Create(&host); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var hostinfo dto.HostInfo
|
||||||
|
if err := copier.Copy(&hostinfo, &host); err != nil {
|
||||||
|
return nil, errors.WithMessage(constant.ErrStructTransform, err.Error())
|
||||||
|
}
|
||||||
|
return &hostinfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *HostService) BatchDelete(ids []uint) error {
|
||||||
|
if len(ids) == 1 {
|
||||||
|
host, _ := hostRepo.Get(commonRepo.WithByID(ids[0]))
|
||||||
|
if host.ID == 0 {
|
||||||
|
return constant.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hostRepo.Delete(commonRepo.WithIdsIn(ids))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *HostService) Update(id uint, upMap map[string]interface{}) error {
|
||||||
|
return hostRepo.Update(id, upMap)
|
||||||
|
}
|
|
@ -56,7 +56,7 @@ func (u *OperationService) BatchDelete(ids []uint) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func filterSensitive(vars string) string {
|
func filterSensitive(vars string) string {
|
||||||
var Sensitives = []string{"password", "Password"}
|
var Sensitives = []string{"password", "Password", "privateKey"}
|
||||||
ops := make(map[string]interface{})
|
ops := make(map[string]interface{})
|
||||||
if err := json.Unmarshal([]byte(vars), &ops); err != nil {
|
if err := json.Unmarshal([]byte(vars), &ops); err != nil {
|
||||||
return vars
|
return vars
|
||||||
|
|
|
@ -17,7 +17,7 @@ import (
|
||||||
type UserService struct{}
|
type UserService struct{}
|
||||||
|
|
||||||
type IUserService interface {
|
type IUserService interface {
|
||||||
Get(name uint) (*dto.UserBack, error)
|
Get(id uint) (*dto.UserInfo, error)
|
||||||
Page(search dto.UserPage) (int64, interface{}, error)
|
Page(search dto.UserPage) (int64, interface{}, error)
|
||||||
Register(userDto dto.UserCreate) error
|
Register(userDto dto.UserCreate) error
|
||||||
Login(c *gin.Context, info dto.Login) (*dto.UserLoginInfo, error)
|
Login(c *gin.Context, info dto.Login) (*dto.UserLoginInfo, error)
|
||||||
|
@ -32,12 +32,12 @@ func NewIUserService() IUserService {
|
||||||
return &UserService{}
|
return &UserService{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UserService) Get(id uint) (*dto.UserBack, error) {
|
func (u *UserService) Get(id uint) (*dto.UserInfo, error) {
|
||||||
user, err := userRepo.Get(commonRepo.WithByID(id))
|
user, err := userRepo.Get(commonRepo.WithByID(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, constant.ErrRecordNotFound
|
return nil, constant.ErrRecordNotFound
|
||||||
}
|
}
|
||||||
var dtoUser dto.UserBack
|
var dtoUser dto.UserInfo
|
||||||
if err := copier.Copy(&dtoUser, &user); err != nil {
|
if err := copier.Copy(&dtoUser, &user); err != nil {
|
||||||
return nil, errors.WithMessage(constant.ErrStructTransform, err.Error())
|
return nil, errors.WithMessage(constant.ErrStructTransform, err.Error())
|
||||||
}
|
}
|
||||||
|
@ -46,9 +46,9 @@ func (u *UserService) Get(id uint) (*dto.UserBack, error) {
|
||||||
|
|
||||||
func (u *UserService) Page(search dto.UserPage) (int64, interface{}, error) {
|
func (u *UserService) Page(search dto.UserPage) (int64, interface{}, error) {
|
||||||
total, users, err := userRepo.Page(search.Page, search.PageSize, commonRepo.WithLikeName(search.Name))
|
total, users, err := userRepo.Page(search.Page, search.PageSize, commonRepo.WithLikeName(search.Name))
|
||||||
var dtoUsers []dto.UserBack
|
var dtoUsers []dto.UserInfo
|
||||||
for _, user := range users {
|
for _, user := range users {
|
||||||
var item dto.UserBack
|
var item dto.UserInfo
|
||||||
if err := copier.Copy(&item, &user); err != nil {
|
if err := copier.Copy(&item, &user); err != nil {
|
||||||
return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error())
|
return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error())
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ func Init() {
|
||||||
migrations.InitTable,
|
migrations.InitTable,
|
||||||
migrations.AddData,
|
migrations.AddData,
|
||||||
migrations.AddTableOperationLog,
|
migrations.AddTableOperationLog,
|
||||||
|
migrations.AddTableHost,
|
||||||
})
|
})
|
||||||
if err := m.Migrate(); err != nil {
|
if err := m.Migrate(); err != nil {
|
||||||
global.LOG.Error(err)
|
global.LOG.Error(err)
|
||||||
|
|
|
@ -31,3 +31,10 @@ var AddTableOperationLog = &gormigrate.Migration{
|
||||||
return tx.AutoMigrate(&model.OperationLog{})
|
return tx.AutoMigrate(&model.OperationLog{})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var AddTableHost = &gormigrate.Migration{
|
||||||
|
ID: "20200818-add-table-host",
|
||||||
|
Migrate: func(tx *gorm.DB) error {
|
||||||
|
return tx.AutoMigrate(&model.Host{})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
|
@ -39,6 +39,7 @@ func Routers() *gin.Engine {
|
||||||
{
|
{
|
||||||
systemRouter.InitBaseRouter(PrivateGroup)
|
systemRouter.InitBaseRouter(PrivateGroup)
|
||||||
systemRouter.InitUserRouter(PrivateGroup)
|
systemRouter.InitUserRouter(PrivateGroup)
|
||||||
|
systemRouter.InitHostRouter(PrivateGroup)
|
||||||
systemRouter.InitTerminalRouter(PrivateGroup)
|
systemRouter.InitTerminalRouter(PrivateGroup)
|
||||||
systemRouter.InitOperationLogRouter(PrivateGroup)
|
systemRouter.InitOperationLogRouter(PrivateGroup)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package router
|
||||||
type RouterGroup struct {
|
type RouterGroup struct {
|
||||||
BaseRouter
|
BaseRouter
|
||||||
UserRouter
|
UserRouter
|
||||||
|
HostRouter
|
||||||
OperationLogRouter
|
OperationLogRouter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
23
backend/router/ro_host.go
Normal file
23
backend/router/ro_host.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
v1 "github.com/1Panel-dev/1Panel/app/api/v1"
|
||||||
|
"github.com/1Panel-dev/1Panel/middleware"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type HostRouter struct{}
|
||||||
|
|
||||||
|
func (s *HostRouter) InitHostRouter(Router *gin.RouterGroup) {
|
||||||
|
userRouter := Router.Group("hosts")
|
||||||
|
userRouter.Use(middleware.JwtAuth()).Use(middleware.SessionAuth())
|
||||||
|
withRecordRouter := userRouter.Use(middleware.OperationRecord())
|
||||||
|
baseApi := v1.ApiGroupApp.BaseApi
|
||||||
|
{
|
||||||
|
withRecordRouter.POST("", baseApi.Create)
|
||||||
|
withRecordRouter.POST("/del", baseApi.DeleteHost)
|
||||||
|
userRouter.POST("/search", baseApi.PageHosts)
|
||||||
|
userRouter.PUT(":id", baseApi.UpdateHost)
|
||||||
|
}
|
||||||
|
}
|
24
frontend/src/api/interface/host.ts
Normal file
24
frontend/src/api/interface/host.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { CommonModel } from '.';
|
||||||
|
|
||||||
|
export namespace Host {
|
||||||
|
export interface Host extends CommonModel {
|
||||||
|
name: string;
|
||||||
|
addr: string;
|
||||||
|
port: number;
|
||||||
|
user: string;
|
||||||
|
authMode: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
export interface HostOperate {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
addr: string;
|
||||||
|
port: number;
|
||||||
|
user: string;
|
||||||
|
authMode: string;
|
||||||
|
privateKey: string;
|
||||||
|
password: string;
|
||||||
|
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,23 +1,19 @@
|
||||||
// * 请求响应参数(不包含data)
|
|
||||||
export interface Result {
|
export interface Result {
|
||||||
code: number;
|
code: number;
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// * 请求响应参数(包含data)
|
|
||||||
export interface ResultData<T> {
|
export interface ResultData<T> {
|
||||||
code: number;
|
code: number;
|
||||||
message: string;
|
message: string;
|
||||||
data: T;
|
data: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
// * 分页响应参数
|
|
||||||
export interface ResPage<T> {
|
export interface ResPage<T> {
|
||||||
items: T[];
|
items: T[];
|
||||||
total: number;
|
total: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// * 分页请求参数
|
|
||||||
export interface ReqPage {
|
export interface ReqPage {
|
||||||
page: number;
|
page: number;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
|
|
20
frontend/src/api/modules/host.ts
Normal file
20
frontend/src/api/modules/host.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import http from '@/api';
|
||||||
|
import { ResPage, ReqPage } from '../interface';
|
||||||
|
import { Host } from '../interface/host';
|
||||||
|
|
||||||
|
export const getHostList = (params: ReqPage) => {
|
||||||
|
return http.post<ResPage<Host.Host>>(`/hosts/search`, params);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addHost = (params: Host.HostOperate) => {
|
||||||
|
return http.post<Host.HostOperate>(`/hosts`, params);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const editHost = (params: Host.HostOperate) => {
|
||||||
|
console.log(params.id);
|
||||||
|
return http.put(`/hosts/` + params.id, params);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteHost = (params: { ids: number[] }) => {
|
||||||
|
return http.post(`/hosts/del`, params);
|
||||||
|
};
|
|
@ -13,7 +13,7 @@ const terminalRouter = {
|
||||||
{
|
{
|
||||||
path: '/terminal',
|
path: '/terminal',
|
||||||
name: 'Terminal',
|
name: 'Terminal',
|
||||||
component: () => import('@/views/terminal/index2.vue'),
|
component: () => import('@/views/terminal/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
|
|
|
@ -49,9 +49,9 @@ const props = withDefaults(defineProps<OperateProps>(), {
|
||||||
});
|
});
|
||||||
|
|
||||||
const rules = reactive<FormRules>({
|
const rules = reactive<FormRules>({
|
||||||
name: [Rules.required, Rules.name],
|
name: [Rules.requiredInput, Rules.name],
|
||||||
email: [Rules.required, Rules.email],
|
email: [Rules.requiredInput, Rules.email],
|
||||||
password: [Rules.required],
|
password: [Rules.requiredInput],
|
||||||
});
|
});
|
||||||
|
|
||||||
const submitForm = async (formEl: FormInstance | undefined) => {
|
const submitForm = async (formEl: FormInstance | undefined) => {
|
||||||
|
|
|
@ -1,21 +1,253 @@
|
||||||
<template>
|
<template>
|
||||||
<LayoutContent :header="$t('menu.terminal')">
|
<LayoutContent :header="$t('menu.terminal')">
|
||||||
<div>
|
<div>
|
||||||
<el-tabs v-model="terminalValue">
|
<el-tabs editable type="card" v-model="terminalValue" @edit="handleTabsEdit">
|
||||||
<el-tab-pane :key="item.name" v-for="item in terminalTabs" :label="item.title" :name="item.name">
|
<el-tab-pane :key="item.name" v-for="item in terminalTabs" :label="item.title" :name="item.name">
|
||||||
<iframe id="iframeTerminal" name="iframeTerminal" width="100%" frameborder="0" :src="item.src" />
|
<iframe
|
||||||
|
v-if="item.type === 'local'"
|
||||||
|
id="iframeTerminal"
|
||||||
|
name="iframeTerminal"
|
||||||
|
width="100%"
|
||||||
|
frameborder="0"
|
||||||
|
:src="item.src"
|
||||||
|
/>
|
||||||
|
<Terminal v-else :ref="'Ref' + item.name" :id="item.wsID"></Terminal>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
</div>
|
</div>
|
||||||
|
<el-button class="term-tool-button" icon="arrowLeftBold" @click="hostDrawer = true"></el-button>
|
||||||
|
|
||||||
|
<el-drawer :size="320" v-model="hostDrawer" title="历史主机信息" direction="rtl">
|
||||||
|
<el-button @click="onAddHost">添加主机</el-button>
|
||||||
|
<div v-infinite-scroll="nextPage" style="overflow: auto">
|
||||||
|
<div v-for="(item, index) in data" :key="item.id" @mouseover="hover = index" @mouseleave="hover = null">
|
||||||
|
<el-card @click="onConn(item)" style="margin-top: 5px" :title="item.name" shadow="hover">
|
||||||
|
<div :inline="true">
|
||||||
|
<span style="font-size: 14px; line-height: 25px">
|
||||||
|
[ {{ item.addr + ':' + item.port }} ]
|
||||||
|
<el-button
|
||||||
|
style="float: right; margin-left: 5px"
|
||||||
|
size="small"
|
||||||
|
circle
|
||||||
|
@click="onDeleteHost(item)"
|
||||||
|
v-if="hover === index"
|
||||||
|
icon="delete"
|
||||||
|
></el-button>
|
||||||
|
<el-button
|
||||||
|
style="float: right; margin-left: 5px"
|
||||||
|
size="small"
|
||||||
|
circle
|
||||||
|
@click="onEditHost(item)"
|
||||||
|
v-if="hover === index"
|
||||||
|
icon="edit"
|
||||||
|
></el-button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-drawer>
|
||||||
|
|
||||||
|
<el-dialog v-model="connVisiable" title="添加主机信息" width="30%">
|
||||||
|
<el-form ref="hostInfoRef" label-width="80px" :model="hostInfo" :rules="rules">
|
||||||
|
<el-form-item label="名称" prop="name">
|
||||||
|
<el-input v-model="hostInfo.name" style="width: 80%" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="addr" prop="addr">
|
||||||
|
<el-input v-model="hostInfo.addr" style="width: 80%" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="端口" prop="port">
|
||||||
|
<el-input v-model="hostInfo.port" style="width: 80%" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="用户" prop="user">
|
||||||
|
<el-input v-model="hostInfo.user" style="width: 80%" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="认证方式" prop="authMode">
|
||||||
|
<el-radio-group v-model="hostInfo.authMode">
|
||||||
|
<el-radio label="password">密码输入</el-radio>
|
||||||
|
<el-radio label="key">密钥输入</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="密码" show-password v-if="hostInfo.authMode === 'password'" prop="password">
|
||||||
|
<el-input type="password" v-model="hostInfo.password" style="width: 80%" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="密钥" v-if="hostInfo.authMode === 'key'" prop="password">
|
||||||
|
<el-input type="textarea" v-model="hostInfo.privateKey" style="width: 80%" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="connVisiable = false">取消</el-button>
|
||||||
|
<el-button v-if="operation === 'conn'" type="primary" @click="submitAddHost(hostInfoRef)">
|
||||||
|
连 接
|
||||||
|
</el-button>
|
||||||
|
<el-button v-else type="primary" @click="submitAddHost(hostInfoRef)"> 提 交 </el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</LayoutContent>
|
</LayoutContent>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref, nextTick } from 'vue';
|
import { onMounted, ref, nextTick, reactive, getCurrentInstance } from 'vue';
|
||||||
|
import { Rules } from '@/global/form-rues';
|
||||||
|
import { getHostList, addHost, editHost, deleteHost } from '@/api/modules/host';
|
||||||
|
import { useDeleteData } from '@/hooks/use-delete-data';
|
||||||
import LayoutContent from '@/layout/layout-content.vue';
|
import LayoutContent from '@/layout/layout-content.vue';
|
||||||
|
import i18n from '@/lang';
|
||||||
|
import type { ElForm } from 'element-plus';
|
||||||
|
import { Host } from '@/api/interface/host';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import Terminal from '@/views/terminal/terminal.vue';
|
||||||
|
|
||||||
const terminalValue = ref();
|
const terminalValue = ref();
|
||||||
const terminalTabs = ref([]) as any;
|
const terminalTabs = ref([]) as any;
|
||||||
|
const hostDrawer = ref(false);
|
||||||
|
const data = ref();
|
||||||
|
|
||||||
|
const paginationConfig = reactive({
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const connVisiable = ref<boolean>(false);
|
||||||
|
const operation = ref();
|
||||||
|
const hover = ref();
|
||||||
|
type FormInstance = InstanceType<typeof ElForm>;
|
||||||
|
const hostInfoRef = ref<FormInstance>();
|
||||||
|
const rules = reactive({
|
||||||
|
name: [Rules.requiredInput, Rules.name],
|
||||||
|
addr: [Rules.requiredInput, Rules.ip],
|
||||||
|
port: [Rules.requiredInput, Rules.port],
|
||||||
|
user: [Rules.requiredInput],
|
||||||
|
authMode: [Rules.requiredSelect],
|
||||||
|
password: [Rules.requiredInput],
|
||||||
|
privateKey: [Rules.requiredInput],
|
||||||
|
});
|
||||||
|
|
||||||
|
let hostInfo = reactive<Host.HostOperate>({
|
||||||
|
id: 0,
|
||||||
|
name: '',
|
||||||
|
addr: '',
|
||||||
|
port: 22,
|
||||||
|
user: '',
|
||||||
|
authMode: 'password',
|
||||||
|
password: '',
|
||||||
|
privateKey: '',
|
||||||
|
description: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const ctx = getCurrentInstance() as any;
|
||||||
|
|
||||||
|
const handleTabsEdit = (targetName: string, action: 'remove' | 'add') => {
|
||||||
|
if (action === 'add') {
|
||||||
|
connVisiable.value = true;
|
||||||
|
operation.value = 'conn';
|
||||||
|
} else if (action === 'remove') {
|
||||||
|
if (ctx) {
|
||||||
|
ctx.refs[`Ref${targetName}`] && ctx.refs[`Ref${targetName}`][0].onClose();
|
||||||
|
}
|
||||||
|
const tabs = terminalTabs.value;
|
||||||
|
let activeName = terminalValue.value;
|
||||||
|
if (activeName === targetName) {
|
||||||
|
tabs.forEach((tab: any, index: any) => {
|
||||||
|
if (tab.name === targetName) {
|
||||||
|
const nextTab = tabs[index + 1] || tabs[index - 1];
|
||||||
|
if (nextTab) {
|
||||||
|
activeName = nextTab.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
terminalValue.value = activeName;
|
||||||
|
terminalTabs.value = tabs.filter((tab: any) => tab.name !== targetName);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadHost = async () => {
|
||||||
|
const res = await getHostList({ page: paginationConfig.currentPage, pageSize: paginationConfig.pageSize });
|
||||||
|
data.value = res.data.items;
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextPage = () => {
|
||||||
|
if (paginationConfig.pageSize >= paginationConfig.total) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
paginationConfig.pageSize = paginationConfig.pageSize + 3;
|
||||||
|
loadHost();
|
||||||
|
};
|
||||||
|
|
||||||
|
function onAddHost() {
|
||||||
|
connVisiable.value = true;
|
||||||
|
operation.value = 'create';
|
||||||
|
if (hostInfoRef.value) {
|
||||||
|
hostInfoRef.value.resetFields();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEditHost(row: Host.Host) {
|
||||||
|
hostInfo.id = row.id;
|
||||||
|
hostInfo.name = row.name;
|
||||||
|
hostInfo.addr = row.addr;
|
||||||
|
hostInfo.port = row.port;
|
||||||
|
hostInfo.user = row.user;
|
||||||
|
hostInfo.authMode = row.authMode;
|
||||||
|
hostInfo.password = '';
|
||||||
|
hostInfo.privateKey = '';
|
||||||
|
operation.value = 'update';
|
||||||
|
connVisiable.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitAddHost = (formEl: FormInstance | undefined) => {
|
||||||
|
if (!formEl) return;
|
||||||
|
formEl.validate(async (valid) => {
|
||||||
|
if (!valid) return;
|
||||||
|
try {
|
||||||
|
switch (operation.value) {
|
||||||
|
case 'create':
|
||||||
|
await addHost(hostInfo);
|
||||||
|
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||||
|
break;
|
||||||
|
case 'update':
|
||||||
|
await editHost(hostInfo);
|
||||||
|
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||||
|
break;
|
||||||
|
case 'conn':
|
||||||
|
const res = await addHost(hostInfo);
|
||||||
|
terminalTabs.value.push({
|
||||||
|
name: res.data.addr,
|
||||||
|
title: res.data.addr,
|
||||||
|
wsID: res.data.id,
|
||||||
|
type: 'remote',
|
||||||
|
});
|
||||||
|
terminalValue.value = res.data.addr;
|
||||||
|
}
|
||||||
|
connVisiable.value = false;
|
||||||
|
loadHost();
|
||||||
|
ElMessage.success(i18n.global.t('commons.msg.operationSuccess'));
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.success(i18n.global.t('commons.msg.loginSuccess') + ':' + error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onConn = (row: Host.Host) => {
|
||||||
|
terminalTabs.value.push({
|
||||||
|
name: row.addr,
|
||||||
|
title: row.addr,
|
||||||
|
wsID: row.id,
|
||||||
|
type: 'remote',
|
||||||
|
});
|
||||||
|
terminalValue.value = row.addr;
|
||||||
|
hostDrawer.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDeleteHost = async (row: Host.Host) => {
|
||||||
|
let ids: Array<number> = [row.id];
|
||||||
|
await useDeleteData(deleteHost, { ids: ids }, 'commons.msg.delete');
|
||||||
|
loadHost();
|
||||||
|
};
|
||||||
|
|
||||||
function changeFrameHeight() {
|
function changeFrameHeight() {
|
||||||
let ifm = document.getElementById('iframeTerminal') as HTMLInputElement | null;
|
let ifm = document.getElementById('iframeTerminal') as HTMLInputElement | null;
|
||||||
|
@ -24,16 +256,28 @@ function changeFrameHeight() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.onresize = function () {
|
|
||||||
changeFrameHeight();
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
terminalTabs.value.push({ name: '本地服务器', title: '本地服务器', src: 'http://localhost:8080' });
|
terminalTabs.value.push({ name: '本地服务器', title: '本地服务器', src: 'http://localhost:8080', type: 'local' });
|
||||||
terminalValue.value = '本地服务器';
|
terminalValue.value = '本地服务器';
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
changeFrameHeight();
|
changeFrameHeight();
|
||||||
|
window.addEventListener('resize', changeFrameHeight);
|
||||||
});
|
});
|
||||||
|
loadHost();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped></style>
|
<style lang="scss" scoped>
|
||||||
|
.term-tool-button {
|
||||||
|
position: absolute;
|
||||||
|
right: -7px;
|
||||||
|
top: 50%;
|
||||||
|
width: 28px;
|
||||||
|
height: 60px;
|
||||||
|
background-color: #565656;
|
||||||
|
border-top-left-radius: 30px;
|
||||||
|
border-bottom-left-radius: 30px;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 999;
|
||||||
|
margin-top: -30px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
143
frontend/src/views/terminal/terminal.vue
Normal file
143
frontend/src/views/terminal/terminal.vue
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
<template>
|
||||||
|
<div :id="'terminal' + props.id"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||||
|
import { Terminal } from 'xterm';
|
||||||
|
import { AttachAddon } from 'xterm-addon-attach';
|
||||||
|
import { Base64 } from 'js-base64';
|
||||||
|
import 'xterm/css/xterm.css';
|
||||||
|
|
||||||
|
interface WsProps {
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
const props = withDefaults(defineProps<WsProps>(), {
|
||||||
|
id: 0,
|
||||||
|
});
|
||||||
|
const loading = ref(true);
|
||||||
|
let terminalSocket = ref(null) as unknown as WebSocket;
|
||||||
|
let term = ref(null) as unknown as Terminal;
|
||||||
|
|
||||||
|
const runRealTerminal = () => {
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onWSReceive = (message: any) => {
|
||||||
|
if (!isJson(message.data)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = JSON.parse(message.data);
|
||||||
|
term.element && term.focus();
|
||||||
|
term.write(data.Data);
|
||||||
|
};
|
||||||
|
|
||||||
|
function isJson(str: string) {
|
||||||
|
try {
|
||||||
|
if (typeof JSON.parse(str) === 'object') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorRealTerminal = (ex: any) => {
|
||||||
|
let message = ex.message;
|
||||||
|
if (!message) message = 'disconnected';
|
||||||
|
term.write(`\x1b[31m${message}\x1b[m\r\n`);
|
||||||
|
console.log('err');
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeRealTerminal = () => {
|
||||||
|
console.log('close');
|
||||||
|
};
|
||||||
|
|
||||||
|
const initTerm = () => {
|
||||||
|
let ifm = document.getElementById('terminal' + props.id) as HTMLInputElement | null;
|
||||||
|
term = new Terminal({
|
||||||
|
lineHeight: 1.2,
|
||||||
|
fontSize: 12,
|
||||||
|
fontFamily: "Monaco, Menlo, Consolas, 'Courier New', monospace",
|
||||||
|
theme: {
|
||||||
|
background: '#181d28',
|
||||||
|
},
|
||||||
|
cursorBlink: true,
|
||||||
|
cursorStyle: 'underline',
|
||||||
|
scrollback: 100,
|
||||||
|
tabStopWidth: 4,
|
||||||
|
cols: ifm ? Math.floor(document.documentElement.clientWidth / 7) : 200,
|
||||||
|
rows: ifm ? Math.floor(document.documentElement.clientHeight / 20) : 25,
|
||||||
|
});
|
||||||
|
if (ifm) {
|
||||||
|
term.open(ifm);
|
||||||
|
terminalSocket = new WebSocket(
|
||||||
|
`ws://localhost:9999/api/v1/terminals?id=${props.id}&cols=${term.cols}&rows=${term.rows}`,
|
||||||
|
);
|
||||||
|
terminalSocket.onopen = runRealTerminal;
|
||||||
|
terminalSocket.onmessage = onWSReceive;
|
||||||
|
terminalSocket.onclose = closeRealTerminal;
|
||||||
|
terminalSocket.onerror = errorRealTerminal;
|
||||||
|
term.onData((data: any) => {
|
||||||
|
if (isWsOpen()) {
|
||||||
|
terminalSocket.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'cmd',
|
||||||
|
cmd: Base64.encode(data),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
term.loadAddon(new AttachAddon(terminalSocket));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isWsOpen = () => {
|
||||||
|
const readyState = terminalSocket && terminalSocket.readyState;
|
||||||
|
return readyState === 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
function onClose() {
|
||||||
|
window.removeEventListener('resize', changeTerminalSize);
|
||||||
|
terminalSocket && terminalSocket.close();
|
||||||
|
term && term.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeTerminalSize() {
|
||||||
|
let ifm = document.getElementById('terminal' + props.id) as HTMLInputElement | null;
|
||||||
|
if (ifm) {
|
||||||
|
ifm.style.height = document.documentElement.clientHeight - 300 + 'px';
|
||||||
|
if (isWsOpen()) {
|
||||||
|
terminalSocket.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'resize',
|
||||||
|
cols: Math.floor(document.documentElement.clientWidth / 7),
|
||||||
|
rows: Math.floor(document.documentElement.clientHeight / 20),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
onClose,
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
initTerm();
|
||||||
|
changeTerminalSize();
|
||||||
|
window.addEventListener('resize', changeTerminalSize);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
onClose();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
#terminal {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
Add table
Reference in a new issue