Vue3 + xterm + eventSource

xterm 是一个使用 typescript 编写的前端终端组件,可以在浏览器中实现一个命令行终端应用,通常与 websocket一起使用。

一、安装

pnpm install xterm
or
yarn add xterm
or
pnpm install xterm

二、代码实现: 实现日志展示

<template>
	<a-modal class="task-log-dialog" :title="title" :visible="visible" :footer="false" @cancel="onCancel" width="1200px" :mask-closable="false">
		<div class="main-box">
			<div class="top-box flex-row-start-center">
				<a-input v-model:value="grep" placeholder="Filter(regexp)..." size="small" allow-clear @input="searchLog"></a-input>
				<a-input v-model:value="podName" placeholder="podName" size="small" allow-clear @input="searchLog"></a-input>
			</div>
			<div class="content-box">
				<div ref="terminal" id="terminal" v-loading="loading" element-loading-text="拼命加载中"></div>
			</div>

			<div class="bottom-box">Logs may not appear for pods that are deleted.</div>
		</div>
	</a-modal>
</template>

<script lang="ts" setup>
import { ref, watch } from 'vue'
import useSystemStore from '@/store/modules/system'
import { debounce } from '@/utils/common'
import { Terminal } from 'xterm'
import { FitAddon } from 'xterm-addon-fit'
import 'xterm/css/xterm.css'

type propsType = {
	currentTask: {
		projectUuid: string
		name: string
		podName: string
		grep: string
		follow: boolean
	}
}
const props = defineProps<propsType>()
const title = ref<string>('构建日志')
const visible = ref<boolean>(false)
const grep = ref<string>('')
const podName = ref<string>('')
const showModel = () => {
	visible.value = true
}
defineExpose({
	showModel
})
const emit = defineEmits(['hide'])
const onCancel = () => {
	visible.value = false
	eventSource.value?.close()
	term.value?.reset()
	term.value?.clear()
	if (terminal.value?.innerHTML) {
		terminal.value.innerHTML = ''
		terminal.value = null
	}
	emit('hide')
}

const baseUrl = import.meta.env.VITE_APP_BASE_URL
const eventSource = ref<EventSource | null>(null)
const getLog = async () => {
	if (!props.currentTask.name) return
	initTerm()
	createEventSource()
}
const createEventSource = () => {
	eventSource.value = new EventSource(
		`${baseUrl}/v1/projects/${useSystemStore().projectUuid}/tasks/${props.currentTask.name}/log?podName=${podName.value}&grep=${grep.value}&follow=true`
	)
	eventSource.value.onopen = event => {
		loading.value = false
		term.value?.clear()
		console.log('onopen', event)
	}
	eventSource.value.onmessage = event => {
		term.value?.clear()
		if (eventSource.value?.readyState === 1 && !JSON.parse(event.data).result?.completed) {
			const eventData = JSON.parse(event.data).result
			loading.value = false
			term.value?.write(eventData.PodName + ': ' + eventData.Content + '\r\n')
		}
		if (eventSource.value?.readyState === 1 && JSON.parse(event.data).result?.completed) {
			console.log('complete')
			eventSource.value?.close()
		}
	}

	eventSource.value.onerror = event => {
		console.log('error', event)
		eventSource.value?.close()
	}
}

const terminal = ref<HTMLElement | null>(null)
const fitAddon = new FitAddon()
const loading = ref(true)
const term = ref<null | Terminal>(null)
const initTerm = () => {
	if (!term.value) {
		term.value = new Terminal({
			fontSize: 14,
			scrollback: 999999999999,
			allowTransparency: true,
			fontFamily: 'Monaco, Menlo, Consolas, Courier New, monospace',
			rows: 40,
			disableStdin: true, //是否应禁用输入
			cursorStyle: 'underline',
			cursorBlink: false,
			theme: {
				foreground: '#fff',
				background: '#000',
				cursor: 'help'
			}
		})
	}

	setTimeout(() => {
		term.value?.open(terminal.value as HTMLElement)
		term.value?.clear()
		term.value?.loadAddon(fitAddon)
		fitAddon.fit()
	}, 5)
}

const searchLog = debounce(() => {
	term.value?.clear()
	terminal.value = null
	getLog()
}, 1000)

watch(visible, value => {
	if (value) {
		title.value = '构建日志 - ' + props.currentTask.name
		getLog()
	} else {
		eventSource.value?.close()
		term.value?.reset()
		term.value?.clear()
		if (terminal.value?.innerHTML) {
			terminal.value.innerHTML = ''
			terminal.value = null
		}
	}
})
</script>

<style lang="less">
.task-log-dialog {
	.main-box {
		width: 1152px;
		.top-box {
			margin-bottom: 20px;
			height: 30px;

			.ant-input-affix-wrapper,
			.ant-select-selector {
				width: 280px;
				height: 30px;
				margin-right: 20px;
			}
		}
		.content-box {
			.content {
				width: 100%;
			}
		}

		.bottom-box {
			margin-top: 30px;
		}
	}
}
</style>


三、效果如下

在这里插入图片描述

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要在 Vue 中使用 xterm.js 和 WebSocket 实现终端,你需要将用户输入的命令发送给后端,然后将后端返回的结果输出到 xterm.js 终端中。以下是一个简单的示例: ```html <template> <div id="terminal"></div> </template> <script> import { Terminal } from 'xterm'; import { FitAddon } from 'xterm-addon-fit'; export default { data() { return { socket: null, // WebSocket 实例 term: null, // Terminal 实例 }; }, mounted() { // 创建 WebSocket 实例 this.socket = new WebSocket('ws://localhost:8080'); // 创建 Terminal 实例 this.term = new Terminal(); const fitAddon = new FitAddon(); this.term.loadAddon(fitAddon); this.term.open(document.getElementById('terminal')); // 处理 WebSocket 消息 this.socket.onmessage = (event) => { this.term.write(event.data); }; // 处理输入事件 this.term.onData(data => { this.socket.send(data); }); // 调整终端大小 this.term.onResize(size => { const cols = size.cols; const rows = size.rows; this.socket.send(JSON.stringify({ type: 'resize', cols, rows })); }); // 发送 resize 消息 const cols = this.term.cols; const rows = this.term.rows; this.socket.send(JSON.stringify({ type: 'resize', cols, rows })); }, beforeDestroy() { // 关闭 WebSocket 连接 this.socket.close(); } } </script> ``` 以上代码中,我们首先在 `mounted` 钩子函数中创建了一个 WebSocket 实例和一个 Terminal 实例。然后我们为 WebSocket 实例添加了一个 `onmessage` 事件监听器,该监听器会在接收到服务器返回的消息时触发,我们在该事件处理函数中将消息输出到终端中。 接着,我们为 Terminal 实例添加了一个 `onData` 事件监听器,该监听器会在用户输入时触发,我们在该事件处理函数中向服务器发送用户输入的命令。同时,我们还为 Terminal 实例添加了一个 `onResize` 事件监听器,该监听器会在终端大小调整时触发,我们在该事件处理函数中向服务器发送终端大小变化的消息。 最后,我们在 `beforeDestroy` 钩子函数中关闭了 WebSocket 连接。 需要注意的是,以上代码中的 WebSocket 连接是通过 `ws://localhost:8080` 连接本地服务器的,你需要根据实际情况修改 WebSocket 连接地址。另外,代码中的消息格式和处理逻辑也需要根据实际情况进行修改。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值