实现连接配置的导入/导出功能
This commit is contained in:
@@ -86,7 +86,15 @@
|
||||
"test": {
|
||||
"success": "Connection test successful!",
|
||||
"failed": "Connection test failed: {error}"
|
||||
}
|
||||
},
|
||||
"exportConnections": "Export Connections",
|
||||
"importConnections": "Import Connections",
|
||||
"exportError": "Failed to export connections: {message}",
|
||||
"importError": "Failed to import connections: {message}",
|
||||
"importErrorFileType": "Invalid file type. Please select a JSON file.",
|
||||
"importErrorUnknown": "Unknown import error occurred.",
|
||||
"importErrorNetwork": "Network error during import.",
|
||||
"importSuccess": "Import completed. Successful: {successCount}, Failed: {failureCount}."
|
||||
},
|
||||
"proxies": {
|
||||
"title": "Proxy Management",
|
||||
|
||||
@@ -86,7 +86,15 @@
|
||||
"test": {
|
||||
"success": "连接测试成功!",
|
||||
"failed": "连接测试失败: {error}"
|
||||
}
|
||||
},
|
||||
"exportConnections": "导出连接",
|
||||
"importConnections": "导入连接",
|
||||
"exportError": "导出连接失败: {message}",
|
||||
"importError": "导入连接失败: {message}",
|
||||
"importErrorFileType": "文件类型无效。请选择 JSON 文件。",
|
||||
"importErrorUnknown": "发生未知导入错误。",
|
||||
"importErrorNetwork": "导入过程中发生网络错误。",
|
||||
"importSuccess": "导入完成。成功: {successCount}, 失败: {failureCount}."
|
||||
},
|
||||
"proxies": {
|
||||
"title": "代理管理",
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user