实现连接配置的导入/导出功能

This commit is contained in:
Baobhan Sith
2025-04-15 08:27:42 +08:00
parent fa27d40eb2
commit b58f5da52b
8 changed files with 762 additions and 28 deletions
+143 -1
View File
@@ -62,6 +62,143 @@ 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>
@@ -69,7 +206,12 @@ const closeForm = () => {
<h2>{{ t('connections.title') }}</h2>
<div class="actions-bar">
<button @click="openAddForm" v-if="!showForm">{{ t('connections.addConnection') }}</button>
<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>