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:
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>JRedisDesktop</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,68 @@
|
||||
<script setup lang="ts">
|
||||
// src/renderer/src/App.vue
|
||||
import TitleBar from './components/TitleBar.vue'
|
||||
import StatusBar from './components/StatusBar.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-shell">
|
||||
<TitleBar />
|
||||
<main class="app-main">
|
||||
<div class="placeholder-content">
|
||||
<div class="placeholder-icon">R</div>
|
||||
<h2>JRedisDesktop</h2>
|
||||
<p>A modern Redis desktop manager</p>
|
||||
</div>
|
||||
</main>
|
||||
<StatusBar />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.app-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.app-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--bg-primary);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.placeholder-content {
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.placeholder-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
color: #fff;
|
||||
margin: 0 auto 20px;
|
||||
box-shadow: 0 8px 32px rgba(99, 102, 241, 0.2);
|
||||
}
|
||||
|
||||
.placeholder-content h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.placeholder-content p {
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
// src/renderer/src/components/StatusBar.vue
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer class="statusbar">
|
||||
<span class="statusbar-left">No connection</span>
|
||||
<span class="statusbar-right">JRedisDesktop v2.0</span>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.statusbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 26px;
|
||||
padding: 0 14px;
|
||||
background: var(--bg-secondary);
|
||||
border-top: 1px solid var(--border-color);
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,99 @@
|
||||
<script setup lang="ts">
|
||||
// src/renderer/src/components/TitleBar.vue
|
||||
import { useAppStore } from '@renderer/stores/app'
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
function minimize() { window.electronAPI?.window.minimize() }
|
||||
function maximize() { window.electronAPI?.window.maximize() }
|
||||
function close() { window.electronAPI?.window.close() }
|
||||
function toggleTheme() {
|
||||
appStore.setTheme(appStore.isDark ? 'light' : 'dark')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="titlebar">
|
||||
<div class="titlebar-drag">
|
||||
<span class="titlebar-logo">R</span>
|
||||
<span class="titlebar-title">JRedisDesktop</span>
|
||||
</div>
|
||||
<div class="titlebar-actions">
|
||||
<button class="titlebar-btn" title="Toggle theme" @click="toggleTheme">
|
||||
{{ appStore.isDark ? '☀' : '☾' }}
|
||||
</button>
|
||||
<button class="titlebar-btn" title="Minimize" @click="minimize">─</button>
|
||||
<button class="titlebar-btn" title="Maximize" @click="maximize">☐</button>
|
||||
<button class="titlebar-btn titlebar-btn-close" title="Close" @click="close">✕</button>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.titlebar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 38px;
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
-webkit-app-region: drag;
|
||||
user-select: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.titlebar-drag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding-left: 14px;
|
||||
}
|
||||
|
||||
.titlebar-logo {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.titlebar-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.titlebar-actions {
|
||||
display: flex;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.titlebar-btn {
|
||||
width: 42px;
|
||||
height: 38px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.titlebar-btn:hover {
|
||||
background: var(--bg-card-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.titlebar-btn-close:hover {
|
||||
background: #ef4444;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<object, object, unknown>
|
||||
export default component
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// src/renderer/src/main.ts
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
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' })
|
||||
app.mount('#app')
|
||||
@@ -0,0 +1,16 @@
|
||||
// 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
|
||||
@@ -0,0 +1,36 @@
|
||||
// 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>('system')
|
||||
const isDark = ref(true)
|
||||
|
||||
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)
|
||||
window.electronAPI?.theme.set(mode)
|
||||
}
|
||||
|
||||
return { theme, isDark, setTheme }
|
||||
})
|
||||
@@ -0,0 +1,26 @@
|
||||
/* 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; }
|
||||
@@ -0,0 +1,42 @@
|
||||
/* 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);
|
||||
}
|
||||
Reference in New Issue
Block a user