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
@@ -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 = () => {