feat: electron control buttons

This commit is contained in:
Netfan
2025-04-09 04:05:27 +08:00
parent 7536be5e49
commit 20f902ed2c
17 changed files with 175 additions and 4 deletions

View File

@@ -45,6 +45,7 @@ export {
Menu,
Minimize,
Minimize2,
Minus,
MoonStar,
Palette,
PanelLeft,
@@ -58,6 +59,8 @@ export {
Settings,
Shrink,
Square,
SquareArrowDownLeft,
SquareArrowUpRight,
SquareCheckBig,
SquareMinus,
Sun,

View File

@@ -1,6 +1,11 @@
import type { IpcRendererEvent } from 'electron';
export type IpcRendererInvoke = 'open-win';
export type IpcRendererInvoke =
| 'app-close'
| 'app-maximize'
| 'app-minimize'
| 'is-maximized'
| 'open-win';
declare global {
interface Window {

View File

@@ -64,7 +64,7 @@ const logoStyle = computed((): CSSProperties => {
<header
:class="theme"
:style="style"
class="border-border bg-header top-0 flex w-full flex-[0_0_auto] items-center border-b pl-2 transition-[margin-top] duration-200"
class="app-header border-border bg-header top-0 flex w-full flex-[0_0_auto] items-center border-b pl-2 transition-[margin-top] duration-200"
>
<div v-if="slots.logo" :style="logoStyle">
<slot name="logo"></slot>
@@ -75,3 +75,17 @@ const logoStyle = computed((): CSSProperties => {
<slot></slot>
</header>
</template>
<style lang="scss" scoped>
.bg-header {
app-region: drag;
user-select: none;
:deep(.cursor-pointer),
:deep(.vben-sub-menu),
:deep(.vben-menu-item),
:deep(button),
:deep(a) {
app-region: no-drag;
}
}
</style>

View File

@@ -39,5 +39,8 @@
"@vueuse/core": "catalog:",
"vue": "catalog:",
"vue-router": "catalog:"
},
"devDependencies": {
"@vben-core/typings": "workspace:*"
}
}

View File

@@ -6,6 +6,9 @@ import { computed } from 'vue';
import { preferences } from '@vben/preferences';
import {
AppClose,
AppMaxmize,
AppMinmize,
AuthenticationColorToggle,
AuthenticationLayoutToggle,
LanguageToggle,
@@ -28,6 +31,8 @@ const showColor = computed(() => props.toolbarList.includes('color'));
const showLayout = computed(() => props.toolbarList.includes('layout'));
const showLanguage = computed(() => props.toolbarList.includes('language'));
const showTheme = computed(() => props.toolbarList.includes('theme'));
const isElectron = window?.ipcRenderer !== undefined;
</script>
<template>
@@ -45,5 +50,10 @@ const showTheme = computed(() => props.toolbarList.includes('theme'));
<!-- Always show Language and Theme toggles -->
<LanguageToggle v-if="showLanguage && preferences.widget.languageToggle" />
<ThemeToggle v-if="showTheme && preferences.widget.themeToggle" />
<template v-if="isElectron">
<AppMinmize />
<AppMaxmize />
<AppClose />
</template>
</div>
</template>

View File

@@ -9,6 +9,9 @@ import { useAccessStore } from '@vben/stores';
import { VbenFullScreen, VbenIconButton } from '@vben-core/shadcn-ui';
import {
AppClose,
AppMaxmize,
AppMinmize,
GlobalSearch,
LanguageToggle,
PreferencesButton,
@@ -41,6 +44,13 @@ const { refresh } = useRefresh();
const rightSlots = computed(() => {
const list = [{ index: REFERENCE_VALUE + 100, name: 'user-dropdown' }];
if (window.ipcRenderer) {
list.push(
{ index: REFERENCE_VALUE + 110, name: 'app-minimize' },
{ index: REFERENCE_VALUE + 120, name: 'app-maximize' },
{ index: REFERENCE_VALUE + 120, name: 'app-close' },
);
}
if (preferences.widget.globalSearch) {
list.push({
index: REFERENCE_VALUE,
@@ -166,6 +176,15 @@ function clearPreferencesAndLogout() {
<template v-else-if="slot.name === 'fullscreen'">
<VbenFullScreen class="mr-1" />
</template>
<template v-else-if="slot.name === 'app-minimize'">
<AppMinmize class="mr-1" />
</template>
<template v-else-if="slot.name === 'app-maximize'">
<AppMaxmize class="mr-1" />
</template>
<template v-else-if="slot.name === 'app-close'">
<AppClose class="mr-1" />
</template>
</slot>
</template>
</div>

View File

@@ -0,0 +1,16 @@
<script lang="ts" setup>
import { X } from '@vben/icons';
import { VbenIconButton } from '@vben-core/shadcn-ui';
function handleAppClose() {
window.ipcRenderer?.invoke('app-close');
}
</script>
<template>
<div class="flex-center mr-2 h-full" @click.stop="handleAppClose()">
<VbenIconButton class="bell-button text-foreground relative">
<X class="size-4" />
</VbenIconButton>
</div>
</template>

View File

@@ -0,0 +1 @@
export { default as AppClose } from './app-close.vue';

View File

@@ -0,0 +1,30 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { SquareArrowDownLeft, SquareArrowUpRight } from '@vben/icons';
import { VbenIconButton } from '@vben-core/shadcn-ui';
const isMaximized = ref(false);
if (window.ipcRenderer) {
onMounted(async () => {
isMaximized.value = await window.ipcRenderer.invoke('is-maximized');
window.ipcRenderer.on('maximize-changed', (_, maximized) => {
isMaximized.value = maximized;
});
});
}
function handleAppMaximize() {
window.ipcRenderer?.invoke('app-maximize');
}
</script>
<template>
<div class="flex-center mr-2 h-full" @click.stop="handleAppMaximize()">
<VbenIconButton class="bell-button text-foreground relative">
<SquareArrowDownLeft v-if="isMaximized" class="size-4" />
<SquareArrowUpRight v-else class="size-4" />
</VbenIconButton>
</div>
</template>

View File

@@ -0,0 +1 @@
export { default as AppMaxmize } from './app-maximize.vue';

View File

@@ -0,0 +1,16 @@
<script lang="ts" setup>
import { Minus } from '@vben/icons';
import { VbenIconButton } from '@vben-core/shadcn-ui';
function handleAppMinimize() {
window.ipcRenderer?.invoke('app-minimize');
}
</script>
<template>
<div class="flex-center mr-2 h-full" @click.stop="handleAppMinimize()">
<VbenIconButton class="bell-button text-foreground relative">
<Minus class="size-4" />
</VbenIconButton>
</div>
</template>

View File

@@ -0,0 +1 @@
export { default as AppMinmize } from './app-minimize.vue';

View File

@@ -1,3 +1,6 @@
export * from './app-close';
export * from './app-maximize';
export * from './app-minimize';
export { default as Breadcrumb } from './breadcrumb.vue';
export * from './check-updates';
export { default as AuthenticationColorToggle } from './color-toggle.vue';

View File

@@ -1,6 +1,9 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/web.json",
"compilerOptions": {
"types": ["@vben-core/typings/electron"]
},
"include": ["src"],
"exclude": ["node_modules"]
}

View File

@@ -41,8 +41,10 @@ const indexHtml = path.join(RENDERER_DIST, 'index.html');
async function createWindow() {
win = new BrowserWindow({
autoHideMenuBar: true,
frame: false,
height: 900,
icon: path.join(process.env.VITE_PUBLIC as string, 'favicon.ico'),
movable: true,
show: false,
title: 'Main window',
webPreferences: {
@@ -60,6 +62,14 @@ async function createWindow() {
win?.show(); // 显示窗口
});
win.on('maximize', () => {
win?.webContents.send('maximize-changed', true);
});
win.on('unmaximize', () => {
win?.webContents.send('maximize-changed', false);
});
if (VITE_DEV_SERVER_URL) {
win.loadURL(VITE_DEV_SERVER_URL);
} else {
@@ -132,6 +142,7 @@ app.on('activate', () => {
// New window example arg: new windows url
ipcMain.handle('open-win', (_, arg) => {
const childWindow = new BrowserWindow({
frame: false,
webPreferences: {
contextIsolation: true,
nodeIntegration: true,
@@ -142,8 +153,40 @@ ipcMain.handle('open-win', (_, arg) => {
if (VITE_DEV_SERVER_URL) {
childWindow.loadURL(`${VITE_DEV_SERVER_URL}#${arg}`);
childWindow.webContents.openDevTools();
} else {
childWindow.loadFile(indexHtml, { hash: arg });
}
});
ipcMain.handle('app-minimize', (event) => {
const browserWindow = BrowserWindow.fromWebContents(event.sender);
if (browserWindow) {
browserWindow.minimize();
}
});
ipcMain.handle('app-maximize', (event) => {
const browserWindow = BrowserWindow.fromWebContents(event.sender);
if (browserWindow) {
if (browserWindow.isMaximized()) {
browserWindow.restore();
} else {
browserWindow.maximize();
}
}
});
ipcMain.handle('app-close', (event) => {
const browserWindow = BrowserWindow.fromWebContents(event.sender);
if (browserWindow) {
browserWindow.close();
}
});
ipcMain.handle('is-maximized', (event) => {
const browserWindow = BrowserWindow.fromWebContents(event.sender);
if (browserWindow) {
return browserWindow.isMaximized();
}
return false;
});

View File

@@ -20,7 +20,6 @@ contextBridge.exposeInMainWorld('ipcRenderer', {
const [channel, ...omit] = args;
return ipcRenderer.send(channel, ...omit);
},
// You can expose other APTs you need here.
// ...
});

4
pnpm-lock.yaml generated
View File

@@ -1687,6 +1687,10 @@ importers:
vue-router:
specifier: 'catalog:'
version: 4.5.0(vue@3.5.13(typescript@5.8.3))
devDependencies:
'@vben-core/typings':
specifier: workspace:*
version: link:../../@core/base/typings
packages/effects/plugins:
dependencies: