mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 04:22:33 +08:00
feat: ssh多机管理
This commit is contained in:
@@ -17,10 +17,6 @@ const (
|
||||
SettingKeyBackupPath SettingKey = "backup_path"
|
||||
SettingKeyWebsitePath SettingKey = "website_path"
|
||||
SettingKeyMySQLRootPassword SettingKey = "mysql_root_password"
|
||||
SettingKeySshHost SettingKey = "ssh_host"
|
||||
SettingKeySshPort SettingKey = "ssh_port"
|
||||
SettingKeySshUser SettingKey = "ssh_user"
|
||||
SettingKeySshPassword SettingKey = "ssh_password"
|
||||
SettingKeyOfflineMode SettingKey = "offline_mode"
|
||||
)
|
||||
|
||||
|
||||
@@ -9,14 +9,19 @@ import (
|
||||
|
||||
type SSH struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Host string `json:"host"`
|
||||
Port uint `json:"port"`
|
||||
Config ssh.ClientConfig `json:"config"`
|
||||
Name string `gorm:"not null" json:"name"`
|
||||
Host string `gorm:"not null" json:"host"`
|
||||
Port uint `gorm:"not null" json:"port"`
|
||||
Config ssh.ClientConfig `gorm:"not null;serializer:json" json:"config"`
|
||||
Remark string `gorm:"not null" json:"remark"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type SSHRepo interface {
|
||||
GetInfo() (map[string]any, error)
|
||||
UpdateInfo(req *request.SSHUpdateInfo) error
|
||||
List(page, limit uint) ([]*SSH, int64, error)
|
||||
Get(id uint) (*SSH, error)
|
||||
Create(req *request.SSHCreate) error
|
||||
Update(req *request.SSHUpdate) error
|
||||
Delete(id uint) error
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
@@ -54,10 +53,6 @@ func (r *cronRepo) Get(id uint) (*biz.Cron, error) {
|
||||
}
|
||||
|
||||
func (r *cronRepo) Create(req *request.CronCreate) error {
|
||||
if !regexp.MustCompile(`^((\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+)(,(\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+))*\s?){5}$`).MatchString(req.Time) {
|
||||
return errors.New("时间格式错误")
|
||||
}
|
||||
|
||||
var script string
|
||||
if req.Type == "backup" {
|
||||
if req.BackupType == "website" {
|
||||
@@ -136,10 +131,6 @@ func (r *cronRepo) Update(req *request.CronUpdate) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if !regexp.MustCompile(`^((\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+)(,(\*|\d+|\d+-\d+|\d+/\d+|\d+-\d+/\d+|\*/\d+))*\s?){5}$`).MatchString(req.Time) {
|
||||
return errors.New("时间格式错误")
|
||||
}
|
||||
|
||||
if !cron.Status {
|
||||
return errors.New("计划任务已禁用")
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
"fmt"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
pkgssh "github.com/TheTNB/panel/pkg/ssh"
|
||||
)
|
||||
|
||||
type sshRepo struct {
|
||||
@@ -19,36 +19,71 @@ func NewSSHRepo() biz.SSHRepo {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *sshRepo) GetInfo() (map[string]any, error) {
|
||||
host, _ := r.settingRepo.Get(biz.SettingKeySshHost)
|
||||
port, _ := r.settingRepo.Get(biz.SettingKeySshPort)
|
||||
user, _ := r.settingRepo.Get(biz.SettingKeySshUser)
|
||||
password, _ := r.settingRepo.Get(biz.SettingKeySshPassword)
|
||||
if len(host) == 0 || len(user) == 0 || len(password) == 0 {
|
||||
return nil, errors.New("SSH 配置不完整")
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"host": host,
|
||||
"port": cast.ToInt(port),
|
||||
"user": user,
|
||||
"password": password,
|
||||
}, nil
|
||||
func (r *sshRepo) List(page, limit uint) ([]*biz.SSH, int64, error) {
|
||||
var ssh []*biz.SSH
|
||||
var total int64
|
||||
err := app.Orm.Model(&biz.SSH{}).Omit("Hosts").Order("id desc").Count(&total).Offset(int((page - 1) * limit)).Limit(int(limit)).Find(&ssh).Error
|
||||
return ssh, total, err
|
||||
}
|
||||
|
||||
func (r *sshRepo) UpdateInfo(req *request.SSHUpdateInfo) error {
|
||||
if err := r.settingRepo.Set(biz.SettingKeySshHost, req.Host); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.settingRepo.Set(biz.SettingKeySshPort, cast.ToString(req.Port)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.settingRepo.Set(biz.SettingKeySshUser, req.User); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.settingRepo.Set(biz.SettingKeySshPassword, req.Password); err != nil {
|
||||
return err
|
||||
func (r *sshRepo) Get(id uint) (*biz.SSH, error) {
|
||||
ssh := new(biz.SSH)
|
||||
if err := app.Orm.Where("id = ?", id).First(ssh).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil
|
||||
return ssh, nil
|
||||
}
|
||||
|
||||
func (r *sshRepo) Create(req *request.SSHCreate) error {
|
||||
conf := pkgssh.ClientConfig{
|
||||
AuthMethod: pkgssh.AuthMethod(req.AuthMethod),
|
||||
Host: fmt.Sprintf("%s:%d", req.Host, req.Port),
|
||||
User: req.User,
|
||||
Password: req.Password,
|
||||
Key: req.Key,
|
||||
}
|
||||
_, err := pkgssh.NewSSHClient(conf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check ssh connection: %v", err)
|
||||
}
|
||||
|
||||
ssh := &biz.SSH{
|
||||
Name: req.Name,
|
||||
Host: req.Host,
|
||||
Port: req.Port,
|
||||
Config: conf,
|
||||
Remark: req.Remark,
|
||||
}
|
||||
|
||||
return app.Orm.Create(ssh).Error
|
||||
}
|
||||
|
||||
func (r *sshRepo) Update(req *request.SSHUpdate) error {
|
||||
conf := pkgssh.ClientConfig{
|
||||
AuthMethod: pkgssh.AuthMethod(req.AuthMethod),
|
||||
Host: fmt.Sprintf("%s:%d", req.Host, req.Port),
|
||||
User: req.User,
|
||||
Password: req.Password,
|
||||
Key: req.Key,
|
||||
}
|
||||
_, err := pkgssh.NewSSHClient(conf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check ssh connection: %v", err)
|
||||
}
|
||||
|
||||
ssh := &biz.SSH{
|
||||
ID: req.ID,
|
||||
Name: req.Name,
|
||||
Host: req.Host,
|
||||
Port: req.Port,
|
||||
Config: conf,
|
||||
Remark: req.Remark,
|
||||
}
|
||||
|
||||
return app.Orm.Model(ssh).Updates(ssh).Error
|
||||
}
|
||||
|
||||
func (r *sshRepo) Delete(id uint) error {
|
||||
return app.Orm.Delete(&biz.SSH{}, id).Error
|
||||
}
|
||||
|
||||
@@ -1,8 +1,24 @@
|
||||
package request
|
||||
|
||||
type SSHUpdateInfo struct {
|
||||
Host string `json:"host" form:"host" validate:"required"`
|
||||
Port int `json:"port" form:"port" validate:"required,number,gte=1,lte=65535"`
|
||||
User string `json:"user" form:"user" validate:"required"`
|
||||
Password string `json:"password" form:"password" validate:"required"`
|
||||
type SSHCreate struct {
|
||||
Name string `json:"name" form:"name"`
|
||||
Host string `json:"host" form:"host" validate:"required"`
|
||||
Port uint `json:"port" form:"port" validate:"required,number,gte=1,lte=65535"`
|
||||
AuthMethod string `json:"auth_method" form:"auth_method" validate:"required,oneof=password publickey"`
|
||||
User string `json:"user" form:"user" validate:"required_if=AuthMethod password"`
|
||||
Password string `json:"password" form:"password" validate:"required_if=AuthMethod password"`
|
||||
Key string `json:"key" form:"key" validate:"required_if=AuthMethod publickey"`
|
||||
Remark string `json:"remark" form:"remark"`
|
||||
}
|
||||
|
||||
type SSHUpdate struct {
|
||||
ID uint `form:"id" json:"id" validate:"required,exists=sshes id"`
|
||||
Name string `json:"name" form:"name"`
|
||||
Host string `json:"host" form:"host" validate:"required"`
|
||||
Port uint `json:"port" form:"port" validate:"required,number,gte=1,lte=65535"`
|
||||
AuthMethod string `json:"auth_method" form:"auth_method" validate:"required,oneof=password publickey"`
|
||||
User string `json:"user" form:"user" validate:"required_if=AuthMethod password"`
|
||||
Password string `json:"password" form:"password" validate:"required_if=AuthMethod password"`
|
||||
Key string `json:"key" form:"key" validate:"required_if=AuthMethod publickey"`
|
||||
Remark string `json:"remark" form:"remark"`
|
||||
}
|
||||
|
||||
@@ -42,4 +42,17 @@ func init() {
|
||||
)
|
||||
},
|
||||
})
|
||||
Migrations = append(Migrations, &gormigrate.Migration{
|
||||
ID: "20241022-ssh",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.AutoMigrate(
|
||||
&biz.SSH{},
|
||||
)
|
||||
},
|
||||
Rollback: func(tx *gorm.DB) error {
|
||||
return tx.Migrator().DropTable(
|
||||
&biz.SSH{},
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -158,8 +158,11 @@ func Http(r chi.Router) {
|
||||
r.Route("/ssh", func(r chi.Router) {
|
||||
r.Use(middleware.MustLogin)
|
||||
ssh := service.NewSSHService()
|
||||
r.Get("/info", ssh.GetInfo)
|
||||
r.Post("/info", ssh.UpdateInfo)
|
||||
r.Get("/", ssh.List)
|
||||
r.Post("/", ssh.Create)
|
||||
r.Put("/{id}", ssh.Update)
|
||||
r.Get("/{id}", ssh.Get)
|
||||
r.Delete("/{id}", ssh.Delete)
|
||||
})
|
||||
|
||||
r.Route("/container", func(r chi.Router) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/go-rat/chix"
|
||||
"net/http"
|
||||
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
@@ -18,25 +19,82 @@ func NewSSHService() *SSHService {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SSHService) GetInfo(w http.ResponseWriter, r *http.Request) {
|
||||
info, err := s.sshRepo.GetInfo()
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, info)
|
||||
}
|
||||
|
||||
func (s *SSHService) UpdateInfo(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.SSHUpdateInfo](r)
|
||||
func (s *SSHService) List(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.Paginate](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.sshRepo.UpdateInfo(req); err != nil {
|
||||
cron, total, err := s.sshRepo.List(req.Page, req.Limit)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, chix.M{
|
||||
"total": total,
|
||||
"items": cron,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *SSHService) Create(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.SSHCreate](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.sshRepo.Create(req); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *SSHService) Update(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.SSHUpdate](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.sshRepo.Update(req); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
func (s *SSHService) Get(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ID](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
cron, err := s.sshRepo.Get(req.ID)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, cron)
|
||||
}
|
||||
|
||||
func (s *SSHService) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.ID](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.sshRepo.Delete(req.ID); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, nil)
|
||||
}
|
||||
|
||||
@@ -3,17 +3,16 @@ package service
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"github.com/TheTNB/panel/internal/http/request"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/TheTNB/panel/internal/app"
|
||||
"github.com/TheTNB/panel/internal/biz"
|
||||
"github.com/TheTNB/panel/internal/data"
|
||||
"github.com/TheTNB/panel/pkg/shell"
|
||||
"github.com/TheTNB/panel/pkg/ssh"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type WsService struct {
|
||||
@@ -27,11 +26,17 @@ func NewWsService() *WsService {
|
||||
}
|
||||
|
||||
func (s *WsService) Session(w http.ResponseWriter, r *http.Request) {
|
||||
info, err := s.sshRepo.GetInfo()
|
||||
req, err := Bind[request.ID](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
info, err := s.sshRepo.Get(req.ID)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
ws, err := s.upgrade(w, r)
|
||||
if err != nil {
|
||||
ErrorSystem(w)
|
||||
@@ -39,12 +44,7 @@ func (s *WsService) Session(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
defer ws.Close()
|
||||
|
||||
config := ssh.ClientConfigPassword(
|
||||
cast.ToString(info["host"])+":"+cast.ToString(info["port"]),
|
||||
cast.ToString(info["user"]),
|
||||
cast.ToString(info["password"]),
|
||||
)
|
||||
client, err := ssh.NewSSHClient(config)
|
||||
client, err := ssh.NewSSHClient(info.Config)
|
||||
if err != nil {
|
||||
_ = ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, err.Error()))
|
||||
return
|
||||
|
||||
@@ -6,43 +6,47 @@ import (
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type AuthMethod int8
|
||||
type AuthMethod string
|
||||
|
||||
const (
|
||||
PASSWORD AuthMethod = iota + 1
|
||||
PUBLICKEY
|
||||
PASSWORD AuthMethod = "password"
|
||||
PUBLICKEY AuthMethod = "publickey"
|
||||
)
|
||||
|
||||
type ClientConfig struct {
|
||||
AuthMethod AuthMethod
|
||||
HostAddr string
|
||||
User string
|
||||
Password string
|
||||
Key string
|
||||
Timeout time.Duration
|
||||
AuthMethod AuthMethod `json:"auth_method"`
|
||||
Host string `json:"host"`
|
||||
User string `json:"user"`
|
||||
Password string `json:"password"`
|
||||
Key string `json:"key"`
|
||||
Timeout time.Duration `json:"timeout"`
|
||||
}
|
||||
|
||||
func ClientConfigPassword(hostAddr, user, Password string) *ClientConfig {
|
||||
func ClientConfigPassword(host, user, Password string) *ClientConfig {
|
||||
return &ClientConfig{
|
||||
Timeout: time.Second * 5,
|
||||
Timeout: 10 * time.Second,
|
||||
AuthMethod: PASSWORD,
|
||||
HostAddr: hostAddr,
|
||||
Host: host,
|
||||
User: user,
|
||||
Password: Password,
|
||||
}
|
||||
}
|
||||
|
||||
func ClientConfigPublicKey(hostAddr, user, key string) *ClientConfig {
|
||||
func ClientConfigPublicKey(host, user, key string) *ClientConfig {
|
||||
return &ClientConfig{
|
||||
Timeout: time.Second * 5,
|
||||
Timeout: 10 * time.Second,
|
||||
AuthMethod: PUBLICKEY,
|
||||
HostAddr: hostAddr,
|
||||
Host: host,
|
||||
User: user,
|
||||
Key: key,
|
||||
}
|
||||
}
|
||||
|
||||
func NewSSHClient(conf *ClientConfig) (*ssh.Client, error) {
|
||||
func NewSSHClient(conf ClientConfig) (*ssh.Client, error) {
|
||||
if conf.Timeout == 0 {
|
||||
conf.Timeout = 10 * time.Second
|
||||
}
|
||||
|
||||
config := &ssh.ClientConfig{}
|
||||
config.SetDefaults()
|
||||
config.Timeout = conf.Timeout
|
||||
@@ -59,7 +63,7 @@ func NewSSHClient(conf *ClientConfig) (*ssh.Client, error) {
|
||||
}
|
||||
config.Auth = []ssh.AuthMethod{ssh.PublicKeys(signer)}
|
||||
}
|
||||
c, err := ssh.Dial("tcp", conf.HostAddr, config) // TODO support ipv6
|
||||
c, err := ssh.Dial("tcp", conf.Host, config) // TODO support ipv6
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource-variable/noto-sans-sc": "^5.1.0",
|
||||
"@fontsource/jetbrains-mono": "^5.1.1",
|
||||
"@guolao/vue-monaco-editor": "^1.5.4",
|
||||
"@vue-js-cron/naive-ui": "^2.0.5",
|
||||
"@vueuse/core": "^11.1.0",
|
||||
|
||||
30
web/pnpm-lock.yaml
generated
30
web/pnpm-lock.yaml
generated
@@ -8,6 +8,12 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@fontsource-variable/noto-sans-sc':
|
||||
specifier: ^5.1.0
|
||||
version: 5.1.0
|
||||
'@fontsource/jetbrains-mono':
|
||||
specifier: ^5.1.1
|
||||
version: 5.1.1
|
||||
'@guolao/vue-monaco-editor':
|
||||
specifier: ^1.5.4
|
||||
version: 1.5.4(monaco-editor@0.52.0)(vue@3.5.12(typescript@5.6.3))
|
||||
@@ -667,6 +673,12 @@ packages:
|
||||
resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
|
||||
'@fontsource-variable/noto-sans-sc@5.1.0':
|
||||
resolution: {integrity: sha512-PnnGFWTVyoSdiZU/QvH3w8NroJkBazLWEzrrfqdGX/S6txq6iDoNMG+bolTsAQ4dXbdvfKBTyX6kw/xg5bmdcg==}
|
||||
|
||||
'@fontsource/jetbrains-mono@5.1.1':
|
||||
resolution: {integrity: sha512-5rwvmdQQpXev4LlBX1P+7h2dguu6iwW/9Npjde4+DEq+HgpVJI/7QY8DI1NVVFdvLtXZNP+vO2L/5BQED6FUhA==}
|
||||
|
||||
'@guolao/vue-monaco-editor@1.5.4':
|
||||
resolution: {integrity: sha512-eyBAqxJeDpV4mZYZSpNvh3xUgKCld5eEe0dBtjJhsy2+L0MB6PYFZ/FbPHNwskgp2RoIpfn1DLrIhXXE3lVbwQ==}
|
||||
peerDependencies:
|
||||
@@ -845,30 +857,35 @@ packages:
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-arm64-glibc@2.4.1':
|
||||
resolution: {integrity: sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-arm64-musl@2.4.1':
|
||||
resolution: {integrity: sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-linux-x64-glibc@2.4.1':
|
||||
resolution: {integrity: sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-x64-musl@2.4.1':
|
||||
resolution: {integrity: sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-win32-arm64@2.4.1':
|
||||
resolution: {integrity: sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==}
|
||||
@@ -936,46 +953,55 @@ packages:
|
||||
resolution: {integrity: sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.24.0':
|
||||
resolution: {integrity: sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.24.0':
|
||||
resolution: {integrity: sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.24.0':
|
||||
resolution: {integrity: sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-powerpc64le-gnu@4.24.0':
|
||||
resolution: {integrity: sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.24.0':
|
||||
resolution: {integrity: sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.24.0':
|
||||
resolution: {integrity: sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.24.0':
|
||||
resolution: {integrity: sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.24.0':
|
||||
resolution: {integrity: sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-win32-arm64-msvc@4.24.0':
|
||||
resolution: {integrity: sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==}
|
||||
@@ -3814,6 +3840,10 @@ snapshots:
|
||||
|
||||
'@eslint/js@8.57.1': {}
|
||||
|
||||
'@fontsource-variable/noto-sans-sc@5.1.0': {}
|
||||
|
||||
'@fontsource/jetbrains-mono@5.1.1': {}
|
||||
|
||||
'@guolao/vue-monaco-editor@1.5.4(monaco-editor@0.52.0)(vue@3.5.12(typescript@5.6.3))':
|
||||
dependencies:
|
||||
'@monaco-editor/loader': 1.4.0(monaco-editor@0.52.0)
|
||||
|
||||
@@ -15,8 +15,6 @@ export default {
|
||||
request.put('/cron/' + id, { name, time, script }),
|
||||
// 删除任务
|
||||
delete: (id: number): Promise<AxiosResponse<any>> => request.delete('/cron/' + id),
|
||||
// 获取任务日志
|
||||
log: (id: number): Promise<AxiosResponse<any>> => request.get('/cron/' + id + '/log'),
|
||||
// 修改任务状态
|
||||
status: (id: number, status: boolean): Promise<AxiosResponse<any>> =>
|
||||
request.post('/cron/' + id + '/status', { status })
|
||||
|
||||
@@ -3,13 +3,15 @@ import type { AxiosResponse } from 'axios'
|
||||
import { request } from '@/utils'
|
||||
|
||||
export default {
|
||||
// 获取信息
|
||||
info: (): Promise<AxiosResponse<any>> => request.get('/ssh/info'),
|
||||
// 保存信息
|
||||
saveInfo: (
|
||||
host: string,
|
||||
port: number,
|
||||
user: string,
|
||||
password: string
|
||||
): Promise<AxiosResponse<any>> => request.post('/ssh/info', { host, port, user, password })
|
||||
// 获取主机列表
|
||||
list: (page: number, limit: number): Promise<AxiosResponse<any>> =>
|
||||
request.get('/ssh', { params: { page, limit } }),
|
||||
// 获取主机信息
|
||||
get: (id: number): Promise<AxiosResponse<any>> => request.get(`/ssh/${id}`),
|
||||
// 创建主机
|
||||
create: (req: any): Promise<AxiosResponse<any>> => request.post('/ssh', req),
|
||||
// 修改主机
|
||||
update: (id: number, req: any): Promise<AxiosResponse<any>> => request.put(`/ssh/${id}`, req),
|
||||
// 删除主机
|
||||
delete: (id: number): Promise<AxiosResponse<any>> => request.delete(`/ssh/${id}`)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
|
||||
const base = `${protocol}://${window.location.host}/api/ws/`
|
||||
const base = `${protocol}://${window.location.host}/api/ws`
|
||||
|
||||
export default {
|
||||
// 执行命令
|
||||
exec: (cmd: string): Promise<WebSocket> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ws = new WebSocket(base + 'exec')
|
||||
const ws = new WebSocket(`${base}/exec`)
|
||||
ws.onopen = () => {
|
||||
ws.send(cmd)
|
||||
resolve(ws)
|
||||
@@ -14,9 +14,9 @@ export default {
|
||||
})
|
||||
},
|
||||
// 连接SSH
|
||||
ssh: (): Promise<WebSocket> => {
|
||||
ssh: (id: number): Promise<WebSocket> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ws = new WebSocket(base + 'ssh')
|
||||
const ws = new WebSocket(`${base}/ssh?id=${id}`)
|
||||
ws.onopen = () => resolve(ws)
|
||||
ws.onerror = (e) => reject(e)
|
||||
})
|
||||
|
||||
@@ -14,7 +14,7 @@ import { setupNaiveDiscreteApi } from './utils'
|
||||
import { install as VueMonacoEditorPlugin } from '@guolao/vue-monaco-editor'
|
||||
|
||||
import dashboard from '@/api/panel/dashboard'
|
||||
import CronNaivePlugin, { CronNaive } from '@vue-js-cron/naive-ui'
|
||||
import CronNaivePlugin from '@vue-js-cron/naive-ui'
|
||||
|
||||
async function setupApp() {
|
||||
const app = createApp(App)
|
||||
@@ -27,7 +27,6 @@ async function setupApp() {
|
||||
}
|
||||
})
|
||||
app.use(CronNaivePlugin)
|
||||
app.component('CronNaive', CronNaive)
|
||||
await setupStore(app)
|
||||
await setupNaiveDiscreteApi()
|
||||
await setupPanel().then(() => {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@use '@fontsource-variable/noto-sans-sc';
|
||||
|
||||
html {
|
||||
font-size: 4px; // * 方便unocss计算:1单位 = 0.25rem = 1px
|
||||
}
|
||||
@@ -8,7 +10,7 @@ body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: #f2f2f2;
|
||||
font-family: 'Encode Sans Condensed', sans-serif;
|
||||
font-family: 'Noto Sans SC Variable', sans-serif;
|
||||
}
|
||||
|
||||
#app {
|
||||
@@ -37,7 +39,7 @@ body {
|
||||
overflow: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 8;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,7 +244,7 @@ onMounted(() => {
|
||||
<common-page show-footer>
|
||||
<template #action>
|
||||
<div flex items-center>
|
||||
<n-button class="ml-16" type="primary" @click="handleUpdateCache">
|
||||
<n-button type="primary" @click="handleUpdateCache">
|
||||
<TheIcon :size="18" icon="material-symbols:refresh" />
|
||||
{{ $t('appIndex.buttons.updateCache') }}
|
||||
</n-button>
|
||||
|
||||
@@ -281,8 +281,8 @@ const handleTest = async () => {
|
||||
:disabled="inTest"
|
||||
:loading="inTest"
|
||||
@click="handleTest"
|
||||
w-200
|
||||
mt-40
|
||||
w-200
|
||||
>
|
||||
{{ inTest ? '跑分中...' : '开始跑分' }}
|
||||
</n-button>
|
||||
|
||||
@@ -76,7 +76,7 @@ onMounted(() => {
|
||||
</n-alert>
|
||||
</n-card>
|
||||
<n-card title="修改端口" rounded-10>
|
||||
<n-input-number v-model:value="newPort" min="1" />
|
||||
<n-input-number v-model:value="newPort" :min="1" :max="65535" />
|
||||
修改 phpMyAdmin 访问端口
|
||||
</n-card>
|
||||
</n-space>
|
||||
|
||||
@@ -279,7 +279,7 @@ onMounted(() => {
|
||||
</n-space>
|
||||
</n-card>
|
||||
<n-card title="端口设置" rounded-10>
|
||||
<n-input-number v-model:value="port" min="1" />
|
||||
<n-input-number v-model:value="port" :min="1" :max="65535" />
|
||||
修改 Pure-Ftpd 监听端口
|
||||
</n-card>
|
||||
</n-space>
|
||||
|
||||
@@ -106,7 +106,7 @@ const statusText = (percentage: number) => {
|
||||
return '运行流畅'
|
||||
}
|
||||
|
||||
const chartDisk = computed(() => {
|
||||
const chartOptions = computed(() => {
|
||||
return {
|
||||
title: {
|
||||
text: chartType.value == 'net' ? '网络' : '硬盘',
|
||||
@@ -804,7 +804,7 @@ if (import.meta.hot) {
|
||||
<n-tag>读写延迟 {{ current.diskRWTime }}ms</n-tag>
|
||||
</n-flex>
|
||||
<n-card :bordered="false" h-497>
|
||||
<v-chart class="chart" :option="chartDisk" autoresize />
|
||||
<v-chart class="chart" :option="chartOptions" autoresize />
|
||||
</n-card>
|
||||
</n-flex>
|
||||
<n-skeleton v-else text :repeat="24" />
|
||||
|
||||
92
web/src/views/ssh/CreateModal.vue
Normal file
92
web/src/views/ssh/CreateModal.vue
Normal file
@@ -0,0 +1,92 @@
|
||||
<script setup lang="ts">
|
||||
import ssh from '@/api/panel/ssh'
|
||||
import { NInput } from 'naive-ui'
|
||||
|
||||
const show = defineModel<boolean>('show', { type: Boolean, required: true })
|
||||
const loading = ref(false)
|
||||
|
||||
const model = ref({
|
||||
name: '',
|
||||
host: '127.0.0.1',
|
||||
port: 22,
|
||||
auth_method: 'password',
|
||||
user: 'root',
|
||||
password: '',
|
||||
key: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const handleSubmit = async () => {
|
||||
loading.value = true
|
||||
await ssh
|
||||
.create(model.value)
|
||||
.then(() => {
|
||||
window.$message.success('创建成功')
|
||||
loading.value = false
|
||||
show.value = false
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="show"
|
||||
preset="card"
|
||||
title="创建主机"
|
||||
style="width: 60vw"
|
||||
size="huge"
|
||||
:bordered="false"
|
||||
:segmented="false"
|
||||
>
|
||||
<n-form>
|
||||
<n-form-item label="名称">
|
||||
<n-input v-model:value="model.name" placeholder="127.0.0.1" />
|
||||
</n-form-item>
|
||||
<n-row :gutter="[0, 24]" pt-20>
|
||||
<n-col :span="15">
|
||||
<n-form-item label="主机">
|
||||
<n-input v-model:value="model.host" placeholder="127.0.0.1" />
|
||||
</n-form-item>
|
||||
</n-col>
|
||||
<n-col :span="2"> </n-col>
|
||||
<n-col :span="7">
|
||||
<n-form-item label="端口">
|
||||
<n-input-number v-model:value="model.port" :min="1" :max="65535" />
|
||||
</n-form-item>
|
||||
</n-col>
|
||||
</n-row>
|
||||
<n-form-item label="认证方式">
|
||||
<n-select
|
||||
v-model:value="model.auth_method"
|
||||
:options="[
|
||||
{ label: '密码', value: 'password' },
|
||||
{ label: '私钥', value: 'publickey' }
|
||||
]"
|
||||
>
|
||||
</n-select>
|
||||
</n-form-item>
|
||||
<n-form-item v-if="model.auth_method == 'password'" label="用户名">
|
||||
<n-input v-model:value="model.user" placeholder="root" />
|
||||
</n-form-item>
|
||||
<n-form-item v-if="model.auth_method == 'password'" label="密码">
|
||||
<n-input v-model:value="model.password" />
|
||||
</n-form-item>
|
||||
<n-form-item v-if="model.auth_method == 'publickey'" label="私钥">
|
||||
<n-input v-model:value="model.key" type="textarea" />
|
||||
</n-form-item>
|
||||
<n-form-item label="备注">
|
||||
<n-input v-model:value="model.remark" type="textarea" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-row :gutter="[0, 24]" pt-20>
|
||||
<n-col :span="24">
|
||||
<n-button type="info" block :loading="loading" @click="handleSubmit"> 提交 </n-button>
|
||||
</n-col>
|
||||
</n-row>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -3,7 +3,13 @@ defineOptions({
|
||||
name: 'ssh-index'
|
||||
})
|
||||
|
||||
import ssh from '@/api/panel/ssh'
|
||||
import ws from '@/api/ws'
|
||||
import TheIcon from '@/components/custom/TheIcon.vue'
|
||||
import CreateModal from '@/views/ssh/CreateModal.vue'
|
||||
import UpdateModal from '@/views/ssh/UpdateModal.vue'
|
||||
import '@fontsource/jetbrains-mono/400-italic.css'
|
||||
import '@fontsource/jetbrains-mono/400.css'
|
||||
import { AttachAddon } from '@xterm/addon-attach'
|
||||
import { ClipboardAddon } from '@xterm/addon-clipboard'
|
||||
import { FitAddon } from '@xterm/addon-fit'
|
||||
@@ -11,18 +17,8 @@ import { WebLinksAddon } from '@xterm/addon-web-links'
|
||||
import { WebglAddon } from '@xterm/addon-webgl'
|
||||
import { Terminal } from '@xterm/xterm'
|
||||
import '@xterm/xterm/css/xterm.css'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ssh from '@/api/panel/ssh'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const model = ref({
|
||||
host: '',
|
||||
port: 22,
|
||||
user: '',
|
||||
password: ''
|
||||
})
|
||||
import type { MenuOption } from 'naive-ui'
|
||||
import { NButton, NFlex, NPopconfirm } from 'naive-ui'
|
||||
|
||||
const terminal = ref<HTMLElement | null>(null)
|
||||
const term = ref()
|
||||
@@ -30,30 +26,112 @@ let sshWs: WebSocket | null = null
|
||||
const fitAddon = new FitAddon()
|
||||
const webglAddon = new WebglAddon()
|
||||
|
||||
const handleSave = () => {
|
||||
ssh
|
||||
.saveInfo(model.value.host, model.value.port, model.value.user, model.value.password)
|
||||
.then(() => {
|
||||
window.$message.success(t('sshIndex.alerts.save'))
|
||||
const current = ref(0)
|
||||
const collapsed = ref(true)
|
||||
const createModal = ref(false)
|
||||
const updateModal = ref(false)
|
||||
const updateId = ref(0)
|
||||
|
||||
const list = ref<MenuOption[]>([])
|
||||
|
||||
const fetchData = async () => {
|
||||
list.value = []
|
||||
const { data } = await ssh.list(1, 10000)
|
||||
if (data.items.length === 0) {
|
||||
window.$message.info('请先创建主机')
|
||||
return
|
||||
}
|
||||
data.items.forEach((item: any) => {
|
||||
list.value.push({
|
||||
label: item.name === '' ? item.host : item.name,
|
||||
key: item.id,
|
||||
extra: () => {
|
||||
return h(
|
||||
NFlex,
|
||||
{
|
||||
size: 'small',
|
||||
style: 'float: right'
|
||||
},
|
||||
{
|
||||
default: () => [
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
type: 'primary',
|
||||
size: 'small',
|
||||
onClick: () => {
|
||||
updateModal.value = true
|
||||
updateId.value = item.id
|
||||
}
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
return '编辑'
|
||||
}
|
||||
}
|
||||
),
|
||||
h(
|
||||
NPopconfirm,
|
||||
{
|
||||
onPositiveClick: () => handleDelete(item.id)
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
return '确定删除主机吗?'
|
||||
},
|
||||
trigger: () => {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'error'
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
return '删除'
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getInfo = () => {
|
||||
ssh.info().then((res) => {
|
||||
model.value.host = res.data.host
|
||||
model.value.port = res.data.port
|
||||
model.value.user = res.data.user
|
||||
model.value.password = res.data.password
|
||||
})
|
||||
await openSession(updateId.value === 0 ? Number(list.value[0].key) : updateId.value)
|
||||
}
|
||||
|
||||
const openSession = () => {
|
||||
ws.ssh().then((ws) => {
|
||||
const handleDelete = async (id: number) => {
|
||||
await ssh.delete(id)
|
||||
list.value = list.value.filter((item: any) => item.key !== id)
|
||||
if (current.value === id) {
|
||||
if (list.value.length > 0) {
|
||||
await openSession(Number(list.value[0].key))
|
||||
} else {
|
||||
term.value.dispose()
|
||||
}
|
||||
if (list.value.length === 0) {
|
||||
createModal.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleChange = (key: number) => {
|
||||
console.log(key)
|
||||
openSession(key)
|
||||
}
|
||||
|
||||
const openSession = async (id: number) => {
|
||||
closeSession()
|
||||
await ws.ssh(id).then((ws) => {
|
||||
sshWs = ws
|
||||
term.value = new Terminal({
|
||||
lineHeight: 1.2,
|
||||
fontSize: 14,
|
||||
fontFamily: "Monaco, Menlo, Consolas, 'Courier New', monospace",
|
||||
fontFamily: 'JetBrains Mono',
|
||||
cursorBlink: true,
|
||||
cursorStyle: 'underline',
|
||||
tabStopWidth: 4,
|
||||
@@ -68,24 +146,17 @@ const openSession = () => {
|
||||
webglAddon.onContextLoss(() => {
|
||||
webglAddon.dispose()
|
||||
})
|
||||
|
||||
term.value.open(terminal.value!)
|
||||
|
||||
fitAddon.fit()
|
||||
term.value.focus()
|
||||
window.addEventListener(
|
||||
'resize',
|
||||
() => {
|
||||
fitAddon.fit()
|
||||
},
|
||||
false
|
||||
)
|
||||
window.addEventListener('resize', onResize, false)
|
||||
current.value = id
|
||||
|
||||
ws.onclose = () => {
|
||||
term.value.write('\r\n连接已关闭,请刷新重试。')
|
||||
term.value.write('\r\nConnection closed. Please refresh.')
|
||||
window.removeEventListener('resize', () => {
|
||||
fitAddon.fit()
|
||||
})
|
||||
window.removeEventListener('resize', onResize)
|
||||
}
|
||||
|
||||
ws.onerror = (event) => {
|
||||
@@ -94,21 +165,33 @@ const openSession = () => {
|
||||
console.error(event)
|
||||
ws.close()
|
||||
}
|
||||
|
||||
term.value.onResize(({ cols, rows }: { cols: number; rows: number }) => {
|
||||
if (ws.readyState === 1) {
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
resize: true,
|
||||
columns: cols,
|
||||
rows: rows
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const closeSession = () => {
|
||||
try {
|
||||
term.value.dispose()
|
||||
sshWs?.close()
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
terminal.value!.innerHTML = ''
|
||||
}
|
||||
|
||||
const onResize = () => {
|
||||
fitAddon.fit()
|
||||
if (sshWs != null && sshWs.readyState === 1) {
|
||||
const { cols, rows } = term.value
|
||||
sshWs.send(
|
||||
JSON.stringify({
|
||||
resize: true,
|
||||
columns: cols,
|
||||
rows: rows
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const onTermWheel = (event: WheelEvent) => {
|
||||
if (event.ctrlKey) {
|
||||
event.preventDefault()
|
||||
@@ -123,63 +206,99 @@ const onTermWheel = (event: WheelEvent) => {
|
||||
}
|
||||
}
|
||||
|
||||
watch(createModal, () => {
|
||||
if (!createModal.value) fetchData()
|
||||
})
|
||||
|
||||
watch(updateModal, () => {
|
||||
if (!updateModal.value) {
|
||||
fetchData()
|
||||
updateId.value = 0
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getInfo()
|
||||
openSession()
|
||||
// https://github.com/xtermjs/xterm.js/pull/5178
|
||||
document.fonts.ready.then((fontFaceSet: any) =>
|
||||
Promise.all(Array.from(fontFaceSet).map((el: any) => el.load())).then(fetchData)
|
||||
)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (sshWs) {
|
||||
sshWs.close()
|
||||
}
|
||||
closeSession()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<common-page show-footer>
|
||||
<n-space vertical>
|
||||
<n-form inline>
|
||||
<n-form-item :label="$t('sshIndex.save.fields.host.label')">
|
||||
<n-input
|
||||
v-model:value="model.host"
|
||||
:placeholder="$t('sshIndex.save.fields.host.placeholder')"
|
||||
clearable
|
||||
size="small"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$t('sshIndex.save.fields.port.label')">
|
||||
<n-input-number
|
||||
v-model:value="model.port"
|
||||
:placeholder="$t('sshIndex.save.fields.port.placeholder')"
|
||||
clearable
|
||||
size="small"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$t('sshIndex.save.fields.username.label')">
|
||||
<n-input
|
||||
v-model:value="model.user"
|
||||
:placeholder="$t('sshIndex.save.fields.username.placeholder')"
|
||||
clearable
|
||||
size="small"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$t('sshIndex.save.fields.password.label')">
|
||||
<n-input
|
||||
v-model:value="model.password"
|
||||
:placeholder="$t('sshIndex.save.fields.password.placeholder')"
|
||||
clearable
|
||||
size="small"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-button type="primary" size="small" @click="handleSave">
|
||||
{{ $t('sshIndex.save.actions.submit') }}
|
||||
</n-button>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-card>
|
||||
<div ref="terminal" @wheel="onTermWheel" h-600></div>
|
||||
</n-card>
|
||||
</n-space>
|
||||
<template #action>
|
||||
<div flex items-center>
|
||||
<n-button type="primary" @click="createModal = true">
|
||||
<TheIcon :size="18" icon="material-symbols:add" />
|
||||
创建主机
|
||||
</n-button>
|
||||
</div>
|
||||
</template>
|
||||
<n-layout has-sider sider-placement="right">
|
||||
<n-layout content-style="overflow: visible">
|
||||
<div ref="terminal" @wheel="onTermWheel" h-75vh></div>
|
||||
</n-layout>
|
||||
<n-layout-sider
|
||||
bordered
|
||||
:collapsed-width="0"
|
||||
:collapsed="collapsed"
|
||||
show-trigger
|
||||
:native-scrollbar="false"
|
||||
@collapse="collapsed = true"
|
||||
@expand="collapsed = false"
|
||||
@after-enter="onResize"
|
||||
@after-leave="onResize"
|
||||
>
|
||||
<n-menu
|
||||
v-model:value="current"
|
||||
:collapsed="collapsed"
|
||||
:collapsed-width="0"
|
||||
:collapsed-icon-size="0"
|
||||
:options="list"
|
||||
@update-value="handleChange"
|
||||
/>
|
||||
</n-layout-sider>
|
||||
</n-layout>
|
||||
</common-page>
|
||||
<CreateModal v-model:show="createModal" />
|
||||
<UpdateModal v-if="updateModal" v-model:show="updateModal" v-model:id="updateId" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.xterm) {
|
||||
padding: 4rem !important;
|
||||
}
|
||||
|
||||
:deep(.xterm .xterm-viewport::-webkit-scrollbar) {
|
||||
border-radius: 0.4rem;
|
||||
height: 6px;
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
:deep(.xterm .xterm-viewport::-webkit-scrollbar-thumb) {
|
||||
background-color: #666;
|
||||
border-radius: 0.4rem;
|
||||
box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
|
||||
transition: all 1s;
|
||||
}
|
||||
|
||||
:deep(.xterm .xterm-viewport:hover::-webkit-scrollbar-thumb) {
|
||||
background-color: #aaa;
|
||||
}
|
||||
|
||||
:deep(.xterm .xterm-viewport::-webkit-scrollbar-track) {
|
||||
background-color: #111;
|
||||
border-radius: 0.4rem;
|
||||
box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
|
||||
transition: all 1s;
|
||||
}
|
||||
|
||||
:deep(.xterm .xterm-viewport:hover::-webkit-scrollbar-track) {
|
||||
background-color: #444;
|
||||
}
|
||||
</style>
|
||||
|
||||
108
web/src/views/ssh/UpdateModal.vue
Normal file
108
web/src/views/ssh/UpdateModal.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<script setup lang="ts">
|
||||
import ssh from '@/api/panel/ssh'
|
||||
import { NInput } from 'naive-ui'
|
||||
|
||||
const show = defineModel<boolean>('show', { type: Boolean, required: true })
|
||||
const id = defineModel<number>('id', { type: Number, required: true })
|
||||
const loading = ref(false)
|
||||
|
||||
const model = ref({
|
||||
name: '',
|
||||
host: '127.0.0.1',
|
||||
port: 22,
|
||||
auth_method: 'password',
|
||||
user: 'root',
|
||||
password: '',
|
||||
key: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const handleSubmit = async () => {
|
||||
loading.value = true
|
||||
await ssh
|
||||
.update(id.value, model.value)
|
||||
.then(() => {
|
||||
window.$message.success('更新成功')
|
||||
loading.value = false
|
||||
show.value = false
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (id.value > 0) {
|
||||
ssh.get(id.value).then((res) => {
|
||||
model.value.name = res.data.name
|
||||
model.value.host = res.data.host
|
||||
model.value.port = res.data.port
|
||||
model.value.auth_method = res.data.config.auth_method
|
||||
model.value.user = res.data.config.user
|
||||
model.value.password = res.data.config.password
|
||||
model.value.key = res.data.config.key
|
||||
model.value.remark = res.data.remark
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="show"
|
||||
preset="card"
|
||||
title="创建主机"
|
||||
style="width: 60vw"
|
||||
size="huge"
|
||||
:bordered="false"
|
||||
:segmented="false"
|
||||
>
|
||||
<n-form>
|
||||
<n-form-item label="名称">
|
||||
<n-input v-model:value="model.name" placeholder="127.0.0.1" />
|
||||
</n-form-item>
|
||||
<n-row :gutter="[0, 24]" pt-20>
|
||||
<n-col :span="15">
|
||||
<n-form-item label="主机">
|
||||
<n-input v-model:value="model.host" placeholder="127.0.0.1" />
|
||||
</n-form-item>
|
||||
</n-col>
|
||||
<n-col :span="2"> </n-col>
|
||||
<n-col :span="7">
|
||||
<n-form-item label="端口">
|
||||
<n-input-number v-model:value="model.port" :min="1" :max="65535" />
|
||||
</n-form-item>
|
||||
</n-col>
|
||||
</n-row>
|
||||
<n-form-item label="认证方式">
|
||||
<n-select
|
||||
v-model:value="model.auth_method"
|
||||
:options="[
|
||||
{ label: '密码', value: 'password' },
|
||||
{ label: '私钥', value: 'publickey' }
|
||||
]"
|
||||
>
|
||||
</n-select>
|
||||
</n-form-item>
|
||||
<n-form-item v-if="model.auth_method == 'password'" label="用户名">
|
||||
<n-input v-model:value="model.user" placeholder="root" />
|
||||
</n-form-item>
|
||||
<n-form-item v-if="model.auth_method == 'password'" label="密码">
|
||||
<n-input v-model:value="model.password" />
|
||||
</n-form-item>
|
||||
<n-form-item v-if="model.auth_method == 'publickey'" label="私钥">
|
||||
<n-input v-model:value="model.key" type="textarea" />
|
||||
</n-form-item>
|
||||
<n-form-item label="备注">
|
||||
<n-input v-model:value="model.remark" type="textarea" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-row :gutter="[0, 24]" pt-20>
|
||||
<n-col :span="24">
|
||||
<n-button type="info" block :loading="loading" @click="handleSubmit"> 提交 </n-button>
|
||||
</n-col>
|
||||
</n-row>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -174,7 +174,7 @@ onMounted(async () => {
|
||||
:on-create="onCreateListen"
|
||||
>
|
||||
<template #default="{ value }">
|
||||
<div flex items-center w-full>
|
||||
<div w-full flex items-center >
|
||||
<n-input v-model:value="value.address" clearable />
|
||||
<n-checkbox v-model:checked="value.https" ml-20 mr-20 w-120> HTTPS </n-checkbox>
|
||||
<n-checkbox v-model:checked="value.quic" w-200> QUIC(HTTP3) </n-checkbox>
|
||||
|
||||
Reference in New Issue
Block a user