This commit is contained in:
Baobhan Sith
2025-04-17 20:26:30 +08:00
parent 09cba0b3d3
commit 9eb0bcc5f3
40 changed files with 2607 additions and 326 deletions
+110 -22
View File
@@ -2,7 +2,7 @@
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue';
import { ITheme } from 'xterm';
import { Terminal } from 'xterm';
import { useSettingsStore } from '../stores/settings.store'; // 导入设置 store
import { useAppearanceStore } from '../stores/appearance.store'; // 导入外观 store
import { storeToRefs } from 'pinia'; // 导入 storeToRefs
import { FitAddon } from 'xterm-addon-fit';
import { WebLinksAddon } from 'xterm-addon-web-links';
@@ -27,25 +27,25 @@ let terminal: Terminal | null = null;
let fitAddon: FitAddon | null = null;
let resizeObserver: ResizeObserver | null = null;
let debounceTimer: number | null = null; // 用于防抖的计时器 ID
const fontSize = ref(14); // 字体大小状态, 默认为14
const fontSize = ref(14); // 字体大小状态, 默认为14 (这个可以保留,或者也移到 appearance store)
// --- Settings Store ---
const settingsStore = useSettingsStore();
const { currentXtermTheme } = storeToRefs(settingsStore); // 获取响应式的 xterm 主题
// --- Appearance Store ---
const appearanceStore = useAppearanceStore();
const { currentTerminalTheme, currentTerminalFontFamily, terminalBackgroundImage, terminalBackgroundOpacity } = storeToRefs(appearanceStore); // 获取外观状态
// 防抖函数
const debounce = (func: Function, delay: number) => {
let timeoutId: number | null = null; // Use a local variable for the timeout ID
return (...args: any[]) => {
if (debounceTimer) {
clearTimeout(debounceTimer);
if (timeoutId !== null) {
clearTimeout(timeoutId);
}
debounceTimer = window.setTimeout(() => {
timeoutId = window.setTimeout(() => {
func(...args);
debounceTimer = null;
timeoutId = null;
}, delay);
};
};
// 防抖处理由 ResizeObserver 触发的 resize 事件
const debouncedEmitResize = debounce((term: Terminal) => {
if (term && props.isActive) { // 仅当标签仍处于活动状态时才发送防抖后的 resize
@@ -75,9 +75,9 @@ onMounted(() => {
if (terminalRef.value) {
terminal = new Terminal({
cursorBlink: true,
fontSize: fontSize.value,
fontFamily: 'Consolas, "Courier New", monospace, "Microsoft YaHei", "微软雅黑"',
theme: currentXtermTheme.value, // *** 使用 store 中的当前 xterm 主题 ***
fontSize: fontSize.value, // 初始字体大小
fontFamily: currentTerminalFontFamily.value, // 使用 store 中的字体设置
theme: currentTerminalTheme.value, // 使用 store 中的当前 xterm 主题
rows: 24, // 初始行数
cols: 80, // 初始列数
allowTransparency: true,
@@ -185,19 +185,35 @@ onMounted(() => {
emit('ready', { sessionId: props.sessionId, terminal: terminal });
}
// --- 监听 xterm 主题变化 ---
watch(currentXtermTheme, (newTheme) => {
// --- 监听外观变化 ---
watch(currentTerminalTheme, (newTheme) => {
if (terminal) {
console.log(`[Terminal ${props.sessionId}] Applying new xterm theme.`); // 日志改为中文
console.log(`[Terminal ${props.sessionId}] 应用新终端主题。`);
terminal.options.theme = newTheme;
// 可能需要重新渲染或刷新终端以完全应用主题,但通常 xterm 会自动处理
// terminal.refresh(0, terminal.rows - 1); // 如果需要强制刷新
}
}, { deep: true }); // 使用 deep watch
}, { deep: true });
// 聚焦终端
terminal.focus();
watch(currentTerminalFontFamily, (newFontFamily) => {
if (terminal) {
console.log(`[Terminal ${props.sessionId}] 应用新终端字体: ${newFontFamily}`);
terminal.options.fontFamily = newFontFamily;
// 字体变化可能影响尺寸,重新 fit
fitAndEmitResizeNow(terminal);
}
});
// 监听背景图片和透明度 (恢复之前的监听方式,因为监听整个对象可能引入其他问题)
watch([terminalBackgroundImage, terminalBackgroundOpacity], () => {
console.log(`[Terminal Watcher] terminalBackgroundImage or Opacity changed. New image: ${terminalBackgroundImage.value}`); // 添加日志确认 watcher 触发
applyTerminalBackground();
}, { immediate: true }); // 添加 immediate: true,强制立即执行一次
// 移除 onMounted 中的 applyTerminalBackground 调用,完全依赖 watch
// applyTerminalBackground(); // 初始应用一次
// 聚焦终端 (添加 null check)
if (terminal) {
terminal.focus();
}
// 重新添加鼠标滚轮缩放功能
if (terminalRef.value) {
terminalRef.value.addEventListener('wheel', (event: WheelEvent) => {
@@ -264,6 +280,53 @@ const write = (data: string | Uint8Array) => {
};
defineExpose({ write });
// --- 应用终端背景 ---
const applyTerminalBackground = () => {
if (terminalRef.value) {
if (terminalBackgroundImage.value) {
// --- 修改开始 ---
// 使用环境变量获取后端基础 URL
const backendUrl = import.meta.env.VITE_API_BASE_URL || ''; // 提供一个默认空字符串以防万一
const imagePath = terminalBackgroundImage.value;
console.log(`[Terminal applyTerminalBackground] backendUrl: "${backendUrl}", imagePath: "${imagePath}"`); // 详细日志
const fullImageUrl = `${backendUrl}${imagePath}`;
console.log(`[Terminal applyTerminalBackground] fullImageUrl: "${fullImageUrl}"`); // 打印完整 URL
// --- 修改结束 ---
// --- 使用 nextTick 包装样式应用 ---
nextTick(() => {
if (terminalRef.value) { // 再次检查 ref 是否存在
terminalRef.value.style.backgroundImage = `url(${fullImageUrl})`;
terminalRef.value.style.backgroundSize = 'cover'; // Or 'contain', 'auto', etc.
terminalRef.value.style.backgroundPosition = 'center';
terminalRef.value.style.backgroundRepeat = 'no-repeat';
// 添加 CSS 类
terminalRef.value.classList.add('has-terminal-background');
}
});
// 应用透明度: 通过设置背景色实现,需要 xterm 的 allowTransparency: true
// 注意:这会影响整个终端的背景,包括文本后的背景
// 一个常见的做法是设置一个稍微透明的背景色,让图片透出来
// 例如,将 xterm 主题的 background 设置为 rgba(r, g, b, opacity)
// 这里我们简单设置容器的 opacity,但这会影响文本!更好的方法是修改主题。
// 另一种方法是用伪元素做背景层。
// 为了简单起见,我们暂时只设置背景图,透明度让用户在主题中调整 background 的 alpha 值。
// terminalRef.value.style.opacity = terminalBackgroundOpacity.value.toString(); // 不推荐直接设置 opacity
console.log(`[Terminal ${props.sessionId}] 应用终端背景图片: ${terminalBackgroundImage.value}`);
} else {
// --- 使用 nextTick 包装样式移除 ---
nextTick(() => {
if (terminalRef.value) { // 再次检查 ref 是否存在
terminalRef.value.style.backgroundImage = 'none';
// 移除 CSS 类
terminalRef.value.classList.remove('has-terminal-background');
}
});
// terminalRef.value.style.opacity = '1'; // 移除背景时恢复不透明
console.log(`[Terminal ${props.sessionId}] 移除终端背景图片。`);
}
}
};
</script>
<template>
@@ -276,7 +339,32 @@ defineExpose({ write });
width: 100%;
height: 100%; /* 高度需要由父容器控制 */
overflow: hidden; /* 阻止此容器本身产生滚动条 */
position: relative; /* 用于可能的伪元素背景 */
}
/* 移除 :deep 样式,让 xterm 内部自然处理滚动 */
/* 示例:使用伪元素添加带透明度的背景层 (如果需要独立于主题的透明度) */
/*
.terminal-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image: var(--terminal-bg-image);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
opacity: var(--terminal-bg-opacity);
z-index: -1; // 确保在 xterm 内容后面
}
*/
/* 当容器有背景图时,强制内部 xterm 视口和屏幕背景透明 */
.terminal-container.has-terminal-background :deep(.xterm-viewport),
.terminal-container.has-terminal-background :deep(.xterm-screen) {
background-color: transparent !important;
}
</style>