vue网页使用远程终端加终端多开

       只会前端所以只有前端代码,想看ws服务怎么弄的就不用看了~

        这边终端用到的前端插件是xterm。首先需要三个插件,都是xterm的依赖。

        单独一个终端:使用xterm,创建一个标签后,将标签给予xterm作为终端标签。然后写入终端所需如光标、行数、样式。根据是否keep-alive缓存决定在created和mounted里面将带属性的xterm给予标签(initTerminal)链接ws服务(websocket)(在vue里面分别写成两个方法,括号内为我起的方法名)。

        多开终端:xterm获取标签时要根据id,那根据打开终端的个数动态创建标签并赋予id(比如用字母和循环的index数字组合)。然后将initTerminalwebsocket改为传参的方法,这样就可以确定要创建的标签和要链接的ws服务是哪个标签了。然后在添加删除终端的地方做好ws服务的断开就可以了,写一个数组存放服务,退出哪个根据index做好xterm带的close()方法。【所有方法要对应好标签名,根据参数传递的方法判断要执行的操作是哪一个终端的】

        多开除了麻烦一点和开一个是一摸一样的,就是做好服务关闭。否则服务多了后台受不了。

1、xterm

npm install xterm

2、xterm-addon-fit

npm install xterm-addon-fit

3、xterm-addon-attac

npm install xterm-addon-attach

还用到了Elementui,这个就不教怎么安装了一搜就有

直接粘贴代码,里面每一句都注释了,先看懂了再根据需求改吧。

<template>
  <div class="app-container">
    <!-- elementui的tabs标签,用于控制终端数量 -->
    <el-tabs v-model="editableTabsValue" type="card" editable @edit="handleTabsEdit">
      <el-tab-pane
        :key="item.name"
        v-for="item in editableTabs"
        :label="item.title"
        :name="item.name"
      >
        <!-- 用tab的标签名称命名 -->
        <div :id='item.title' class="terminal" style="width: 100%"></div>
      </el-tab-pane>
    </el-tabs>
  </div>
</template>

<script>
//引入依赖
import 'xterm/css/xterm.css'
import { Terminal } from 'xterm'
import { FitAddon } from 'xterm-addon-fit'
import { AttachAddon } from 'xterm-addon-attach'

let wsTime = null//终端出错用于储存展示出错信息。
export default {
  name: 'Terminal',
  data() {
    return {
      editableTabsValue: '0',//标签名称累加
      editableTabs: [{title: 'terminal_0',name: '0',}],//标签数组
      tabIndex: 0,//标签目前到哪一位
      term:[],//用于保存每一个终端配置
      //下面两个属性是我这边业务要链接ws的参数,根据你们业务改成自己的
      containerId:'',//容器id  
      containerIp:'',//容器ip
      ws:[],// 保存websocket链接,用于统一关闭
      isConnected: false,//判断链接状态
      heartbeatInterval: null, // 保存心跳定时器的ID
      heartbeatTimeout: 1000, // 心跳间隔时间
    }
  },
  created(){
    // 默认初始化第一个终端
    this.initTerminal(0)
  },
  mounted() {
    //下面两个属性是我这边业务要链接ws的参数,根据你们业务改成自己的
    this.containerId = this.$route.query.containerId
    this.containerIp = this.$route.query.containerIp
    //业务不需要可以不用这个判断
    if(this.containerId !== ''){
      // 建立websocket建立第一个终端的连接
      // 我这边用了keep-alive所以可以保证mounted只执行一次,只要调用mounted就是第一次进页面所以可以写死
      this.websocket(0)
    }
  },
  //keep-alive缓存用到了,建议加个keep-alive
  //已开启一个容器的终端,但是没关闭又开了另一个容器的终端
  activated(){
    //这个属性还是根据你们业务改成自己的
    this.containerIp = this.$route.query.containerIp
    //这边业务是判断开启不同地址的终端还是同一个地址多开一个终端
    if(this.$route.query.containerId !== this.containerId){
      // 换成新的id建立websocket连接
      this.containerId = this.$route.query.containerId
      //tab增加方法
      this.handleTabsEdit(null, 'add')
    }
  },
  //销毁组件回调
  beforeDestroy() {
    //map循环保存ws服务的数组,循环一圈关闭ws
    this.ws.map((item,index) =>{
      return this.ws[index].close()
    })
  },
  methods:{
    //elementui的tabs标签,去搜一下不难,就一个增加一个删除方法
    //添加和删除终端标签方法
    handleTabsEdit(targetName, action) {
      //增加tab标签
      if (action === 'add') {
        //动态根据数量命名id
        let newTabName = ++this.tabIndex + '';
        //将新加的标签名加入标签名列表
        this.editableTabs.push({
          title: `terminal_${newTabName}`,
          name: newTabName,
        });
        //用于计数,保证下次增加是新的index并且加1了
        this.editableTabsValue = newTabName;
        //创建新的终端
        this.initTerminal(newTabName)
        //链接新的ws服务
        this.websocket(newTabName)
      }
      //删除tabs标签
      if (action === 'remove') {
        //获取所有标签列表
        let tabs = this.editableTabs;
        //获取要删除的是哪个tab
        let activeName = this.editableTabsValue;
        //关闭ws服务
        this.ws[targetName].close()
        // 如果删除的是当前正在使用的终端
        if (activeName === targetName) {
          tabs.forEach((tab, index) => {
            //精确判断删除的是哪个
            if (tab.name === targetName) {  
              //下面就是删除完正在使用的终端,如果还有其他终端,就跳过去。不让页面空着
              let nextTab = tabs[index + 1] || tabs[index - 1];
              if (nextTab) {
                activeName = nextTab.name;
              }
            }
          });
        }
        //计数index到几了
        this.editableTabsValue = activeName;
        //在保存标签数组中删除掉刚刚删除的标签
        this.editableTabs = tabs.filter(tab => tab.name !== targetName);
      }
    },
    // 初始化终端配置,index表示第几位置的
    initTerminal(index){
      this.term[index] = new Terminal({
        rendererType: "canvas", //渲染类型
        rows: 35, //行数,影响最小高度
        // 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'//字体
      });
    },
    // 自定义终端默认展示内容
    // 里面有属性是关于业务的可以删掉,如 ${this.$route.query.name},${this.containerId}
    writeDefaultInfo(index){
        let defaultInfo = [
        '',
        `\x1b[1;34m containerName:\x1b[1;31m ${this.$route.query.name}\x1b[0m\x1b[0m \x1b[1;34m containerId:\x1b[1;31m ${this.containerId}\x1b[0m\x1b[0m`,
        '┌\x1b[1m terminals \x1b[0m─────────────────────────────────────────────────────────────────┐ ',
        '│                                                                            │ ',
        `│  \x1b[1;34m welcome Container Terminal\x1b[0m                                               │ `,
        '│                                                                            │ ',
        `└────────────────────────────────────────────────────────────────────────────┘\n `]
        //将默认展示内容写入终端
        this.term[index].write(defaultInfo.join('\n\r'))
    },
    // 建立websocket连接
    websocket(index) {
      // WebSocket start
      if ('WebSocket' in window) {
        //根据后端需要的参数业务改
        const url = 'ws://' + `${window.PLATFROM_CONFIG.filePath}:${window.PLATFROM_CONFIG.port}/ws/webdocker/`.split('//')[1] + `${this.containerIp}`
        //存一下新的ws服务
        const ws = new WebSocket(url)
        //将新的服务加入ws服务数组列表
        this.ws[index] = ws
        //链接成功回调
        ws.onopen = (event) => {
          //打印一下链接成功
          console.log('已建立连接:',event)
          //改变链接状态
          this.isConnected = true
          //输入换行符可让终端显示当前用户的工作路径,我这边顺便进入默认目录
          ws.send('cd C:/app\r\n')
          //窗口自适应插件
          const fitAddon = new FitAddon();
          //websocket自动收发消息插件
          const attachAddon = new AttachAddon(ws)
          //给新的这个ws服务使用依赖初始化
          this.term[index].loadAddon(attachAddon)
          //给新的这个ws服务使用依赖设置样式
          this.term[index].loadAddon(fitAddon)
          //将新的终端给予标签
          this.term[index].open(document.getElementById(`terminal_${index}`));
          // 聚焦闪烁光标
          this.term[index].focus()
          //先默认一次适应大小
          fitAddon.fit()
          // 窗口尺寸变化时,终端尺寸自适应
          window.onresize = () => {
            fitAddon.fit()
          }
          // 自定义终端默认展示内容
          this.writeDefaultInfo(index)
        };
        //发消息的回调,一般用不到
        ws.onmessage = (event) => {

        };
        //报错的回调
        ws.onerror = (event) => {
          console.log('错误信息:', event)
          //之前定义的wsTime全局属性用到了
          if (wsTime) {
            window.clearTimeout(wsTime)
            wsTime = null
          }
          //展示完清空一下,等待下次出错展示新的
          wsTime = window.setTimeout(() => {
            this.websocket()
          }, 3000)
        };
        //关闭链接回调
        ws.onclose = (event) => {
          console.log('已关闭连接:', event)
        };
      } else {
        //不多说了,换个浏览器提示
        console.log('浏览器不支持 WebSocket..')
      }
    }
  }
}
</script>
<style scoped>
/* 整体样式,自己删掉背景图吧动动手多改改就全会了哈哈 */
.app-container {
  position: absolute;
  width: 100%;
  height: 100%;
  min-height: 550px;
  padding: 0;
  background: url("../../assets/images/backImg-1.png") 50%;
}
/* tabs样式 */
::v-deep .el-tabs__header{
  margin: 0;
}
/* 终端大小占100% */
::v-deep .el-tabs{
  height: 100%;
  background-color: #2d2e2c;
}
/* tabs内容 */
::v-deep .el-tabs__content{
  height: 100%;
  width: 100%;
}
/* tabs忘了.. */
::v-deep .el-tab-pane{
  height: 100%;
}
/* tab的title颜色 */
::v-deep .el-tabs__item{
  color: white;
  height: 30px;
  line-height: 30px;
  padding: 0 5px;
}
/* 添加tab按钮 */
::v-deep .el-tabs__new-tab{
  margin: 12px 12px 0 10px;
}
/* tab选中时 */
::v-deep .el-tabs__item.is-active{
  color: #1890ff;
  background-color: #2b2927;
  border-bottom: none;
}
/* xterm默认样式改变 */
::v-deep .xterm .xterm-viewport{
  overflow-y: auto;
}
/* xterm默认样式改变 */
::v-deep .xterm .xterm-viewport::-webkit-scrollbar{
  width: 10px;
}
/* xterm默认样式改变 */
::v-deep .xterm .xterm-viewport::-webkit-scrollbar-thumb{
  border-radius: 10px;
  background: rgba(62,68,107);
}
/* xterm默认样式改变 */
::v-deep .xterm .xterm-viewport::-webkit-scrollbar-track{
  border-radius: 0;
  background: rgba(0,0,0,0.1);
}
</style>

  除了css里面几乎每句都有注释,复制完慢慢改。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值