mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2025-08-25 16:16:20 +08:00
feat: electron control buttons
This commit is contained in:
@@ -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,
|
||||
|
7
packages/@core/base/typings/electron.d.ts
vendored
7
packages/@core/base/typings/electron.d.ts
vendored
@@ -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 {
|
||||
|
@@ -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>
|
||||
|
@@ -39,5 +39,8 @@
|
||||
"@vueuse/core": "catalog:",
|
||||
"vue": "catalog:",
|
||||
"vue-router": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vben-core/typings": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
16
packages/effects/layouts/src/widgets/app-close/app-close.vue
Normal file
16
packages/effects/layouts/src/widgets/app-close/app-close.vue
Normal 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>
|
1
packages/effects/layouts/src/widgets/app-close/index.ts
Normal file
1
packages/effects/layouts/src/widgets/app-close/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as AppClose } from './app-close.vue';
|
@@ -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>
|
@@ -0,0 +1 @@
|
||||
export { default as AppMaxmize } from './app-maximize.vue';
|
@@ -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>
|
@@ -0,0 +1 @@
|
||||
export { default as AppMinmize } from './app-minimize.vue';
|
@@ -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';
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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;
|
||||
});
|
||||
|
@@ -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
4
pnpm-lock.yaml
generated
@@ -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:
|
||||
|
Reference in New Issue
Block a user