xterm + vue3 + websocket 终端界面
xterm.js 下载插件
// xterm
npm install --save xterm// xterm-addon-fit 使终端适应包含元素
npm install --save xterm-addon-fit// xterm-addon-attach 通过websocket附加到运行中的服务器进程
npm install --save xterm-addon-attach
<template><div :class="props.type ? 'height305' : 'height160'"><el-row><el-col :span="20"><div:class="['xterm', props.type ? 'heightA' : 'heightB']"ref="terminal"v-loading="loading"element-loading-text="拼命连接中"><div class="terminal" id="terminal" ref="terminal"></div></div><div class="textarea"><textarea ref="textarea" v-model="quickCmd" /><div class="bottomOperate flexEnd"><el-button type="primary" @click="sendCmd" :disabled="!quickCmd">发送命令</el-button></div></div></el-col><el-col :span="4"><div :class="['xtermR', props.type ? 'heightA' : 'heightBR']"><el-tabsv-model="tabActiveName"class="demo-tabs"@tab-click="handleClick"><el-tab-pane label="常用命令" name="first"><div v-if="filteredGroups?.length > 0"><div class="marginBottom10"><el-buttontype="primary"size="small"@click="addCmdGroup('addGroup')">新增命令组</el-button><el-button type="primary" size="small" @click="addCmd('add')">新增命令</el-button></div><el-collapsev-loading="loadingR":class="props.type ? 'listBoxA' : 'listBoxB'"><el-collapse-itemv-for="group in filteredGroups":name="group.name":key="group.name"class="custom-collapse-item"><template #title><divclass="flexSpaceBetween"style="width: 100%"@mouseenter="showActions(group.id, true)"@mouseleave="showActions(group.id, false)"><span class="collapse-title">{{ group.name }}</span><span v-show="actionStates[group.id]"><el-buttonlinktype="primary"@click="addCmdGroup('editGroup', group, $event)">编辑</el-button><el-buttonlinktype="primary"@click="del(group.id, 'group', $event)">删除</el-button></span></div></template><template #default><divv-for="item in group.device_command":key="item.id"class="item flexSpaceBetween paddingRight20 marginBottom10"@mouseenter="showActions1(item.id, true)"@mouseleave="showActions1(item.id, false)"><spanclass="usualName"@click="getName(item.name)":title="item.name">{{ item.name }}</span><span v-show="actionStates1[item.id]" class="btns"><el-buttonlinktype="primary"@click="addCmd('edit', item, group.id)">编辑</el-button><el-button link type="primary" @click="del(item.id)">删除</el-button></span></div></template></el-collapse-item></el-collapse></div><div class="flexCenter" v-else>暂无常用命令</div></el-tab-pane><el-tab-pane label="命令记录" name="second"><div:class="props.type ? 'listBoxA' : 'listBoxB'"v-if="globalStore.cmdRecordList?.length > 0"><divv-for="item in globalStore.cmdRecordList":key="item"class="item flexSpaceBetween paddingRight20 marginBottom10"><span class="recordName" @click="getName(item)">{{item}}</span></div></div><div class="flexCenter" v-else>暂无命令记录</div></el-tab-pane></el-tabs></div></el-col></el-row></div><!-- 新增命令组 --><AddTerminalGroup ref="addTerminalGroup" /><!-- 新增命令 --><AddTerminal ref="addTerminal" />
</template>
<script setup>
import "xterm/css/xterm.css";
import { Terminal } from "xterm";
import { FitAddon } from "xterm-addon-fit";
import { debounce } from "lodash";
import { ElMessage, ElMessageBox } from "element-plus";
import {ref,reactive,onMounted,onBeforeUnmount,computed,nextTick,getCurrentInstance,
} from "vue";
import { useGlobalStore } from "@/stores/modules/global.js";
import AddTerminalGroup from "./AddTerminalGroup.vue";
import AddTerminal from "./AddTerminal.vue";
import {commandGroupIndex,commandGroupDel,commandDel,
} from "@/api/equipment";
import { WebSocketUrl } from "@/api/request";const props = defineProps({type: {type: String,default: () => {return "";},},currentPathRes: {type: String,default: () => {return "/";},},
});
const globalStore = useGlobalStore();
const { proxy } = getCurrentInstance();
const searchTerm = ref("");
const tabActiveName = ref("first");
const cmdRecordList = ref(globalStore.cmdRecordList); // 命令历史记录
const loadingR = ref(false);
const groups = ref([]);
const quickCmd = ref("");
const actionStates = ref({});
const actionStates1 = ref({});const filteredGroups = computed(() => {if (!searchTerm.value) {return groups.value;}return groups.value.map((group) => {const filteredItems = group.device_command.filter((item) =>item.includes(searchTerm.value));return {...group,device_command: filteredItems,};}).filter((group) => group.device_command.length > 0);
});const showActions = (id, show) => {actionStates.value[id] = show;
};const showActions1 = (id, show) => {actionStates1.value[id] = show;
};const addCmdGroup = (type, row, event) => {if (event) event.stopPropagation();nextTick(() => {proxy.$refs["addTerminalGroup"].showDialog({type,row,});});
};const addCmd = (type, row, group_id) => {nextTick(() => {proxy.$refs["addTerminal"].showDialog({type,groupList: groups.value,row,group_id,});});
};const getName = (val) => {quickCmd.value = val;
};// 发送命令
const sendCmd = () => {if (isWsOpen()) {terminalSocket.value.send(quickCmd.value);// 处理命令历史记录handleCmdRecordList(quickCmd.value);}
};const handleCmdRecordList = (newCmd) => {if (newCmd) {// 对新命令进行trim处理const trimmedCmd = newCmd.trim();// 检查是否有重复值并删除const index = cmdRecordList.value.indexOf(trimmedCmd);if (index !== -1) {cmdRecordList.value.splice(index, 1);}// 将新命令添加到数组最前面cmdRecordList.value.unshift(trimmedCmd);globalStore.setCmdRecordList(cmdRecordList.value);}
};const del = (id, group, event) => {if (event) event.stopPropagation();ElMessageBox.confirm("确认删除吗?", "删除", {confirmButtonText: "确定",cancelButtonText: "取消",type: "warning",}).then(() => {if (group) {commandGroupDel({ id }).then((res) => {if (res.status === 200) {ElMessage.success("删除成功");getTableData();}});} else {commandDel({ id }).then((res) => {if (res.status === 200) {ElMessage.success("删除成功");getTableData();}});}}).catch(() => {});
};//获取表格数据
const getTableData = () => {loadingR.value = true;commandGroupIndex().then((res) => {loadingR.value = false;if (res.status === 200) {groups.value = res.data?.list;}}).catch((error) => {loadingR.value = false;});
};
// 命令列表
getTableData();
//终端信息
const loading = ref(false);
const terminal = ref(null);
const fitAddon = new FitAddon();
let first = ref(true);
let terminalSocket = ref(null);
let term = ref(null);// 初始化WS
const initWS = () => {if (!terminalSocket.value) {createWS();}if (terminalSocket.value && terminalSocket.value.readyState > 1) {terminalSocket.value.close();createWS();}
};// 创建WS
const createWS = () => {loading.value = true;terminalSocket.value = new WebSocket(WebSocketUrl + globalStore.wsUrl);terminalSocket.value.onopen = runRealTerminal; //WebSocket 连接已建立terminalSocket.value.onmessage = onWSReceive; //收到服务器消息terminalSocket.value.onclose = closeRealTerminal; //WebSocket 连接已关闭terminalSocket.value.onerror = errorRealTerminal; //WebSocket 连接出错
};//WebSocket 连接已建立
const runRealTerminal = () => {loading.value = false;let sendData = JSON.stringify({t: "conn",});terminalSocket.value.send(sendData);
};
//WebSocket收到服务器消息
const onWSReceive = (event) => {// 首次接收消息,发送给后端,进行同步适配尺寸if (first.value === true) {first.value = false;resizeRemoteTerminal();if (props.type === "termDia") {autoWriteInfo();}}const blob = new Blob([event.data.toString()], {type: "text/plain",});//将Blob 对象转换成字符串const reader = new FileReader();reader.readAsText(blob, "utf-8");reader.onload = (e) => {// 可以根据返回值判断使用何种颜色或者字体,不过返回值自带了一些字体颜色writeOfColor(reader.result);};
};//WebSocket 连接出错
const errorRealTerminal = (ex) => {let message = ex.message;if (!message) message = "disconnected";term.value.write(`\x1b[31m${message}\x1b[m\r\n`);loading.value = false;
};
//WebSocket 连接已关闭
const closeRealTerminal = () => {loading.value = false;
};// 初始化Terminal
const initTerm = () => {term.value = new Terminal({rendererType: "canvas", //渲染类型// rows: 50, //行数,影响最小高度// cols: 100, // 列数,影响最小宽度convertEol: true, //启用时,光标将设置为下一行的开头// scrollback: 50, //终端中的滚动条回滚量disableStdin: false, //是否应禁用输入。cursorStyle: "underline", //光标样式cursorBlink: true, //光标闪烁theme: {foreground: "#F8F8F8",background: "#2D2E2C",cursor: "help", //设置光标lineHeight: 16,},fontFamily: '"Cascadia Code", Menlo, monospace',});// writeDefaultInfo();// 弹框自动输入term.value.open(terminal.value); //挂载dom窗口term.value.loadAddon(fitAddon); //自适应尺寸term.value.focus();termData(); //Terminal 事件挂载
};const autoWriteInfo = () => {let sendData = "\n" + "cd " + props.currentPathRes + "\n";// term.value.write(`\x1b[37m${sendData}\x1b[m`);// term.value.write("\r\n");if (isWsOpen()) {terminalSocket.value.send(sendData);}
};const writeDefaultInfo = () => {let defaultInfo = ["┌\x1b[1m terminals \x1b[0m─────────────────────────────────────────────────────────────────┐ ","│ │ ","│ \x1b[1;34m 欢迎使用XS SSH \x1b[0m │ ","│ │ ","└────────────────────────────────────────────────────────────────────────────┘ ",];term.value.write(defaultInfo.join("\n\r"));term.value.write("\r\n");// writeOfColor('我是加粗斜体红色的字呀', '1;3;', '31m')
};const writeOfColor = (txt, fontCss = "", bgColor = "") => {// 在Linux脚本中以 \x1B[ 开始,中间前部分是样式+内容,以 \x1B[0m 结尾// 示例 \x1B[1;3;31m 内容 \x1B[0m// fontCss// 0;-4;字体样式(0;正常 1;加粗 2;变细 3;斜体 4;下划线)// bgColor// 30m-37m字体颜色(30m:黑色 31m:红色 32m:绿色 33m:棕色字 34m:蓝色 35m:洋红色/紫色 36m:蓝绿色/浅蓝色 37m:白色)// 40m-47m背景颜色(40m:黑色 41m:红色 42m:绿色 43m:棕色字 44m:蓝色 45m:洋红色/紫色 46m:蓝绿色/浅蓝色 47m:白色)// console.log("writeOfColor", term)term.value.write(`\x1b[37m${fontCss}${bgColor}${txt}\x1b[m`);// term.value.write(`\x1B[${fontCss}${bgColor}${txt}\x1B[0m`);
};// 终端输入触发事件
const termData = () => {fitAddon.fit();// 输入与粘贴的情况,onData不能重复绑定,不然会发送多次term.value.onData((data) => {// console.log(data, "传入服务器");if (isWsOpen()) {terminalSocket.value.send(data);}});// 终端尺寸变化触发term.value.onResize(() => {resizeRemoteTerminal();});
};//尺寸同步 发送给后端,调整后端终端大小,和前端保持一致,不然前端只是范围变大了,命令还是会换行
const resizeRemoteTerminal = () => {const { cols, rows } = term.value;if (isWsOpen()) {terminalSocket.value.send(JSON.stringify({t: "resize",width: rows,height: cols,}));}
};// 是否连接中0 1 2 3 状态
const isWsOpen = () => {// console.log(terminalSocket.value, "terminalSocket.value");const readyState = terminalSocket.value && terminalSocket.value.readyState;return readyState === 1;
};// 适应浏览器尺寸变化
const fitTerm = () => {fitAddon.fit();
};
const onResize = debounce(() => fitTerm(), 500);
const onTerminalResize = () => {window.addEventListener("resize", onResize);
};
const removeResizeListener = () => {window.removeEventListener("resize", onResize);
};//*生命周期函数
onMounted(() => {initWS();initTerm();onTerminalResize();
});onBeforeUnmount(() => {removeResizeListener();let sendData = JSON.stringify({t: "close",});if (isWsOpen()) {terminalSocket.value.send(sendData);terminalSocket.value && terminalSocket.value.close();}
});// 暴露方法
defineExpose({ getTableData });
</script>
<style lang="scss" scoped>
.xterm {position: relative;width: 100%;background: rgb(45, 46, 44);
}.xtermR {position: relative;width: 100%;background: #fff;padding: 10px;position: relative;// overflow: hidden;.listBoxA {overflow-y: auto;height: calc(100vh - 450px);}.listBoxB {overflow-y: auto;height: calc(100vh - 300px);}
}.heightA {height: calc(100vh - 400px);
}
.heightB {height: calc(100vh - 235px);
}
.heightBR {height: calc(100vh - 155px);
}.usualName {width: calc(100% - 80px);display: inline-block;cursor: pointer;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;
}.btns {width: 80px;
}.textarea {overflow: hidden;position: relative;height: 80px;background: #ffffff;textarea {width: 100%;height: 90px;border: 0 none;outline: none;resize: none;font-size: 15px;overflow-y: auto;padding: 5px;background: #ffffff;}.bottomOperate {position: absolute;right: 10px;bottom: 10px;}
}
.recordName {font-size: 13px;color: #303133;cursor: pointer;margin-bottom: 10px;width: 100%;
}
.flexCenter {font-size: 14px;padding-top: 150px;
}
</style>

此页面兼容了弹框和非弹框页面,做了两种样式处理判断
相关文章:
xterm + vue3 + websocket 终端界面
xterm.js 下载插件 // xterm npm install --save xterm// xterm-addon-fit 使终端适应包含元素 npm install --save xterm-addon-fit// xterm-addon-attach 通过websocket附加到运行中的服务器进程 npm install --save xterm-addon-attach <template><div :…...
医疗数仓业务数据采集与同步
业务数据采集与同步 业务采集组件配置业务数据同步概述数据同步策略选择数据同步工具概述1.1.4 全量表数据同步DataX配置文件生成全量表数据同步脚本增量表数据同步 MySQL - Maxwell - Kafka - Flume - HDFSMaxwell配置增量表首日全量同步 业务采集组件配置 Maxwell将业务采集到…...
数字孪生智慧水利与水务所包含的应用场景有哪些?二者有何区别
水利和水务是两个密切相关但有所区别的概念,它们在水资源管理和保护方面各自承担着不同的职责和功能。 定义 智慧水务:智慧水务是指通过物联网、大数据、云计算、人工智能等新一代信息技术,对城市供水、排水、污水处理、水质监测等水务系统…...
Qt Creator项目构建配置说明
QT安装好之后,在安装目录的Tools\QtCreator\bin下找到qtcreator.exe文件并双击打开 点击文件-新建文件或项目 选择Qt Widgets Application 设置项目名称以及路径 make工具选择qmake(cmake还未尝试过) 设置主界面对应类的名称、父类&#…...
进程间通信的“五大武器”
😄作者简介: 小曾同学.com,一个致力于测试开发的博主⛽️,主要职责:测试开发、CI/CD 如果文章知识点有错误的地方,还请大家指正,让我们一起学习,一起进步。 😊 座右铭:不…...
全国青少年信息学奥林匹克竞赛(信奥赛)备考实战之循环结构(for循环语句)(六)
实战训练1—输出九九乘法表 问题描述: 在学校里学过九九乘法表,编程实现打印九九乘法表。 输入格式: 无输入 输出格式: 1*11 2*12 2*24 3*13 3*26 3*39 4*14 4*28 4*312 4*416 5*15 5*210 5*315 5*420 5*525 6*16 6*212 6*318 6*424 6*5…...
封装echarts成vue component
封装echarts成vue component EChartsLineComponent 文章目录 封装echarts成vue component EChartsLineComponent封装说明重写重点EChartsLineComponent的源码 使用说明调用EChartsLineComponent示例源码 封装说明 为了减少一些公共代码和方便使用echarts的line图形,…...
uniapp Stripe 支付
引入 Stripe npm install stripe/stripe-js import { loadStripe } from stripe/stripe-js; Stripe 提供两种不同类型组件 Payment Element 和 Card Element:如果你使用的是 Payment Element,它是一个更高级别的组件,能够自动处理多种支…...
Windows onnxruntime编译openvino
理论上来说,可以直接访问 ONNXRuntime Releases 下载 dll 文件,然后从官方文档中下载缺少的头文件以直接调用,但我没有尝试过。 1. 下载 OpenVINO 包 从官网下载 OpenVINO 的安装包并放置在 C:\Program Files (x86) 路径下,例如…...
vue3+TS+vite中Echarts的安装与使用
概述 技术栈:Vue3TsViteEcharts 简述:图文详解,教你如何在Vue项目中引入Echarts,封装Echarts组件,并实现常用Echats图列 文章目录 一,效果图 二,引入Echarts 2.1安装Echarts 2.2main.ts中引…...
期末算法分析程序填空题
目录 5-1 最小生成树(普里姆算法) 5-2 快速排序(分治法) 输入样例: 输出样例: 5-3 归并排序(递归法) 输入样例: 输出样例: 5-4 求解编辑距离问题(动态规划法)…...
搭建android开发环境 android studio
1、环境介绍 在进行安卓开发时,需要掌握java,需要安卓SDK,需要一款编辑器,还需要软件的测试环境(真机或虚拟机)。 早起开发安卓app,使用的是eclipse加安卓SDK,需要自行搭建。 目前开…...
R语言6种将字符转成数字的方法,写在新年来临之际
咱们临床研究中,拿到数据后首先要对数据进行清洗,把数据变成咱们想要的格式,才能进行下一步分析,其中数据中的字符转成数字是个重要的内容,因为字符中常含有特殊符号,不利于分析,转成数字后才能…...
RocketMQ学习笔记(持续更新中......)
目录 1. 单机搭建 2. 测试RocketMQ 3. 集群搭建 4. 集群启动 5. RocketMQ-DashBoard搭建 6. 不同类型消息发送 1.同步消息 2. 异步消息发送 3. 单向发送消息 7. 消费消息 1. 单机搭建 1. 先从rocketmq官网下载二进制包,ftp上传至linux服务器,…...
强化学习的基础概念
这节课会介绍一些基本的概念,并结合例子讲解。 在马尔科夫决策框架下介绍这些概念 本博客是基于西湖大学强化学习课程的视屏进行笔记的,这是链接: 课程链接 目录 强化学习的基本概念 state和state space Action和Action Space State transiti…...
excel怎么删除右边无限列(亲测有效)
excel怎么删除右边无限列(亲测有效) 网上很多只用第1步的,删除了根本没用,还是存在,但是隐藏后取消隐藏却是可以的。 找到右边要删除的列的第一个空白列,选中整个列按“ctrlshift>(向右的小箭头)”&am…...
STM32-笔记23-超声波传感器HC-SR04
一、简介 HC-SR04 工作参数: • 探测距离:2~600cm • 探测精度:0.1cm1% • 感应角度:<15 • 输出方式:GPIO • 工作电压:DC 3~5.5V • 工作电流:5.3mA • 工作温度:-40~85℃ 怎么…...
Linux | Ubuntu零基础安装学习cURL文件传输工具
目录 介绍 检查安装包 下载安装 手册 介绍 cURL是一个利用URL语法在命令行下工作的文件传输工具,首次发行于1997年12。cURL支持多种协议,包括FTP、FTPS、HTTP、HTTPS、TFTP、SFTP、Gopher、SCP、Telnet、DICT、FILE、LDAP、LDAPS、IMAP、POP3…...
什么是 GPT?Transformer 工作原理的动画展示
大家读完觉得有意义记得关注和点赞!!! 目录 1 图解 “Generative Pre-trained Transformer”(GPT) 1.1 Generative:生成式 1.1.1 可视化 1.1.2 生成式 vs. 判别式(译注) 1.2 Pr…...
SpringCloudAlibaba实战入门之路由网关Gateway过滤器(十三)
承接上篇,我们知道除了断言,还有一个重要的功能是过滤器,本节课我们就讲一下常见的网关过滤器及其一般使用。 一、Filter介绍 类似SpringMVC里面的的拦截器Interceptor,Servlet的过滤器。“pre”和“post”分别会在请求被执行前调用和被执行后调用,用来修改请求和响应信…...
基于距离变化能量开销动态调整的WSN低功耗拓扑控制开销算法matlab仿真
目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.核心程序 4.算法仿真参数 5.算法理论概述 6.参考文献 7.完整程序 1.程序功能描述 通过动态调整节点通信的能量开销,平衡网络负载,延长WSN生命周期。具体通过建立基于距离的能量消耗模型&am…...
生成 Git SSH 证书
🔑 1. 生成 SSH 密钥对 在终端(Windows 使用 Git Bash,Mac/Linux 使用 Terminal)执行命令: ssh-keygen -t rsa -b 4096 -C "your_emailexample.com" 参数说明: -t rsa&#x…...
工业自动化时代的精准装配革新:迁移科技3D视觉系统如何重塑机器人定位装配
AI3D视觉的工业赋能者 迁移科技成立于2017年,作为行业领先的3D工业相机及视觉系统供应商,累计完成数亿元融资。其核心技术覆盖硬件设计、算法优化及软件集成,通过稳定、易用、高回报的AI3D视觉系统,为汽车、新能源、金属制造等行…...
是否存在路径(FIFOBB算法)
题目描述 一个具有 n 个顶点e条边的无向图,该图顶点的编号依次为0到n-1且不存在顶点与自身相连的边。请使用FIFOBB算法编写程序,确定是否存在从顶点 source到顶点 destination的路径。 输入 第一行两个整数,分别表示n 和 e 的值(1…...
【C++进阶篇】智能指针
C内存管理终极指南:智能指针从入门到源码剖析 一. 智能指针1.1 auto_ptr1.2 unique_ptr1.3 shared_ptr1.4 make_shared 二. 原理三. shared_ptr循环引用问题三. 线程安全问题四. 内存泄漏4.1 什么是内存泄漏4.2 危害4.3 避免内存泄漏 五. 最后 一. 智能指针 智能指…...
省略号和可变参数模板
本文主要介绍如何展开可变参数的参数包 1.C语言的va_list展开可变参数 #include <iostream> #include <cstdarg>void printNumbers(int count, ...) {// 声明va_list类型的变量va_list args;// 使用va_start将可变参数写入变量argsva_start(args, count);for (in…...
在 Spring Boot 项目里,MYSQL中json类型字段使用
前言: 因为程序特殊需求导致,需要mysql数据库存储json类型数据,因此记录一下使用流程 1.java实体中新增字段 private List<User> users 2.增加mybatis-plus注解 TableField(typeHandler FastjsonTypeHandler.class) private Lis…...
水泥厂自动化升级利器:Devicenet转Modbus rtu协议转换网关
在水泥厂的生产流程中,工业自动化网关起着至关重要的作用,尤其是JH-DVN-RTU疆鸿智能Devicenet转Modbus rtu协议转换网关,为水泥厂实现高效生产与精准控制提供了有力支持。 水泥厂设备众多,其中不少设备采用Devicenet协议。Devicen…...
WEB3全栈开发——面试专业技能点P7前端与链上集成
一、Next.js技术栈 ✅ 概念介绍 Next.js 是一个基于 React 的 服务端渲染(SSR)与静态网站生成(SSG) 框架,由 Vercel 开发。它简化了构建生产级 React 应用的过程,并内置了很多特性: ✅ 文件系…...
Vue3 PC端 UI组件库我更推荐Naive UI
一、Vue3生态现状与UI库选择的重要性 随着Vue3的稳定发布和Composition API的广泛采用,前端开发者面临着UI组件库的重新选择。一个好的UI库不仅能提升开发效率,还能确保项目的长期可维护性。本文将对比三大主流Vue3 UI库(Naive UI、Element …...
