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
+49
View File
@@ -0,0 +1,49 @@
// src/renderer/src/stores/app.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
export type ThemeMode = 'system' | 'dark' | 'light'
export const useAppStore = defineStore('app', () => {
const theme = ref<ThemeMode>((localStorage.getItem('theme') as ThemeMode) || 'system')
const isDark = ref(true)
const fontSize = ref<number>(Number(localStorage.getItem('fontSize')) || 13)
const scanCount = ref<number>(Number(localStorage.getItem('scanCount')) || 100)
function applyTheme(mode: ThemeMode, osDark?: boolean) {
theme.value = mode
const dark = mode === 'system' ? (osDark ?? true) : mode === 'dark'
isDark.value = dark
document.documentElement.classList.toggle('light', !dark)
}
if (window.electronAPI) {
window.electronAPI.theme.onOsUpdated((osDark: boolean) => {
if (theme.value === 'system') {
applyTheme('system', osDark)
}
})
}
window.electronAPI?.theme.get().then((osDark: boolean) => {
applyTheme(theme.value, osDark)
})
function setTheme(mode: ThemeMode) {
applyTheme(mode)
localStorage.setItem('theme', mode)
window.electronAPI?.theme.set(mode)
}
function setFontSize(size: number) {
fontSize.value = size
localStorage.setItem('fontSize', String(size))
}
function setScanCount(count: number) {
scanCount.value = count
localStorage.setItem('scanCount', String(count))
}
return { theme, isDark, fontSize, scanCount, setTheme, setFontSize, setScanCount }
})
+182
View File
@@ -0,0 +1,182 @@
// src/renderer/src/stores/connection.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
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 const useConnectionStore = defineStore('connection', () => {
const connections = ref<ConnectionConfig[]>([])
const activeId = ref<string | null>(null)
const connecting = ref(false)
const error = ref<string | null>(null)
const serverInfo = ref<string | null>(null)
const healthOk = ref(true)
let healthTimer: ReturnType<typeof setInterval> | null = null
const activeConnection = computed(() =>
connections.value.find((c: ConnectionConfig) => c.id === activeId.value) ?? null
)
const isConnected = computed(() => activeId.value !== null)
async function loadConnections() {
connections.value = await window.electronAPI.storage.getConnections()
}
async function saveConnection(conn: ConnectionConfig) {
if (conn.auth && !conn.auth.startsWith('enc:')) {
conn.auth = 'enc:' + await window.electronAPI.credential.encrypt(conn.auth)
}
connections.value = await window.electronAPI.storage.saveConnection(conn)
}
async function deleteConnection(id: string) {
if (activeId.value === id) await disconnect()
connections.value = await window.electronAPI.storage.deleteConnection(id)
}
async function reorderConnections(ids: string[]) {
connections.value = await window.electronAPI.storage.reorderConnections(ids)
}
async function connect(conn: ConnectionConfig) {
connecting.value = true
error.value = null
try {
let auth = conn.auth
if (auth.startsWith('enc:')) {
auth = await window.electronAPI.credential.decrypt(auth.slice(4))
}
const result = await window.electronAPI.redis.connect(conn.id, {
host: conn.host,
port: conn.port,
password: auth || undefined,
username: conn.username || undefined,
tls: conn.tls,
})
if (result.success) {
activeId.value = conn.id
serverInfo.value = await window.electronAPI.redis.getInfo(conn.id)
startHealthCheck(conn.id)
} else {
error.value = result.error || 'Connection failed'
}
} catch (err: any) {
error.value = err.message
} finally {
connecting.value = false
}
}
async function disconnect() {
stopHealthCheck()
if (activeId.value) {
await window.electronAPI.redis.disconnect(activeId.value)
activeId.value = null
serverInfo.value = null
}
}
function startHealthCheck(connId: string) {
stopHealthCheck()
healthOk.value = true
healthTimer = setInterval(async () => {
try {
const ok = await window.electronAPI.redis.isConnected(connId)
healthOk.value = ok
if (!ok && activeId.value === connId) {
// Connection lost
error.value = 'Connection lost'
}
} catch {
healthOk.value = false
}
}, 5000)
}
function stopHealthCheck() {
if (healthTimer) {
clearInterval(healthTimer)
healthTimer = null
}
}
async function testConnection(opts: {
host: string; port: number; auth?: string; username?: string; tls?: boolean
}): Promise<{ success: boolean; error?: string }> {
if (!window.electronAPI) {
return { success: false, error: 'Electron API not ready' }
}
const testId = `test_${Date.now()}`
try {
const result = await window.electronAPI.redis.connect(testId, {
host: opts.host,
port: opts.port,
password: opts.auth || undefined,
username: opts.username || undefined,
tls: opts.tls,
})
if (result.success) {
await window.electronAPI.redis.disconnect(testId)
return { success: true }
}
return { success: false, error: result.error }
} catch (err: any) {
return { success: false, error: err.message }
}
}
async function exportConnections(): Promise<string> {
const data = JSON.stringify(connections.value, null, 2)
return data
}
async function importConnections(jsonStr: string): Promise<number> {
try {
const imported = JSON.parse(jsonStr) as ConnectionConfig[]
if (!Array.isArray(imported)) throw new Error('Invalid format')
let count = 0
for (const conn of imported) {
if (conn.id && conn.name && conn.host) {
const exists = connections.value.find((c: ConnectionConfig) => c.id === conn.id)
if (!exists) {
await saveConnection(conn)
count++
}
}
}
return count
} catch (err: any) {
throw new Error('Import failed: ' + err.message)
}
}
return {
connections, activeId, connecting, error, serverInfo, healthOk,
activeConnection, isConnected,
loadConnections, saveConnection, deleteConnection, reorderConnections,
connect, disconnect, testConnection, exportConnections, importConnections,
}
})
+186
View File
@@ -0,0 +1,186 @@
// src/renderer/src/stores/key.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
export interface KeyItem {
name: string
type: string
ttl: number
level: number
children?: KeyItem[]
isLeaf: boolean
}
interface ScanResult {
cursor: string
keys: string[]
}
export const useKeyStore = defineStore('key', () => {
const keys = ref<KeyItem[]>([])
const treeKeys = ref<KeyItem[]>([])
const selectedKey = ref<string | null>(null)
const selectedType = ref<string | null>(null)
const selectedTTL = ref<number>(-1)
const keyData = ref<any>(null)
const loading = ref(false)
const scanCursor = ref('0')
const hasMore = ref(false)
const pattern = ref('*')
const useTree = ref(false)
const currentDb = ref(0)
function buildTree(flatKeys: string[], separator: string): KeyItem[] {
const root: KeyItem[] = []
const map = new Map<string, KeyItem>()
for (const key of flatKeys) {
const parts = key.split(separator)
let currentLevel = root
for (let i = 0; i < parts.length; i++) {
const part = parts[i]
const isLast = i === parts.length - 1
const path = parts.slice(0, i + 1).join(separator)
if (map.has(path)) {
const existing = map.get(path)!
if (isLast) {
existing.name = key
existing.isLeaf = true
existing.type = ''
existing.ttl = -1
}
currentLevel = existing.children || []
} else {
const node: KeyItem = {
name: isLast ? key : part,
type: isLast ? '' : 'branch',
ttl: -1,
level: i,
children: isLast ? undefined : [],
isLeaf: isLast,
}
map.set(path, node)
currentLevel.push(node)
if (!isLast) currentLevel = node.children!
}
}
}
return root
}
async function scanKeys(connId: string, cursor: string = '0', reset: boolean = false) {
if (reset) {
scanCursor.value = '0'
keys.value = []
hasMore.value = false
}
loading.value = true
try {
const result: ScanResult = await window.electronAPI.redis.scanKeys(
connId, cursor, pattern.value, 100
)
const newKeys = result.keys.map(k => ({
name: k,
type: '',
ttl: -1,
level: 0,
isLeaf: true,
}))
if (reset) {
keys.value = newKeys
} else {
keys.value.push(...newKeys)
}
scanCursor.value = result.cursor
hasMore.value = result.cursor !== '0'
if (useTree.value) {
const conn = (await window.electronAPI.storage.getConnections())
.find((c: any) => c.id === connId)
const sep = conn?.separator || ':'
treeKeys.value = buildTree(keys.value.map(k => k.name), sep)
}
} finally {
loading.value = false
}
}
async function loadKeyTypeAndTTL(connId: string, key: string) {
const [type, ttl] = await Promise.all([
window.electronAPI.redis.getKeyType(connId, key),
window.electronAPI.redis.getKeyTTL(connId, key),
])
selectedType.value = type
selectedTTL.value = ttl
}
async function loadKeyData(connId: string, key: string) {
loading.value = true
try {
await loadKeyTypeAndTTL(connId, key)
selectedKey.value = key
switch (selectedType.value) {
case 'string':
keyData.value = await window.electronAPI.redis.getString(connId, key)
break
case 'hash': {
const result = await window.electronAPI.redis.hashScan(connId, key, '0', 100)
keyData.value = result.fields
break
}
case 'list': {
const len = await window.electronAPI.redis.listLength(connId, key)
keyData.value = { length: len }
break
}
case 'set': {
const size = await window.electronAPI.redis.setSize(connId, key)
keyData.value = { size }
break
}
case 'zset': {
const size = await window.electronAPI.redis.zsetSize(connId, key)
keyData.value = { size }
break
}
case 'stream': {
try {
const info = await window.electronAPI.redis.streamInfo(connId, key)
keyData.value = { length: info.length }
} catch {
keyData.value = { length: 0 }
}
break
}
default:
keyData.value = null
}
} finally {
loading.value = false
}
}
async function selectDb(connId: string, db: number) {
await window.electronAPI.redis.selectDb(connId, db)
currentDb.value = db
keys.value = []
treeKeys.value = []
selectedKey.value = null
keyData.value = null
scanCursor.value = '0'
hasMore.value = false
}
return {
keys, treeKeys, selectedKey, selectedType, selectedTTL, keyData,
loading, scanCursor, hasMore, pattern, useTree, currentDb,
scanKeys, loadKeyTypeAndTTL, loadKeyData, selectDb,
}
})