feat(phase1): architecture skeleton

- Electron 33 main process with frameless window, IPC, win-state persistence
- Preload with contextBridge typed API (window, theme, dialog)
- Vue 3 + Pinia + Element Plus + Vue Router renderer
- Dark/light dual theme via CSS custom properties
- TitleBar with window controls + theme toggle
- StatusBar placeholder
- Design spec document
This commit is contained in:
2026-07-04 17:21:00 +08:00
parent 3e06def637
commit 7e5d61c099
20 changed files with 8338 additions and 2 deletions
+51
View File
@@ -0,0 +1,51 @@
// src/main/index.ts
import { app, BrowserWindow, nativeTheme } from 'electron'
import { join } from 'path'
import { restoreAndTrack } from './win-state'
import { registerIpcHandlers } from './ipc-handlers'
let mainWindow: BrowserWindow | null = null
function createWindow(): void {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
minWidth: 900,
minHeight: 600,
title: 'JRedisDesktop',
backgroundColor: nativeTheme.shouldUseDarkColors ? '#0f0f1a' : '#f8fafc',
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
contextIsolation: true,
nodeIntegration: false,
sandbox: false,
},
frame: false,
titleBarStyle: 'hidden',
})
restoreAndTrack(mainWindow)
if (process.env.ELECTRON_RENDERER_URL) {
mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL)
} 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()
})
+35
View File
@@ -0,0 +1,35 @@
// src/main/ipc-handlers.ts
import { ipcMain, BrowserWindow, nativeTheme, dialog } from 'electron'
export function registerIpcHandlers(): void {
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()
})
ipcMain.handle('theme:get', () => nativeTheme.shouldUseDarkColors)
ipcMain.on('theme:set', (_event, theme: 'system' | 'dark' | 'light') => {
nativeTheme.themeSource = theme
})
ipcMain.handle('dialog:openFile', async (_event, 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 {
const content = fs.readFileSync(result.filePaths[0], 'utf-8')
return { path: result.filePaths[0], content }
} catch { return null }
})
}
+60
View File
@@ -0,0 +1,60 @@
// 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))
}