refactor: restructure project layout

- Move main process to electron/main/
- Move preload to electron/preload/
- Add shared types in electron/shared/
- Split redis-service into modular redis/ directory
- Flatten renderer to src/ (remove src/renderer/ nesting)
- Update electron.vite.config.ts paths
- Update tsconfig paths
- Output to electron-dist/
This commit is contained in:
2026-07-04 21:12:50 +08:00
parent bb885c6d6d
commit bdcf4fe299
59 changed files with 654 additions and 624 deletions
+1 -1
View File
@@ -7,6 +7,6 @@
</head>
<body>
<div id="app"></div>
<script type="module" src="./src/main.ts"></script>
<script type="module" src="./main.ts"></script>
</body>
</html>
+1 -4
View File
@@ -1,16 +1,13 @@
// src/renderer/src/main.ts
// src/renderer/main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import App from './App.vue'
import router from './router'
import './styles/global.css'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(ElementPlus, { size: 'small' })
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
-16
View File
@@ -1,16 +0,0 @@
// src/main/credential.ts
import { safeStorage } from 'electron'
export function encrypt(text: string): string {
if (!safeStorage.isEncryptionAvailable()) {
return Buffer.from(text).toString('base64')
}
return safeStorage.encryptString(text).toString('base64')
}
export function decrypt(encoded: string): string {
if (!safeStorage.isEncryptionAvailable()) {
return Buffer.from(encoded, 'base64').toString('utf-8')
}
return safeStorage.decryptString(Buffer.from(encoded, 'base64'))
}
-64
View File
@@ -1,64 +0,0 @@
// src/main/index.ts
import { app, BrowserWindow, nativeTheme, nativeImage } from 'electron'
import { join } from 'path'
import { restoreAndTrack } from './win-state'
import { registerIpcHandlers } from './ipc-handlers'
let mainWindow: BrowserWindow | null = null
function getIconPath(): string {
return join(__dirname, '../../build/icons/icon_256.png')
}
function createWindow(): void {
const iconPath = getIconPath()
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
minWidth: 900,
minHeight: 600,
title: 'JRedisDesktop',
icon: nativeImage.createFromPath(iconPath),
backgroundColor: nativeTheme.shouldUseDarkColors ? '#0f0f1a' : '#f8fafc',
webPreferences: {
preload: join(__dirname, '../preload/index.mjs'),
contextIsolation: true,
nodeIntegration: false,
sandbox: false,
},
frame: false,
titleBarStyle: 'hidden',
})
// Linux taskbar icon
if (process.platform === 'linux' && mainWindow) {
mainWindow.setIcon(nativeImage.createFromPath(iconPath))
}
restoreAndTrack(mainWindow)
if (process.env.ELECTRON_RENDERER_URL) {
mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL)
mainWindow.webContents.openDevTools()
} else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
}
mainWindow.on('closed', () => { mainWindow = null })
nativeTheme.on('updated', () => {
mainWindow?.webContents.send('theme:os-updated', nativeTheme.shouldUseDarkColors)
})
}
app.whenReady().then(() => {
registerIpcHandlers()
createWindow()
})
app.on('window-all-closed', () => { app.quit() })
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
-218
View File
@@ -1,218 +0,0 @@
// src/main/ipc-handlers.ts
import { ipcMain, BrowserWindow, nativeTheme, dialog } from 'electron'
import store, { ConnectionConfig } from './store'
import { encrypt, decrypt } from './credential'
import * as redisService from './redis-service'
export function registerIpcHandlers(): void {
// Window
ipcMain.on('window:minimize', () => BrowserWindow.getFocusedWindow()?.minimize())
ipcMain.on('window:maximize', () => {
const win = BrowserWindow.getFocusedWindow()
if (win) win.isMaximized() ? win.unmaximize() : win.maximize()
})
ipcMain.on('window:close', () => BrowserWindow.getFocusedWindow()?.close())
// Theme
ipcMain.handle('theme:get', () => nativeTheme.shouldUseDarkColors)
ipcMain.on('theme:set', (_e, theme: 'system' | 'dark' | 'light') => {
nativeTheme.themeSource = theme
})
// Dialog
ipcMain.handle('dialog:openFile', async (_e, options: Electron.OpenDialogOptions) => {
const win = BrowserWindow.getFocusedWindow()
if (!win) return null
const result = await dialog.showOpenDialog(win, options)
if (result.canceled || result.filePaths.length === 0) return null
const fs = await import('fs')
try {
return { path: result.filePaths[0], content: fs.readFileSync(result.filePaths[0], 'utf-8') }
} catch { return null }
})
// Credential
ipcMain.handle('credential:encrypt', (_e, text: string) => encrypt(text))
ipcMain.handle('credential:decrypt', (_e, encoded: string) => decrypt(encoded))
// Storage - connections
ipcMain.handle('storage:getConnections', () => store.get('connections', []))
ipcMain.handle('storage:saveConnection', (_e, conn: ConnectionConfig) => {
const connections = store.get('connections', [])
const idx = connections.findIndex((c: ConnectionConfig) => c.id === conn.id)
if (idx >= 0) connections[idx] = conn
else connections.push(conn)
store.set('connections', connections)
return connections
})
ipcMain.handle('storage:deleteConnection', (_e, id: string) => {
const connections = store.get('connections', []).filter((c: ConnectionConfig) => c.id !== id)
store.set('connections', connections)
return connections
})
ipcMain.handle('storage:reorderConnections', (_e, ids: string[]) => {
const connections = store.get('connections', [])
const reordered = ids.map((id, i) => {
const conn = connections.find((c: ConnectionConfig) => c.id === id)!
conn.order = i
return conn
})
store.set('connections', reordered)
return reordered
})
// Storage - settings
ipcMain.handle('storage:getSettings', () => store.get('settings'))
ipcMain.handle('storage:saveSettings', (_e, settings) => {
store.set('settings', settings)
})
// Redis
ipcMain.handle('redis:connect', async (_e, id: string, opts) => {
try {
console.log(`[IPC] redis:connect id=${id}`, opts)
await redisService.connect(id, opts)
return { success: true }
} catch (err: any) {
console.error(`[IPC] redis:connect error:`, err.message)
return { success: false, error: err.message || String(err) }
}
})
ipcMain.handle('redis:disconnect', async (_e, id: string) => {
await redisService.disconnect(id)
})
ipcMain.handle('redis:execute', async (_e, id: string, command: string, args: string[]) => {
return redisService.execute(id, command, args)
})
ipcMain.handle('redis:getInfo', async (_e, id: string) => {
return redisService.getServerInfo(id)
})
ipcMain.handle('redis:scanKeys', async (_e, id: string, cursor: string, pattern: string, count: number) => {
return redisService.scanKeys(id, cursor, pattern, count)
})
ipcMain.handle('redis:getKeyType', async (_e, id: string, key: string) => {
return redisService.getKeyType(id, key)
})
ipcMain.handle('redis:getKeyTTL', async (_e, id: string, key: string) => {
return redisService.getKeyTTL(id, key)
})
ipcMain.handle('redis:getString', async (_e, id: string, key: string) => {
return redisService.getStringValue(id, key)
})
ipcMain.handle('redis:setString', async (_e, id: string, key: string, value: string) => {
await redisService.setStringValue(id, key, value)
})
ipcMain.handle('redis:deleteKey', async (_e, id: string, key: string) => {
await redisService.deleteKey(id, key)
})
ipcMain.handle('redis:hashScan', async (_e, id: string, key: string, cursor: string, count: number) => {
return redisService.getHashFields(id, key, cursor, count)
})
ipcMain.handle('redis:hashSet', async (_e, id: string, key: string, field: string, value: string) => {
await redisService.setHashField(id, key, field, value)
})
ipcMain.handle('redis:hashDel', async (_e, id: string, key: string, field: string) => {
await redisService.deleteHashField(id, key, field)
})
ipcMain.handle('redis:setTTL', async (_e, id: string, key: string, ttl: number) => {
await redisService.setKeyTTL(id, key, ttl)
})
ipcMain.handle('redis:selectDb', async (_e, id: string, db: number) => {
await redisService.selectDb(id, db)
})
ipcMain.handle('redis:dbSize', async (_e, id: string) => {
return redisService.dbSize(id)
})
// List
ipcMain.handle('redis:listRange', async (_e, id: string, key: string, start: number, stop: number) => {
return redisService.listRange(id, key, start, stop)
})
ipcMain.handle('redis:listLength', async (_e, id: string, key: string) => {
return redisService.listLength(id, key)
})
ipcMain.handle('redis:listPush', async (_e, id: string, key: string, values: string[]) => {
return redisService.listPush(id, key, ...values)
})
ipcMain.handle('redis:listSet', async (_e, id: string, key: string, index: number, value: string) => {
await redisService.listSet(id, key, index, value)
})
ipcMain.handle('redis:listRemove', async (_e, id: string, key: string, count: number, value: string) => {
return redisService.listRemove(id, key, count, value)
})
// Set
ipcMain.handle('redis:setMembers', async (_e, id: string, key: string) => {
return redisService.setMembers(id, key)
})
ipcMain.handle('redis:setSize', async (_e, id: string, key: string) => {
return redisService.setSize(id, key)
})
ipcMain.handle('redis:setAdd', async (_e, id: string, key: string, members: string[]) => {
return redisService.setAdd(id, key, ...members)
})
ipcMain.handle('redis:setRemove', async (_e, id: string, key: string, members: string[]) => {
return redisService.setRemove(id, key, ...members)
})
// Sorted set
ipcMain.handle('redis:zsetRange', async (_e, id: string, key: string, start: number, stop: number, withScores: boolean) => {
return redisService.zsetRange(id, key, start, stop, withScores)
})
ipcMain.handle('redis:zsetSize', async (_e, id: string, key: string) => {
return redisService.zsetSize(id, key)
})
ipcMain.handle('redis:zsetAdd', async (_e, id: string, key: string, score: number, member: string) => {
return redisService.zsetAdd(id, key, score, member)
})
ipcMain.handle('redis:zsetRemove', async (_e, id: string, key: string, members: string[]) => {
return redisService.zsetRemove(id, key, ...members)
})
// Slow log
ipcMain.handle('redis:slowLogGet', async (_e, id: string, count: number) => {
return redisService.slowLogGet(id, count)
})
ipcMain.handle('redis:slowLogLen', async (_e, id: string) => {
return redisService.slowLogLen(id)
})
// Memory
ipcMain.handle('redis:memoryUsage', async (_e, id: string, key: string) => {
return redisService.memoryUsage(id, key)
})
ipcMain.handle('redis:batchDelete', async (_e, id: string, keys: string[]) => {
return redisService.batchDelete(id, keys)
})
// Stream
ipcMain.handle('redis:streamInfo', async (_e, id: string, key: string) => {
return redisService.streamInfo(id, key)
})
ipcMain.handle('redis:streamRange', async (_e, id: string, key: string, start: string, end: string, count: number) => {
return redisService.streamRange(id, key, start, end, count)
})
ipcMain.handle('redis:streamAdd', async (_e, id: string, key: string, streamId: string, fieldValues: string[]) => {
return redisService.streamAdd(id, key, streamId, fieldValues)
})
ipcMain.handle('redis:streamTrim', async (_e, id: string, key: string, maxLen: number) => {
return redisService.streamTrim(id, key, maxLen)
})
ipcMain.handle('redis:streamDel', async (_e, id: string, key: string, ids: string[]) => {
return redisService.streamDel(id, key, ...ids)
})
// Key ops
ipcMain.handle('redis:renameKey', async (_e, id: string, oldKey: string, newKey: string) => {
await redisService.renameKey(id, oldKey, newKey)
})
ipcMain.handle('redis:existsKey', async (_e, id: string, key: string) => {
return redisService.existsKey(id, key)
})
ipcMain.handle('redis:ping', async (_e, id: string) => {
return redisService.ping(id)
})
ipcMain.handle('redis:isConnected', (_e, id: string) => {
return redisService.isConnected(id)
})
}
-373
View File
@@ -1,373 +0,0 @@
// src/main/redis-service.ts
import Redis, { RedisOptions, Cluster, ClusterNode } from 'ioredis'
export type RedisClient = Redis | Cluster
interface ConnectionOptions {
id: string
host: string
port: number
password?: string
username?: string
tls?: boolean
tlsCa?: string
tlsCert?: string
tlsKey?: string
}
const clients = new Map<string, RedisClient>()
function buildRedisOptions(opts: ConnectionOptions): RedisOptions {
const options: RedisOptions = {
host: opts.host,
port: opts.port,
username: opts.username || undefined,
password: opts.password || undefined,
stringNumbers: true,
enableReadyCheck: true,
family: 0,
connectTimeout: 10000,
retryStrategy(times: number) {
if (times > 3) return null
return Math.min(times * 200, 1000)
},
maxRetriesPerRequest: 3,
}
if (opts.tls) {
options.tls = {
rejectUnauthorized: false,
checkServerIdentity: () => undefined,
}
if (opts.tlsCa) options.tls.ca = opts.tlsCa
if (opts.tlsCert) options.tls.cert = opts.tlsCert
if (opts.tlsKey) options.tls.key = opts.tlsKey
}
return options
}
export async function connect(id: string, opts: ConnectionOptions): Promise<void> {
if (clients.has(id)) {
await disconnect(id)
}
return new Promise((resolve, reject) => {
console.log(`[Redis] Connecting to ${opts.host}:${opts.port}...`)
const redis = new Redis(buildRedisOptions(opts))
redis.once('ready', () => {
console.log(`[Redis] Connected to ${opts.host}:${opts.port}`)
clients.set(id, redis)
resolve()
})
redis.once('error', (err: Error) => {
console.error(`[Redis] Connection error:`, err.message)
redis.disconnect()
reject(err)
})
// Timeout
setTimeout(() => {
if (!clients.has(id)) {
console.error(`[Redis] Connection timeout`)
redis.disconnect()
reject(new Error('连接超时,请检查主机和端口'))
}
}, 12000)
})
}
export async function disconnect(id: string): Promise<void> {
const client = clients.get(id)
if (client) {
client.disconnect()
clients.delete(id)
}
}
export function getClient(id: string): RedisClient | undefined {
return clients.get(id)
}
export function disconnectAll(): void {
for (const [id] of clients) {
disconnect(id)
}
}
export async function execute(id: string, command: string, args: string[]): Promise<unknown> {
const client = clients.get(id)
if (!client) throw new Error(`Client ${id} not found`)
return client.call(command, ...args)
}
export async function getServerInfo(id: string): Promise<string> {
const client = clients.get(id)
if (!client) throw new Error(`Client ${id} not found`)
return client.info() as Promise<string>
}
export async function scanKeys(
id: string,
cursor: string,
pattern: string,
count: number
): Promise<{ cursor: string; keys: string[] }> {
const client = clients.get(id)
if (!client) throw new Error(`Client ${id} not found`)
const [nextCursor, keys] = await client.scan(cursor, 'MATCH', pattern, 'COUNT', count)
return { cursor: nextCursor, keys }
}
export async function getKeyType(id: string, key: string): Promise<string> {
const client = clients.get(id)
if (!client) throw new Error(`Client ${id} not found`)
return client.type(key)
}
export async function getKeyTTL(id: string, key: string): Promise<number> {
const client = clients.get(id)
if (!client) throw new Error(`Client ${id} not found`)
return client.ttl(key)
}
export async function getStringValue(id: string, key: string): Promise<string | null> {
const client = clients.get(id)
if (!client) throw new Error(`Client ${id} not found`)
return client.get(key)
}
export async function setStringValue(id: string, key: string, value: string): Promise<void> {
const client = clients.get(id)
if (!client) throw new Error(`Client ${id} not found`)
await client.set(key, value)
}
export async function deleteKey(id: string, key: string): Promise<void> {
const client = clients.get(id)
if (!client) throw new Error(`Client ${id} not found`)
await client.del(key)
}
export async function getHashFields(
id: string, key: string, cursor: string, count: number
): Promise<{ cursor: string; fields: [string, string][] }> {
const client = clients.get(id)
if (!client) throw new Error(`Client ${id} not found`)
const [nextCursor, raw] = await client.hscan(key, cursor, 'COUNT', count)
const fields: [string, string][] = []
for (let i = 0; i < raw.length; i += 2) {
fields.push([raw[i], raw[i + 1]])
}
return { cursor: nextCursor, fields }
}
export async function setHashField(
id: string, key: string, field: string, value: string
): Promise<void> {
const client = clients.get(id)
if (!client) throw new Error(`Client ${id} not found`)
await client.hset(key, field, value)
}
export async function deleteHashField(id: string, key: string, field: string): Promise<void> {
const client = clients.get(id)
if (!client) throw new Error(`Client ${id} not found`)
await client.hdel(key, field)
}
export async function setKeyTTL(id: string, key: string, ttl: number): Promise<void> {
const client = clients.get(id)
if (!client) throw new Error(`Client ${id} not found`)
if (ttl < 0) {
await client.persist(key)
} else {
await client.expire(key, ttl)
}
}
export async function selectDb(id: string, db: number): Promise<void> {
const client = clients.get(id)
if (!client) throw new Error(`Client ${id} not found`)
await client.select(db)
}
export async function dbSize(id: string): Promise<number> {
const client = clients.get(id)
if (!client) throw new Error(`Client ${id} not found`)
return client.dbsize() as Promise<number>
}
// List operations
export async function listRange(id: string, key: string, start: number, stop: number): Promise<string[]> {
const client = clients.get(id)
if (!client) throw new Error(`Client ${id} not found`)
return client.lrange(key, start, stop)
}
export async function listLength(id: string, key: string): Promise<number> {
const client = clients.get(id)
if (!client) throw new Error(`Client ${id} not found`)
return client.llen(key)
}
export async function listPush(id: string, key: string, ...values: string[]): Promise<number> {
const client = clients.get(id)
if (!client) throw new Error(`Client ${id} not found`)
return client.rpush(key, ...values)
}
export async function listSet(id: string, key: string, index: number, value: string): Promise<void> {
const client = clients.get(id)
if (!client) throw new Error(`Client ${id} not found`)
await client.lset(key, index, value)
}
export async function listRemove(id: string, key: string, count: number, value: string): Promise<number> {
const client = clients.get(id)
if (!client) throw new Error(`Client ${id} not found`)
return client.lrem(key, count, value)
}
// Set operations
export async function setMembers(id: string, key: string): Promise<string[]> {
const client = clients.get(id)
if (!client) throw new Error(`Client ${id} not found`)
return client.smembers(key)
}
export async function setSize(id: string, key: string): Promise<number> {
const client = clients.get(id)
if (!client) throw new Error(`Client ${id} not found`)
return client.scard(key)
}
export async function setAdd(id: string, key: string, ...members: string[]): Promise<number> {
const client = clients.get(id)
if (!client) throw new Error(`Client ${id} not found`)
return client.sadd(key, ...members)
}
export async function setRemove(id: string, key: string, ...members: string[]): Promise<number> {
const client = clients.get(id)
if (!client) throw new Error(`Client ${id} not found`)
return client.srem(key, ...members)
}
// Sorted set operations
export async function zsetRange(id: string, key: string, start: number, stop: number, withScores: boolean): Promise<string[]> {
const client = clients.get(id)
if (!client) throw new Error(`Client ${id} not found`)
if (withScores) return client.zrange(key, start, stop, 'WITHSCORES')
return client.zrange(key, start, stop)
}
export async function zsetSize(id: string, key: string): Promise<number> {
const client = clients.get(id)
if (!client) throw new Error(`Client ${id} not found`)
return client.zcard(key)
}
export async function zsetAdd(id: string, key: string, score: number, member: string): Promise<number> {
const client = clients.get(id)
if (!client) throw new Error(`Client ${id} not found`)
return client.zadd(key, score, member)
}
export async function zsetRemove(id: string, key: string, ...members: string[]): Promise<number> {
const client = clients.get(id)
if (!client) throw new Error(`Client ${id} not found`)
return client.zrem(key, ...members)
}
// Slow log
export async function slowLogGet(id: string, count: number): Promise<any[]> {
const client = clients.get(id)
if (!client) throw new Error(`Client ${id} not found`)
return client.slowlog('GET', count) as Promise<any[]>
}
export async function slowLogLen(id: string): Promise<number> {
const client = clients.get(id)
if (!client) throw new Error(`Client ${id} not found`)
return client.slowlog('LEN') as Promise<number>
}
// Memory info
export async function memoryUsage(id: string, key: string): Promise<number | null> {
const client = clients.get(id)
if (!client) throw new Error(`Client ${id} not found`)
return client.memory('USAGE', key) as Promise<number | null>
}
export async function batchDelete(id: string, keys: string[]): Promise<number> {
const client = clients.get(id)
if (!client) throw new Error(`Client ${id} not found`)
if (keys.length === 0) return 0
const pipeline = client.pipeline()
for (const key of keys) {
pipeline.del(key)
}
const results = await pipeline.exec()
return results?.filter(([err]) => !err).length ?? 0
}
// Stream operations
export async function streamInfo(id: string, key: string): Promise<any> {
const client = clients.get(id)
if (!client) throw new Error(`Client ${id} not found`)
return client.xinfo('STREAM', key)
}
export async function streamRange(
id: string, key: string, start: string, end: string, count: number
): Promise<any[]> {
const client = clients.get(id)
if (!client) throw new Error(`Client ${id} not found`)
return client.xrange(key, start, end, 'COUNT', count)
}
export async function streamAdd(
id: string, key: string, streamId: string, fieldValues: string[]
): Promise<string | null> {
const client = clients.get(id)
if (!client) throw new Error(`Client ${id} not found`)
return client.xadd(key, streamId, ...fieldValues)
}
export async function streamTrim(id: string, key: string, maxLen: number): Promise<number> {
const client = clients.get(id)
if (!client) throw new Error(`Client ${id} not found`)
return client.xtrim(key, 'MAXLEN', maxLen)
}
export async function streamDel(id: string, key: string, ...ids: string[]): Promise<number> {
const client = clients.get(id)
if (!client) throw new Error(`Client ${id} not found`)
return client.xdel(key, ...ids)
}
export async function renameKey(id: string, oldKey: string, newKey: string): Promise<void> {
const client = clients.get(id)
if (!client) throw new Error(`Client ${id} not found`)
await client.rename(oldKey, newKey)
}
export async function existsKey(id: string, key: string): Promise<boolean> {
const client = clients.get(id)
if (!client) throw new Error(`Client ${id} not found`)
return (await client.exists(key)) === 1
}
export async function ping(id: string): Promise<string> {
const client = clients.get(id)
if (!client) throw new Error(`Client ${id} not found`)
return client.ping()
}
export function isConnected(id: string): boolean {
const client = clients.get(id)
return client?.status === 'ready'
}
-55
View File
@@ -1,55 +0,0 @@
// src/main/store.ts
import Store from 'electron-store'
interface ConnectionConfig {
id: string
name: string
host: string
port: number
auth: string
username: string
tls: boolean
tlsCaPath: string
tlsCertPath: string
tlsKeyPath: string
sshEnabled: boolean
sshHost: string
sshPort: number
sshUsername: string
sshPassword: string
sshPrivateKeyPath: string
cluster: boolean
sentinelMasterName: string
separator: string
order: number
createdAt: number
color?: string
}
interface AppSettings {
theme: 'system' | 'dark' | 'light'
locale: string
fontSize: number
scanCount: number
}
interface AppStorage {
connections: ConnectionConfig[]
settings: AppSettings
}
const store = new Store<AppStorage>({
name: 'jrdm-config',
defaults: {
connections: [],
settings: {
theme: 'system',
locale: 'en',
fontSize: 13,
scanCount: 500,
},
},
})
export default store
export type { ConnectionConfig, AppSettings }
-60
View File
@@ -1,60 +0,0 @@
// src/main/win-state.ts
import { app, screen, BrowserWindow } from 'electron'
import { join } from 'path'
import { readFileSync, writeFile } from 'fs'
interface WinState {
x: number | null
y: number | null
width: number
height: number
maximized: boolean
}
const STATE_FILE = join(app.getPath('userData'), 'jrdm-win-state.json')
const MIN_WIDTH = 900
const MIN_HEIGHT = 600
const DEFAULT_WIDTH = 1200
const DEFAULT_HEIGHT = 800
function parseJson(str: string): WinState | null {
try { return JSON.parse(str) }
catch { return null }
}
function getLastState(): WinState {
try {
const raw = readFileSync(STATE_FILE, 'utf-8')
const state = parseJson(raw)
if (state) {
const display = screen.getPrimaryDisplay().workArea
if (state.x != null && (state.x < 0 || state.x > display.width - 100)) {
state.x = null; state.y = null
}
if (state.y != null && (state.y < 0 || state.y > display.height - 100)) {
state.x = null; state.y = null
}
state.width = Math.max(state.width || DEFAULT_WIDTH, MIN_WIDTH)
state.height = Math.max(state.height || DEFAULT_HEIGHT, MIN_HEIGHT)
return state
}
} catch { /* file doesn't exist yet */ }
return { x: null, y: null, width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT, maximized: false }
}
function saveState(win: BrowserWindow): void {
const bounds = win.getBounds()
const state = { ...bounds, maximized: win.isMaximized() }
writeFile(STATE_FILE, JSON.stringify(state), 'utf-8', () => {})
}
export function restoreAndTrack(win: BrowserWindow): void {
const lastState = getLastState()
if (lastState.x != null && lastState.y != null) {
win.setBounds({ x: lastState.x, y: lastState.y, width: lastState.width, height: lastState.height })
}
if (lastState.maximized) {
win.maximize()
}
win.on('close', () => saveState(win))
}
-106
View File
@@ -1,106 +0,0 @@
// src/preload/index.d.ts
export interface ConnectionConfig {
id: string
name: string
host: string
port: number
auth: string
username: string
tls: boolean
tlsCaPath: string
tlsCertPath: string
tlsKeyPath: string
sshEnabled: boolean
sshHost: string
sshPort: number
sshUsername: string
sshPassword: string
sshPrivateKeyPath: string
cluster: boolean
sentinelMasterName: string
separator: string
order: number
createdAt: number
color?: string
}
export interface ElectronAPI {
window: {
minimize: () => void
maximize: () => void
close: () => void
}
theme: {
get: () => Promise<boolean>
set: (theme: 'system' | 'dark' | 'light') => void
onOsUpdated: (callback: (isDark: boolean) => void) => void
}
dialog: {
openFile: (options: { title?: string; filters?: { name: string; extensions: string[] }[] }) =>
Promise<{ path: string; content: string } | null>
}
credential: {
encrypt: (text: string) => Promise<string>
decrypt: (encoded: string) => Promise<string>
}
storage: {
getConnections: () => Promise<ConnectionConfig[]>
saveConnection: (conn: ConnectionConfig) => Promise<ConnectionConfig[]>
deleteConnection: (id: string) => Promise<ConnectionConfig[]>
reorderConnections: (ids: string[]) => Promise<ConnectionConfig[]>
getSettings: () => Promise<any>
saveSettings: (settings: any) => Promise<void>
}
redis: {
connect: (id: string, opts: any) => Promise<{ success: boolean; error?: string }>
disconnect: (id: string) => Promise<void>
execute: (id: string, command: string, args: string[]) => Promise<any>
getInfo: (id: string) => Promise<string>
scanKeys: (id: string, cursor: string, pattern: string, count: number) =>
Promise<{ cursor: string; keys: string[] }>
getKeyType: (id: string, key: string) => Promise<string>
getKeyTTL: (id: string, key: string) => Promise<number>
getString: (id: string, key: string) => Promise<string | null>
setString: (id: string, key: string, value: string) => Promise<void>
deleteKey: (id: string, key: string) => Promise<void>
hashScan: (id: string, key: string, cursor: string, count: number) =>
Promise<{ cursor: string; fields: [string, string][] }>
hashSet: (id: string, key: string, field: string, value: string) => Promise<void>
hashDel: (id: string, key: string, field: string) => Promise<void>
setTTL: (id: string, key: string, ttl: number) => Promise<void>
selectDb: (id: string, db: number) => Promise<void>
dbSize: (id: string) => Promise<number>
listRange: (id: string, key: string, start: number, stop: number) => Promise<string[]>
listLength: (id: string, key: string) => Promise<number>
listPush: (id: string, key: string, values: string[]) => Promise<number>
listSet: (id: string, key: string, index: number, value: string) => Promise<void>
listRemove: (id: string, key: string, count: number, value: string) => Promise<number>
setMembers: (id: string, key: string) => Promise<string[]>
setSize: (id: string, key: string) => Promise<number>
setAdd: (id: string, key: string, members: string[]) => Promise<number>
setRemove: (id: string, key: string, members: string[]) => Promise<number>
zsetRange: (id: string, key: string, start: number, stop: number, withScores: boolean) => Promise<string[]>
zsetSize: (id: string, key: string) => Promise<number>
zsetAdd: (id: string, key: string, score: number, member: string) => Promise<number>
zsetRemove: (id: string, key: string, members: string[]) => Promise<number>
slowLogGet: (id: string, count: number) => Promise<any[]>
slowLogLen: (id: string) => Promise<number>
memoryUsage: (id: string, key: string) => Promise<number | null>
batchDelete: (id: string, keys: string[]) => Promise<number>
streamInfo: (id: string, key: string) => Promise<any>
streamRange: (id: string, key: string, start: string, end: string, count: number) => Promise<any[]>
streamAdd: (id: string, key: string, streamId: string, fieldValues: string[]) => Promise<string>
streamTrim: (id: string, key: string, maxLen: number) => Promise<number>
streamDel: (id: string, key: string, ids: string[]) => Promise<number>
renameKey: (id: string, oldKey: string, newKey: string) => Promise<void>
existsKey: (id: string, key: string) => Promise<boolean>
ping: (id: string) => Promise<string>
isConnected: (id: string) => Promise<boolean>
}
}
declare global {
interface Window {
electronAPI: ElectronAPI
}
}
-120
View File
@@ -1,120 +0,0 @@
// src/preload/index.ts
import { contextBridge, ipcRenderer } from 'electron'
const electronAPI = {
window: {
minimize: () => ipcRenderer.send('window:minimize'),
maximize: () => ipcRenderer.send('window:maximize'),
close: () => ipcRenderer.send('window:close'),
},
theme: {
get: (): Promise<boolean> => ipcRenderer.invoke('theme:get'),
set: (theme: 'system' | 'dark' | 'light') => ipcRenderer.send('theme:set', theme),
onOsUpdated: (callback: (isDark: boolean) => void) => {
ipcRenderer.on('theme:os-updated', (_e, isDark: boolean) => callback(isDark))
},
},
dialog: {
openFile: (options: { title?: string; filters?: { name: string; extensions: string[] }[] }) =>
ipcRenderer.invoke('dialog:openFile', options),
},
credential: {
encrypt: (text: string): Promise<string> => ipcRenderer.invoke('credential:encrypt', text),
decrypt: (encoded: string): Promise<string> => ipcRenderer.invoke('credential:decrypt', encoded),
},
storage: {
getConnections: (): Promise<any[]> => ipcRenderer.invoke('storage:getConnections'),
saveConnection: (conn: any): Promise<any[]> => ipcRenderer.invoke('storage:saveConnection', conn),
deleteConnection: (id: string): Promise<any[]> => ipcRenderer.invoke('storage:deleteConnection', id),
reorderConnections: (ids: string[]): Promise<any[]> => ipcRenderer.invoke('storage:reorderConnections', ids),
getSettings: (): Promise<any> => ipcRenderer.invoke('storage:getSettings'),
saveSettings: (settings: any): Promise<void> => ipcRenderer.invoke('storage:saveSettings', settings),
},
redis: {
connect: (id: string, opts: any): Promise<any> => ipcRenderer.invoke('redis:connect', id, opts),
disconnect: (id: string): Promise<void> => ipcRenderer.invoke('redis:disconnect', id),
execute: (id: string, command: string, args: string[]): Promise<any> =>
ipcRenderer.invoke('redis:execute', id, command, args),
getInfo: (id: string): Promise<string> => ipcRenderer.invoke('redis:getInfo', id),
scanKeys: (id: string, cursor: string, pattern: string, count: number): Promise<any> =>
ipcRenderer.invoke('redis:scanKeys', id, cursor, pattern, count),
getKeyType: (id: string, key: string): Promise<string> => ipcRenderer.invoke('redis:getKeyType', id, key),
getKeyTTL: (id: string, key: string): Promise<number> => ipcRenderer.invoke('redis:getKeyTTL', id, key),
getString: (id: string, key: string): Promise<string | null> => ipcRenderer.invoke('redis:getString', id, key),
setString: (id: string, key: string, value: string): Promise<void> =>
ipcRenderer.invoke('redis:setString', id, key, value),
deleteKey: (id: string, key: string): Promise<void> => ipcRenderer.invoke('redis:deleteKey', id, key),
hashScan: (id: string, key: string, cursor: string, count: number): Promise<any> =>
ipcRenderer.invoke('redis:hashScan', id, key, cursor, count),
hashSet: (id: string, key: string, field: string, value: string): Promise<void> =>
ipcRenderer.invoke('redis:hashSet', id, key, field, value),
hashDel: (id: string, key: string, field: string): Promise<void> =>
ipcRenderer.invoke('redis:hashDel', id, key, field),
setTTL: (id: string, key: string, ttl: number): Promise<void> =>
ipcRenderer.invoke('redis:setTTL', id, key, ttl),
selectDb: (id: string, db: number): Promise<void> =>
ipcRenderer.invoke('redis:selectDb', id, db),
dbSize: (id: string): Promise<number> => ipcRenderer.invoke('redis:dbSize', id),
// List
listRange: (id: string, key: string, start: number, stop: number): Promise<string[]> =>
ipcRenderer.invoke('redis:listRange', id, key, start, stop),
listLength: (id: string, key: string): Promise<number> =>
ipcRenderer.invoke('redis:listLength', id, key),
listPush: (id: string, key: string, values: string[]): Promise<number> =>
ipcRenderer.invoke('redis:listPush', id, key, values),
listSet: (id: string, key: string, index: number, value: string): Promise<void> =>
ipcRenderer.invoke('redis:listSet', id, key, index, value),
listRemove: (id: string, key: string, count: number, value: string): Promise<number> =>
ipcRenderer.invoke('redis:listRemove', id, key, count, value),
// Set
setMembers: (id: string, key: string): Promise<string[]> =>
ipcRenderer.invoke('redis:setMembers', id, key),
setSize: (id: string, key: string): Promise<number> =>
ipcRenderer.invoke('redis:setSize', id, key),
setAdd: (id: string, key: string, members: string[]): Promise<number> =>
ipcRenderer.invoke('redis:setAdd', id, key, members),
setRemove: (id: string, key: string, members: string[]): Promise<number> =>
ipcRenderer.invoke('redis:setRemove', id, key, members),
// Sorted set
zsetRange: (id: string, key: string, start: number, stop: number, withScores: boolean): Promise<string[]> =>
ipcRenderer.invoke('redis:zsetRange', id, key, start, stop, withScores),
zsetSize: (id: string, key: string): Promise<number> =>
ipcRenderer.invoke('redis:zsetSize', id, key),
zsetAdd: (id: string, key: string, score: number, member: string): Promise<number> =>
ipcRenderer.invoke('redis:zsetAdd', id, key, score, member),
zsetRemove: (id: string, key: string, members: string[]): Promise<number> =>
ipcRenderer.invoke('redis:zsetRemove', id, key, members),
// Slow log
slowLogGet: (id: string, count: number): Promise<any[]> =>
ipcRenderer.invoke('redis:slowLogGet', id, count),
slowLogLen: (id: string): Promise<number> =>
ipcRenderer.invoke('redis:slowLogLen', id),
// Memory
memoryUsage: (id: string, key: string): Promise<number | null> =>
ipcRenderer.invoke('redis:memoryUsage', id, key),
batchDelete: (id: string, keys: string[]): Promise<number> =>
ipcRenderer.invoke('redis:batchDelete', id, keys),
// Stream
streamInfo: (id: string, key: string): Promise<any> =>
ipcRenderer.invoke('redis:streamInfo', id, key),
streamRange: (id: string, key: string, start: string, end: string, count: number): Promise<any[]> =>
ipcRenderer.invoke('redis:streamRange', id, key, start, end, count),
streamAdd: (id: string, key: string, streamId: string, fieldValues: string[]): Promise<string> =>
ipcRenderer.invoke('redis:streamAdd', id, key, streamId, fieldValues),
streamTrim: (id: string, key: string, maxLen: number): Promise<number> =>
ipcRenderer.invoke('redis:streamTrim', id, key, maxLen),
streamDel: (id: string, key: string, ids: string[]): Promise<number> =>
ipcRenderer.invoke('redis:streamDel', id, key, ids),
// Key ops
renameKey: (id: string, oldKey: string, newKey: string): Promise<void> =>
ipcRenderer.invoke('redis:renameKey', id, oldKey, newKey),
existsKey: (id: string, key: string): Promise<boolean> =>
ipcRenderer.invoke('redis:existsKey', id, key),
ping: (id: string): Promise<string> =>
ipcRenderer.invoke('redis:ping', id),
isConnected: (id: string): Promise<boolean> =>
ipcRenderer.invoke('redis:isConnected', id),
},
}
contextBridge.exposeInMainWorld('electronAPI', electronAPI)
-7
View File
@@ -1,7 +0,0 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<object, object, unknown>
export default component
}
-16
View File
@@ -1,16 +0,0 @@
// src/renderer/src/router/index.ts
import { createRouter, createMemoryHistory } from 'vue-router'
import App from '@renderer/App.vue'
const router = createRouter({
history: createMemoryHistory(),
routes: [
{
path: '/',
name: 'home',
component: App,
},
],
})
export default router
-26
View File
@@ -1,26 +0,0 @@
/* src/renderer/src/styles/global.css */
@import './variables.css';
*, *::before, *::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body, #app {
height: 100%;
width: 100%;
overflow: hidden;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 3px; }
-42
View File
@@ -1,42 +0,0 @@
/* src/renderer/src/styles/variables.css */
:root {
--bg-primary: #0f0f1a;
--bg-secondary: #1a1a2e;
--bg-card: rgba(255, 255, 255, 0.03);
--bg-card-hover: rgba(255, 255, 255, 0.06);
--bg-card-active: rgba(99, 102, 241, 0.08);
--border-color: rgba(255, 255, 255, 0.06);
--border-accent: rgba(99, 102, 241, 0.2);
--text-primary: #e2e8f0;
--text-secondary: #94a3b8;
--text-muted: #64748b;
--accent: #6366f1;
--accent-light: #a5b4fc;
--success: #22c55e;
--warning: #eab308;
--danger: #ef4444;
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.2);
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.3);
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.4);
--shadow-glow: 0 0 14px rgba(99, 102, 241, 0.2);
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
}
:root.light {
--bg-primary: #f8fafc;
--bg-secondary: #f1f5f9;
--bg-card: #ffffff;
--bg-card-hover: #f1f5f9;
--bg-card-active: rgba(99, 102, 241, 0.06);
--border-color: rgba(0, 0, 0, 0.06);
--border-accent: rgba(99, 102, 241, 0.15);
--text-primary: #1e293b;
--text-secondary: #64748b;
--text-muted: #94a3b8;
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.04);
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.06);
--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.08);
--shadow-glow: 0 0 12px rgba(99, 102, 241, 0.1);
}