update
This commit is contained in:
@@ -527,7 +527,7 @@ watchEffect((onCleanup) => {
|
||||
if (message.requestId === requestId && payload.requestedPath === requestedPath) {
|
||||
const absolutePath = payload.absolutePath;
|
||||
console.log(`[FileManager ${props.sessionId}] 收到 '.' 的绝对路径: ${absolutePath}。开始加载目录。`);
|
||||
currentPath.value = absolutePath;
|
||||
// 不再直接修改 currentPath.value,而是调用 loadDirectory,它内部会更新路径
|
||||
loadDirectory(absolutePath); // 使用 props 中的 loadDirectory
|
||||
initialLoadDone.value = true;
|
||||
cleanupListeners();
|
||||
@@ -700,11 +700,8 @@ const clearError = () => {
|
||||
@dragleave.prevent="handleDragLeave"
|
||||
@drop.prevent="handleDrop"
|
||||
>
|
||||
<!-- Error Alert Box -->
|
||||
<div v-if="error" class="error-alert">
|
||||
<span>{{ error }}</span>
|
||||
<button @click="clearError" class="close-error-btn" :title="t('common.dismiss')">×</button> <!-- Use clearSftpError -->
|
||||
</div>
|
||||
<!-- 移除内联错误提示框 -->
|
||||
<!-- <div v-if="error" class="error-alert"> ... </div> -->
|
||||
|
||||
<!-- 1. Initial Loading Indicator -->
|
||||
<div v-if="isLoading && !initialLoadDone" class="loading">{{ t('fileManager.loading') }}</div>
|
||||
@@ -851,28 +848,9 @@ const clearError = () => {
|
||||
.upload-popup .error { color: red; margin-left: 0.5rem; flex-basis: 100%; font-size: 0.8em; }
|
||||
.upload-popup .cancel-btn { margin-left: auto; padding: 0.1rem 0.4rem; font-size: 0.8em; background-color: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; cursor: pointer; }
|
||||
.loading, .no-files { padding: 1rem; text-align: center; color: #666; }
|
||||
/* Removed .error style for the main container */
|
||||
.error-alert {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
padding: 0.75rem 1.25rem;
|
||||
margin: 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.close-error-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: inherit;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
padding: 0 0.5rem;
|
||||
line-height: 1;
|
||||
}
|
||||
/* 移除 .error-alert 和 .close-error-btn 样式 */
|
||||
/* .error-alert { ... } */
|
||||
/* .close-error-btn { ... } */
|
||||
.file-list-container { flex-grow: 1; overflow-y: auto; position: relative; /* Needed for overlay */ }
|
||||
.file-list-container.drag-over {
|
||||
outline: 2px dashed #007bff; /* Blue dashed outline */
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
<template>
|
||||
<div class="status-monitor">
|
||||
<h4>服务器状态</h4>
|
||||
<div v-if="!serverStatus" class="loading-status">
|
||||
<!-- Corrected state display logic -->
|
||||
<div v-if="statusError" class="status-error">
|
||||
错误: {{ statusError }}
|
||||
</div>
|
||||
<div v-else-if="!serverStatus" class="loading-status">
|
||||
等待数据...
|
||||
</div>
|
||||
<div v-else class="status-grid">
|
||||
<!-- Status items remain here -->
|
||||
<div class="status-item cpu-model">
|
||||
<label>CPU 型号:</label>
|
||||
<!-- 使用 displayCpuModel 计算属性 -->
|
||||
@@ -18,43 +23,54 @@
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<label>CPU:</label>
|
||||
<div class="progress-bar-container">
|
||||
<div class="progress-bar" :style="{ width: `${serverStatus.cpuPercent ?? 0}%` }"></div>
|
||||
<!-- Wrap progress bar and percentage in a div -->
|
||||
<div class="value-wrapper">
|
||||
<div class="progress-bar-container">
|
||||
<div class="progress-bar" :style="{ width: `${serverStatus.cpuPercent ?? 0}%` }"></div>
|
||||
</div>
|
||||
<span>{{ serverStatus.cpuPercent?.toFixed(1) ?? 'N/A' }}%</span>
|
||||
</div>
|
||||
<span>{{ serverStatus.cpuPercent?.toFixed(1) ?? 'N/A' }}%</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<label>内存:</label>
|
||||
<div class="progress-bar-container">
|
||||
<div class="progress-bar" :style="{ width: `${serverStatus.memPercent ?? 0}%` }"></div>
|
||||
<div class="value-wrapper">
|
||||
<div class="progress-bar-container">
|
||||
<div class="progress-bar" :style="{ width: `${serverStatus.memPercent ?? 0}%` }"></div>
|
||||
</div>
|
||||
<span class="mem-disk-details">{{ memDisplay }}</span>
|
||||
</div>
|
||||
<span class="mem-disk-details">{{ memDisplay }}</span>
|
||||
</div>
|
||||
<!-- Removed v-if, Swap will always show -->
|
||||
<div class="status-item">
|
||||
<label>Swap:</label>
|
||||
<div class="progress-bar-container">
|
||||
<div class="progress-bar swap-bar" :style="{ width: `${serverStatus.swapPercent ?? 0}%` }"></div>
|
||||
<div class="value-wrapper">
|
||||
<div class="progress-bar-container">
|
||||
<div class="progress-bar swap-bar" :style="{ width: `${serverStatus.swapPercent ?? 0}%` }"></div>
|
||||
</div>
|
||||
<span class="mem-disk-details">{{ swapDisplay }}</span>
|
||||
</div>
|
||||
<span class="mem-disk-details">{{ swapDisplay }}</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<label>磁盘 (/):</label>
|
||||
<div class="progress-bar-container">
|
||||
<div class="progress-bar" :style="{ width: `${serverStatus.diskPercent ?? 0}%` }"></div>
|
||||
<label>磁盘:</label> <!-- 移除 (/) -->
|
||||
<div class="value-wrapper">
|
||||
<div class="progress-bar-container">
|
||||
<div class="progress-bar" :style="{ width: `${serverStatus.diskPercent ?? 0}%` }"></div>
|
||||
</div>
|
||||
<span class="mem-disk-details">{{ diskDisplay }}</span>
|
||||
</div>
|
||||
<span class="mem-disk-details">{{ diskDisplay }}</span>
|
||||
</div>
|
||||
<div class="status-item network-rate">
|
||||
<label>网络 ({{ serverStatus.netInterface || '...' }}):</label>
|
||||
<span class="rate down">⬇ {{ formatBytesPerSecond(serverStatus.netRxRate) }}</span>
|
||||
<span class="rate up">⬆ {{ formatBytesPerSecond(serverStatus.netTxRate) }}</span>
|
||||
<!-- Wrap rates in a div for alignment -->
|
||||
<div class="value-wrapper network-values">
|
||||
<span class="rate down">{{ formatBytesPerSecond(serverStatus.netRxRate) }}</span>
|
||||
<span class="rate up">{{ formatBytesPerSecond(serverStatus.netTxRate) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="statusError" class="status-error">
|
||||
错误: {{ statusError }}
|
||||
</div>
|
||||
<!-- Error display moved up for correct v-if/v-else-if logic -->
|
||||
</div>
|
||||
<!-- Removed extra closing div -->
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -207,40 +223,48 @@ const swapDisplay = computed(() => {
|
||||
|
||||
.status-item {
|
||||
display: grid;
|
||||
/* Adjusted grid columns for better alignment */
|
||||
grid-template-columns: 65px 1fr auto; /* Label slightly wider */
|
||||
/* Simplified grid columns: Label | Value Area - Further increased label width */
|
||||
grid-template-columns: 100px 1fr;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
gap: 0.8rem; /* Keep increased gap */
|
||||
}
|
||||
|
||||
/* Specific style for CPU model row */
|
||||
/* New wrapper for value area (progress bar + text or just text) */
|
||||
.value-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem; /* Space between progress bar and text */
|
||||
}
|
||||
|
||||
/* Specific style for CPU model row - Keep consistent with general status-item */
|
||||
.status-item.cpu-model {
|
||||
grid-template-columns: 65px 1fr; /* Label, Value */
|
||||
gap: 0.5rem;
|
||||
/* grid-template-columns is inherited */
|
||||
/* gap is inherited */
|
||||
margin-bottom: 0.5rem; /* Add some space below CPU model */
|
||||
}
|
||||
.cpu-model-value {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
grid-column: 2 / 4; /* Span across the value and percentage columns */
|
||||
/* No longer needs grid-column span */
|
||||
text-align: left;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Specific style for OS name row */
|
||||
/* Specific style for OS name row - Keep consistent with general status-item */
|
||||
.status-item.os-name {
|
||||
grid-template-columns: 65px 1fr; /* Label, Value */
|
||||
/* grid-template-columns is inherited */
|
||||
/* Ensure the item itself doesn't align right if the parent has text-align */
|
||||
text-align: left;
|
||||
}
|
||||
/* Increased specificity to override generic span rule */
|
||||
/* OS name value should just occupy the second column */
|
||||
.status-item.os-name .os-name-value {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: left; /* Explicitly left align text */
|
||||
justify-self: start; /* Align grid item to start */
|
||||
/* justify-self: start; No longer needed with 2-col grid */
|
||||
color: #333;
|
||||
min-width: auto; /* Override generic min-width */
|
||||
}
|
||||
@@ -249,7 +273,7 @@ const swapDisplay = computed(() => {
|
||||
.status-item label {
|
||||
font-weight: bold;
|
||||
color: #555;
|
||||
text-align: right;
|
||||
text-align: left; /* 改为左对齐 */
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -278,32 +302,59 @@ const swapDisplay = computed(() => {
|
||||
.status-item span:not(.cpu-model-value) { /* Style for percentage spans */
|
||||
font-variant-numeric: tabular-nums; /* Keep numbers aligned */
|
||||
min-width: 45px; /* Ensure space for percentage */
|
||||
text-align: right;
|
||||
text-align: left; /* 改为左对齐 */
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.mem-disk-details {
|
||||
font-size: 0.9em; /* Slightly smaller font for details */
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
text-align: left; /* 改为左对齐 */
|
||||
}
|
||||
|
||||
/* Network Rate Styles */
|
||||
/* Network Rate Styles - uses the 2-col grid */
|
||||
.status-item.network-rate {
|
||||
grid-template-columns: 65px auto auto; /* Label, Down Rate, Up Rate */
|
||||
/* grid-template-columns is inherited */
|
||||
margin-top: 0.5rem; /* Add space above network */
|
||||
align-items: center; /* Try centering label and rates vertically */
|
||||
}
|
||||
/* Adjust network value wrapper */
|
||||
.network-values {
|
||||
justify-content: start; /* Align rates to the start */
|
||||
gap: 1rem; /* Increase gap between rates */
|
||||
/* Removed margin-left, rely on grid gap */
|
||||
/* Ensure the wrapper itself aligns correctly if needed */
|
||||
/* align-self: center; */ /* Or baseline */
|
||||
}
|
||||
.network-rate .rate {
|
||||
font-size: 0.9em;
|
||||
white-space: nowrap;
|
||||
text-align: right;
|
||||
min-width: 80px; /* Adjust as needed */
|
||||
text-align: left; /* 改为左对齐 */
|
||||
min-width: auto; /* Remove min-width or adjust */
|
||||
/* Rely on parent flexbox for alignment */
|
||||
display: inline-flex; /* Ensure pseudo-element is part of flex flow */
|
||||
align-items: center; /* Vertically align arrow with text */
|
||||
gap: 0.3em; /* Add space between arrow and text */
|
||||
}
|
||||
.network-rate .rate.down {
|
||||
color: #28a745; /* Green for download */
|
||||
}
|
||||
.network-rate .rate.down::before {
|
||||
content: '⬇';
|
||||
/* Removed absolute positioning */
|
||||
font-size: 1em; /* Match parent font size */
|
||||
line-height: 1; /* Adjust line-height for better vertical alignment */
|
||||
}
|
||||
|
||||
.network-rate .rate.up {
|
||||
color: #fd7e14; /* Orange for upload */
|
||||
}
|
||||
.network-rate .rate.up::before {
|
||||
content: '⬆';
|
||||
/* Removed absolute positioning */
|
||||
font-size: 1em; /* Match parent font size */
|
||||
line-height: 1; /* Adjust line-height for better vertical alignment */
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -118,11 +118,20 @@ onMounted(() => {
|
||||
|
||||
// 监听 isActive prop 的变化,当标签变为活动时立即 fit 并发送 resize
|
||||
watch(() => props.isActive, (newValue) => {
|
||||
if (newValue && terminal && terminalRef.value && terminalRef.value.offsetHeight > 0) {
|
||||
console.log(`[Terminal ${props.sessionId}] Tab became active, performing immediate fit and resize.`);
|
||||
// 使用 nextTick 确保 DOM 更新完成
|
||||
if (newValue && terminal && terminalRef.value) {
|
||||
// 当标签变为活动时,等待 DOM 更新和短暂延时后执行 fit
|
||||
console.log(`[Terminal ${props.sessionId}] 标签变为活动状态,准备调整尺寸。`); // 日志改为中文
|
||||
nextTick(() => {
|
||||
fitAndEmitResizeNow(terminal!);
|
||||
// 添加短暂延时,确保元素完全可见且渲染稳定
|
||||
setTimeout(() => {
|
||||
// 再次检查终端实例是否存在且容器可见
|
||||
if (terminal && terminalRef.value && terminalRef.value.offsetHeight > 0) {
|
||||
console.log(`[Terminal ${props.sessionId}] 执行延时后的 fit 和 resize。`); // 日志改为中文
|
||||
fitAndEmitResizeNow(terminal);
|
||||
} else {
|
||||
console.log(`[Terminal ${props.sessionId}] 延时后检查:终端不可见或已销毁,跳过 fit。`); // 日志改为中文
|
||||
}
|
||||
}, 50); // 50ms 延时,可以根据需要调整
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { PropType } from 'vue';
|
||||
|
||||
// 定义会话状态的简化接口 (仅包含标签栏需要的信息)
|
||||
interface SessionTabInfo {
|
||||
sessionId: string;
|
||||
connectionName: string; // 显示在标签上的名称
|
||||
}
|
||||
// 导入会话状态类型
|
||||
import type { SessionTabInfoWithStatus } from '../stores/session.store'; // 导入更新后的类型
|
||||
|
||||
// 定义 Props
|
||||
const props = defineProps({
|
||||
sessions: {
|
||||
type: Array as PropType<SessionTabInfo[]>,
|
||||
type: Array as PropType<SessionTabInfoWithStatus[]>, // 使用更新后的类型
|
||||
required: true,
|
||||
},
|
||||
activeSessionId: {
|
||||
@@ -44,6 +40,8 @@ const closeSession = (event: MouseEvent, sessionId: string) => {
|
||||
@click="activateSession(session.sessionId)"
|
||||
:title="session.connectionName"
|
||||
>
|
||||
<!-- 添加状态点 -->
|
||||
<span :class="['status-dot', `status-${session.status}`]"></span>
|
||||
<span class="tab-name">{{ session.connectionName }}</span>
|
||||
<button class="close-tab-button" @click="closeSession($event, session.sessionId)" title="关闭标签页">
|
||||
× <!-- 使用 HTML 实体 '×' -->
|
||||
@@ -67,6 +65,22 @@ const closeSession = (event: MouseEvent, sessionId: string) => {
|
||||
box-sizing: border-box; /* 确保 padding 不会增加总高度 */
|
||||
}
|
||||
|
||||
/* 状态点样式 */
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-right: 6px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
flex-shrink: 0; /* 防止被压缩 */
|
||||
}
|
||||
.status-dot.status-disconnected { background-color: #dc3545; } /* 红色 */
|
||||
.status-dot.status-connecting { background-color: #ffc107; } /* 黄色 */
|
||||
.status-dot.status-connected { background-color: #28a745; } /* 绿色 */
|
||||
.status-dot.status-error { background-color: #6c757d; } /* 灰色 (或其他表示错误的颜色) */
|
||||
|
||||
|
||||
.tab-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
@@ -105,8 +119,10 @@ const closeSession = (event: MouseEvent, sessionId: string) => {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-right: 1.5rem; /* 为关闭按钮留出足够空间 */
|
||||
/* margin-right: 1.5rem; */ /* 调整右边距,因为关闭按钮现在是 flex item */
|
||||
line-height: normal; /* 默认行高 */
|
||||
flex-grow: 1; /* 允许名称伸展 */
|
||||
margin-left: 4px; /* 在状态点和名称之间添加一点间距 */
|
||||
}
|
||||
|
||||
.close-tab-button {
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
<script setup lang="ts">
|
||||
import { useUiNotificationsStore } from '../stores/uiNotifications.store';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
const notificationsStore = useUiNotificationsStore();
|
||||
const { notifications } = storeToRefs(notificationsStore);
|
||||
|
||||
const getIconClass = (type: string) => {
|
||||
switch (type) {
|
||||
case 'success': return 'fas fa-check-circle';
|
||||
case 'error': return 'fas fa-times-circle';
|
||||
case 'info': return 'fas fa-info-circle';
|
||||
case 'warning': return 'fas fa-exclamation-triangle';
|
||||
default: return '';
|
||||
}
|
||||
};
|
||||
|
||||
const getContainerClass = (type: string) => {
|
||||
return `notification-item notification-${type}`;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="notification-container">
|
||||
<transition-group name="notification-fade" tag="div">
|
||||
<div
|
||||
v-for="notification in notifications"
|
||||
:key="notification.id"
|
||||
:class="getContainerClass(notification.type)"
|
||||
>
|
||||
<i :class="['notification-icon', getIconClass(notification.type)]"></i>
|
||||
<span class="notification-message">{{ notification.message }}</span>
|
||||
<button
|
||||
class="notification-close-btn"
|
||||
@click="notificationsStore.removeNotification(notification.id)"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</transition-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.notification-container {
|
||||
position: fixed;
|
||||
top: 1rem; /* 距离顶部 */
|
||||
right: 1rem; /* 距离右侧 */
|
||||
z-index: 1100; /* 比其他元素层级高 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end; /* 从右侧对齐 */
|
||||
}
|
||||
|
||||
.notification-item {
|
||||
background-color: #fff;
|
||||
color: #fff;
|
||||
padding: 0.8rem 1.2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 250px; /* 最小宽度 */
|
||||
max-width: 400px; /* 最大宽度 */
|
||||
opacity: 0.95;
|
||||
transition: all 0.5s ease; /* 添加过渡效果 */
|
||||
}
|
||||
|
||||
.notification-success { background-color: #28a745; } /* 绿色 */
|
||||
.notification-error { background-color: #dc3545; } /* 红色 */
|
||||
.notification-info { background-color: #17a2b8; } /* 蓝色 */
|
||||
.notification-warning { background-color: #ffc107; color: #333; } /* 黄色,文字用深色 */
|
||||
|
||||
.notification-icon {
|
||||
margin-right: 0.8rem;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.notification-message {
|
||||
flex-grow: 1;
|
||||
word-wrap: break-word; /* 允许长单词换行 */
|
||||
}
|
||||
|
||||
.notification-close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: inherit; /* 继承父元素颜色 */
|
||||
opacity: 0.7;
|
||||
cursor: pointer;
|
||||
font-size: 1.2em;
|
||||
margin-left: 1rem;
|
||||
padding: 0 0.3rem;
|
||||
}
|
||||
.notification-close-btn:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 过渡动画 */
|
||||
.notification-fade-enter-active,
|
||||
.notification-fade-leave-active {
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
.notification-fade-enter-from,
|
||||
.notification-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(30px); /* 从右侧滑入 */
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { ref, computed, onMounted } from 'vue'; // 确保 ref 已导入
|
||||
import { storeToRefs } from 'pinia';
|
||||
// import { useRouter } from 'vue-router'; // 不再需要 router
|
||||
import { useI18n } from 'vue-i18n';
|
||||
@@ -22,6 +22,9 @@ const tagsStore = useTagsStore();
|
||||
const { connections, isLoading: connectionsLoading, error: connectionsError } = storeToRefs(connectionsStore);
|
||||
const { tags, isLoading: tagsLoading, error: tagsError } = storeToRefs(tagsStore);
|
||||
|
||||
// 搜索词
|
||||
const searchTerm = ref('');
|
||||
|
||||
// 右键菜单状态
|
||||
const contextMenuVisible = ref(false);
|
||||
const contextMenuPosition = ref({ x: 0, y: 0 });
|
||||
@@ -30,38 +33,59 @@ const contextTargetConnection = ref<ConnectionInfo | null>(null);
|
||||
// 分组展开状态
|
||||
const expandedGroups = ref<Record<string, boolean>>({}); // 使用 Record<string, boolean>
|
||||
|
||||
// 计算属性:按标签分组连接
|
||||
const groupedConnections = computed(() => {
|
||||
// 计算属性:过滤并按标签分组连接
|
||||
const filteredAndGroupedConnections = computed(() => {
|
||||
const groups: Record<string, ConnectionInfo[]> = {};
|
||||
const untagged: ConnectionInfo[] = [];
|
||||
const tagMap = new Map(tags.value.map(tag => [tag.id, tag]));
|
||||
const lowerSearchTerm = searchTerm.value.toLowerCase();
|
||||
|
||||
connections.value.forEach(conn => {
|
||||
// 1. 过滤连接
|
||||
const filteredConnections = connections.value.filter(conn => {
|
||||
const nameMatch = conn.name && conn.name.toLowerCase().includes(lowerSearchTerm);
|
||||
const hostMatch = conn.host.toLowerCase().includes(lowerSearchTerm);
|
||||
// 如果有 IP 地址字段,也应包含在此处
|
||||
// const ipMatch = conn.ipAddress && conn.ipAddress.toLowerCase().includes(lowerSearchTerm);
|
||||
return nameMatch || hostMatch; // || ipMatch;
|
||||
});
|
||||
|
||||
// 2. 分组过滤后的连接
|
||||
filteredConnections.forEach(conn => {
|
||||
if (conn.tag_ids && conn.tag_ids.length > 0) {
|
||||
let tagged = false; // 标记是否至少加入了一个分组
|
||||
conn.tag_ids.forEach(tagId => {
|
||||
const tag = tagMap.get(tagId);
|
||||
const groupName = tag ? tag.name : t('workspaceConnectionList.untagged'); // Fallback if tag not found
|
||||
if (!groups[groupName]) {
|
||||
groups[groupName] = [];
|
||||
if (expandedGroups.value[groupName] === undefined) {
|
||||
expandedGroups.value[groupName] = true; // 默认展开
|
||||
// 确保标签存在才分组
|
||||
if (tag) {
|
||||
const groupName = tag.name;
|
||||
if (!groups[groupName]) {
|
||||
groups[groupName] = [];
|
||||
if (expandedGroups.value[groupName] === undefined) {
|
||||
expandedGroups.value[groupName] = true; // 默认展开
|
||||
}
|
||||
}
|
||||
// 避免重复添加(如果一个连接有多个标签)
|
||||
if (!groups[groupName].some(c => c.id === conn.id)) {
|
||||
groups[groupName].push(conn);
|
||||
}
|
||||
tagged = true;
|
||||
}
|
||||
groups[groupName].push(conn);
|
||||
});
|
||||
// 如果所有标签都无效或未找到,则归入未标记
|
||||
if (!tagged) {
|
||||
untagged.push(conn);
|
||||
}
|
||||
} else {
|
||||
untagged.push(conn);
|
||||
}
|
||||
});
|
||||
|
||||
// 对每个分组内的连接按名称或主机排序
|
||||
// 3. 排序和格式化输出
|
||||
for (const groupName in groups) {
|
||||
groups[groupName].sort((a, b) => (a.name || a.host).localeCompare(b.name || b.host));
|
||||
}
|
||||
untagged.sort((a, b) => (a.name || a.host).localeCompare(b.name || b.host));
|
||||
|
||||
|
||||
// 将未标记的分组放在最后
|
||||
const sortedGroupNames = Object.keys(groups).sort();
|
||||
const result: { groupName: string; connections: ConnectionInfo[] }[] = sortedGroupNames.map(name => ({
|
||||
groupName: name,
|
||||
@@ -150,34 +174,62 @@ const handleOpenInNewTab = (connectionId: number) => {
|
||||
</div>
|
||||
<div v-else-if="connections.length === 0" class="no-connections">
|
||||
{{ t('connections.noConnections') }}
|
||||
<button @click="handleMenuAction('add')">{{ t('connections.addConnection') }}</button>
|
||||
<!-- 保留添加按钮,即使列表为空 -->
|
||||
<!-- <button @click="handleMenuAction('add')">{{ t('connections.addConnection') }}</button> -->
|
||||
</div>
|
||||
<div v-else>
|
||||
<!-- 添加连接按钮(总是在顶部) -->
|
||||
<button class="add-connection-button" @click="handleMenuAction('add')">
|
||||
<i class="fas fa-plus"></i> {{ t('connections.addConnection') }}
|
||||
<!-- 搜索和添加栏 -->
|
||||
<div class="search-add-bar">
|
||||
<input
|
||||
type="text"
|
||||
v-model="searchTerm"
|
||||
:placeholder="t('workspaceConnectionList.searchPlaceholder')"
|
||||
class="search-input"
|
||||
/>
|
||||
<button class="add-button" @click="handleMenuAction('add')" :title="t('connections.addConnection')">
|
||||
<i class="fas fa-plus"></i>
|
||||
</button>
|
||||
<div v-for="group in groupedConnections" :key="group.groupName" class="connection-group">
|
||||
<div class="group-header" @click="toggleGroup(group.groupName)">
|
||||
<i :class="['fas', expandedGroups[group.groupName] ? 'fa-chevron-down' : 'fa-chevron-right']"></i>
|
||||
<span>{{ group.groupName }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 连接列表区域 -->
|
||||
<div class="connection-list-area">
|
||||
<div v-if="connectionsLoading || tagsLoading" class="loading">
|
||||
{{ t('common.loading') }}
|
||||
</div>
|
||||
<div v-else-if="connectionsError || tagsError" class="error">
|
||||
{{ connectionsError || tagsError }}
|
||||
</div>
|
||||
<div v-else-if="filteredAndGroupedConnections.length === 0 && connections.length > 0" class="no-results">
|
||||
{{ t('workspaceConnectionList.noResults') }} "{{ searchTerm }}"
|
||||
</div>
|
||||
<div v-else-if="connections.length === 0" class="no-connections">
|
||||
{{ t('connections.noConnections') }}
|
||||
</div>
|
||||
<div v-else>
|
||||
<!-- 修正: 循环 filteredAndGroupedConnections -->
|
||||
<div v-for="groupData in filteredAndGroupedConnections" :key="groupData.groupName" class="connection-group">
|
||||
<div class="group-header" @click="toggleGroup(groupData.groupName)">
|
||||
<i :class="['fas', expandedGroups[groupData.groupName] ? 'fa-chevron-down' : 'fa-chevron-right']"></i>
|
||||
<span>{{ groupData.groupName }}</span>
|
||||
</div>
|
||||
<!-- 修正: 使用 groupData.groupName 和 groupData.connections -->
|
||||
<ul v-show="expandedGroups[groupData.groupName]" class="connection-items">
|
||||
<li
|
||||
v-for="conn in groupData.connections"
|
||||
:key="conn.id"
|
||||
class="connection-item"
|
||||
@click.left="handleConnect(conn.id)"
|
||||
@click.middle.prevent="handleOpenInNewTab(conn.id)"
|
||||
@auxclick.prevent="handleOpenInNewTab(conn.id)"
|
||||
@contextmenu.prevent="showContextMenu($event, conn)"
|
||||
>
|
||||
<i class="fas fa-server connection-icon"></i>
|
||||
<span class="connection-name" :title="conn.name || conn.host">
|
||||
{{ conn.name || conn.host }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<ul v-show="expandedGroups[group.groupName]" class="connection-items">
|
||||
<li
|
||||
v-for="conn in group.connections"
|
||||
:key="conn.id"
|
||||
class="connection-item"
|
||||
@click.left="handleConnect(conn.id)"
|
||||
@click.middle.prevent="handleOpenInNewTab(conn.id)"
|
||||
@auxclick.prevent="handleOpenInNewTab(conn.id)"
|
||||
@contextmenu.prevent="showContextMenu($event, conn)"
|
||||
>
|
||||
<i class="fas fa-server connection-icon"></i>
|
||||
<span class="connection-name" :title="conn.name || conn.host">
|
||||
{{ conn.name || conn.host }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<!-- 移除重复的 ul 块 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -200,14 +252,58 @@ const handleOpenInNewTab = (connectionId: number) => {
|
||||
|
||||
<style scoped>
|
||||
.workspace-connection-list {
|
||||
padding: 0.5rem 0;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
background-color: #f8f9fa; /* Slightly different background */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden; /* 防止内部滚动条影响布局 */
|
||||
background-color: #f8f9fa;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.loading, .error, .no-connections {
|
||||
.search-add-bar {
|
||||
display: flex;
|
||||
padding: 0.5rem;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
background-color: #e9ecef; /* 给搜索栏一个背景色 */
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex-grow: 1;
|
||||
padding: 0.4rem 0.6rem;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 4px 0 0 4px; /* 左侧圆角 */
|
||||
font-size: 0.9em;
|
||||
outline: none;
|
||||
}
|
||||
.search-input:focus {
|
||||
border-color: #80bdff;
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
.add-button {
|
||||
padding: 0.4rem 0.8rem;
|
||||
border: 1px solid #ced4da;
|
||||
border-left: none; /* 移除左边框,与输入框合并 */
|
||||
background-color: #f8f9fa;
|
||||
cursor: pointer;
|
||||
border-radius: 0 4px 4px 0; /* 右侧圆角 */
|
||||
color: #495057;
|
||||
}
|
||||
.add-button:hover {
|
||||
background-color: #e2e6ea;
|
||||
}
|
||||
.add-button i {
|
||||
font-size: 1em; /* 图标大小 */
|
||||
}
|
||||
|
||||
.connection-list-area {
|
||||
flex-grow: 1; /* 占据剩余空间 */
|
||||
overflow-y: auto; /* 列表内容滚动 */
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
|
||||
.loading, .error, .no-connections, .no-results {
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
color: #6c757d;
|
||||
|
||||
Reference in New Issue
Block a user