This commit is contained in:
Baobhan Sith
2025-04-18 14:36:02 +08:00
parent 15fe6a8279
commit 1b2466899d
9 changed files with 407 additions and 44 deletions
+17
View File
@@ -1357,6 +1357,22 @@
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
"license": "MIT"
},
"node_modules/@xterm/addon-search": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/@xterm/addon-search/-/addon-search-0.15.0.tgz",
"integrity": "sha512-ZBZKLQ+EuKE83CqCmSSz5y1tx+aNOCUaA7dm6emgOX+8J9H1FWXZyrKfzjwzV+V14TV3xToz1goIeRhXBS5qjg==",
"license": "MIT",
"peerDependencies": {
"@xterm/xterm": "^5.0.0"
}
},
"node_modules/@xterm/xterm": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
"license": "MIT",
"peer": true
},
"node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@@ -6369,6 +6385,7 @@
"dependencies": {
"@fortawesome/fontawesome-free": "^6.7.2",
"@simplewebauthn/browser": "^13.1.0",
"@xterm/addon-search": "^0.15.0",
"axios": "^1.8.4",
"monaco-editor": "^0.52.2",
"pinia": "^3.0.2",
+1
View File
@@ -10,6 +10,7 @@
"dependencies": {
"@fortawesome/fontawesome-free": "^6.7.2",
"@simplewebauthn/browser": "^13.1.0",
"@xterm/addon-search": "^0.15.0",
"axios": "^1.8.4",
"monaco-editor": "^0.52.2",
"pinia": "^3.0.2",
@@ -1,34 +1,120 @@
<script setup lang="ts">
import { ref } from 'vue';
import { ref, watch } from 'vue'; // Remove computed
import { useI18n } from 'vue-i18n';
// 假设你有一个图标库,例如 unplugin-icons 或类似库
// import SearchIcon from '~icons/mdi/magnify';
// import ArrowUpIcon from '~icons/mdi/arrow-up';
// import ArrowDownIcon from '~icons/mdi/arrow-down';
// import CloseIcon from '~icons/mdi/close';
const emit = defineEmits(['send-command']);
const emit = defineEmits(['send-command', 'search', 'find-next', 'find-previous', 'close-search']);
const { t } = useI18n();
// Props definition is now empty as search results are no longer handled here
const props = defineProps<{
// No props defined here currently
}>();
const commandInput = ref('');
const isSearching = ref(false);
const searchTerm = ref('');
// *** 移除本地的搜索结果 ref ***
// const searchResultCount = ref(0);
// const currentSearchResultIndex = ref(0);
const sendCommand = () => {
const command = commandInput.value; // 获取原始输入,不进行 trim
// 无论输入框是否为空,都发送内容(空字符串或命令)加上换行符
console.log(`[CommandInputBar] Sending command: ${command || '<Enter>'} `); // 日志记录空回车
emit('send-command', command + '\n'); // 发送命令(或空字符串)并附加换行符
commandInput.value = ''; // 清空输入框
const command = commandInput.value;
console.log(`[CommandInputBar] Sending command: ${command || '<Enter>'} `);
emit('send-command', command + '\n');
commandInput.value = '';
};
const toggleSearch = () => {
isSearching.value = !isSearching.value;
if (!isSearching.value) {
searchTerm.value = ''; // 关闭搜索时清空
emit('close-search'); // 通知父组件关闭搜索
} else {
// 可以在这里聚焦搜索输入框
// nextTick(() => searchInputRef.value?.focus());
}
};
const performSearch = () => {
emit('search', searchTerm.value);
// 实际的计数更新逻辑应该由父组件通过 props 或事件传递回来
};
const findNext = () => {
emit('find-next');
};
const findPrevious = () => {
emit('find-previous');
};
// 监听搜索词变化,执行搜索
watch(searchTerm, (newValue) => {
if (isSearching.value) {
performSearch();
}
});
// 可以在这里添加一个 ref 用于聚焦搜索框
// const searchInputRef = ref<HTMLInputElement | null>(null);
// Removed debug computed property
</script>
<template>
<div class="command-input-bar">
<div class="input-wrapper">
<!-- 命令输入框 -->
<input
v-if="!isSearching"
type="text"
v-model="commandInput"
:placeholder="t('commandInputBar.placeholder')"
class="command-input"
@keydown.enter="sendCommand"
/>
<!-- 可以在这里添加按钮 -->
<!-- 搜索输入框 -->
<input
v-if="isSearching"
type="text"
v-model="searchTerm"
:placeholder="t('commandInputBar.searchPlaceholder')"
class="search-input"
@keydown.enter="findNext"
@keydown.shift.enter="findPrevious"
ref="searchInputRef"
/>
<!-- 搜索控制按钮 -->
<div class="search-controls">
<button @click="toggleSearch" class="icon-button" :title="isSearching ? t('commandInputBar.closeSearch') : t('commandInputBar.openSearch')">
<!-- 使用图标代替文字 -->
<span v-if="!isSearching">🔍</span> <!-- 临时使用 emoji -->
<span v-else></span> <!-- 临时使用 emoji -->
<!-- <SearchIcon v-if="!isSearching" /> -->
<!-- <CloseIcon v-else /> -->
</button>
<template v-if="isSearching">
<button @click="findPrevious" class="icon-button" :title="t('commandInputBar.findPrevious')">
<span></span> <!-- 临时使用 emoji -->
<!-- <ArrowUpIcon /> -->
</button>
<button @click="findNext" class="icon-button" :title="t('commandInputBar.findNext')">
<span></span> <!-- 临时使用 emoji -->
<!-- <ArrowDownIcon /> -->
</button>
<!-- 搜索结果显示已移除 -->
</template>
</div>
</div>
<!-- 可以在这里添加其他按钮 -->
<!-- Removed hidden span -->
</div>
</template>
@@ -36,41 +122,83 @@ const sendCommand = () => {
.command-input-bar {
display: flex;
align-items: center;
padding: 5px 0px;
background-color: var(--app-bg-color); /* Use theme variable */
min-height: 30px; /* 保证一定高度 */
padding: 5px 10px; /* 增加左右 padding */
background-color: var(--app-bg-color);
min-height: 30px;
gap: 10px; /* 在输入框和控件之间添加间隙 */
}
.input-wrapper {
flex-grow: 1; /* 让输入框容器占据大部分空间 */
flex-grow: 1;
display: flex;
justify-content: center; /* 水平居中输入框 */
align-items: center; /* 垂直居中对齐 */
background-color: transparent;
position: relative; /* 为了按钮定位 */
}
.command-input {
.command-input,
.search-input {
padding: 6px 10px;
border: 1px solid var(--border-color); /* Use theme variable */
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 0.9em;
background-color: var(--app-bg-color); /* Use theme variable */
color: var(--text-color); /* Use theme variable */
width: 60%; /* 输入框宽度,可调整 */
max-width: 800px; /* 最大宽度 */
background-color: var(--input-bg-color, var(--app-bg-color)); /* Use specific or fallback theme variable */
color: var(--text-color);
flex-grow: 1; /* 输入框占据可用空间 */
outline: none;
transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
margin-right: 5px; /* 与右侧控件保持距离 */
}
.command-input:focus {
border-color: var(--button-bg-color); /* Use theme variable */
box-shadow: 0 0 5px var(--button-bg-color, #007bff); /* Use theme variable for glow */
.command-input:focus,
.search-input:focus {
border-color: var(--button-bg-color);
box-shadow: 0 0 5px var(--button-bg-color, #007bff);
}
/* 可以添加按钮样式 */
.search-controls {
display: flex;
align-items: center;
gap: 5px; /* 控件之间的间隙 */
background-color: var(--app-bg-color); /* 确保背景色一致 */
}
.icon-button {
background: none;
border: none;
padding: 4px;
cursor: pointer;
color: var(--text-color);
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: background-color 0.2s;
}
.icon-button:hover {
background-color: var(--hover-bg-color, #eee); /* Use theme variable */
}
.icon-button span { /* 临时 emoji 样式 */
font-size: 1.1em;
}
/* 实际使用图标库时可以这样设置大小 */
/*
.command-input-bar button {
margin-left: 10px;
padding: 6px 12px;
.icon-button svg {
width: 18px;
height: 18px;
}
*/
.search-results {
font-size: 0.8em;
color: var(--text-secondary-color, #666); /* Use theme variable */
margin-left: 5px;
white-space: nowrap; /* 防止换行 */
}
.search-results.no-results {
color: var(--warning-color, #ffc107); /* Use theme variable */
}
</style>
@@ -29,6 +29,7 @@ const props = defineProps({
type: String as PropType<string | null>,
default: null,
},
// Removed terminalManager prop definition
});
// --- Emits ---
@@ -47,7 +48,12 @@ const emit = defineEmits({
'request-edit-connection': null, // (conn: any)
// *** terminal-ready payload ***
'terminal-ready': (payload: { sessionId: string; terminal: any }) => // 使 any Terminal
typeof payload === 'object' && typeof payload.sessionId === 'string' && typeof payload.terminal === 'object'
typeof payload === 'object' && typeof payload.sessionId === 'string' && typeof payload.terminal === 'object',
// *** ***
'search': null, // (searchTerm: string)
'find-next': null, // ()
'find-previous': null, // ()
'close-search': null, // ()
});
// --- Setup ---
@@ -146,9 +152,15 @@ const componentProps = computed(() => {
};
case 'commandBar':
// CommandInputBar send-command
// searchResultCount currentSearchResultIndex terminalManager
return {
class: 'pane-content',
onSendCommand: (command: string) => emit('sendCommand', command),
//
onSearch: (term: string) => emit('search', term),
onFindNext: () => emit('find-next'),
onFindPrevious: () => emit('find-previous'),
onCloseSearch: () => emit('close-search'),
};
case 'connections':
// WorkspaceConnectionList connect-request
@@ -238,6 +250,10 @@ const handlePaneResize = (eventData: { panes: Array<{ size: number; [key: string
emit('request-add-connection');
}"
@request-edit-connection="emit('request-edit-connection', $event)"
@search="emit('search', $event)"
@find-next="emit('find-next')"
@find-previous="emit('find-previous')"
@close-search="emit('close-search')"
/>
</pane>
</splitpanes>
@@ -309,6 +325,12 @@ const handlePaneResize = (eventData: { panes: Array<{ size: number; [key: string
emit('request-add-connection');
}"
/>
<!-- 渲染 CommandInputBar -->
<component
v-else-if="layoutNode.component === 'commandBar'"
:is="currentComponent"
v-bind="componentProps"
/>
<!-- 渲染其他组件 -->
<component
v-else
+32 -5
View File
@@ -6,7 +6,9 @@ import { useAppearanceStore } from '../stores/appearance.store'; // 导入外观
import { storeToRefs } from 'pinia'; // storeToRefs
import { FitAddon } from 'xterm-addon-fit';
import { WebLinksAddon } from 'xterm-addon-web-links';
import { SearchAddon, type ISearchOptions } from '@xterm/addon-search'; // *** ***
import 'xterm/css/xterm.css'; // xterm
// *** CSS ***
// props emits
const props = defineProps<{
@@ -19,12 +21,14 @@ const props = defineProps<{
const emit = defineEmits<{
(e: 'data', data: string): void; //
(e: 'resize', dimensions: { cols: number; rows: number }): void; //
(e: 'ready', payload: { sessionId: string; terminal: Terminal }): void; // *** ready sessionId terminal ***
// *** ready payload searchAddon ***
(e: 'ready', payload: { sessionId: string; terminal: Terminal; searchAddon: SearchAddon | null }): void;
}>();
const terminalRef = ref<HTMLElement | null>(null); //
let terminal: Terminal | null = null;
let fitAddon: FitAddon | null = null;
let searchAddon: SearchAddon | null = null; // *** searchAddon ***
let resizeObserver: ResizeObserver | null = null;
let debounceTimer: number | null = null; // ID
// const fontSize = ref(14); // store
@@ -111,8 +115,10 @@ onMounted(() => {
//
fitAddon = new FitAddon();
searchAddon = new SearchAddon(); // *** SearchAddon ***
terminal.loadAddon(fitAddon);
terminal.loadAddon(new WebLinksAddon());
terminal.loadAddon(searchAddon); // *** SearchAddon ***
// DOM
terminal.open(terminalRef.value);
@@ -199,11 +205,11 @@ onMounted(() => {
}
}, { immediate: true }); // watch
// ready sessionId terminal
// ready sessionId, terminal searchAddon
if (terminal) {
emit('ready', { sessionId: props.sessionId, terminal: terminal });
emit('ready', { sessionId: props.sessionId, terminal: terminal, searchAddon: searchAddon });
}
// --- ---
watch(currentTerminalTheme, (newTheme) => {
if (terminal) {
@@ -311,7 +317,28 @@ onBeforeUnmount(() => {
const write = (data: string | Uint8Array) => {
terminal?.write(data);
};
defineExpose({ write });
// *** ***
const findNext = (term: string, options?: ISearchOptions): boolean => {
if (searchAddon) {
return searchAddon.findNext(term, options);
}
return false;
};
const findPrevious = (term: string, options?: ISearchOptions): boolean => {
if (searchAddon) {
return searchAddon.findPrevious(term, options);
}
return false;
};
const clearSearch = () => {
searchAddon?.clearDecorations();
};
defineExpose({ write, findNext, findPrevious, clearSearch });
// --- ---
const applyTerminalBackground = () => {
@@ -1,6 +1,7 @@
import { ref, readonly, type Ref, ComputedRef } from 'vue'; // 修正导入,移除大写 Readonly
import { ref, readonly, type Ref, ComputedRef } from 'vue';
// import { useWebSocketConnection } from './useWebSocketConnection'; // 移除全局导入
import type { Terminal } from 'xterm';
import type { SearchAddon, ISearchOptions } from '@xterm/addon-search'; // *** 移除 ISearchResult 导入 ***
import type { WebSocketMessage, MessagePayload } from '../types/websocket.types';
// 定义与 WebSocket 相关的依赖接口
@@ -22,6 +23,10 @@ export function createSshTerminalManager(sessionId: string, wsDeps: SshTerminalD
const { sendMessage, onMessage, isConnected } = wsDeps;
const terminalInstance = ref<Terminal | null>(null);
const searchAddon = ref<SearchAddon | null>(null); // Keep searchAddon ref
// Removed search result state refs
// const searchResultCount = ref(0);
// const currentSearchResultIndex = ref(-1);
const terminalOutputBuffer = ref<string[]>([]); // 缓冲 WebSocket 消息直到终端准备好
const isSshConnected = ref(false); // 新增:跟踪 SSH 连接状态
@@ -35,9 +40,38 @@ export function createSshTerminalManager(sessionId: string, wsDeps: SshTerminalD
// --- 终端事件处理 ---
const handleTerminalReady = (term: Terminal) => {
console.log(`[会话 ${sessionId}][SSH终端模块] 终端实例已就绪。`);
// *** 更新 handleTerminalReady 签名以接收 searchAddon ***
const handleTerminalReady = (payload: { terminal: Terminal; searchAddon: SearchAddon | null }) => {
const { terminal: term, searchAddon: addon } = payload;
console.log(`[会话 ${sessionId}][SSH终端模块] 终端实例已就绪。SearchAddon 实例:`, addon ? '存在' : '不存在');
terminalInstance.value = term;
searchAddon.value = addon; // *** 存储 searchAddon 实例 ***
// *** 监听搜索结果变化 ***
if (searchAddon.value) {
// *** 移除错误的类型注解,让 TS 推断 ***
searchAddon.value.onDidChangeResults((results) => {
// *** 添加更详细的日志 ***
console.log(`[会话 ${sessionId}][SearchAddon] onDidChangeResults 事件触发! results:`, JSON.stringify(results)); // 使用 JSON.stringify 查看完整结构
if (results && typeof results.resultIndex === 'number' && typeof results.resultCount === 'number') {
// 确认 results 包含预期的数字属性
searchResultCount.value = results.resultCount;
currentSearchResultIndex.value = results.resultIndex; // xterm 的索引是从 0 开始的
console.log(`[会话 ${sessionId}][SearchAddon] 状态已更新: index=${currentSearchResultIndex.value}, count=${searchResultCount.value}`);
} else {
// 没有结果、搜索被清除或 results 结构不符合预期
console.log(`[会话 ${sessionId}][SearchAddon] 清除搜索状态或结果无效。 results:`, JSON.stringify(results)); // 使用 JSON.stringify 查看完整结构
searchResultCount.value = 0;
currentSearchResultIndex.value = -1;
// console.log(`[会话 ${sessionId}][SearchAddon] 搜索结果清除或无匹配。`); // 这行日志有点重复,可以注释掉
}
});
// *** 添加确认日志 ***
console.log(`[会话 ${sessionId}][SearchAddon] onDidChangeResults 监听器已附加。`);
} else {
console.warn(`[会话 ${sessionId}][SearchAddon] 无法附加 onDidChangeResults 监听器,searchAddon 实例为空。`);
}
// --- 添加日志:检查缓冲区处理 ---
console.log(`[会话 ${sessionId}][SSH前端] handleTerminalReady: 准备处理缓冲区,缓冲区长度: ${terminalOutputBuffer.value.length}`);
if (terminalOutputBuffer.value.length > 0) {
@@ -313,6 +347,44 @@ export function createSshTerminalManager(sessionId: string, wsDeps: SshTerminalD
sendMessage({ type: 'ssh:input', sessionId, payload: { data } });
};
// --- 搜索相关方法 (移除计数逻辑) ---
// Removed countOccurrences helper function
const searchNext = (term: string, options?: ISearchOptions): boolean => {
if (searchAddon.value) {
console.log(`[会话 ${sessionId}][SSH终端模块] 执行 searchNext: "${term}"`);
const found = searchAddon.value.findNext(term, options);
// Removed manual count and state update
return found;
}
console.warn(`[会话 ${sessionId}][SSH终端模块] searchNext 调用失败,searchAddon 不可用。`);
// Removed state reset on failure
return false;
};
const searchPrevious = (term: string, options?: ISearchOptions): boolean => {
if (searchAddon.value) {
console.log(`[会话 ${sessionId}][SSH终端模块] 执行 searchPrevious: "${term}"`);
const found = searchAddon.value.findPrevious(term, options);
// Removed manual count and state update
return found;
}
console.warn(`[会话 ${sessionId}][SSH终端模块] searchPrevious 调用失败,searchAddon 不可用。`);
// Removed state reset on failure
return false;
};
const clearTerminalSearch = () => {
if (searchAddon.value) {
console.log(`[会话 ${sessionId}][SSH终端模块] 清除搜索高亮。`);
searchAddon.value.clearDecorations();
}
// Removed state reset
console.log(`[会话 ${sessionId}][SSH终端模块] 搜索高亮已清除 (状态不再管理)。`);
};
// 返回工厂实例
return {
// 公共接口
@@ -321,9 +393,16 @@ export function createSshTerminalManager(sessionId: string, wsDeps: SshTerminalD
handleTerminalResize,
sendData, // 新增:允许外部直接发送数据
cleanup,
// --- 新增暴露 ---
// --- 搜索方法 ---
searchNext,
searchPrevious,
clearTerminalSearch,
// --- 暴露状态 ---
isSshConnected: readonly(isSshConnected), // 暴露 SSH 连接状态 (只读)
terminalInstance // 暴露 terminal 实例,以便 WorkspaceView 可以写入提示信息
terminalInstance, // 暴露 terminal 实例,以便 WorkspaceView 可以写入提示信息
// Removed search result state exposure
// searchResultCount: readonly(searchResultCount),
// currentSearchResultIndex: readonly(currentSearchResultIndex),
};
}
+9
View File
@@ -682,5 +682,14 @@
"errorCommandRequired": "Command cannot be empty",
"add": "Add"
}
},
"commandInputBar": {
"placeholder": "Enter command here...",
"searchPlaceholder": "Search in terminal...",
"openSearch": "Open terminal search",
"closeSearch": "Close terminal search",
"findPrevious": "Find previous",
"findNext": "Find next",
"noResults": "No results"
}
}
+9
View File
@@ -687,5 +687,14 @@
"errorCommandRequired": "指令内容不能为空",
"add": "添加"
}
},
"commandInputBar": {
"placeholder": "在此输入命令...",
"searchPlaceholder": "在终端中搜索...",
"openSearch": "打开终端搜索",
"closeSearch": "关闭终端搜索",
"findPrevious": "查找上一个",
"findNext": "查找下一个",
"noResults": "无结果"
}
}
+77 -6
View File
@@ -14,6 +14,7 @@ import { useLayoutStore } from '../stores/layout.store';
import { useCommandHistoryStore } from '../stores/commandHistory.store';
import type { ConnectionInfo } from '../stores/connections.store';
import type { Terminal } from 'xterm'; // *** Terminal ***
import type { ISearchOptions } from '@xterm/addon-search'; // *** ***
// --- Setup ---
const { t } = useI18n();
@@ -53,6 +54,9 @@ const showAddEditForm = ref(false);
const connectionToEdit = ref<ConnectionInfo | null>(null);
const showLayoutConfigurator = ref(false); //
// --- ---
const currentSearchTerm = ref(''); //
// --- ---
onMounted(() => {
console.log('[工作区视图] 组件已挂载。');
@@ -163,14 +167,76 @@ onBeforeUnmount(() => {
// ( Terminal)
// LayoutRenderer Terminal emit('terminal-ready', payload)
const handleTerminalReady = (payload: { sessionId: string; terminal: Terminal }) => { // *** sessionId terminal payload ***
console.log(`[工作区视图 ${payload.sessionId}] 收到 terminal-ready 事件。`); //
sessionStore.sessions.get(payload.sessionId)?.terminalManager.handleTerminalReady(payload.terminal); // *** terminal ***
};
// *** payload searchAddon ***
const handleTerminalReady = (payload: { sessionId: string; terminal: Terminal; searchAddon: any | null }) => { // 使 any SearchAddon
console.log(`[工作区视图 ${payload.sessionId}] 收到 terminal-ready 事件。Payload:`, payload); // *** Payload ***
// *** payload searchAddon ***
if (payload && payload.searchAddon) {
console.log(`[工作区视图 ${payload.sessionId}] Payload 包含 searchAddon 实例。`);
} else {
console.warn(`[工作区视图 ${payload.sessionId}] Payload 未包含 searchAddon 实例! Payload:`, payload);
}
// *** terminal searchAddon payload ***
sessionStore.sessions.get(payload.sessionId)?.terminalManager.handleTerminalReady(payload);
};
// --- ---
const handleSearch = (term: string) => {
currentSearchTerm.value = term;
if (!term) {
//
handleCloseSearch();
return;
}
console.log(`[WorkspaceView] Received search event: "${term}"`);
//
handleFindNext();
};
// --- ( FileEditorContainer) ---
const handleCloseEditorTab = (tabId: string) => {
const handleFindNext = () => {
const manager = activeSession.value?.terminalManager;
if (manager && currentSearchTerm.value) {
console.log(`[WorkspaceView] Calling findNext for term: "${currentSearchTerm.value}"`);
const found = manager.searchNext(currentSearchTerm.value, { incremental: true });
console.log(`[WorkspaceView] findNext returned: ${found}`); //
if (!found) {
console.log(`[WorkspaceView] findNext: No more results for "${currentSearchTerm.value}"`);
// UI
}
} else {
console.warn(`[WorkspaceView] Cannot findNext, no active session manager or search term.`);
}
};
const handleFindPrevious = () => {
const manager = activeSession.value?.terminalManager;
if (manager && currentSearchTerm.value) {
console.log(`[WorkspaceView] Calling findPrevious for term: "${currentSearchTerm.value}"`);
const found = manager.searchPrevious(currentSearchTerm.value, { incremental: true });
console.log(`[WorkspaceView] findPrevious returned: ${found}`); //
if (!found) {
console.log(`[WorkspaceView] findPrevious: No previous results for "${currentSearchTerm.value}"`);
// UI
}
} else {
console.warn(`[WorkspaceView] Cannot findPrevious, no active session manager or search term.`);
}
};
const handleCloseSearch = () => {
console.log(`[WorkspaceView] Received close-search event.`);
currentSearchTerm.value = ''; //
const manager = activeSession.value?.terminalManager;
if (manager) {
manager.clearTerminalSearch();
} else {
console.warn(`[WorkspaceView] Cannot clear search, no active session manager.`);
}
};
// Removed computed properties for search results, will pass manager directly
// --- ( FileEditorContainer) ---
const handleCloseEditorTab = (tabId: string) => {
const isShared = shareFileEditorTabsBoolean.value;
console.log(`[WorkspaceView] handleCloseEditorTab: ${tabId}, Shared mode: ${isShared}`);
if (isShared) {
@@ -261,6 +327,7 @@ onBeforeUnmount(() => {
class="layout-renderer-wrapper"
:editor-tabs="editorTabs"
:active-editor-tab-id="activeEditorTabId"
<!-- Removed terminalManager prop -->
@send-command="handleSendCommand"
@terminal-input="handleTerminalInput"
@terminal-resize="handleTerminalResize"
@@ -273,6 +340,10 @@ onBeforeUnmount(() => {
@open-new-session="handleOpenNewSession"
@request-add-connection="handleRequestAddConnection"
@request-edit-connection="handleRequestEditConnection"
@search="handleSearch"
@find-next="handleFindNext"
@find-previous="handleFindPrevious"
@close-search="handleCloseSearch"
></LayoutRenderer> <!-- 修正使用单独的结束标签 -->
<div v-else class="pane-placeholder"> <!-- 确保 v-else 紧随 v-if -->
{{ t('layout.loading', '加载布局中...') }}