update
This commit is contained in:
Generated
+17
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -687,5 +687,14 @@
|
||||
"errorCommandRequired": "指令内容不能为空",
|
||||
"add": "添加"
|
||||
}
|
||||
},
|
||||
"commandInputBar": {
|
||||
"placeholder": "在此输入命令...",
|
||||
"searchPlaceholder": "在终端中搜索...",
|
||||
"openSearch": "打开终端搜索",
|
||||
"closeSearch": "关闭终端搜索",
|
||||
"findPrevious": "查找上一个",
|
||||
"findNext": "查找下一个",
|
||||
"noResults": "无结果"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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', '加载布局中...') }}
|
||||
|
||||
Reference in New Issue
Block a user