diff --git a/README.md b/README.md index c81934f..ec84c83 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,7 @@ | ⌨️ **快捷命令** | 内置常用命令,Ctrl+K 快速调出 | | ☁️ **云端同步** | MySQL 数据库同步,多设备配置共享 | | 🏷️ **分组管理** | 主机分组、颜色标识、便捷管理 | +| 📤 **导入导出** | 支持主机配置的 JSON 导入导出,方便备份迁移 | | 📱 **跨平台** | 桌面端和移动端统一体验 | --- @@ -220,6 +221,45 @@ npm run android:run --- +## 📤 导入导出 + +### 导出主机配置 + +1. 打开「主机管理」 +2. 点击右上角「导出」按钮 +3. 选择保存位置,生成 JSON 文件 + +### 导入主机配置 + +1. 打开「主机管理」 +2. 点击「导入」按钮 +3. 选择导入模式: + - **合并导入**:保留现有主机,更新重复的 + - **替换导入**:清空现有主机,完全替换 +4. 选择 JSON 配置文件 + +### 配置文件格式 + +```json +{ + "version": "1.0", + "appName": "EasyShell", + "hosts": [ + { + "name": "生产服务器", + "host": "192.168.1.100", + "port": 22, + "username": "root", + "password": "******", + "groupName": "生产环境", + "color": "#3fb950" + } + ] +} +``` + +--- + ## 🔧 配置说明 ### 数据存储 diff --git a/main.js b/main.js index cd9edf8..2a2fd51 100644 --- a/main.js +++ b/main.js @@ -1,8 +1,9 @@ /** * EasyShell - Electron 主进程 */ -const { app, BrowserWindow, ipcMain, Menu } = require('electron'); +const { app, BrowserWindow, ipcMain, Menu, dialog } = require('electron'); const path = require('path'); +const fs = require('fs'); const Store = require('electron-store'); const databaseService = require('./src/services/database'); const sshService = require('./src/services/ssh'); @@ -187,6 +188,63 @@ ipcMain.handle('hosts:delete', async (event, id) => { return await databaseService.deleteHost(id); }); +// 导出主机 +ipcMain.handle('hosts:export', async () => { + try { + const data = databaseService.exportHosts(); + + // 打开保存对话框 + const { filePath, canceled } = await dialog.showSaveDialog(mainWindow, { + title: '导出主机配置', + defaultPath: `easyshell-hosts-${new Date().toISOString().slice(0, 10)}.json`, + filters: [ + { name: 'JSON 文件', extensions: ['json'] }, + { name: '所有文件', extensions: ['*'] } + ] + }); + + if (canceled || !filePath) { + return { success: false, canceled: true }; + } + + // 写入文件 + fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8'); + return { success: true, filePath, count: data.hosts.length }; + } catch (error) { + console.error('❌ 导出失败:', error); + return { success: false, error: error.message }; + } +}); + +// 导入主机 +ipcMain.handle('hosts:import', async (event, mode) => { + try { + // 打开文件选择对话框 + const { filePaths, canceled } = await dialog.showOpenDialog(mainWindow, { + title: '导入主机配置', + filters: [ + { name: 'JSON 文件', extensions: ['json'] }, + { name: '所有文件', extensions: ['*'] } + ], + properties: ['openFile'] + }); + + if (canceled || filePaths.length === 0) { + return { success: false, canceled: true }; + } + + // 读取文件 + const fileContent = fs.readFileSync(filePaths[0], 'utf-8'); + const data = JSON.parse(fileContent); + + // 导入数据 + return databaseService.importHosts(data, mode); + } catch (error) { + console.error('❌ 导入失败:', error); + return { success: false, error: error.message }; + } +}); + // 命令 ipcMain.handle('commands:search', (event, keyword) => { return databaseService.searchCommands(keyword); diff --git a/preload.js b/preload.js index 7e8c7aa..fd4606d 100644 --- a/preload.js +++ b/preload.js @@ -32,6 +32,8 @@ contextBridge.exposeInMainWorld('electronAPI', { add: (host) => ipcRenderer.invoke('hosts:add', host), update: (id, host) => ipcRenderer.invoke('hosts:update', { id, host }), delete: (id) => ipcRenderer.invoke('hosts:delete', id), + export: () => ipcRenderer.invoke('hosts:export'), + import: (mode) => ipcRenderer.invoke('hosts:import', mode), }, // 命令 diff --git a/src/components/HostManager.js b/src/components/HostManager.js index 9cff226..c19625a 100644 --- a/src/components/HostManager.js +++ b/src/components/HostManager.js @@ -11,6 +11,8 @@ import { FiEye, FiEyeOff, FiPlay, + FiDownload, + FiUpload, } from 'react-icons/fi'; const colors = [ @@ -24,6 +26,9 @@ function HostManager({ hosts, initialEditHost, onClose, onConnect, onUpdate }) { const [showPassword, setShowPassword] = useState(false); const [testing, setTesting] = useState(false); const [testResult, setTestResult] = useState(null); + const [importing, setImporting] = useState(false); + const [exporting, setExporting] = useState(false); + const [importExportResult, setImportExportResult] = useState(null); const [formData, setFormData] = useState({ name: '', host: '', @@ -125,6 +130,75 @@ function HostManager({ hosts, initialEditHost, onClose, onConnect, onUpdate }) { } }; + // 导出主机 + const handleExport = async () => { + if (!window.electronAPI) return; + + setExporting(true); + setImportExportResult(null); + + try { + const result = await window.electronAPI.hosts.export(); + if (result.canceled) { + setImportExportResult(null); + } else if (result.success) { + setImportExportResult({ + type: 'success', + message: `成功导出 ${result.count} 个主机配置` + }); + } else { + setImportExportResult({ + type: 'error', + message: result.error || '导出失败' + }); + } + } catch (error) { + setImportExportResult({ + type: 'error', + message: error.message + }); + } finally { + setExporting(false); + } + }; + + // 导入主机 + const handleImport = async (mode = 'merge') => { + if (!window.electronAPI) return; + + setImporting(true); + setImportExportResult(null); + + try { + const result = await window.electronAPI.hosts.import(mode); + if (result.canceled) { + setImportExportResult(null); + } else if (result.success) { + let message = `导入完成:新增 ${result.imported} 个`; + if (result.updated > 0) message += `,更新 ${result.updated} 个`; + if (result.skipped > 0) message += `,跳过 ${result.skipped} 个`; + + setImportExportResult({ + type: 'success', + message + }); + onUpdate(); // 刷新列表 + } else { + setImportExportResult({ + type: 'error', + message: result.error || '导入失败' + }); + } + } catch (error) { + setImportExportResult({ + type: 'error', + message: error.message + }); + } finally { + setImporting(false); + } + }; + return ( )} - +
+ {/* 导入按钮 */} +
+ + {/* 下拉菜单 */} +
+ + +
+
+ {/* 导出按钮 */} + +
+ +
+ + {/* 导入导出结果提示 */} + {importExportResult && ( +
+
+
+ {importExportResult.type === 'success' ? : } + {importExportResult.message} +
+ +
+
+ )}
{/* 主机列表 */} diff --git a/src/services/database.js b/src/services/database.js index 2ede75f..3a86a62 100644 --- a/src/services/database.js +++ b/src/services/database.js @@ -623,6 +623,112 @@ class DatabaseService { return { success: true }; } + // ========== 导入导出方法 ========== + + /** + * 导出所有主机信息为 JSON + */ + exportHosts() { + const hosts = this.runQuery('SELECT * FROM hosts ORDER BY group_name, name'); + + // 移除敏感信息的内部字段,只保留必要字段 + const exportData = hosts.map(host => ({ + name: host.name, + host: host.host, + port: host.port, + username: host.username, + password: host.password, + privateKey: host.private_key, + groupName: host.group_name, + color: host.color, + description: host.description, + })); + + return { + version: '1.0', + exportTime: new Date().toISOString(), + appName: 'EasyShell', + hosts: exportData, + }; + } + + /** + * 导入主机信息 + * @param {Object} data - 导入的 JSON 数据 + * @param {string} mode - 导入模式: 'merge'(合并) | 'replace'(替换) + */ + importHosts(data, mode = 'merge') { + if (!data || !data.hosts || !Array.isArray(data.hosts)) { + return { success: false, error: '无效的导入数据格式' }; + } + + let imported = 0; + let skipped = 0; + let updated = 0; + + try { + // 如果是替换模式,先清空所有主机 + if (mode === 'replace') { + this.sqliteDb.run('DELETE FROM hosts'); + } + + for (const host of data.hosts) { + // 验证必要字段 + if (!host.name || !host.host || !host.username) { + skipped++; + continue; + } + + // 检查是否已存在(以 host 地址为唯一标识) + const existing = this.runQuerySingle('SELECT id FROM hosts WHERE host = ?', [host.host]); + + if (existing) { + if (mode === 'merge') { + // 合并模式:更新已存在的记录 + this.sqliteDb.run(` + UPDATE hosts SET + name = ?, port = ?, username = ?, password = ?, + private_key = ?, group_name = ?, color = ?, description = ?, + updated_at = CURRENT_TIMESTAMP + WHERE host = ? + `, [ + host.name, host.port || 22, host.username, host.password || '', + host.privateKey || '', host.groupName || '默认分组', + host.color || '#58a6ff', host.description || '', host.host + ]); + updated++; + } else { + skipped++; + } + } else { + // 插入新记录 + this.sqliteDb.run(` + INSERT INTO hosts (name, host, port, username, password, private_key, group_name, color, description) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + `, [ + host.name, host.host, host.port || 22, host.username, + host.password || '', host.privateKey || '', + host.groupName || '默认分组', host.color || '#58a6ff', + host.description || '' + ]); + imported++; + } + } + + this.saveDatabase(); + return { + success: true, + imported, + updated, + skipped, + total: data.hosts.length + }; + } catch (error) { + console.error('❌ 导入失败:', error); + return { success: false, error: error.message }; + } + } + // ========== 关闭数据库 ========== close() {