初始化 SSH 面板:Electron + React + electron-vite

- 登录页:服务器地址/端口/用户名/密码表单,ssh2 建立真实 SSH 连接
- 面板页:深蓝色顶栏菜单(概览/容器/终端占位),显示当前连接
- 凭据保存:密码经系统安全存储(DPAPI)加密后保存在本机,支持一键填充/删除
- 隐藏 Electron 默认菜单,开发模式 F12 开关开发者工具

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-09-06 17:31:05 +08:00
commit 0acd71d76d
14 changed files with 3232 additions and 0 deletions
+169
View File
@@ -0,0 +1,169 @@
import { app, BrowserWindow, Menu, ipcMain, safeStorage } from 'electron'
import path from 'path'
import fs from 'fs'
import { Client } from 'ssh2'
// 隐藏 Electron 默认菜单(File / Edit 等)
Menu.setApplicationMenu(null)
let mainWindow = null
let activeConnection = null // 当前活动的 SSH 连接
let activeSession = null // { host, username },登录成功后记录
// ---------- 凭据存储 ----------
const credFile = () => path.join(app.getPath('userData'), 'credentials.json')
function readCredentials() {
try {
return JSON.parse(fs.readFileSync(credFile(), 'utf8'))
} catch {
return []
}
}
function writeCredentials(list) {
fs.mkdirSync(path.dirname(credFile()), { recursive: true })
fs.writeFileSync(credFile(), JSON.stringify(list, null, 2), 'utf8')
}
// 密码优先用系统安全存储加密(Windows DPAPI),不可用时降级为 base64
function encryptPassword(password) {
if (safeStorage.isEncryptionAvailable()) {
return { data: safeStorage.encryptString(password).toString('base64'), encrypted: true }
}
return { data: Buffer.from(password, 'utf8').toString('base64'), encrypted: false }
}
function decryptPassword(entry) {
if (entry.encrypted) {
return safeStorage.decryptString(Buffer.from(entry.data, 'base64'))
}
return Buffer.from(entry.data, 'base64').toString('utf8')
}
// ---------- 窗口 ----------
function createWindow() {
mainWindow = new BrowserWindow({
width: 1280,
height: 800,
backgroundColor: '#d2f8fc',
webPreferences: {
preload: path.join(__dirname, '../preload/index.js'),
sandbox: false,
contextIsolation: true,
nodeIntegration: false,
},
})
if (process.env['ELECTRON_RENDERER_URL']) {
// 开发模式:加载 Vite 开发服务器(支持热更新)
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
// 菜单已隐藏,用 F12 开关开发者工具
mainWindow.webContents.on('before-input-event', (event, input) => {
if (input.key === 'F12') mainWindow.webContents.toggleDevTools()
})
} else {
mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'))
}
mainWindow.on('closed', () => {
mainWindow = null
if (activeConnection) {
activeConnection.end()
activeConnection = null
}
})
}
// ---------- IPC:SSH 连接 ----------
ipcMain.handle('ssh:connect', (event, { host, port, username, password }) => {
return new Promise((resolve) => {
// 已有连接时先断开旧连接
if (activeConnection) {
activeConnection.end()
activeConnection = null
}
const client = new Client()
client.on('ready', () => {
activeConnection = client
activeSession = { host, username }
resolve({ ok: true })
})
client.on('error', (err) => {
resolve({ ok: false, message: err.message })
})
client.on('close', () => {
if (activeConnection === client) activeConnection = null
})
client.connect({
host,
port: Number(port),
username,
password,
readyTimeout: 10000,
})
})
})
ipcMain.handle('ssh:disconnect', () => {
if (activeConnection) {
activeConnection.end()
activeConnection = null
}
activeSession = null
return { ok: true }
})
ipcMain.handle('ssh:session', () => activeSession)
// ---------- IPC:凭据 ----------
ipcMain.handle('cred:save', (event, { host, port, username, password }) => {
const list = readCredentials()
const entry = { host, port: Number(port), username, password: encryptPassword(password) }
// 同一 host + username 覆盖旧记录
const idx = list.findIndex((c) => c.host === host && c.username === username)
if (idx >= 0) list[idx] = entry
else list.push(entry)
writeCredentials(list)
return { ok: true }
})
ipcMain.handle('cred:list', () => {
return readCredentials()
.map((c) => {
try {
return {
host: c.host,
port: c.port,
username: c.username,
password: decryptPassword(c.password),
}
} catch {
return null // 本机无法解密(比如从别的机器拷贝来的),跳过
}
})
.filter(Boolean)
})
ipcMain.handle('cred:delete', (event, { host, username }) => {
writeCredentials(readCredentials().filter((c) => !(c.host === host && c.username === username)))
return { ok: true }
})
// ---------- 应用生命周期 ----------
app.whenReady().then(() => {
createWindow()
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})