1
0
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:
ssongliu 2022-08-18 18:54:21 +08:00 committed by ssongliu
parent 977625395b
commit 777c03b84e
24 changed files with 790 additions and 35 deletions

View File

@ -10,5 +10,6 @@ var ApiGroupApp = new(ApiGroup)
var (
userService = service.ServiceGroupApp.UserService
hostService = service.ServiceGroupApp.HostService
operationService = service.ServiceGroupApp.OperationService
)

View 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)
}

View File

@ -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
View 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"`
}

View File

@ -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
View 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"`
}

View File

@ -2,6 +2,7 @@ package repo
type RepoGroup struct {
UserRepo
HostRepo
OperationRepo
CommonRepo
}

58
backend/app/repo/host.go Normal file
View 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
}

View File

@ -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
)

View 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)
}

View File

@ -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

View File

@ -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())
}

View File

@ -12,6 +12,7 @@ func Init() {
migrations.InitTable,
migrations.AddData,
migrations.AddTableOperationLog,
migrations.AddTableHost,
})
if err := m.Migrate(); err != nil {
global.LOG.Error(err)

View File

@ -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{})
},
}

View File

@ -39,6 +39,7 @@ func Routers() *gin.Engine {
{
systemRouter.InitBaseRouter(PrivateGroup)
systemRouter.InitUserRouter(PrivateGroup)
systemRouter.InitHostRouter(PrivateGroup)
systemRouter.InitTerminalRouter(PrivateGroup)
systemRouter.InitOperationLogRouter(PrivateGroup)
}

View File

@ -3,6 +3,7 @@ package router
type RouterGroup struct {
BaseRouter
UserRouter
HostRouter
OperationLogRouter
}

23
backend/router/ro_host.go Normal file
View 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)
}
}

View 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;
}
}

View File

@ -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;

View 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);
};

View File

@ -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,

View File

@ -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) => {

View File

@ -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>

View 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>