Administrator
Published on 2024-10-30 / 50 Visits
0
0

flask-仿真linux终端实现,超低配版本xshell或CRT连接工具

1.flask-sock初始化

# 由于flask 项目启动就要注册websocket,则使用app项目启动反向蓝图注册

##### 下方单独使用文件夹websockets做管理

# 文件名:sock_manager.py     ### sock实例初始化
from flask_sock import Sock

# 创建 Sock 实例
sock = Sock()


def init_sock(app):
    """初始化 Sock 实例"""
    sock.init_app(app)

# 文件名:ssh_connect.py   ### websocket终端交互,保持长连接防止终端,并且设定15分钟终端无操作则自动退出
import time
import paramiko
import threading
from applications.websockets.sock_manager import sock
from applications.models import Ssh_connect
from flask import Blueprint, request
import logging
import io
import simple_websocket

logger = logging.getLogger(__name__)

bp = Blueprint('connect', __name__, url_prefix='/connect')


class SSHConnection:
    def __init__(self, host, port, username, password, private_keys):
        self.host = host
        self.port = port
        self.username = username
        self.password = password
        self.private_keys = private_keys
        self.ssh_client = None
        self.ssh_channel = None
        self.last_activity_time = time.time()
        self.timeout = 15 * 60  # 控制服务器多少分钟没接收前端请求则主动断开连接
        self.lock = threading.Lock()
        self.active = True

    def init_ssh_connection(self, cols=220, rows=80):
        self.ssh_client = paramiko.SSHClient()
        self.ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        self.ssh_client.load_system_host_keys()

        try:
            if self.private_keys:
                private_key = paramiko.RSAKey(file_obj=io.StringIO(self.private_keys))
                self.ssh_client.connect(self.host, port=self.port, username=self.username, pkey=private_key)
                logger.info("SSH connection established using public key.")
            else:
                self.ssh_client.connect(self.host, port=self.port, username=self.username, password=self.password, look_for_keys=False)
                logger.info("SSH connection established using password.")

            self.ssh_channel = self.ssh_client.invoke_shell(term='xterm-256color')
            self.ssh_channel.settimeout(0.1)
            self.ssh_channel.resize_pty(width=cols, height=rows)
        except paramiko.AuthenticationException:
            logger.error("Authentication failed, please verify your credentials.")
        except paramiko.SSHException as e:
            logger.error(f"Unable to establish SSH connection: {e}")
        except Exception as e:
            logger.error(f"连接错误,请检查账号密码以及ip信息是否正确: {e}")
        if not self.ssh_channel:
            logger.error("SSH channel could not be created.")

    def stream_output(self, ws):
        buffer_size = 1024 * 1024  # 1 MB buffer size
        while self.active:
            if self.ssh_channel and self.ssh_channel.recv_ready():
                try:
                    output = self.ssh_channel.recv(buffer_size).decode('utf-8')
                    if output:
                        ws.send(output)
                        with self.lock:
                            self.last_activity_time = time.time()
                except Exception as e:
                    logger.error(f"Error streaming output: {e}")
                    break
            time.sleep(0.1)

    def handle_commands(self, ws):
        while self.active:
            try:
                command = ws.receive()
                if command:
                    logger.info(f"Received command: {command}")
                    if self.ssh_channel:
                        try:
                            self.ssh_channel.send(command)
                            with self.lock:
                                self.last_activity_time = time.time()
                        except Exception as e:
                            logger.error(f"Error sending command to SSH: {e}")
                            ws.send(f"Error: {e}")
            except simple_websocket.errors.ConnectionClosed as e:
                logger.error(f"WebSocket closed: {e}")
                break
            except Exception as e:
                logger.error(f"WebSocket error: {e}")
                ws.send(f"WebSocket error: {e}")
                break

    def monitor_timeout(self, ws):
        while self.active:
            time.sleep(10)  # Check every 10 seconds
            with self.lock:
                if time.time() - self.last_activity_time > self.timeout:
                    ws.send("终端在15分钟内没有任何操作,主动断开连接,请重新连接机器!")
                    self.close()
                    break

    def close(self):
        self.active = False
        if self.ssh_channel:
            self.ssh_channel.close()
        if self.ssh_client:
            self.ssh_client.close()
        logger.info("SSH connection closed.")

@sock.route('/ws/term')
def term(ws):
    id = request.args.get('id')

    if not id:
        ws.send("Error: Missing ID")
        logger.error("Error: Missing ID")
        return

    try:
        id = int(id)
    except ValueError:
        ws.send("Error: Invalid ID format")
        logger.error("Error: Invalid ID format")
        return

    record = Ssh_connect.query.filter_by(id=id).first()
    if not record:
        ws.send("Error: Record not found")
        logger.error("Error: Record not found")
        return

    host = record.ipaddr
    port = int(record.ports)
    username = record.users
    password = record.passwd
    private_keys = record.secerts

    ssh_connection = SSHConnection(host, port, username, password, private_keys)
    ssh_connection.init_ssh_connection()

    if ssh_connection.ssh_channel:
        try:
            ws.send('Connection established.\r\n')
        except Exception as e:
            logger.error(f"Error sending initial message: {e}")
            return

    output_thread = threading.Thread(target=ssh_connection.stream_output, args=(ws,))
    command_thread = threading.Thread(target=ssh_connection.handle_commands, args=(ws,))
    timeout_thread = threading.Thread(target=ssh_connection.monitor_timeout, args=(ws,))

    output_thread.start()
    command_thread.start()
    timeout_thread.start()

    try:
        while ssh_connection.active:
            time.sleep(1)
    except KeyboardInterrupt:
        logger.info("Server shutting down.")
    finally:
        ssh_connection.close()
        output_thread.join(timeout=5)
        command_thread.join(timeout=5)
        timeout_thread.join(timeout=5)

# 文件名:__init__.py  ## 项目init模仿蓝图方式
# applications/websockets/__init__.py
from flask import Flask
from applications.websockets.sock_manager import init_sock
from applications.websockets.ssh_connect import bp as ssh_connect


def register_websockets_bps(app: Flask):
    # 初始化 sock
    init_sock(app)

    # 注册 WebSocket 蓝图
    app.register_blueprint(ssh_connect)

2.在flask 项目蓝图以及配置引入位置统一注册

# 我的项目是在applications 下的 __init__.py 做了注册
import os
from flask import Flask
from applications.common.script import init_script
from applications.config import BaseConfig
from applications.extensions import init_plugs
from applications.view import init_bps
from applications.websockets import register_websockets_bps  # 导入websocket注册
from applications.logging_utils import setup_logging


def create_app():
    app = Flask(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
    # 引入配置
    app.config.from_object(BaseConfig)

    # 设置日志
    setup_logging(app.config)

    # 注册flask组件
    init_plugs(app)

    # 注册蓝图
    init_bps(app)

    # 注册websocket
    register_websockets_bps(app)   # 注册配置的websocket

    # 注册命令
    init_script(app)

    return app

3.界面增删机器配置以及连接后端操作

#下方是路由位置绑定的,由于我使用的是layui老式传统都是后端渲染前端,可以按照自己的方式进行修改
from flask import Blueprint, render_template, request, jsonify
from applications.common.utils.http import success_api
from applications.common.utils.validate import str_escape
from applications.extensions.init_sqlalchemy import db
from applications.models import Ssh_connect


bp = Blueprint('ssh', __name__, url_prefix='/ssh')  # 这是蓝图绑定,我是蓝图绑定蓝图会有多级目录,要使用则需要已下方的前端访问地址为准

# SSH 配置信息
ssh_client = None
ssh_channel = None


# 渲染前端
@bp.get('/')
def main():
    return render_template('system/ssh/main.html')


@bp.get('/info')
def info():
    page = int(request.args.get('page', 1))
    limit = int(request.args.get('limit', 10))

    # 计算分页参数
    offset = (page - 1) * limit

    total = db.session.query(db.func.count(Ssh_connect.id)).scalar()
    configs = Ssh_connect.query.offset(offset).limit(limit).all()
    # 构造响应数据
    response = {
        'code': 0,
        'msg': '',
        'count': total,
        'data': [
            {'id': c.id, 'remark': c.info, 'ipaddr': c.ipaddr, 'ports': c.ports, 'users': c.users, 'passwd': c.passwd,
             'secerts': c.secerts}
            for c in configs]
    }

    return jsonify(response)


# 渲染添加
@bp.get('/add')
def ssh_add():
    return render_template('system/ssh/add.html')


# 处理添加信息
@bp.post('/add')
def ssh_add_put():
    try:
        data = request.get_json()
        new_config = Ssh_connect(
            info=data.get('remark'),
            ipaddr=data.get('ipaddr'),
            ports=data.get('ports'),
            users=data.get('users'),
            passwd=data.get('passwd'),
            secerts=data.get('secerts')
        )
        db.session.add(new_config)
        db.session.commit()
        return jsonify({'code': 0, 'message': '配置添加成功'})
    except Exception as e:
        db.session.rollback()
        return jsonify({'code': 1, 'message': str(e)}), 500


# 编辑配置信息
@bp.get('/edit/<int:_id>')
def ssh_edit(_id):
    dict_get = Ssh_connect.query.filter_by(id=_id).first()
    if dict_get:
        dict_list = {
            'success': True,
            'data': {
                'id': dict_get.id,
                'remark': dict_get.info,
                'ipaddr': dict_get.ipaddr,
                'ports': dict_get.ports,
                'users': dict_get.users,
                'passwd': dict_get.passwd,
                'secrets': dict_get.secerts
            }

        }
    return render_template('/system/ssh/edit.html', dict=dict_list)


# 更新数据
@bp.put('/edit/update')
def ssh_edit_put():
    req_json = request.get_json(force=True)

    id = str_escape(req_json.get("dictId"))
    info = str_escape(req_json.get("remark"))
    ipaddr = str_escape(req_json.get("ipaddr"))
    ports = str_escape(req_json.get("ports"))
    users = str_escape(req_json.get("users"))
    passwd = str_escape(req_json.get("passwd"))
    secerts = str_escape(req_json.get("secrets"))

    dict_type = Ssh_connect.query.filter_by(id=id).first()

    if not dict_type:
        return jsonify({"SUCCESS": False, "msg": "字典数据没找到"})

    dict_type.info = info
    dict_type.ipaddr = ipaddr
    dict_type.ports = ports
    dict_type.users = users
    dict_type.passwd = passwd
    dict_type.secerts = secerts

    db.session.commit()
    return success_api(msg="更新成功")


# 删除机器配置
@bp.delete('/delete/<int:_id>')
def ssh_delete(_id):
    try:
        record = Ssh_connect.query.filter_by(id=_id).first()
        print(record)

        if record:
            db.session.delete(record)
            db.session.commit()
            return jsonify(success=True, msg="删除成功")
        else:
            return jsonify(success=False, msg='记录未找到')
    except Exception as e:
        # 处理异常
        print(f"删除失败: {str(e)}")
        db.session.rollback()
        return jsonify(success=False, msg="删除失败")


# 连接机器页面渲染
@bp.get('/connect/<int:id>')
def ssh_connects(id):
    return render_template('/system/ssh/terminal.html', id=id)

由于不擅长前端,前端界面有点丑陋,就献丑了吧图片发出来,连接的操作基本与xshell 或者crt等操作终端差不多,这个只是功能只是给开发或者测试使用,账号权限会有限制

4.前端代码信息

# 主界面 main
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>设备管理</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='system/component/layui/css/layui.css') }}">
    <style>
        .layui-container {
            padding: 20px;
            display: flex;
            flex-direction: column;
            position: relative;
        }
        .layui-table {
            width: 100%;
            table-layout: auto;
            margin-top: 40px;
        }
        .layui-table .layui-table-cell {
            text-align: center;
            word-break: break-word;
        }
        .password-hidden {
            color: #999;
        }
        .btn-add-config {
            position: absolute;
            top: 10px;
            left: 20px;
            background-color: #4caf50;
            color: white;
            border: none;
            border-radius: 4px;
            padding: 10px 20px;
            font-size: 14px;
            cursor: pointer;
        }
        .btn-add-config:hover {
            background-color: #45a049;
        }
        @media (max-width: 768px) {
            .layui-table th, .layui-table td {
                font-size: 12px;
                padding: 8px;
            }
            .btn-add-config {
                font-size: 12px;
                padding: 8px 16px;
            }
        }
    </style>
</head>
<body>
    <div class="layui-container">
        <button class="btn-add-config" id="addConfigButton">新增配置</button>

        <table class="layui-table" id="device-table" lay-filter="device-table-filter">
            <tbody>
                <!-- 数据由 JavaScript 动态填充 -->
            </tbody>
        </table>
    </div>

    <script src="{{ url_for('static', filename='system/component/layui/layui.js') }}"></script>
    <script src="{{ url_for('static', filename='system/component/pear/module/tinymce/tinymce/plugins/kityformula-editor/kityformula/js/jquery-3.6.0.min.js') }}"></script>
    <script>
        layui.use(['table', 'jquery', 'layer'], function(){
            var table = layui.table;
            var $ = layui.$;
            var layer = layui.layer;

            // 请求数据
            $.ajax({
                url: '/system/ssh/info',  // 这是请求后端的接口
                method: 'GET',
                success: function(res) {
                    if (res.code === 0) {
                        table.render({
                            elem: '#device-table',
                            height: 315,
                            page: true,
                            data: res.data,
                            cols: [[
                                {field: 'id', title: 'ID', width: 80, sort: true},
                                {field: 'remark', title: '备注信息', width: 80, sort: true},
                                {field: 'ipaddr', title: 'IP 地址', minWidth: 120},
                                {field: 'ports', title: '端口', width: 80},
                                {field: 'users', title: '用户', minWidth: 100},
                                {field: 'passwd', title: '密码', minWidth: 120, templet: function(d) {
                                    return d.passwd ? '<span class="password-hidden">******</span>' : '未配置';
                                }},
                                {field: 'secerts', title: '密钥', minWidth: 120, templet: function(d) {
                                    return d.secerts ? '<span class="password-hidden">******</span>' : '未配置';
                                }},
                                {title: '操作', toolbar: '#action-buttons', width: 260}
                            ]],
                            done: function(res, curr, count){
                                // 额外处理逻辑
                            }
                        });
                    } else {
                        alert('数据请求失败');
                    }
                },
                error: function() {
                    alert('请求错误');
                }
            });

            // 监听操作按钮事件
            table.on('tool(device-table-filter)', function(obj){
                var data = obj.data;
                var layEvent = obj.event;

                if (layEvent === 'connect') {
                    // 打开新页面并传递 ID
                    window.open('/system/ssh/connect/' + data.id, '_blank');
                } else if (layEvent === 'edit') {
                    layer.open({
                        type: 2,
                        title: '编辑配置',
                        content: '/system/ssh/edit/' + data.id,
                        area: ['70%', '70%'],
                        maxWidth: 800,
                        maxHeight: 600
                    });
                } else if (layEvent === 'delete') {
                    layer.confirm('确定删除此记录吗?', {icon: 3, title:'提示'}, function(index){
                        $.ajax({
                            url: '/system/ssh/delete/' + data.id,
                            type: 'DELETE',
                            success: function(response) {
                                if (response.success) {
                                    obj.del(); // 删除行
                                    layer.msg('删除成功', {icon: 1});
                                } else {
                                    layer.msg('删除失败: ' + response.msg, {icon: 5});
                                }
                            },
                            error: function() {
                                layer.msg('请求错误', {icon: 5});
                            }
                        });
                        layer.close(index);
                    });
                }
            });

            // 新增配置按钮点击事件
            $('#addConfigButton').on('click', function() {
                layer.open({
                    type: 2,
                    title: '新增配置',
                    content: '/system/ssh/add',
                    area: ['70%', '70%'],
                    maxWidth: 800,
                    maxHeight: 600
                });
            });
        });
    </script>

    <script type="text/html" id="action-buttons">
        <div class="layui-btn-group">
            <button class="layui-btn layui-btn-normal layui-btn-sm" lay-event="connect">连接</button>
            <button class="layui-btn layui-btn-warm layui-btn-sm" lay-event="edit">编辑</button>
            <button class="layui-btn layui-btn-danger layui-btn-sm" lay-event="delete">删除</button>
        </div>
    </script>
</body>
</html>

##################
// 添加用户配置
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>配置表单</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='system/component/layui/css/layui.css') }}" integrity="sha512-..." crossorigin="anonymous" referrerpolicy="no-referrer" />
    <style>
        body {
            font-family: Arial, sans-serif;
            background-color: #f2f2f2;
            margin: 0;
            padding: 20px;
        }
        .container {
            max-width: 600px;
            margin: 0 auto;
            background: #fff;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
        }
        .layui-form-item {
            margin-bottom: 20px;
        }
        .layui-form-label {
            width: 120px;
        }
        .layui-input {
            width: calc(100% - 140px);
        }
        .layui-form-item .layui-input-block {
            margin-left: 140px;
        }
        .layui-btn {
            margin-top: 20px;
            background-color: #4caf50;
            color: #fff;
        }
        .layui-btn:hover {
            background-color: #45a049;
        }
    </style>
</head>
<body>
    <div class="container">
        <form class="layui-form" lay-filter="config-form">
            <div class="layui-form-item">
                <label class="layui-form-label">备注信息</label>
                <div class="layui-input-block">
                    <input type="text" name="remark" required lay-verify="required" placeholder="请输入机器备注信息" autocomplete="off" class="layui-input">
                </div>
            </div>
            <div class="layui-form-item">
                <label class="layui-form-label">IP 地址</label>
                <div class="layui-input-block">
                    <input type="text" name="ipaddr" required lay-verify="required|ip" placeholder="请输入 IP 地址" autocomplete="off" class="layui-input">
                </div>
            </div>
            <div class="layui-form-item">
                <label class="layui-form-label">端口</label>
                <div class="layui-input-block">
                    <input type="text" name="ports" required lay-verify="required" placeholder="请输入端口" autocomplete="off" class="layui-input">
                </div>
            </div>
            <div class="layui-form-item">
                <label class="layui-form-label">用户</label>
                <div class="layui-input-block">
                    <input type="text" name="users" required lay-verify="required" placeholder="请输入用户名" autocomplete="off" class="layui-input">
                </div>
            </div>
            <div class="layui-form-item">
                <label class="layui-form-label">密码</label>
                <div class="layui-input-block">
                    <input type="password" name="passwd" placeholder="请输入密码" autocomplete="off" class="layui-input">
                </div>
            </div>
            <div class="layui-form-item">
                <label class="layui-form-label">密钥</label>
                <div class="layui-input-block">
                    <input type="text" name="secerts" placeholder="请输入密钥" autocomplete="off" class="layui-input">
                </div>
            </div>
            <div class="layui-form-item">
                <div class="layui-input-block">
                    <button class="layui-btn" lay-submit lay-filter="submit-form">提交</button>
                </div>
            </div>
        </form>
    </div>
    <script src="{{ url_for('static', filename='system/component/layui/layui.js') }}" integrity="sha512-..." crossorigin="anonymous" referrerpolicy="no-referrer"></script>
    <script src="{{ url_for('static', filename='system/component/pear/module/tinymce/tinymce/plugins/kityformula-editor/kityformula/js/jquery-3.6.0.min.js') }}"></script>
    <script>
        layui.use(['form', 'jquery', 'layer'], function(){
            var form = layui.form;
            var $ = layui.$;
            var layer = layui.layer;

            // 自定义 IP 地址验证规则
            form.verify({
                ip: function(value) {
                    var ipPattern = /^(25[0-5]|2[0-4][0-9]|[0-1]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[0-1]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[0-1]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[0-1]?[0-9][0-9]?)$/;
                    if (!ipPattern.test(value)) {
                        return '请输入有效的 IP 地址';
                    }
                }
            });

            // 监听表单提交事件
            form.on('submit(submit-form)', function(data){
                // 获取密码和密钥字段的值
                var passwd = data.field.passwd;
                var secerts = data.field.secrets;

                // 验证密码和密钥字段
                if (!passwd && !secerts) {
                    layer.msg('密码和密钥不能同时为空!', {icon: 5});
                    return false; // 阻止表单的默认提交行为
                }

                // 发送表单数据的 AJAX 请求
                $.ajax({
                    url: '/system/ssh/add',
                    type: 'POST',
                    contentType: 'application/json',
                    data: JSON.stringify(data.field),
                    success: function(response) {
                        if (response.code === 0) {
                            layer.msg('配置添加成功!', {icon: 1});
                            setTimeout(function() {
                                var index = parent.layer.getFrameIndex(window.name); // 获取弹出层索引
                                parent.layer.close(index); // 关闭当前弹出层
                                parent.location.reload(); // 刷新父窗口
                            }, 1000);
                        } else {
                            layer.msg('配置添加失败: ' + response.message, {icon: 5});
                        }
                    },
                    error: function() {
                        layer.msg('请求错误', {icon: 5});
                    }
                });

                return false; // 阻止表单的默认提交行为
            });
        });
    </script>
</body>
</html>

##################
// 修改配置信息
<!DOCTYPE html>
<html>
<head>
    <title>编辑设备配置</title>
    {% include 'system/common/header.html' %}
    <link rel="stylesheet" href="{{ url_for('static', filename='system/admin/css/other/dict.css') }}"/>
    <style>
        .custom-textarea {
            width: 100%;
            box-sizing: border-box;
            overflow-wrap: break-word;
            white-space: pre-wrap;
            height: 200px; /* 根据需要调整高度 */
        }
    </style>
</head>
<body>
<form class="layui-form" action="">
    <div class="mainBox">
        <div class="main-container">
            <div class="layui-form-item layui-hide">
                <label class="layui-form-label">ID</label>
                <div class="layui-input-block">
                    <input type="text" id="dict-id" value="{{ dict.data.id }}" name="dictId" lay-verify="title"
                           autocomplete="off" placeholder="ID" class="layui-input" readonly>
                </div>
            </div>
            <div class="layui-form-item">
                <label class="layui-form-label">备注信息</label>
                <div class="layui-input-block">
                    <input type="text" id="dict-remark" value="{{ dict.data.remark }}" name="remark"
                           lay-verify="title" autocomplete="off" placeholder="备注信息" class="layui-input">
                </div>
            </div>
            <div class="layui-form-item">
                <label class="layui-form-label">IP 地址</label>
                <div class="layui-input-block">
                    <input type="text" id="dict-ipaddr" value="{{ dict.data.ipaddr }}" name="ipaddr"
                           lay-verify="title" autocomplete="off" placeholder="IP 地址" class="layui-input">
                </div>
            </div>
            <div class="layui-form-item">
                <label class="layui-form-label">端口</label>
                <div class="layui-input-block">
                    <input type="text" id="dict-ports" value="{{ dict.data.ports }}" name="ports"
                           lay-verify="title" autocomplete="off" placeholder="端口" class="layui-input">
                </div>
            </div>
            <div class="layui-form-item">
                <label class="layui-form-label">用户</label>
                <div class="layui-input-block">
                    <input type="text" id="dict-users" value="{{ dict.data.users }}" name="users"
                           lay-verify="title" autocomplete="off" placeholder="用户" class="layui-input">
                </div>
            </div>
            <div class="layui-form-item">
                <label class="layui-form-label">密码</label>
                <div class="layui-input-block">
                    <input type="text" id="dict-passwd" value="{{ dict.data.passwd }}" name="passwd"
                           lay-verify="title" autocomplete="off" placeholder="密码" class="layui-input">
                </div>
            </div>
            <div class="layui-form-item">
                <label class="layui-form-label">密钥</label>
                <div class="layui-input-block">
                    <textarea id="dict-secrets" name="secrets" lay-verify="title"
                              autocomplete="off" placeholder="密钥" class="layui-textarea custom-textarea">{{ dict.data.secrets }}</textarea>
                </div>
            </div>
        </div>
    </div>
    <div class="bottom">
        <div class="button-container">
            <button type="submit" class="pear-btn pear-btn-primary pear-btn-sm" lay-submit="" lay-filter="dict-update">
                <i class="layui-icon layui-icon-ok"></i> 提交
            </button>
            <button type="reset" class="pear-btn pear-btn-sm">
                <i class="layui-icon layui-icon-refresh"></i> 重置
            </button>
        </div>
    </div>
</form>
{% include 'system/common/footer.html' %}
<script>
    layui.use(['form', 'jquery', 'layer'], function () {
        let form = layui.form;
        let $ = layui.jquery;
        let layer = layui.layer;

        form.on('submit(dict-update)', function (data) {
            $.ajax({
                url: '/system/ssh/edit/update',
                data: JSON.stringify(data.field),
                dataType: 'json',
                contentType: 'application/json',
                type: 'PUT',
                success: function (result) {
                    if (result.success) {
                        layer.msg('更新成功', {icon: 1, time: 1000}, function () {
                            let index = parent.layer.getFrameIndex(window.name); // 获取当前弹出层的索引
                            parent.layer.close(index); // 关闭弹出层
                            if (parent.layui && parent.layui.table) {
                                parent.layui.table.reload('device-table'); // 刷新主页面表格
                                parent.location.reload(); // 刷新父窗口
                            }
                        });
                    } else {
                        layer.msg('更新失败: ' + result.msg, {icon: 2, time: 1000});
                    }
                },
                error: function () {
                    layer.msg('请求失败,请稍后再试', {icon: 2});
                }
            });
            return false; // 阻止表单的默认提交
        });
    });
</script>
</body>
</html>

下方是终端前端代码引入的xterm.js 是5.2版本的。关联其他引入则手动查询一下关联的版本

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Web Terminal</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='terminal/xterm.css') }}">
    <style>
        html, body {
            margin: 0;
            height: 100vh;
            width: 100vw;
            overflow: hidden; /* Ensure no overflow is hidden */
        }

        #terminal {
            width: 100vw;       /* Full viewport width */
            height: 100vh;      /* Full viewport height */
            display: flex;
            box-sizing: border-box; /* Include padding and border in width and height */
            font-family: monospace;
            overflow: auto; /* Ensure overflow is visible */
            white-space: pre-wrap; /* Preserve whitespace formatting */
        }
    </style>
</head>
<body>
    <div id="terminal"></div>

    <script src="{{ url_for('static', filename='terminal/xterm.js') }}"></script>
    <script src="{{ url_for('static', filename='terminal/xterm-addon-fit.min.js') }}"></script>
    <script src="{{ url_for('static', filename='terminal/xterm-addon-web-links.min.js') }}"></script>
    <script>
    let terminal;
    let fitAddon;

    function setupWebSocket() {
        const id = {{ id|tojson }};
        const socket = new WebSocket(`ws://${window.location.host}/ws/term?id=${id}`);

        socket.onopen = () => {
            console.log('WebSocket connection established.');
            terminal.focus();
        };

        socket.onmessage = (event) => {
            console.log('Received message:', event.data);
            terminal.write(event.data);
        };

        socket.onerror = (error) => {
            console.error('WebSocket Error:', error);
        };

        socket.onclose = (event) => {
            console.log('WebSocket closed:', event);
            setTimeout(setupWebSocket, 5000); // Reconnect if needed
        };

        terminal = new Terminal({
            cursorBlink: true,
            fontSize: 14,
            theme: {
                background: '#000000',
                foreground: '#ffffff'
            },
            lineHeight: 1.2,
            scrollback: 1000
        });

        fitAddon = new FitAddon.FitAddon();
        terminal.loadAddon(fitAddon);
        terminal.open(document.getElementById('terminal'));
        fitAddon.fit();

        terminal.onData(data => {
            console.log('Sending data:', data);
            socket.send(data);
        });

        window.addEventListener('resize', () => {
            fitAddon.fit();
        });
    }

    function copySelectedText() {
        const selectedText = window.getSelection().toString();
        if (selectedText) {
            navigator.clipboard.writeText(selectedText).then(() => {
                console.log('Text copied to clipboard:', selectedText);
            }).catch(err => {
                console.error('Failed to copy text:', err);
                alert('Failed to copy text. Please try manually.');
            });
        }
    }

    // Set up WebSocket and terminal
    setupWebSocket();

    // Add event listeners for text selection and copy
    document.getElementById('terminal').addEventListener('mouseup', copySelectedText);
    document.getElementById('terminal').addEventListener('dblclick', copySelectedText);
    </script>
</body>
</html>

Comment