update
This commit is contained in:
@@ -225,7 +225,6 @@ const isElementVisibleAndFocusable = (element: HTMLElement): boolean => {
|
|||||||
<nav ref="navRef">
|
<nav ref="navRef">
|
||||||
<div class="nav-left"> <!-- Group left-aligned links -->
|
<div class="nav-left"> <!-- Group left-aligned links -->
|
||||||
<RouterLink to="/">{{ t('nav.dashboard') }}</RouterLink>
|
<RouterLink to="/">{{ t('nav.dashboard') }}</RouterLink>
|
||||||
<RouterLink to="/connections">{{ t('nav.connections') }}</RouterLink>
|
|
||||||
<RouterLink to="/workspace">{{ t('nav.terminal') }}</RouterLink>
|
<RouterLink to="/workspace">{{ t('nav.terminal') }}</RouterLink>
|
||||||
<RouterLink to="/proxies">{{ t('nav.proxies') }}</RouterLink>
|
<RouterLink to="/proxies">{{ t('nav.proxies') }}</RouterLink>
|
||||||
<RouterLink to="/notifications">{{ t('nav.notifications') }}</RouterLink>
|
<RouterLink to="/notifications">{{ t('nav.notifications') }}</RouterLink>
|
||||||
|
|||||||
@@ -16,12 +16,6 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
name: 'Login',
|
name: 'Login',
|
||||||
component: () => import('../views/LoginView.vue') // 指向实际的登录组件
|
component: () => import('../views/LoginView.vue') // 指向实际的登录组件
|
||||||
},
|
},
|
||||||
// 连接管理页面
|
|
||||||
{
|
|
||||||
path: '/connections',
|
|
||||||
name: 'Connections',
|
|
||||||
component: () => import('../views/ConnectionsView.vue')
|
|
||||||
},
|
|
||||||
// 新增:代理管理页面
|
// 新增:代理管理页面
|
||||||
{
|
{
|
||||||
path: '/proxies',
|
path: '/proxies',
|
||||||
|
|||||||
@@ -1,280 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { ref, computed, onMounted } from 'vue'; // 引入 computed 和 onMounted
|
|
||||||
import { useI18n } from 'vue-i18n';
|
|
||||||
import { storeToRefs } from 'pinia'; // 引入 storeToRefs
|
|
||||||
import ConnectionList from '../components/ConnectionList.vue';
|
|
||||||
import AddConnectionForm from '../components/AddConnectionForm.vue';
|
|
||||||
import { useConnectionsStore, ConnectionInfo } from '../stores/connections.store'; // 引入 ConnectionInfo 和 Store
|
|
||||||
import { useTagsStore } from '../stores/tags.store'; // 引入 Tags Store
|
|
||||||
|
|
||||||
const { t } = useI18n();
|
|
||||||
const connectionsStore = useConnectionsStore(); // 获取 Connections Store
|
|
||||||
const tagsStore = useTagsStore(); // 获取 Tags Store
|
|
||||||
const { connections } = storeToRefs(connectionsStore); // 获取连接列表
|
|
||||||
const { tags } = storeToRefs(tagsStore); // 获取标签列表
|
|
||||||
|
|
||||||
const showForm = ref(false);
|
|
||||||
const editingConnection = ref<ConnectionInfo | null>(null);
|
|
||||||
const selectedTagId = ref<number | null>(null); // 用于存储选中的标签 ID,null 表示所有
|
|
||||||
|
|
||||||
// 计算筛选后的连接列表
|
|
||||||
const filteredConnections = computed(() => {
|
|
||||||
if (selectedTagId.value === null) {
|
|
||||||
return connections.value; // 返回所有连接
|
|
||||||
}
|
|
||||||
return connections.value.filter(conn =>
|
|
||||||
conn.tag_ids?.includes(selectedTagId.value!) // 筛选包含选中标签 ID 的连接
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 组件挂载时获取连接和标签列表
|
|
||||||
onMounted(() => {
|
|
||||||
connectionsStore.fetchConnections(); // 添加获取连接列表的调用
|
|
||||||
tagsStore.fetchTags();
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleConnectionAdded = () => {
|
|
||||||
showForm.value = false; // 使用新变量名
|
|
||||||
// ConnectionList 组件会自动从 store 获取更新后的列表
|
|
||||||
// 如果添加后需要清除筛选,可以在这里设置 selectedTagId.value = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 新增:处理编辑成功后的逻辑
|
|
||||||
const handleConnectionUpdated = () => {
|
|
||||||
editingConnection.value = null; // 清除正在编辑的连接
|
|
||||||
showForm.value = false; // 编辑成功后隐藏表单
|
|
||||||
};
|
|
||||||
|
|
||||||
// 新增:处理来自 ConnectionList 的编辑请求
|
|
||||||
const handleEditRequest = (connection: ConnectionInfo) => {
|
|
||||||
editingConnection.value = connection; // 设置要编辑的连接
|
|
||||||
showForm.value = true; // 显示表单
|
|
||||||
};
|
|
||||||
|
|
||||||
// 新增:显式打开添加表单的方法
|
|
||||||
const openAddForm = () => {
|
|
||||||
editingConnection.value = null; // 确保不在编辑模式
|
|
||||||
showForm.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 新增:统一的关闭表单方法
|
|
||||||
const closeForm = () => {
|
|
||||||
editingConnection.value = null; // 清除编辑状态
|
|
||||||
showForm.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 新增:处理导出连接按钮点击事件
|
|
||||||
const handleExportConnections = async () => {
|
|
||||||
try {
|
|
||||||
// 调用后端导出 API
|
|
||||||
const response = await fetch('/api/v1/connections/export', {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
// 如果需要认证,可能需要添加 Authorization 头
|
|
||||||
// 'Authorization': `Bearer ${token}`, // 示例
|
|
||||||
'Accept': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
// 处理错误响应
|
|
||||||
const errorData = await response.json();
|
|
||||||
console.error('导出连接失败:', errorData.message || response.statusText);
|
|
||||||
alert(t('connections.exportError', { message: errorData.message || response.statusText })); // 提示用户错误
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取文件名 (从 Content-Disposition 头)
|
|
||||||
const disposition = response.headers.get('Content-Disposition');
|
|
||||||
let filename = 'nexus-terminal-connections.json'; // 默认文件名
|
|
||||||
if (disposition && disposition.includes('attachment')) {
|
|
||||||
const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
|
|
||||||
const matches = filenameRegex.exec(disposition);
|
|
||||||
if (matches != null && matches[1]) {
|
|
||||||
filename = matches[1].replace(/['"]/g, '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建 Blob 对象并触发下载
|
|
||||||
const blob = await response.blob();
|
|
||||||
const url = window.URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = filename;
|
|
||||||
document.body.appendChild(a); // 需要添加到 DOM 才能触发点击
|
|
||||||
a.click();
|
|
||||||
a.remove(); // 清理
|
|
||||||
window.URL.revokeObjectURL(url); // 释放对象 URL
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('导出连接时发生网络或处理错误:', error);
|
|
||||||
alert(t('connections.exportError', { message: error.message || '未知错误' }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Import/Export Logic ---
|
|
||||||
const fileInput = ref<HTMLInputElement | null>(null); // Ref for the hidden file input
|
|
||||||
|
|
||||||
// 点击导入按钮时触发文件选择
|
|
||||||
const handleImportClick = () => {
|
|
||||||
fileInput.value?.click(); // 触发隐藏 input 的点击事件
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理文件选择变化
|
|
||||||
const handleFileChange = async (event: Event) => {
|
|
||||||
const target = event.target as HTMLInputElement;
|
|
||||||
const file = target.files?.[0];
|
|
||||||
|
|
||||||
if (!file) {
|
|
||||||
return; // 没有选择文件
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.type !== 'application/json') {
|
|
||||||
alert(t('connections.importErrorFileType')); // 提示文件类型错误
|
|
||||||
target.value = ''; // 清空文件输入,以便可以再次选择相同文件
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('connectionsFile', file); // 'connectionsFile' 必须与后端 multer 配置匹配
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/v1/connections/import', {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData,
|
|
||||||
headers: {
|
|
||||||
// 不需要 'Content-Type': 'multipart/form-data',浏览器会自动设置
|
|
||||||
// 如果需要认证,添加 Authorization 头
|
|
||||||
'Accept': 'application/json', // 期望服务器返回 JSON
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 首先检查响应是否成功
|
|
||||||
if (!response.ok) {
|
|
||||||
let errorMsg = `${response.status} ${response.statusText}`;
|
|
||||||
try {
|
|
||||||
// 尝试解析可能的 JSON 错误体
|
|
||||||
const errorResult = await response.json();
|
|
||||||
console.error('导入连接失败 (JSON):', errorResult.message || response.statusText, errorResult.errors);
|
|
||||||
errorMsg = errorResult.message || errorMsg;
|
|
||||||
if (errorResult.errors && Array.isArray(errorResult.errors)) {
|
|
||||||
errorMsg += '\n' + errorResult.errors.map((e: any) => `- ${e.connectionName || '未知'}: ${e.message}`).join('\n');
|
|
||||||
}
|
|
||||||
} catch (jsonError) {
|
|
||||||
// 如果解析 JSON 失败,尝试读取文本
|
|
||||||
try {
|
|
||||||
const textError = await response.text();
|
|
||||||
console.error('导入连接失败 (Text):', textError);
|
|
||||||
// 如果文本错误信息不为空,则使用它,否则保留状态码/文本
|
|
||||||
if (textError) {
|
|
||||||
errorMsg = textError.substring(0, 500); // 限制长度避免过长的 HTML 错误页
|
|
||||||
}
|
|
||||||
} catch (textReadError) {
|
|
||||||
console.error('读取错误响应文本失败:', textReadError);
|
|
||||||
// 保留原始的状态码/文本错误
|
|
||||||
}
|
|
||||||
}
|
|
||||||
alert(t('connections.importError', { message: errorMsg }));
|
|
||||||
} else {
|
|
||||||
// 响应成功,解析 JSON
|
|
||||||
try {
|
|
||||||
const result = await response.json();
|
|
||||||
console.log('导入连接成功:', result);
|
|
||||||
alert(t('connections.importSuccess', { successCount: result.successCount, failureCount: result.failureCount }));
|
|
||||||
// 导入成功后刷新连接列表
|
|
||||||
connectionsStore.fetchConnections();
|
|
||||||
} catch (jsonParseError: any) {
|
|
||||||
console.error('解析成功响应 JSON 时出错:', jsonParseError);
|
|
||||||
alert(t('connections.importError', { message: `无法解析服务器成功响应: ${jsonParseError.message}` }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('导入连接时发生网络或处理错误:', error);
|
|
||||||
alert(t('connections.importError', { message: error.message || t('connections.importErrorNetwork') }));
|
|
||||||
} finally {
|
|
||||||
// 无论成功或失败,都清空文件输入框的值,以便用户可以重新上传相同的文件
|
|
||||||
if (target) {
|
|
||||||
target.value = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="connections-view">
|
|
||||||
<h2>{{ t('connections.title') }}</h2>
|
|
||||||
|
|
||||||
<div class="actions-bar">
|
|
||||||
<div> <!-- Wrap buttons -->
|
|
||||||
<button @click="openAddForm" v-if="!showForm" style="margin-right: 0.5rem;">{{ t('connections.addConnection') }}</button>
|
|
||||||
<button @click="handleExportConnections" style="margin-right: 0.5rem;">{{ t('connections.exportConnections') }}</button> <!-- Export Button -->
|
|
||||||
<button @click="handleImportClick">{{ t('connections.importConnections') }}</button> <!-- Import Button -->
|
|
||||||
<input type="file" ref="fileInput" @change="handleFileChange" accept=".json" style="display: none;" /> <!-- Hidden File Input -->
|
|
||||||
</div>
|
|
||||||
<!-- 标签筛选下拉框 -->
|
|
||||||
<select v-model="selectedTagId" class="tag-filter-select">
|
|
||||||
<option :value="null">{{ t('connections.filterAllTags') }}</option>
|
|
||||||
<option v-for="tag in tags" :key="tag.id" :value="tag.id">
|
|
||||||
{{ tag.name }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 添加/编辑连接表单 (条件渲染) -->
|
|
||||||
<AddConnectionForm
|
|
||||||
v-if="showForm"
|
|
||||||
:connection-to-edit="editingConnection"
|
|
||||||
@close="closeForm"
|
|
||||||
@connection-added="handleConnectionAdded"
|
|
||||||
@connection-updated="handleConnectionUpdated"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 连接列表,传入筛选后的列表 -->
|
|
||||||
<ConnectionList :connections="filteredConnections" @edit-connection="handleEditRequest" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.connections-view {
|
|
||||||
padding: var(--base-padding, 1rem); /* 使用变量 */
|
|
||||||
color: var(--text-color);
|
|
||||||
background-color: var(--app-bg-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions-bar {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between; /* 让按钮和下拉框分开 */
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: var(--base-margin, 1rem); /* 使用变量 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions-bar button {
|
|
||||||
/* margin-bottom: 1rem; */ /* 移除按钮的下边距 */
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: var(--button-bg-color, #007bff); /* 使用变量 */
|
|
||||||
color: var(--button-text-color, #ffffff); /* 使用变量 */
|
|
||||||
border: none; /* 移除默认边框 */
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions-bar button:hover:not(:disabled) {
|
|
||||||
background-color: var(--button-hover-bg-color, #0056b3); /* 使用变量 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions-bar button:disabled {
|
|
||||||
cursor: not-allowed;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-filter-select {
|
|
||||||
padding: 0.4rem 0.8rem;
|
|
||||||
border: 1px solid var(--border-color, #ccc); /* 使用变量 */
|
|
||||||
border-radius: 4px;
|
|
||||||
min-width: 150px; /* 给下拉框一个最小宽度 */
|
|
||||||
color: var(--text-color); /* 确保文本颜色 */
|
|
||||||
background-color: var(--app-bg-color); /* 确保背景色 */
|
|
||||||
font-family: var(--font-family-sans-serif, sans-serif); /* 使用变量 */
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
Reference in New Issue
Block a user