Update WorkspaceConnectionList.vue
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onBeforeUnmount, defineExpose } from 'vue'; // 确保 ref, defineExpose, onBeforeUnmount 已导入
|
import { ref, computed, onMounted, onBeforeUnmount, defineExpose, watch, nextTick } from 'vue'; // 确保 ref, defineExpose, onBeforeUnmount, watch, nextTick 已导入
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
// import { useRouter } from 'vue-router'; // 不再需要 router
|
// import { useRouter } from 'vue-router'; // 不再需要 router
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
@@ -38,6 +38,31 @@ const contextTargetConnection = ref<ConnectionInfo | null>(null);
|
|||||||
// 分组展开状态
|
// 分组展开状态
|
||||||
const expandedGroups = ref<Record<string, boolean>>({}); // 使用 Record<string, boolean>
|
const expandedGroups = ref<Record<string, boolean>>({}); // 使用 Record<string, boolean>
|
||||||
|
|
||||||
|
// 键盘导航状态
|
||||||
|
const highlightedIndex = ref(-1); // -1 表示没有高亮项
|
||||||
|
const listAreaRef = ref<HTMLElement | null>(null); // 列表容器的 ref
|
||||||
|
|
||||||
|
// 计算属性:扁平化的、当前可见的连接列表(用于键盘导航)
|
||||||
|
const flatVisibleConnections = computed(() => {
|
||||||
|
const flatList: ConnectionInfo[] = [];
|
||||||
|
filteredAndGroupedConnections.value.forEach(group => {
|
||||||
|
// 只添加展开分组中的连接
|
||||||
|
if (expandedGroups.value[group.groupName]) {
|
||||||
|
flatList.push(...group.connections);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return flatList;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算属性:当前高亮连接的 ID
|
||||||
|
const highlightedConnectionId = computed(() => {
|
||||||
|
if (highlightedIndex.value >= 0 && highlightedIndex.value < flatVisibleConnections.value.length) {
|
||||||
|
return flatVisibleConnections.value[highlightedIndex.value].id;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// 计算属性:过滤并按标签分组连接
|
// 计算属性:过滤并按标签分组连接
|
||||||
const filteredAndGroupedConnections = computed(() => {
|
const filteredAndGroupedConnections = computed(() => {
|
||||||
const groups: Record<string, ConnectionInfo[]> = {};
|
const groups: Record<string, ConnectionInfo[]> = {};
|
||||||
@@ -106,9 +131,20 @@ const filteredAndGroupedConnections = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 切换分组展开/折叠
|
// 监听搜索词变化,重置高亮索引
|
||||||
|
watch(searchTerm, () => {
|
||||||
|
highlightedIndex.value = -1;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听分组展开状态变化,重置高亮索引
|
||||||
|
watch(expandedGroups, () => {
|
||||||
|
highlightedIndex.value = -1;
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
|
|
||||||
|
// 切换分组展开/折叠
|
||||||
const toggleGroup = (groupName: string) => {
|
const toggleGroup = (groupName: string) => {
|
||||||
expandedGroups.value[groupName] = !expandedGroups.value[groupName];
|
expandedGroups.value[groupName] = !expandedGroups.value[groupName];
|
||||||
};
|
};
|
||||||
@@ -165,6 +201,26 @@ const handleMenuAction = (action: 'add' | 'edit' | 'delete') => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 稍微延迟一下重置,以防是点击列表项导致的失焦
|
||||||
|
// 如果用户点击了列表项,handleConnect 会先触发
|
||||||
|
setTimeout(() => {
|
||||||
|
// 检查此时是否仍然没有焦点在输入框上(避免误清除)
|
||||||
|
if (document.activeElement !== searchInputRef.value) {
|
||||||
|
highlightedIndex.value = -1;
|
||||||
|
}
|
||||||
|
}, 150); // 150ms 延迟可能更稳妥
|
||||||
|
// 处理失焦事件,清除高亮
|
||||||
|
const handleBlur = () => {
|
||||||
|
// 稍微延迟一下重置,以防是点击列表项导致的失焦
|
||||||
|
// 如果用户点击了列表项,handleConnect 会先触发
|
||||||
|
setTimeout(() => {
|
||||||
|
// 检查此时是否仍然没有焦点在输入框上(避免误清除)
|
||||||
|
if (document.activeElement !== searchInputRef.value) {
|
||||||
|
highlightedIndex.value = -1;
|
||||||
|
}
|
||||||
|
}, 150); // 150ms 延迟可能更稳妥
|
||||||
|
};
|
||||||
|
|
||||||
// 获取数据
|
// 获取数据
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
connectionsStore.fetchConnections();
|
connectionsStore.fetchConnections();
|
||||||
@@ -197,6 +253,43 @@ const focusSearchInput = (): boolean => {
|
|||||||
return false; // 聚焦失败
|
return false; // 聚焦失败
|
||||||
};
|
};
|
||||||
defineExpose({ focusSearchInput });
|
defineExpose({ focusSearchInput });
|
||||||
|
|
||||||
|
// --- 键盘导航和确认 ---
|
||||||
|
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
const list = flatVisibleConnections.value;
|
||||||
|
if (!list.length) return;
|
||||||
|
|
||||||
|
switch (event.key) {
|
||||||
|
case 'ArrowDown':
|
||||||
|
event.preventDefault(); // 阻止光标移动
|
||||||
|
highlightedIndex.value = (highlightedIndex.value + 1) % list.length;
|
||||||
|
scrollToHighlighted();
|
||||||
|
break;
|
||||||
|
case 'ArrowUp':
|
||||||
|
event.preventDefault(); // 阻止光标移动
|
||||||
|
highlightedIndex.value = (highlightedIndex.value - 1 + list.length) % list.length;
|
||||||
|
scrollToHighlighted();
|
||||||
|
break;
|
||||||
|
case 'Enter':
|
||||||
|
event.preventDefault(); // 阻止可能的表单提交
|
||||||
|
if (highlightedConnectionId.value !== null) {
|
||||||
|
handleConnect(highlightedConnectionId.value);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 滚动到高亮项
|
||||||
|
const scrollToHighlighted = async () => {
|
||||||
|
await nextTick(); // 等待 DOM 更新
|
||||||
|
if (!listAreaRef.value || highlightedConnectionId.value === null) return;
|
||||||
|
|
||||||
|
const highlightedElement = listAreaRef.value.querySelector(`.connection-item[data-conn-id="${highlightedConnectionId.value}"]`);
|
||||||
|
if (highlightedElement) {
|
||||||
|
highlightedElement.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -217,6 +310,8 @@ defineExpose({ focusSearchInput });
|
|||||||
ref="searchInputRef"
|
ref="searchInputRef"
|
||||||
class="search-input"
|
class="search-input"
|
||||||
data-focus-id="connectionListSearch"
|
data-focus-id="connectionListSearch"
|
||||||
|
@keydown="handleKeyDown"
|
||||||
|
@blur="handleBlur"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
class="add-button"
|
class="add-button"
|
||||||
@@ -228,7 +323,7 @@ defineExpose({ focusSearchInput });
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 连接列表区域 -->
|
<!-- 连接列表区域 -->
|
||||||
<div class="connection-list-area">
|
<div class="connection-list-area" ref="listAreaRef">
|
||||||
<div v-if="connectionsLoading || tagsLoading" class="loading">
|
<div v-if="connectionsLoading || tagsLoading" class="loading">
|
||||||
{{ t('common.loading') }}
|
{{ t('common.loading') }}
|
||||||
</div>
|
</div>
|
||||||
@@ -254,8 +349,11 @@ defineExpose({ focusSearchInput });
|
|||||||
v-for="conn in groupData.connections"
|
v-for="conn in groupData.connections"
|
||||||
:key="conn.id"
|
:key="conn.id"
|
||||||
class="connection-item"
|
class="connection-item"
|
||||||
|
:class="{ 'highlighted': conn.id === highlightedConnectionId }"
|
||||||
|
:data-conn-id="conn.id"
|
||||||
@click.left="handleConnect(conn.id)"
|
@click.left="handleConnect(conn.id)"
|
||||||
@contextmenu.prevent="showContextMenu($event, conn)"
|
@contextmenu.prevent="showContextMenu($event, conn)"
|
||||||
|
@mouseenter="highlightedIndex = flatVisibleConnections.findIndex(c => c.id === conn.id)"
|
||||||
>
|
>
|
||||||
<i class="fas fa-server connection-icon"></i>
|
<i class="fas fa-server connection-icon"></i>
|
||||||
<span class="connection-name" :title="conn.name || conn.host">
|
<span class="connection-name" :title="conn.name || conn.host">
|
||||||
@@ -426,6 +524,16 @@ defineExpose({ focusSearchInput });
|
|||||||
background-color: var(--header-bg-color); /* Use theme variable */
|
background-color: var(--header-bg-color); /* Use theme variable */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 新增高亮样式 */
|
||||||
|
.connection-item.highlighted {
|
||||||
|
background-color: var(--button-hover-bg-color); /* Use theme variable */
|
||||||
|
color: var(--button-text-color); /* Use theme variable */
|
||||||
|
}
|
||||||
|
.connection-item.highlighted .connection-icon {
|
||||||
|
color: var(--button-text-color); /* Use theme variable */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.connection-icon {
|
.connection-icon {
|
||||||
margin-right: 0.6rem;
|
margin-right: 0.6rem;
|
||||||
color: var(--text-color-secondary); /* Use theme variable */
|
color: var(--text-color-secondary); /* Use theme variable */
|
||||||
|
|||||||
Reference in New Issue
Block a user