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”分别会在请求被执行前调用和被执行后调用,用来修改请求和响应信…...
使用分级同态加密防御梯度泄漏
抽象 联邦学习 (FL) 支持跨分布式客户端进行协作模型训练,而无需共享原始数据,这使其成为在互联和自动驾驶汽车 (CAV) 等领域保护隐私的机器学习的一种很有前途的方法。然而,最近的研究表明&…...
大数据零基础学习day1之环境准备和大数据初步理解
学习大数据会使用到多台Linux服务器。 一、环境准备 1、VMware 基于VMware构建Linux虚拟机 是大数据从业者或者IT从业者的必备技能之一也是成本低廉的方案 所以VMware虚拟机方案是必须要学习的。 (1)设置网关 打开VMware虚拟机,点击编辑…...
Golang dig框架与GraphQL的完美结合
将 Go 的 Dig 依赖注入框架与 GraphQL 结合使用,可以显著提升应用程序的可维护性、可测试性以及灵活性。 Dig 是一个强大的依赖注入容器,能够帮助开发者更好地管理复杂的依赖关系,而 GraphQL 则是一种用于 API 的查询语言,能够提…...
c#开发AI模型对话
AI模型 前面已经介绍了一般AI模型本地部署,直接调用现成的模型数据。这里主要讲述讲接口集成到我们自己的程序中使用方式。 微软提供了ML.NET来开发和使用AI模型,但是目前国内可能使用不多,至少实践例子很少看见。开发训练模型就不介绍了&am…...
【JavaWeb】Docker项目部署
引言 之前学习了Linux操作系统的常见命令,在Linux上安装软件,以及如何在Linux上部署一个单体项目,大多数同学都会有相同的感受,那就是麻烦。 核心体现在三点: 命令太多了,记不住 软件安装包名字复杂&…...
【开发技术】.Net使用FFmpeg视频特定帧上绘制内容
目录 一、目的 二、解决方案 2.1 什么是FFmpeg 2.2 FFmpeg主要功能 2.3 使用Xabe.FFmpeg调用FFmpeg功能 2.4 使用 FFmpeg 的 drawbox 滤镜来绘制 ROI 三、总结 一、目的 当前市场上有很多目标检测智能识别的相关算法,当前调用一个医疗行业的AI识别算法后返回…...
什么是Ansible Jinja2
理解 Ansible Jinja2 模板 Ansible 是一款功能强大的开源自动化工具,可让您无缝地管理和配置系统。Ansible 的一大亮点是它使用 Jinja2 模板,允许您根据变量数据动态生成文件、配置设置和脚本。本文将向您介绍 Ansible 中的 Jinja2 模板,并通…...
微软PowerBI考试 PL300-在 Power BI 中清理、转换和加载数据
微软PowerBI考试 PL300-在 Power BI 中清理、转换和加载数据 Power Query 具有大量专门帮助您清理和准备数据以供分析的功能。 您将了解如何简化复杂模型、更改数据类型、重命名对象和透视数据。 您还将了解如何分析列,以便知晓哪些列包含有价值的数据,…...
技术栈RabbitMq的介绍和使用
目录 1. 什么是消息队列?2. 消息队列的优点3. RabbitMQ 消息队列概述4. RabbitMQ 安装5. Exchange 四种类型5.1 direct 精准匹配5.2 fanout 广播5.3 topic 正则匹配 6. RabbitMQ 队列模式6.1 简单队列模式6.2 工作队列模式6.3 发布/订阅模式6.4 路由模式6.5 主题模式…...
基于SpringBoot在线拍卖系统的设计和实现
摘 要 随着社会的发展,社会的各行各业都在利用信息化时代的优势。计算机的优势和普及使得各种信息系统的开发成为必需。 在线拍卖系统,主要的模块包括管理员;首页、个人中心、用户管理、商品类型管理、拍卖商品管理、历史竞拍管理、竞拍订单…...
