This commit is contained in:
Baobhan Sith
2025-04-14 22:51:05 +08:00
parent 286492fc63
commit a974b8b1d9
49 changed files with 13954 additions and 0 deletions
@@ -0,0 +1,139 @@
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue';
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import { WebLinksAddon } from 'xterm-addon-web-links';
import 'xterm/css/xterm.css'; // 引入 xterm 样式
// 定义 props 和 emits
const props = defineProps<{
stream?: ReadableStream<string>; // 用于接收来自 WebSocket 的数据流 (可选)
options?: object; // xterm 的配置选项
}>();
const emit = defineEmits<{
(e: 'data', data: string): void; // 用户输入事件
(e: 'resize', dimensions: { cols: number; rows: number }): void; // 终端大小调整事件
(e: 'ready', terminal: Terminal): void; // 终端准备就绪事件
}>();
const terminalRef = ref<HTMLElement | null>(null); // 终端容器的引用
let terminal: Terminal | null = null;
let fitAddon: FitAddon | null = null;
let resizeObserver: ResizeObserver | null = null;
// 初始化终端
onMounted(() => {
if (terminalRef.value) {
terminal = new Terminal({
cursorBlink: true,
fontSize: 14,
fontFamily: 'Consolas, "Courier New", monospace',
theme: { // 简单主题示例
background: '#1e1e1e',
foreground: '#d4d4d4',
cursor: '#d4d4d4',
},
rows: 24, // 初始行数
cols: 80, // 初始列数
...props.options, // 合并外部传入的选项
});
// 加载插件
fitAddon = new FitAddon();
terminal.loadAddon(fitAddon);
terminal.loadAddon(new WebLinksAddon());
// 将终端附加到 DOM
terminal.open(terminalRef.value);
// 适应容器大小
fitAddon.fit();
emit('resize', { cols: terminal.cols, rows: terminal.rows }); // 触发初始 resize 事件
// 监听用户输入
terminal.onData((data) => {
emit('data', data);
});
// 监听终端大小变化 (通过 ResizeObserver)
if (terminalRef.value) {
resizeObserver = new ResizeObserver(() => {
try {
fitAddon?.fit();
} catch (e) {
console.warn("Fit addon resize failed:", e);
}
});
resizeObserver.observe(terminalRef.value);
}
// 监听 fitAddon 的 resize 事件,获取新的尺寸并触发 emit
// 注意:fitAddon 本身不直接触发 resize 事件,我们需要在 fit() 后手动获取
const originalFit = fitAddon.fit.bind(fitAddon);
fitAddon.fit = () => {
originalFit();
if (terminal) {
emit('resize', { cols: terminal.cols, rows: terminal.rows });
}
};
// 处理传入的数据流 (如果提供了 stream prop)
watch(() => props.stream, async (newStream) => {
if (newStream) {
const reader = newStream.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
if (terminal && value) {
terminal.write(value); // 将流数据写入终端
}
}
} catch (error) {
console.error('读取终端流时出错:', error);
} finally {
reader.releaseLock();
}
}
}, { immediate: true }); // 立即执行一次 watch
// 触发 ready 事件
emit('ready', terminal);
// 聚焦终端
terminal.focus();
}
});
// 组件卸载前清理资源
onBeforeUnmount(() => {
if (resizeObserver && terminalRef.value) {
resizeObserver.unobserve(terminalRef.value);
}
if (terminal) {
terminal.dispose();
terminal = null;
}
});
// 暴露 write 方法给父组件 (可选)
const write = (data: string | Uint8Array) => {
terminal?.write(data);
};
defineExpose({ write });
</script>
<template>
<div ref="terminalRef" class="terminal-container"></div>
</template>
<style scoped>
.terminal-container {
width: 100%;
height: 100%; /* 高度需要由父容器控制 */
overflow: hidden; /* 防止滚动条出现 */
}
</style>