mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-03-13 17:24:44 +08:00
feat: 终端管理 tabs 多终端实现
This commit is contained in:
parent
977625395b
commit
777c03b84e
@ -10,5 +10,6 @@ var ApiGroupApp = new(ApiGroup)
|
||||
|
||||
var (
|
||||
userService = service.ServiceGroupApp.UserService
|
||||
hostService = service.ServiceGroupApp.HostService
|
||||
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"
|
||||
|
||||
"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/terminal"
|
||||
"github.com/gin-gonic/gin"
|
||||
@ -13,14 +14,6 @@ import (
|
||||
)
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
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()
|
||||
|
||||
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"))
|
||||
if wshandleError(wsConn, err) {
|
||||
return
|
||||
@ -37,18 +44,18 @@ func (b *BaseApi) WsSsh(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
client, err := host.NewClient()
|
||||
client, err := connInfo.NewClient()
|
||||
if wshandleError(wsConn, err) {
|
||||
return
|
||||
}
|
||||
defer client.Close()
|
||||
ssConn, err := host.NewSshConn(cols, rows)
|
||||
ssConn, err := connInfo.NewSshConn(cols, rows)
|
||||
if wshandleError(wsConn, err) {
|
||||
return
|
||||
}
|
||||
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) {
|
||||
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"`
|
||||
}
|
||||
|
||||
type UserBack struct {
|
||||
type UserInfo struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
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 {
|
||||
UserRepo
|
||||
HostRepo
|
||||
OperationRepo
|
||||
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 {
|
||||
UserService
|
||||
HostService
|
||||
OperationService
|
||||
}
|
||||
|
||||
@ -11,6 +12,7 @@ var ServiceGroupApp = new(ServiceGroup)
|
||||
|
||||
var (
|
||||
userRepo = repo.RepoGroupApp.UserRepo
|
||||
hostRepo = repo.RepoGroupApp.HostRepo
|
||||
operationRepo = repo.RepoGroupApp.OperationRepo
|
||||
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 {
|
||||
var Sensitives = []string{"password", "Password"}
|
||||
var Sensitives = []string{"password", "Password", "privateKey"}
|
||||
ops := make(map[string]interface{})
|
||||
if err := json.Unmarshal([]byte(vars), &ops); err != nil {
|
||||
return vars
|
||||
|
@ -17,7 +17,7 @@ import (
|
||||
type UserService struct{}
|
||||
|
||||
type IUserService interface {
|
||||
Get(name uint) (*dto.UserBack, error)
|
||||
Get(id uint) (*dto.UserInfo, error)
|
||||
Page(search dto.UserPage) (int64, interface{}, error)
|
||||
Register(userDto dto.UserCreate) error
|
||||
Login(c *gin.Context, info dto.Login) (*dto.UserLoginInfo, error)
|
||||
@ -32,12 +32,12 @@ func NewIUserService() IUserService {
|
||||
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))
|
||||
if err != nil {
|
||||
return nil, constant.ErrRecordNotFound
|
||||
}
|
||||
var dtoUser dto.UserBack
|
||||
var dtoUser dto.UserInfo
|
||||
if err := copier.Copy(&dtoUser, &user); err != nil {
|
||||
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) {
|
||||
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 {
|
||||
var item dto.UserBack
|
||||
var item dto.UserInfo
|
||||
if err := copier.Copy(&item, &user); err != nil {
|
||||
return 0, nil, errors.WithMessage(constant.ErrStructTransform, err.Error())
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ func Init() {
|
||||
migrations.InitTable,
|
||||
migrations.AddData,
|
||||
migrations.AddTableOperationLog,
|
||||
migrations.AddTableHost,
|
||||
})
|
||||
if err := m.Migrate(); err != nil {
|
||||
global.LOG.Error(err)
|
||||
|
@ -31,3 +31,10 @@ var AddTableOperationLog = &gormigrate.Migration{
|
||||
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.InitUserRouter(PrivateGroup)
|
||||
systemRouter.InitHostRouter(PrivateGroup)
|
||||
systemRouter.InitTerminalRouter(PrivateGroup)
|
||||
systemRouter.InitOperationLogRouter(PrivateGroup)
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package router
|
||||
type RouterGroup struct {
|
||||
BaseRouter
|
||||
UserRouter
|
||||
HostRouter
|
||||
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 {
|
||||
code: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// * 请求响应参数(包含data)
|
||||
export interface ResultData<T> {
|
||||
code: number;
|
||||
message: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
// * 分页响应参数
|
||||
export interface ResPage<T> {
|
||||
items: T[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
// * 分页请求参数
|
||||
export interface ReqPage {
|
||||
page: 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',
|
||||
name: 'Terminal',
|
||||
component: () => import('@/views/terminal/index2.vue'),
|
||||
component: () => import('@/views/terminal/index.vue'),
|
||||
meta: {
|
||||
keepAlive: true,
|
||||
requiresAuth: true,
|
||||
|
@ -49,9 +49,9 @@ const props = withDefaults(defineProps<OperateProps>(), {
|
||||
});
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
name: [Rules.required, Rules.name],
|
||||
email: [Rules.required, Rules.email],
|
||||
password: [Rules.required],
|
||||
name: [Rules.requiredInput, Rules.name],
|
||||
email: [Rules.requiredInput, Rules.email],
|
||||
password: [Rules.requiredInput],
|
||||
});
|
||||
|
||||
const submitForm = async (formEl: FormInstance | undefined) => {
|
||||
|
@ -1,21 +1,253 @@
|
||||
<template>
|
||||
<LayoutContent :header="$t('menu.terminal')">
|
||||
<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">
|
||||
<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-tabs>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<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 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 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() {
|
||||
let ifm = document.getElementById('iframeTerminal') as HTMLInputElement | null;
|
||||
@ -24,16 +256,28 @@ function changeFrameHeight() {
|
||||
}
|
||||
}
|
||||
|
||||
window.onresize = function () {
|
||||
changeFrameHeight();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
terminalTabs.value.push({ name: '本地服务器', title: '本地服务器', src: 'http://localhost:8080' });
|
||||
terminalTabs.value.push({ name: '本地服务器', title: '本地服务器', src: 'http://localhost:8080', type: 'local' });
|
||||
terminalValue.value = '本地服务器';
|
||||
nextTick(() => {
|
||||
changeFrameHeight();
|
||||
window.addEventListener('resize', changeFrameHeight);
|
||||
});
|
||||
loadHost();
|
||||
});
|
||||
</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…
x
Reference in New Issue
Block a user