update
This commit is contained in:
@@ -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 = () => {
|
||||
|
||||
Reference in New Issue
Block a user