feat: 实现连接测试功能 API 及前端调用

This commit is contained in:
Baobhan Sith
2025-04-15 08:06:52 +08:00
parent 6cd4977347
commit fa27d40eb2
9 changed files with 586 additions and 72 deletions
@@ -1,25 +1,32 @@
<script setup lang="ts">
import { onMounted, computed } from 'vue'; // 引入 computed
import { onMounted, computed, ref, reactive } from 'vue'; // 统一导入
import { storeToRefs } from 'pinia';
import { useRouter } from 'vue-router'; // 引入 useRouter
import { useI18n } from 'vue-i18n'; // 引入 useI18n
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useConnectionsStore, ConnectionInfo } from '../stores/connections.store'; // 引入 ConnectionInfo 类型
import { useTagsStore } from '../stores/tags.store'; // 引入 Tags Store
const { t } = useI18n(); // 获取 t 函数
const router = useRouter(); // 获取 router 实例
const connectionsStore = useConnectionsStore();
const tagsStore = useTagsStore(); // 获取 Tags Store 实例
// 使用 storeToRefs 来保持 state 属性的响应性
const { connections, isLoading, error } = storeToRefs(connectionsStore);
const { tags: allTags } = storeToRefs(tagsStore); // 获取所有标签
// 不再直接从 connectionsStore 获取 connections, isLoading, error
// const { connections, isLoading, error } = storeToRefs(connectionsStore);
const { tags: allTags, isLoading: isTagsLoading, error: tagsError } = storeToRefs(tagsStore); // 获取所有标签及其状态
// 定义 Props,接收筛选后的连接列表
const props = defineProps<{
connections: ConnectionInfo[];
}>();
// 定义组件发出的事件 (添加 edit-connection)
const emit = defineEmits(['edit-connection']);
// 组件挂载时获取连接和标签列表
// 新增:用于跟踪每个连接测试状态的响应式对象
const testingState = reactive<Record<number, boolean>>({});
// 组件挂载时获取标签列表 (连接列表由父组件传入)
onMounted(() => {
connectionsStore.fetchConnections();
tagsStore.fetchTags(); // 获取标签列表
});
@@ -39,10 +46,68 @@ const getConnectionTagNames = (conn: ConnectionInfo): string[] => {
}
return conn.tag_ids
.map(tagId => tagMap.value.get(tagId)) // 使用映射获取名称
.filter((name): name is string => !!name); // 过滤掉未找到的标签并确保类型为 string
};
.filter((name): name is string => !!name); // 过滤掉未找到的标签并确保类型为 string
};
// 辅助函数:格式化时间戳
// 新增:计算按标签分组的连接
const groupedConnections = computed(() => {
const groups: { [key: string]: ConnectionInfo[] } = {};
const untaggedKey = '_untagged_'; // 特殊键,用于未标记的连接
// 初始化所有标签组(包括未标记)
groups[untaggedKey] = [];
allTags.value.forEach(tag => {
groups[tag.name] = []; // 使用标签名称作为键
});
// 将连接分配到对应的组
props.connections.forEach(conn => {
if (!conn.tag_ids || conn.tag_ids.length === 0) {
groups[untaggedKey].push(conn);
} else {
conn.tag_ids.forEach(tagId => {
const tagName = tagMap.value.get(tagId);
if (tagName && groups[tagName]) { // 确保标签存在于映射和分组中
groups[tagName].push(conn);
} else if (tagName) {
// 如果标签存在但分组未初始化(理论上不应发生),则创建分组
groups[tagName] = [conn];
} else {
// 如果 tagId 无效或未找到对应标签名,归入未标记组
groups[untaggedKey].push(conn);
}
});
}
});
// 过滤掉没有连接的标签组(除了未标记组,即使为空也可能需要显示)
const filteredGroups: { [key: string]: ConnectionInfo[] } = {};
for (const groupName in groups) {
if (groups[groupName].length > 0 || groupName === untaggedKey) {
// 按连接名称排序每个分组内部的连接
groups[groupName].sort((a, b) => a.name.localeCompare(b.name));
filteredGroups[groupName] = groups[groupName];
}
}
// 对分组本身进行排序(未标记的放最后)
const sortedGroupNames = Object.keys(filteredGroups).sort((a, b) => {
if (a === untaggedKey) return 1; // 未标记的排在后面
if (b === untaggedKey) return -1;
return a.localeCompare(b); // 其他按名称排序
});
const sortedGroups: { [key: string]: ConnectionInfo[] } = {};
sortedGroupNames.forEach(name => {
sortedGroups[name] = filteredGroups[name];
});
return sortedGroups;
});
// 辅助函数:格式化时间戳
const formatTimestamp = (timestamp: number | null): string => {
if (!timestamp) return t('connections.status.never'); // 使用 i18n
// TODO: 可以考虑使用更专业的日期格式化库 (如 date-fns 或 dayjs) 并结合 i18n locale
@@ -51,6 +116,8 @@ const formatTimestamp = (timestamp: number | null): string => {
// 新增:处理删除连接的方法
const handleDelete = async (conn: ConnectionInfo) => {
// 在函数内部获取 store 实例
const connectionsStore = useConnectionsStore();
// 使用 i18n 获取确认消息
const confirmMessage = t('connections.prompts.confirmDelete', { name: conn.name });
if (window.confirm(confirmMessage)) {
@@ -60,37 +127,61 @@ const handleDelete = async (conn: ConnectionInfo) => {
// 可以考虑使用更友好的提示方式,例如 toast 通知库
alert(t('connections.errors.deleteFailed', { error: connectionsStore.error || '未知错误' }));
}
// 成功时列表会自动更新,无需额外操作
}
};
// 成功时列表会自动更新,无需额外操作
}
};
</script>
// 新增:处理测试连接的方法
const handleTestConnection = async (connectionId: number) => {
const connectionsStore = useConnectionsStore(); // 获取 store 实例
testingState[connectionId] = true; // 设置为正在测试状态
const result = await connectionsStore.testConnection(connectionId); // 调用 store action
testingState[connectionId] = false; // 清除测试状态
// 显示测试结果
if (result.success) {
alert(t('connections.test.success'));
} else {
alert(t('connections.test.failed', { error: result.message || '未知错误' }));
}
};
</script>
<template>
<div class="connection-list">
<!-- 标题移到父组件 ConnectionsView.vue -->
<div v-if="isLoading" class="loading">{{ t('connections.loading') }}</div>
<div v-else-if="error" class="error">{{ t('connections.error', { error: error }) }}</div>
<div v-else-if="connections.length === 0" class="no-connections">
{{ t('connections.noConnections') }}
</div>
<table v-else>
<thead>
<tr>
<th>{{ t('connections.table.name') }}</th>
<!-- 移除顶部的加载/错误/无数据状态这些由父组件处理 -->
<!-- <div v-if="isLoading" class="loading">{{ t('connections.loading') }}</div> -->
<!-- <div v-else-if="error" class="error">{{ t('connections.error', { error: error }) }}</div> -->
<!-- <div v-else-if="connections.length === 0" class="no-connections"> -->
<!-- {{ t('connections.noConnections') }} -->
<!-- </div> -->
<div v-if="tagsError" class="error">{{ t('tags.error', { error: tagsError }) }}</div> <!-- 显示标签加载错误 -->
<!-- 遍历分组 -->
<div v-for="(groupConnections, groupName) in groupedConnections" :key="groupName" class="connection-group">
<h4 class="group-title">
{{ groupName === '_untagged_' ? t('connections.untaggedGroup') : groupName }}
({{ groupConnections.length }})
</h4>
<table v-if="groupConnections.length > 0">
<thead>
<tr>
<th>{{ t('connections.table.name') }}</th>
<th>{{ t('connections.table.host') }}</th>
<th>{{ t('connections.table.port') }}</th>
<th>{{ t('connections.table.user') }}</th>
<th>{{ t('connections.table.authMethod') }}</th>
<th>{{ t('connections.table.tags') }}</th> <!-- 新增标签列 -->
<th>{{ t('connections.table.lastConnected') }}</th>
<th>{{ t('connections.table.actions') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="conn in connections" :key="conn.id">
<td>{{ conn.name }}</td>
<td>{{ conn.host }}</td>
<th>{{ t('connections.table.actions') }}</th>
</tr>
</thead>
<tbody>
<!-- 遍历分组内的连接 -->
<tr v-for="conn in groupConnections" :key="conn.id">
<td>{{ conn.name }}</td>
<td>{{ conn.host }}</td>
<td>{{ conn.port }}</td>
<td>{{ conn.username }}</td>
<td>{{ conn.auth_method }}</td>
@@ -104,13 +195,24 @@ const handleDelete = async (conn: ConnectionInfo) => {
</td>
<td>{{ formatTimestamp(conn.last_connected_at) }}</td>
<td>
<button @click="connectToServer(conn.id)">{{ t('connections.actions.connect') }}</button>
<button @click="emit('edit-connection', conn)">{{ t('connections.actions.edit') }}</button>
<button @click="handleDelete(conn)">{{ t('connections.actions.delete') }}</button>
<button @click="connectToServer(conn.id)" class="action-button connect-button">{{ t('connections.actions.connect') }}</button>
<button @click="emit('edit-connection', conn)" class="action-button edit-button">{{ t('connections.actions.edit') }}</button>
<button @click="handleTestConnection(conn.id)" class="action-button test-button" :disabled="testingState[conn.id]">{{ testingState[conn.id] ? t('connections.actions.testing') : t('connections.actions.test') }}</button> <!-- 新增测试按钮 -->
<button @click="handleDelete(conn)" class="action-button delete-button">{{ t('connections.actions.delete') }}</button>
</td>
</tr>
</tbody>
</table>
</tbody>
</table>
<!-- 如果未标记组为空可以显示提示 -->
<div v-else-if="groupName === '_untagged_'" class="no-connections-in-group">
{{ t('connections.noUntaggedConnections') }}
</div>
</div>
<!-- 如果所有分组都为空 props.connections 为空显示整体提示 -->
<div v-if="Object.keys(groupedConnections).length === 0 || (Object.keys(groupedConnections).length === 1 && groupedConnections['_untagged_']?.length === 0)" class="no-connections">
{{ t('connections.noConnections') }}
</div>
</div>
</template>
@@ -132,7 +234,19 @@ export default {
margin-top: 1rem;
}
.loading, .error, .no-connections {
.connection-group {
margin-bottom: 1.5rem; /* 分组间距 */
}
.group-title {
margin-bottom: 0.5rem;
font-size: 1.1em;
font-weight: bold;
border-bottom: 1px solid #eee;
padding-bottom: 0.3rem;
}
.loading, .error, .no-connections, .no-connections-in-group {
padding: 1rem;
border: 1px solid #ccc;
border-radius: 4px;
@@ -170,6 +284,38 @@ button {
cursor: pointer;
}
.action-button { /* 统一按钮样式 */
padding: 0.3rem 0.6rem;
margin-right: 0.5rem;
cursor: pointer;
border: none;
border-radius: 4px;
font-size: 0.9em;
min-width: 50px; /* 给按钮一个最小宽度 */
text-align: center;
}
.connect-button {
background-color: #28a745; /* Green */
color: white;
}
.edit-button {
background-color: #ffc107; /* Amber */
color: #333;
}
.test-button {
background-color: #17a2b8; /* Teal */
color: white;
}
.delete-button {
background-color: #dc3545; /* Red */
color: white;
}
.action-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* 标签样式 */
.tag-list {
display: flex;